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
and the corresponding assembly code
0x55fe29015ec0 <zif_tan+0010> jne 0x55fe28e1698c <zif_tan.cold>
→ 0x55fe29015ec6 <zif_tan+0016> cmp BYTE PTR [rdi+0x58], 0x5
0x55fe29015eca <zif_tan+001a> mov rbp, rsi
the $rdi+0x50
points to the zval
structure.
(gdb) p *(*(zval *)($rdi+0x50)).value.arr
$28 = {
gc = {
refcount = 0x2,
u = {
type_info = 0x7
}
},
u = {
v = {
flags = 0x10,
_unused = 0x0,
nIteratorsCount = 0x0,
_unused2 = 0x0
},
flags = 0x10
},
nTableMask = 0xfffffff0,
{
arHash = 0x7fc00d46f980,
arData = 0x7fc00d46f980,
arPacked = 0x7fc00d46f980
},
nNumUsed = 0x8,
nNumOfElements = 0x8,
nTableSize = 0x8,
nInternalPointer = 0x0,
nNextFreeElement = 0x8,
pDestructor = 0x55fe290d0780 <zval_ptr_dtor>
}
(gdb) p *(*(zval *)($rdi+0x50)).value.arr->arData
$29 = {
val = {
value = {
lval = 0x7fc00d4aa8c0,
dval = 6.9397860522723038e-310,
counted = 0x7fc00d4aa8c0,
str = 0x7fc00d4aa8c0,
arr = 0x7fc00d4aa8c0,
obj = 0x7fc00d4aa8c0,
res = 0x7fc00d4aa8c0,
ref = 0x7fc00d4aa8c0,
ast = 0x7fc00d4aa8c0,
zv = 0x7fc00d4aa8c0,
ptr = 0x7fc00d4aa8c0,
ce = 0x7fc00d4aa8c0,
func = 0x7fc00d4aa8c0,
ww = {
w1 = 0xd4aa8c0,
w2 = 0x7fc0
}
},
u1 = {
type_info = 0x106,
v = {
type = 0x6,
type_flags = 0x1,
u = {
extra = 0x0
}
}
},
u2 = {
next = 0xffffffff,
cache_slot = 0xffffffff,
opline_num = 0xffffffff,
lineno = 0xffffffff,
num_args = 0xffffffff,
fe_pos = 0xffffffff,
fe_iter_idx = 0xffffffff,
guard = 0xffffffff,
constant_flags = 0xffffffff,
extra = 0xffffffff
}
},
h = 0x0,
key = 0x0
}
(gdb) p *(*(zval *)($rdi+0x50)).value.arr->arPacked
$148 = {
value = {
lval = 0x7fc00d4aa8c0,
dval = 6.9397860522723038e-310,
counted = 0x7fc00d4aa8c0,
str = 0x7fc00d4aa8c0,
arr = 0x7fc00d4aa8c0,
obj = 0x7fc00d4aa8c0,
res = 0x7fc00d4aa8c0,
ref = 0x7fc00d4aa8c0,
ast = 0x7fc00d4aa8c0,
zv = 0x7fc00d4aa8c0,
ptr = 0x7fc00d4aa8c0,
ce = 0x7fc00d4aa8c0,
func = 0x7fc00d4aa8c0,
ww = {
w1 = 0xd4aa8c0,
w2 = 0x7fc0
}
},
u1 = {
type_info = 0x106,
v = {
type = 0x6,
type_flags = 0x1,
u = {
extra = 0x0
}
}
},
u2 = {
next = 0xffffffff,
cache_slot = 0xffffffff,
opline_num = 0xffffffff,
lineno = 0xffffffff,
num_args = 0xffffffff,
fe_pos = 0xffffffff,
fe_iter_idx = 0xffffffff,
guard = 0xffffffff,
constant_flags = 0xffffffff,
extra = 0xffffffff
}
}
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
/*
* HashTable Data Layout
* =====================
*
* +=============================+
* | HT_HASH(ht, ht->nTableMask) | +=============================+
* | ... | | HT_INVALID_IDX |
* | HT_HASH(ht, -1) | | HT_INVALID_IDX |
* +-----------------------------+ +-----------------------------+
* ht->arData ---> | Bucket[0] | ht->arPacked ---> | ZVAL[0] |
* | ... | | ... |
* | Bucket[ht->nTableSize-1] | | ZVAL[ht->nTableSize-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
Our payload should be 320 - 0x18 - 0x1 bytes.
Now we can successfully reclaim the memory and craft fake Bucket structure.
(gdb) p ((Bucket *)0x7fc00d46f980)[5]
$211 = {
val = {
value = {
lval = 0xaaaabbbbccccdddd,
dval = -3.7299638781336413e-103,
counted = 0xaaaabbbbccccdddd,
str = 0xaaaabbbbccccdddd,
arr = 0xaaaabbbbccccdddd,
obj = 0xaaaabbbbccccdddd,
res = 0xaaaabbbbccccdddd,
ref = 0xaaaabbbbccccdddd,
ast = 0xaaaabbbbccccdddd,
zv = 0xaaaabbbbccccdddd,
ptr = 0xaaaabbbbccccdddd,
ce = 0xaaaabbbbccccdddd,
func = 0xaaaabbbbccccdddd,
ww = {
w1 = 0xccccdddd,
w2 = 0xaaaabbbb
}
},
u1 = {
type_info = 0x5,
v = {
type = 0x5,
type_flags = 0x0,
u = {
extra = 0x0
}
}
},
u2 = {
next = 0x0,
cache_slot = 0x0,
opline_num = 0x0,
lineno = 0x0,
num_args = 0x0,
fe_pos = 0x0,
fe_iter_idx = 0x0,
guard = 0x0,
constant_flags = 0x0,
extra = 0x0
}
},
h = 0x5,
key = 0x0
}
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?
(gdb) x/32xg $rdi
0x7ffff545ab08: 0x00007ffff5470200 0x0000000000000106 <--- zval at $result[0]
0x7ffff545ab18: 0x00007ffff547e540 0x00007fff00000106
0x7ffff545ab28: 0x8000d0b1d3799980 0x0000000000000007
0x7ffff545ab38: 0x00746e65746e6f63 0x00007ffff545abe0
0x7ffff545ab48: 0x0000000000000001 0x0000000000000000
0x7ffff545ab58: 0x0000000000000020 0xeeeeeeeeeeeeeeee
0x7ffff545ab68: 0x00007ffff545ab08 0xffffffff00000307
0x7ffff545ab78: 0x0000000000000000 0x0000000000000000
0x7ffff545ab88: 0x00007ffff5470210 0xffffffff00000307
0x7ffff545ab98: 0x0000000000000000 0x0000000000000000
0x7ffff545aba8: 0x00007ffff5470210 0xffffffff00000307
0x7ffff545abb8: 0x0000000000000000 0x0000000000000000
0x7ffff545abc8: 0x00007ffff5470210 0xffffffff00000307
0x7ffff545abd8: 0x0000005453000000 0x00007ffff545ac80
0x7ffff545abe8: 0x8000d0b1d3799980 0x0000000000000007
0x7ffff545abf8: 0x00746e65746e6f63 0x00007ffff545a8c0
zend_string
is not a null-terminated string structure, instead, it contains a length field.
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.