這篇文章主要分析 php 使用 GD 庫的 imagecreatefrompng() 函數重建 png 圖片可能導致的本地文件包含漏洞。
當系統存在文件包含的點,能包含圖片文件; 另外系統存在圖片上傳,上傳的圖片使用 imagecreatefrompng() 函數重建圖片并保存在本地,則很可能出現文件包含的漏洞。
通常,系統在實現圖片上傳功能時,為了防范用戶上傳含有惡意 php 代碼的圖片,可采用 gd 庫重建圖片,gd 庫重建圖片的一系列函數 imagecreatefrom*,會檢查圖片規范,驗證圖片合法性,以此抵御圖片中含有惡意 php 代碼的攻擊。
那么, imagecreatefrom* 系列函數是否能完全抵御圖片中插入 php 代碼的攻擊呢,本文以 imagecreatefrompng() 函數作為研究對象,探討實現重建 png 格式的圖片中包含惡意 php 代碼的可能性,以及所需要滿足的條件。
png 文件格式, imagecreatefrompng 函數解析, 修改圖片, 上傳, 文件包含 ...
要實現重建的 png 圖片中仍包含有惡意的 php 代碼, 首先要對 png 圖片格式有基本的了解。png 支持三種圖像類型:索引彩色圖像(index-color images),灰度圖像(grayscale images),真彩色圖像(true-color images), 其中索引彩色圖像也稱為基于調色板圖像(Palette-based images)。
標準的 png 文件結構由一個 png 標識頭連接多個 png 數據塊組成,如: png signature | png chunk | png chunk | ... | png chunk
.
png 標識作為 png 圖片的頭部,為固定的 8 字節,如下
89 50 4E 47 OD 0A 1A 0A
png 定義了兩種類型的數據塊,一種是稱為關鍵數據塊(critical chunk),標準的數據塊; 另一種叫做輔助數據塊(ancillary chunks),可選的數據塊。關鍵數據塊定義了3個標準數據塊,每個 png 文件都必須包含它們。3個標準數據塊為: IHDR, IDAT, IEND
.
這里介紹4個數據塊:IHDR, PLTE, IDAT, IEND
png 數據塊結構
png 文件中,每個數據塊由4個部分組成 length | type(name) | data | CRC
, 說明如下
length: 4 bytes, just length of the data, not include type and CRC
type: 4 bytes, ASCII letters([A-Z,a-z])
CRC: 4bytes
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(header chunk):它包含有PNG文件中存儲的圖像數據的基本信息,并要作為第一個數據塊出現在PNG數據流中,而且一個PNG數據流中只能有一個文件頭數據塊。
文件頭數據塊由13字節組成,它的如下所示
域的名稱 | 字節數 | 說明 |
---|---|---|
Width | 4 bytes | 圖像寬度,以像素為單位 |
Height | 4 bytes | 圖像高度,以像素為單位 |
Bit depth | 1 byte | 圖像深度. 索引彩色圖像: 1,2,4或8 灰度圖像: 1,2,4,8或16 真彩色圖像: 8或16 |
ColorType | 1 byte | 顏色類型. 0:灰度圖像, 1,2,4,8或16 2:真彩色圖像,8或16 3:索引彩色圖像,1,2,4或8 4:帶α通道數據的灰度圖像,8或16 6:帶α通道數據的真彩色圖像,8或16 |
Compression method | 1 byte | 壓縮方法(LZ77派生算法) |
Filter method | 1 byte | 濾波器方法 |
Interlace method | 1 byte | 隔行掃描方法. 0:非隔行掃描 1: Adam7(由Adam M. Costello開發的7遍隔行掃描方法) |
調色板數據塊PLTE(palette chunk)包含有與索引彩色圖像(indexed-color image)相關的彩色變換數據,它僅與索引彩色圖像有關,而且要放在圖像數據塊(image data chunk)之前。
PLTE數據塊是定義圖像的調色板信息,PLTE可以包含1~256個調色板信息,每一個調色板信息由3個字節組成:
顏色 | 字節 | 意義 |
---|---|---|
Red | 1 byte | 0 = 黑色, 255 = 紅 |
Green | 1 byte | 0 = 黑色, 255 = 綠色 |
Blue | 1 byte | 0 = 黑色, 255 = 藍色 |
因此,調色板的長度應該是3的倍數,否則,這將是一個非法的調色板。顏色數 = length/3
對于索引圖像,調色板信息是必須的,調色板的顏色索引從0開始編號,然后是1、2……,調色板的顏色數不能超過色深中規定的顏色數(如圖像色深為4的時候,調色板中的顏色數不可以超過2^4=16),否則,這將導致PNG圖像不合法。
圖像數據塊IDAT(image data chunk):它存儲實際的數據,在數據流中可包含多個連續順序的圖像數據塊。IDAT存放著圖像真正的數據信息
圖像結束數據IEND(image trailer chunk):它用來標記PNG文件或者數據流已經結束,并且必須要放在文件的尾部。
正常情況下, png 文件的結尾為如下12個字符:
00 00 00 00 49 45 4E 44 AE 42 60 82
由于數據塊結構的定義,IEND數據塊的長度總是0(00 00 00 00,除非人為加入信息),數據標識總是IEND(49 45 4E 44),因此,CRC碼也總是AE 42 60 82
有了對 png 圖片格式的基本了解,可以幫助我們更好的理解 imagecreatefrompng() 函數的底層實現。分析 php 源碼(php 5.6.20)可知, php imagecreatefrompng() 函數實現重建圖片,核心是 gd 庫的 gdImageCreateFromPngCtx() 函數。
分析 gd 庫中的 gdImageCreateFromPngCtx() 函數可知,函數首先會檢測 png signature, 不合法則返回NULL。然后會讀原始的 png 圖片文件給 png_ptr, 再從 png_ptr 中讀圖片信息到 info_ptr,再之后就是獲取 IHDR 信息,讀 IDAT 數據等,這里不一一討論。這里僅討論 png_read_info() 函數中對讀 PLTE 數據庫的驗證處理。
#!c
gd_png.c/gdImageCreateFromPngCtx
{
...
if (png_sig_cmp(sig, 0, 8) != 0) { /* bad signature */
return NULL; /* bad signature */
}
...
png_set_read_fn (png_ptr, (void *) infile, gdPngReadData);
png_read_info (png_ptr, info_ptr); /* read all PNG info up to image data */
...
}
要了解 png_read_info() 的內部實現,可以通過讀 libpng 的源碼(libpng 1.6.21)進行了解。當圖片類型是索引圖像時,png_read_info() 讀到 PLTE chunk 時會調用 png_handle_PLTE 函數進行 CRC 校驗
#!c
pngread.c/png_read_info
{
...
else if (chunk_name == png_PLTE)
png_handle_PLTE(png_ptr, info_ptr, length);
...
}
pngrutil.c/png_handle_PLTE
{
...
#ifndef PNG_READ_OPT_PLTE_SUPPORTED
if (png_ptr->color_type == PNG_COLOR_TYPE_PALETTE)
#endif
{
png_crc_finish(png_ptr, (int) length - num * 3);
}
...
}
分析底層源碼可知, png signature 是不可能插入 php 代碼的; IHDR 存儲的是 png 的圖片信息,有固定的長度和格式,程序會提取圖片信息數據進行驗證,很難插入 php 代碼;而 PLTE 主要進行了 CRC 校驗和顏色數合法性校驗等簡單的校驗,那么很可能在 data 域插入 php 代碼。
從對 PLTE chunk 驗證的分析可知, 當原始圖片格式給索引圖片時,PLTE 數據塊在滿足 png 格式規范的情況下,程序還會進行 CRC 校驗。因此,要將 PHP 代碼寫入 PLTE 數據塊,不僅要修改 data 域的內容為php代碼,然后修改 CRC 為正確的 CRC 校驗值,當要填充的代碼過長時,可以改變 length 域的數值,滿足 length 為3的倍數, 且顏色數不超過色深中規定的顏色數。例如: IHDR 數據塊中 Bit depth 為 08, 則最大的顏色數為 2^8=256, 那么 PLTE 數據塊 data 的長度不超過 3*256=0x300。 這個長度對寫入 php 一句話木馬或者創建后門文件足夠了。
那么是不是所有 png 圖片都可以在 PLTE 數據塊插入 php 代碼呢?下面通過實驗予以說明。
png 支持索引彩色圖像(index-color images),灰度圖像(grayscale images),真彩色圖像(true-color images)三種類型的圖片,而 PLTE 數據塊是索引圖像所必須的,因此索引圖像極有可能在 PLTE 數據塊插入 php 代碼。
下面摘錄 gd 庫中 gdImageCreateFromPng() 函數的一段說明
If the PNG image being loaded is a truecolor image, the resulting
gdImagePtr will refer to a truecolor image. If the PNG image being
loaded is a palette or grayscale image, the resulting gdImagePtr
will refer to a palette image.
函數將索引彩色圖像和灰度圖像轉換為索引彩色圖像, 將真彩色圖像轉換為真彩色圖像。下面分別轉換這三種類型的圖片,測試圖片地址: 圖片 . php代碼如下
#!php
<?php
$pngfile = 'test.png';
$newpngfile = 'new.png';
$im = imagecreatefrompng($pngfile);
imagepng($im,$newpngfile);
?>
讀 IHDR 數據塊信息,色深為8bits, color type=0x03, 為索引圖像類型,改變其 PLTE 數據塊如下,修改數據為 <?php phpinfo();?>
計算 CRC 過程如下
imagecreatefrompng() 重建的圖片如下
可以看出重建的圖片中 PLTE 數據塊保留了 php 代碼,重建也增加了 pHYs 數據塊,對我們所關心的結果并沒有影響。說明插入 php 代碼成功。
原始圖像, 不含 PLTE 數據塊, 如下所示
插入 PLTE 數據塊,并寫入 php 代碼
重建后的圖片如下
可以看出重建的圖片轉換為 索引圖像類型,并且重寫了 PLTE 數據塊,寫入 php 代碼失敗
原始圖像, 不含 PLTE 數據塊, 如下所示
插入 PLTE 數據塊,并寫入 php 代碼
重建后的圖片如下
可以看出真彩色類型圖片重建后的圖片不含 PLTE 數據塊,寫入 php 代碼失敗
通過以上分析和實驗可知, imagecreatefrompng() 函數并不能完全防止圖片中插入 php 代碼, 當圖片類型為索引圖像時, 在 PLTE chunk 可以成功插入 php 代碼, 而另外其他類型的圖片并不能實現 PLTE chunk 中插入 php 代碼。最后, 一并感謝下面參考資料對我研究的幫助。
附,修改索引圖像插入 php 代碼的地址 github
所附代碼實現了當 payload 長度大于 PLTE 數據長度時, 會重寫 PLTE 數據塊。然而 在實驗過程中發現,imagecreatepng()函數重建的圖片 PLTE 數據塊的長度仍為原始的長度,即并不能隨意擴充 PLTE 數據塊的長度,具體原因還需深入分析源碼, 也就是說要加載的 payload 不能超過 PLTE 數據塊所給的長度。
通常情況下, PLTE 數據塊所給的長度可以滿足我們插入基本的 php 后門代碼,存在了那么一個點,是不是可以撬動地球了呢
當然本文還有許多不足之處,望大家批評指正
4.png book