|
php.net | support | documentation | report a bug | advanced search | search howto | statistics | random bug | login |
[2016-06-16 14:41 UTC] 3v0n1d3 at gmail dot com
Description:
------------
A critical use after free vulnerability was discovered when PHP's garbage collection algorithm interacts with other specific PHP objects.
This vulnerability has wide reaching effects like allowing the exploitation of unserialize to gain remote code execution on a target system.
Affected Versions:
------------------------
Affected are all PHP versions >= PHP 5.3 (including PHP 7).
The configure option "--enable-zip" is required (default on many distributions).
Credits:
------------------------
This vulnerability was discovered by Ruslan Habalov and Dario Weißer.
Description:
------------------------
While analyzing PHP's unserialize function we have found some serious flaws in PHP's internal GC algorithm.
Those flaws can be exploited in a local or even remote context e.g. over PHP's unserialize function.
The POC clearly shows that we can abuse the garbage collector to help freeing a target array.
At this point an attacker can craft a fake zval object and exploit the PHP process by taking over the EIP/RIP.
Since this has been done already several times (c.f. [2]) we will leave out any POC exploit at this point.
Short description of PHP's GC.
--------------------
The GC algorithm is supposed to clean up zvals with cyclic references (c.f. [1]).
Every time a zval is destructed the GC algorithm gets involved and checks if this zval is a possible root candidate i.e. an array or object.
If this is the case this zval is added to a root buffer (this buffer basically keeps track of potential zvals with cyclic references).
This step is repeated until either
a) gc_collect_cycles() is called manually
or
b) more than GC_ROOT_BUFFER_MAX_ENTRIES (defined in the head section of 'Zend/zend_gc.c') zvals have been stored in the root buffer.
This step will also automatically invoke a call to gc_collect_cycles.
gc_collect_cycles will then apply a marking algorithm.
This algorithm can be divded into the following steps:
1) gc_mark_roots(TSRMLS_C);
Apply gc_mark_grey to all elements in the root buffer:
1.1) Traverse all its children in a recursive fashion.
1.2) Decrement every visited zval's reference count by 1 and mark it grey.
2) gc_scan_roots(TSRMLS_C);
Apply gc_mark_white to all elements in the root buffer that have a ref count of 0.
1.1) Traverse all its children in a recursive fashion.
Apply gc_mark_white again if the ref count is 0, else apply gc_mark_black on this zval recursively.
3) gc_collect_roots(TSRMLS_C);
Restore the refcount of all elements and put all white nodes into a list to free.
4) Finally, free all elements that have been marked white.
--------------------
The gc algorithm temporarily decrements the reference counts of all traversed elements as described above.
However, when reaching a ZipArchive object "php_zip_get_properties" will get called instead of a custom gc method.
This method then does the following things ('ext/zip/php_zip.c', 'php_zip_get_properties' method):
[...]
ZEND_HASH_FOREACH_STR_KEY_PTR(obj->prop_handler, key, hnd) {
zval *ret, val;
ret = php_zip_property_reader(obj, hnd, &val);
if (ret == NULL) {
ret = &EG(uninitialized_zval);
}
zend_hash_update(props, key, ret);
} ZEND_HASH_FOREACH_END();
[...]
We can trick the GC algorithm into visitng a specific property of the object first and only then call get_properties on this specific object.
'zend_hash_update' will then decrement the reference count and free it in all PHP versions > 5.3 and < PHP 7, or decrement it to a negative number in PHP 7.
Please consider that this submission is different from our other report in the following ways:
1) Different exploitation technique (the zval gets freed either by 'zend_hash_update' or by unserialize itself).
2) This submission affects all PHP versions > 5.3.
Please note:
Calling "gc_collect_cycles()" manually is not necessary.
The garbage collection can also be invoked during the unserialization process making this vulnerability remotely exploitable.
Further, please consider that using "unserialize" is optional. This bug is very likely to be exploitable in other scenarios, too.
Suggested fix:
------------------------
Similar to the suggest fix in our other report a proper gc method should be implemented for the ZIP class in 'ext/zip/php_zip.c'.
This method can be stragihtforward and return all object properties as long as it avoids calling "zend_hash_update" or
similar functions that could potentially decrement any reference counters.
This bug was very difficult to find since it involves several components interacting together in a relatively sophisticated way.
Due to complexity reasons a lot of details and further descriptions are left out in this report.
Hence, we intend to do a thorough and detailed writeup about this vulnerability once it gets acknowledged in another context.
Thank you for your consideration.
Please feel free to ask for more technical details if necessary.
References:
[1] http://php.net/manual/de/features.gc.collecting-cycles.php
[2] https://hackerone.com/reports/73235
Test script:
---------------
<?php
// The following array will be serialized and this representation will be freed later on.
$free_me = array(new StdClass());
// Create our payload and unserialize it.
$serialized_payload = 'a:3:{i:1;N;i:2;O:10:"ZipArchive":1:{s:8:"filename";'.serialize($free_me).'}i:1;R:4;}';
$unserialized_payload = unserialize($serialized_payload);
gc_collect_cycles();
// The reference counter for $free_me is at -1 for PHP 7 right now.
// Increment the reference counter by 1 -> rc is 0
$a = $unserialized_payload[1];
// Increment the reference counter by 1 again -> rc is 1
$b = $a;
// Trigger free of $free_me (referenced by $m[1]).
unset($b);
$fill_freed_space_1 = "filler_zval_1";
$fill_freed_space_2 = "filler_zval_2";
$fill_freed_space_3 = "filler_zval_3";
$fill_freed_space_4 = "filler_zval_4";
debug_zval_dump($unserialized_payload[1]);
Expected result:
----------------
array(1) refcount(1){
[0]=>
object(stdClass)#3 (0) refcount(2){
}
}
Actual result:
--------------
evonide@localhost:~/$ ./php5621 poc2.php
string(13) "filler_zval_1" refcount(2)
evonide@localhost:~/$ ./php707 poc2.php
array(1) refcount(3601163304){
[0]=>
./php707: line 1: 14327 Segmentation fault php707/sapi/cli/php $
PatchesPull RequestsHistoryAllCommentsChangesGit/SVN commits
|
|||||||||||||||||||||||||||
Copyright © 2001-2025 The PHP GroupAll rights reserved. |
Last updated: Fri Oct 24 01:00:02 2025 UTC |
If I run the testscript with zend.enable_gc=1 I can verify vulnerability: $ php -d zend.enable_gc=1 testscript string(13) "filler_zval_2" refcount(2) but with zend.enable_gc=1 it seems OK: $ php -d zend.enable_gc=0 testscript array(1) refcount(1){ [0]=> object(stdClass)#3 (0) refcount(3){ } } Does setting zend.enable_gc=0 efects only test script or does it disable this vulnerability? Does setting zend.enable_gc=0 in php.ini (and disabling the gc_enable function) seem like a reasonable workaround until updating to non-vulnerable version of PHP?