PHP Master Writeup
How to debug the challenge?
There are plenty of user functions in the interpreter, those mathematic functions can be very useful since the paramters are relatively simpler than other functions.
Let’s take zif_tan
as an example
1 | PHP_FUNCTION(tan) |
and the corresponding assembly code
1 | 0x55fe29015ec0 <zif_tan+0010> jne 0x55fe28e1698c <zif_tan.cold> |
the $rdi+0x50
points to the zval
structure.
1 | (gdb) p *(*(zval *)($rdi+0x50)).value.arr |
1 | (gdb) p *(*(zval *)($rdi+0x50)).value.arr->arData |
Vulnerability?
The vulnerability is rooted in the implemenetation of PHP interpreter https://github.com/php/php-src/issues/13754. Side effect bug is notorius in interpreter of various languages. When $this->prev_data
is assigned to $this->data
, the reference count of $this->data
will be decreased and the object will be released. Meanwhile, in DataForm::get()
and DataForm::append()
, $this->data
has been fetched already which turn this typical side effect problem into a UaF vulnerability.
Control the hash table
The memory layout of hash table is well documented in PHP source code as below
1 | /* |
So, for a hash table which has nTableMask = 0xfffffff0, there will be 16 HT_HASH
entries with 4 * 16 bytes.
And for a hash table with 8 elements, there will be 8 Bucket
entries with 32 * 8 bytes. So to reclaim the UaF’ed arData
, we need to allocate 320 bytes.
Note that the data of zend_string
is stored inline. According to the source code of zend_string
allocation
1 | static zend_always_inline zend_string *zend_string_alloc(size_t len, bool persistent) |
Our payload should be 320 - 0x18 - 0x1 bytes.
1 | zend_string_header = p32(0x1) + p32(0x5) + p64(0x00c0ffee00c0ffee) + p64(0x100) |
Now we can successfully reclaim the memory and craft fake Bucket structure.
1 | (gdb) p ((Bucket *)0x7fc00d46f980)[5] |
Leak something first
One punch, leak more
The vulnerability actually gave us an arbitrary ref_cnt--
primitive which can be applied to any int
. What if we target the $result
array?
1 | (gdb) x/32xg $rdi |
zend_string
is not a null-terminated string structure, instead, it contains a length field.
1 |
|
If we subtract 1 from the value.str
pointer in zval
, we can have a 1 byte shift from the actually string object which means the length will be multiplied by 16 and we will be able to read a lot of data.
Exploit
A failure
We can leak address of zend heap, glibc heap, and, the base address of php-fpm with the ref_cnt--
primitive. An intuitive approach is manipulating some function pointer to get arbitrary code execution in the context of php-fpm worker. But this is really difficult to achieve precisely. The php-fpm main process will fork several worker processes and since we can only substract 1 from a given address, it will be hard to precisely control the pointer since we have no idea about which worker is handling our request right now (well, as least when I wrote this down).
Web master came to rescue
HTTP is a status less protocol, php can store the session data in $_SESSION
hash table. The DataForm
object will be serialized to a zend_string
after handling the request. With our ref_cnt--
primitive we can release this string and reclaim the memory with our callback
string! That gives us an arbitrary unserialization primitive.