cdxy.me
Footprints on Cyber Security and Python

本文译自 bugs.php.net 并适当修改

 

描述

在使用PHP的垃圾回收算法(以下简称GC)与其他特定的PHP对象进行交互时,我们发现了一个严重的释放后重用的漏洞。该漏洞影响广泛,可使反序列化的漏洞利用代码(exploit)成功在远程主机实现命令执行。

在分析PHP的反序列化功能时,我们发现PHP内部GC算法存在严重缺陷。该缺陷可被本地利用以及基于反序列化的远程利用。

本报告中的漏洞不影响PHP 7,而第二个报告中的漏洞会影响所有PHP>=5.3.0的版本。

该PoC表明:我们可以恶意利用垃圾收集器来释放(free)一个目标数组(array)。
此时攻击者可以构造一个假的容器(zval),并通过接管EIP/RIP来攻击PHP进程。

漏洞说明

GC算法被用来清理循环引用的变量容器(zval)。在PHP中,没有任何变量指向这个对象时,这个对象就成为垃圾。PHP会将其在内存中销毁,以防止内存溢出。当一个PHP线程结束时,当前占用的所有内存空间都会被销毁,当前程序中所有对象同时被销毁。

PHP会分配一个固定大小的“根缓冲区”,这个缓冲区用于存放固定数量的zval,如果需要修改则需要修改源代码 Zend/zend_gc.c 中的常量 GC_ROOT_BUFFER_MAX_ENTRIES 然后重新编译。

每当一个zval被销毁时,GC算法会判断这个zval是否为“根缓冲区”的候选者(比如array和object),判断通过后将其移入根缓冲区中。这个过程会循环进行,直到以下事件发生:
a) gc_collect_cycles() 函数被手动调用。
b) 根缓冲区已满。这时系统会自动调用gc_collect_cycles()进行回收。

接着,gc_collect_cycles() 函数会执行一个标记算法,该算法分为以下几个部分:
1) gc_mark_roots(TSRMLS_C);
该函数对所有根缓冲区的元素,按照深度优先算法遍历所有子节点。将所有遍历到的zval节点的引用计数(refcount)减1,同时为了避免对同一zval多次减1(因为可能不同的根能遍历到同一个zval),每次对某个zval减1后就对其执行gc_mark_grey,标记为灰色。
2) gc_scan_roots(TSRMLS_C);
再次对每个缓冲区中的根zval深度优先遍历,对所有引用计数值为0的zval使用gc_mark_white标记为“白”。不为0则使用gc_mark_black标记为黑色。
3) gc_collect_roots(TSRMLS_C);
恢复所有zval的引用计数, 并将标记为白色的zval筛选出来。
4) 销毁被标记为白色的zval,回收其内存。

不幸的是,这个GC算法可以被多次递减的条目欺骗,尽管这些条目已被标记为灰色。
请参考以下例子:

我们初始化一个数组对象(ArrayObject),它引用了另一个数组。(这在反序列化中很容易实现)
一旦GC算法试图访问该数组对象里的元素,它将执行上文提到的 gc_mark_grey 方法:

(Zend/zend_gc.c, gc_mark_grey method)

HashTable *props = get_gc(pz, &table, &n TSRMLS_CC);

这个方法想要调用被查询对象的GC方法,但是我们的数组对象不具有自己的GC。
在对象不具有GC方法时,其get_properties方法会作为替代品被调用。
因此,spl_array_get_properties方法会被替代调用。
此方法将检索内部数组(intern->array)内的所有元素,由于这个array里的元素是一个引用,该方法将返回目标数组的哈希表。

对以上场景加以利用,可以多次减少特定数组内所有元素的引用计数。
如果利用的好,可以使这个元素及其所有子节点都被标记为白色并被GC算法清除,尽管系统内仍存在未释放的指针。

该漏洞无需手动调用gc_mark_roots(TSRMLS_C);
反序列化可触发GC,导致该漏洞可被远程利用。
对该漏洞的远程利用已被验证可行。

此外,“反序列化”仅是该漏洞是一个可选项,该漏洞还适用于更多场景。

这个漏洞发现过程很艰难,因为它涉及到的多个组件以一个复杂的方式协同工作。出于某些原因,本报告可能会遗漏一些细节和进一步的说明,我们将在漏洞确认之后写一份详尽的说明文档。

测试脚本

<?php
// Fill any potential freed spaces until now.
$filler = array();
for($i = 0; $i < 100; $i++)
    $filler[] = "";
// Create our payload and unserialize it.
$serialized_payload = 'a:3:{i:0;r:1;i:1;r:1;i:2;C:11:"ArrayObject":19:{x:i:0;r:1;;m:a:0:{}}}';
$free_me = unserialize($serialized_payload);
// We need to increment the reference counter of our ArrayObject s.t. all reference counters of our unserialized array become 0.
$inc_ref_by_one = $free_me[2];
// The call to gc_collect_cycles will free '$free_me'.
gc_collect_cycles();
// We now have multiple freed spaces. Fill all of them.
$fill_freed_space_1 = "filler_zval_1";
$fill_freed_space_2 = "filler_zval_2";
var_dump($free_me);

预期结果

返回的Array中有两个reference和一个ArrayObject

实际结果

string(13) "filler_zval_2"

修复建议

确保 ext/spl/spl_array.c 有更加合适的GC算法 (就像PHP 7所做的那样)。

参考

[1] http://php.net/manual/de/features.gc.collecting-cycles.php
[2] https://github.com/php/php-src/commit/4e03ba4a6ef4c16b53e49e32eb4992a797ae08a8
[3] https://hackerone.com/reports/73235