getimagesize 函數並不屬於 GD 擴充功能的部分,標準安裝的 PHP 都可以使用這個函數。可以先看看這個函數的文檔說明:http://php.net/manual/zh/function.getimagesize.php
如果指定的檔案如果不是有效的映像,會傳回false,回傳資料中也有表示文檔類型的字段。如果不用來獲取文件的大小而是使用它來判斷上傳文件是否是圖片文件,看起來似乎是個很不錯的方案,當然這需要屏蔽掉可能產生的警告,比如代碼這樣寫:
<?php $filesize = @getimagesize('/path/to/image.png'); if ($filesize) { do_upload(); } # 另外需要注意的是,你不可以像下面这样写: # if ($filesize[2] == 0) # 因为 $filesize[2] 可能是 1 到 16 之间的整数,但却绝对不对是0。
但是如果你只是做了這樣的驗證,那麼很不幸,你成功的在程式碼裡種下了一個webshell 的隱患。
要分析這個問題,我們先來看一下這個函數的原型:
static void php_getimagesize_from_stream(php_stream *stream, zval **info, INTERNAL_FUNCTION_PARAMETERS) { ... itype = php_getimagetype(stream, NULL TSRMLS_CC); switch( itype) { ... } ... } static void php_getimagesize_from_any(INTERNAL_FUNCTION_PARAMETERS, int mode) { ... php_getimagesize_from_stream(stream, info, INTERNAL_FUNCTION_PARAM_PASSTHRU); php_stream_close(stream); } PHP_FUNCTION(getimagesize) { php_getimagesize_from_any(INTERNAL_FUNCTION_PARAM_PASSTHRU, FROM_PATH); }
限於篇幅上面隱藏了一些細節,現在從上面的程式碼中我們知道兩件事情就夠了:
最終處理的函數是php_getimagesize_from_stream
負責判斷文件類型的函數是 php_getimagetype
接下來看一下 php_getimagetype 的實作:
PHPAPI int php_getimagetype(php_stream * stream, char *filetype TSRMLS_DC) { ... 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)) { ... } }
去掉了一些細節,php_sig_mm 文件流的前幾個位元組(文件頭)來判斷的。那麼既然如此,我們可不可以建構一個特殊的 PHP 檔案來繞過這個判斷呢?不如來嘗試一下。
找一個十六進位編輯器來寫一個的PHP 語句,例如:
PHPAPI const char php_sig_gif[3] = {'G', 'I', 'F'}; ... PHPAPI const char php_sig_png[8] = {(char) 0x89, (char) 0x50, (char) 0x4e, (char) 0x47, (char) 0x0d, (char) 0x0a, (char) 0x1a, (char) 0x0a};
這幾個字符的十六進位編碼(UTF-8)是這樣的:
<?php phpinfo(); ?>
我們構造一下,把PNG檔案的頭字節加在前面變成這樣的:
3C3F 7068 7020 7068 7069 6E66 6F28 293B 203F 3E
最後保存成 .php 後綴的檔案(注意上面是檔案的十六進位值),例如test.php。執行 php test.php 你會發現完全可以執行成功。那麼能用 getimagesize 讀取它的文件資訊嗎?新建一個文件寫入程式碼試試看:
8950 4E47 0D0A 1A0A 3C3F 7068 7020 7068 7069 6E66 6F28 293B 203F 3E
執行結果:
<?php print_r(getimagesize('test.php'));
成功讀取出來,並且文件也被正常識別為 PNG 文件,雖然寬和高的值都大的有點離譜。
現在你應該明白為什麼上文說這裡留下了一個 webshell 的隱患的吧。如果這裡只有這樣的上傳判斷,而且上傳之後的檔案是可以存取的,就可以透過這個入口注入任意程式碼執行了。
那為什麼上面的檔案可以 PHP 是可以正常執行的呢?用 token_get_all 函數來看這個檔案:
Array ( [0] => 1885957734 [1] => 1864902971 [2] => 3 [3] => width="1885957734" height="1864902971" [bits] => 32 [mime] => image/png )
如果顯示正常的話你能看到輸出陣列的第一個元素的解析器代號是312,透過 token_name 取得到的名稱會是T_INLINE_HTML,也就是說檔案頭部的資訊被當成正常的內嵌的HTML 程式碼被忽略掉了。
至於為什麼會有一個大的離譜的寬和高,看一下 php_handle_png 函數的實作就能知道,這些資訊也是透過讀取特定的檔案頭的位元來取得的。
所以,對於正常的圖片文件,getimagesize 完全可以勝任,但是對於一些有心構造的文件結構卻不行。
在處理使用者上傳的檔案時,先簡單粗暴的判斷檔案副檔名並對檔案名稱做一下處理,保證伺服器上不是 php 檔案都不能直接執行也是一種有效的方式。然後可以使用 getimagesize 做一些輔助處理。
以上就是getimagesize 函數不是完全可靠的的內容,更多相關內容請關注PHP中文網(www.php.cn)!