php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Sec Bug #72434 ZipArchive class Use After Free Vulnerability in PHP's GC algorithm and unseria
Submitted: 2016-06-16 14:41 UTC Modified: 2016-06-23 12:51 UTC
From: 3v0n1d3 at gmail dot com Assigned: dmitry (profile)
Status: Closed Package: *General Issues
PHP Version: 5.5.36 OS: *
Private report: No CVE-ID: 2016-5773
 [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 $

Patches

Add a Patch

Pull Requests

Add a Pull Request

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2016-06-21 04:37 UTC] stas@php.net
-PHP Version: 7.1.0alpha1 +PHP Version: 5.5.36 -Assigned To: +Assigned To: dmitry
 [2016-06-21 04:37 UTC] stas@php.net
Fix in https://gist.github.com/f3bf828025e86b8f17df011a6f5a1aa5
 and in security repo as f6aef68089221c5ea047d4a74224ee3deead99a6. Please verify.
 [2016-06-21 06:48 UTC] stas@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=f6aef68089221c5ea047d4a74224ee3deead99a6
Log: Fix bug #72434: ZipArchive class Use After Free Vulnerability in PHP's GC algorithm and unserialize
 [2016-06-21 06:48 UTC] stas@php.net
-Status: Assigned +Status: Closed
 [2016-06-21 07:03 UTC] stas@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=f6aef68089221c5ea047d4a74224ee3deead99a6
Log: Fix bug #72434: ZipArchive class Use After Free Vulnerability in PHP's GC algorithm and unserialize
 [2016-06-21 07:26 UTC] stas@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=f6aef68089221c5ea047d4a74224ee3deead99a6
Log: Fix bug #72434: ZipArchive class Use After Free Vulnerability in PHP's GC algorithm and unserialize
 [2016-06-21 07:27 UTC] stas@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=f6aef68089221c5ea047d4a74224ee3deead99a6
Log: Fix bug #72434: ZipArchive class Use After Free Vulnerability in PHP's GC algorithm and unserialize
 [2016-06-21 20:08 UTC] 3v0n1d3 at gmail dot com
Thank you for your fast reply and fast fix of this issue.
The patch is exactly right.
 [2016-06-22 05:58 UTC] krakjoe@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=f6aef68089221c5ea047d4a74224ee3deead99a6
Log: Fix bug #72434: ZipArchive class Use After Free Vulnerability in PHP's GC algorithm and unserialize
 [2016-06-23 12:51 UTC] kaplan@php.net
-CVE-ID: +CVE-ID: 2016-5773
 [2016-07-19 07:47 UTC] stas@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=cab1c3b3708eead315e033359d07049b23b147a3
Log: Fixed bug #72479 - same as #72434
 [2016-07-19 07:53 UTC] stas@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=cab1c3b3708eead315e033359d07049b23b147a3
Log: Fixed bug #72479 - same as #72434
 [2016-07-19 08:39 UTC] stas@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=cab1c3b3708eead315e033359d07049b23b147a3
Log: Fixed bug #72479 - same as #72434
 [2016-07-19 08:55 UTC] stas@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=cab1c3b3708eead315e033359d07049b23b147a3
Log: Fixed bug #72479 - same as #72434
 [2016-07-24 09:58 UTC] uskokovic at gmail dot com
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?
 [2016-10-17 10:11 UTC] bwoebi@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=cab1c3b3708eead315e033359d07049b23b147a3
Log: Fixed bug #72479 - same as #72434
 
PHP Copyright © 2001-2024 The PHP Group
All rights reserved.
Last updated: Tue Mar 19 06:01:30 2024 UTC