getimagesize 函數不是徹底可靠的

getimagesize 函數並不屬於 GD 擴展的部分,標準安裝的 PHP 均可以使用這個函數。能夠先看看這個函數的文檔描述:http://php.net/manual/zh/function.getimagesize.phpphp

若是指定的文件若是不是有效的圖像,會返回 false,返回數據中也有表示文檔類型的字段。若是不用來獲取文件的大小而是使用它來判斷上傳文件是不是圖片文件,看起來彷佛是個很不錯的方案,固然這須要屏蔽掉可能產生的警告,好比代碼這樣寫:web

<?php
$filesize = @getimagesize('/path/to/image.png');
if ($filesize) {
    do_upload();
}

# 另外須要注意的是,你不能夠像下面這樣寫:
# if ($filesize[2] == 0)
# 由於 $filesize[2] 多是 1 到 16 之間的整數,但卻絕對不對是0。

可是若是你僅僅是作了這樣的驗證,那麼很不幸,你成功的在代碼裏種下了一個 webshell 的隱患。shell

要分析這個問題,咱們先來看一下這個函數的原型:數組

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);
}

限於篇幅上面隱藏了一些細節,如今從上面的代碼中咱們知道兩件事情就夠了:服務器

  1. 最終處理的函數是 php_getimagesize_from_stream編輯器

  2. 負責判斷文件類型的函數是 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_gif php_sig_png 等是在文件頭部定義的:.net

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};

能夠看出來 image type 是根據文件流的前幾個字節(文件頭)來判斷的。那麼既然如此,咱們可不能夠構造一個特殊的 PHP 文件來繞過這個判斷呢?不如來嘗試一下。code

找一個十六進制編輯器來寫一個的 PHP 語句,好比:

<?php phpinfo(); ?>

這幾個字符的十六進制編碼(UTF-8)是這樣的:

3C3F 7068 7020 7068 7069 6E66 6F28 293B 203F 3E

咱們構造一下,把 PNG 文件的頭字節加在前面變成這樣的:

8950 4E47 0D0A 1A0A 3C3F 7068 7020 7068 7069 6E66 6F28 293B 203F 3E

最後保存成 .php 後綴的文件(注意上面是文件的十六進制值),好比 test.php。執行一下 php test.php 你會發現徹底能夠執行成功。那麼能用 getimagesize 讀取它的文件信息嗎?新建一個文件寫入代碼試一下:

<?php
print_r(getimagesize('test.php'));

執行結果:

Array
(
    [0] => 1885957734
    [1] => 1864902971
    [2] => 3
    [3] => width="1885957734" height="1864902971"
    [bits] => 32
    [mime] => image/png
)

成功讀取出來,而且文件也被正常識別爲 PNG 文件,雖然寬和高的值都大的有點離譜。

如今你應該明白爲何上文說這裏留下了一個 webshell 的隱患的吧。若是這裏只有這樣的上傳判斷,並且上傳以後的文件是能夠訪問的,就能夠經過這個入口注入任意代碼執行了。

那麼爲何上面的文件能夠 PHP 是能夠正常執行的呢?用 token_get_all 函數來看一下這個文件:

<?php
print_r(token_get_all(file_get_contents('test.php')));

若是顯示正常的話你能看到輸出數組的第一個元素的解析器代號是 312,經過 token_name 獲取到的名稱會是 T_INLINE_HTML,也就是說文件頭部的信息被當成正常的內嵌的 HTML 代碼被忽略掉了。

至於爲何會有一個大的離譜的寬和高,看一下 php_handle_png 函數的實現就能知道,這些信息也是經過讀取特定的文件頭的位來獲取的。

因此,對於正常的圖片文件,getimagesize 徹底能夠勝任,可是對於一些有心構造的文件結構卻不行。

在處理用戶上傳的文件時,先簡單粗暴的判斷文件擴展名並對文件名作一下處理,保證在服務器上不是 php 文件都不能直接執行也是一種有效的方式。而後可使用 getimagesize 作一些輔助處理。

我的博客地址:http://0x1.im

相關文章
相關標籤/搜索