PHP 安全:如何防範用戶上傳 PHP 可執行文件

轉載地址:https://laravel-china.org/topics/19624

每個專業的 PHP 開發者都知道用戶上傳的文件都是極其危險的。不論是後端和前端的黑客都可以利用它們搞事情。

大約在一個月前,我在 reddit 上看了一篇 PHP 上傳漏洞檢測 ,因此, 我決定寫一篇文章。用戶 darpernter 問了一個棘手的問題:

儘管我將其重命名爲 'helloworld.txt', 攻擊者是否仍然能夠運行他的php 腳本?

置頂的答覆是:

如果文件後綴修改爲 .txt ,那麼它不會被當做php文件執行,這樣你安心了吧,不過再三確保不是 .php.txt 的後綴上傳。

不好意思,問題的正確答案並非如此 . 雖然上面的答覆並非全部錯誤,但顯然不全面。讓人驚訝的是,大多數的答案都非常相似。

我想解釋清楚這個問題。所以我要討論的東西變得有點大,我決定讓它變得更大。

問題

人們允許用戶上傳文件,但是擔心用戶上傳的文件在服務器上被執行。

從 php 文件如何被執行開始看。假設一個有 php 環境的服務器,那麼它通常有兩種方法在外部執行 php 文件。一是直接用 URL 請求文件,像 http://example.com/somefile.php 。第二種是 php 現在常用的,將所有請求轉發到 index.php ,並在這個文件中以某種方式引入其他文件。所以,從 php 文件中運行代碼有兩種方式:執行文件或用 include/include_once/require/require_once 的方法引入其他需要運行的文件。

其實還有第三種方法:eval() 函數。它能將傳入的字符串當做 php 代碼執行。這個函數在大多數 CMS 系統中被用來執行存儲在數據庫裏的代碼。eval() 函數非常危險,但如果你用了它,通常就意味着你確認自己在做危險的操作,並確認你已經沒有其他選擇。實際上, eval() 有它的用途,並且在某些情況下非常有用。但如果你是新手的話,我不推薦你使用它。請看 這篇在 OWASP 的文章。我在上面寫了很多。

所以,有兩種方法執行文件裏的代碼:直接執行或者在被執行的文件中引入它。那麼如何避免這種事情發生呢?

解決方法?

我們怎樣才能知道一個文件包含 php 代碼呢?看拓展名,如果以 .php 結尾的,像 somefile.php 我們就認爲它裏面有 php 代碼。

如果在網站根目錄下有一個 somefile.php 文件,那麼在瀏覽器訪問 http://example.com/somefile.php ,這個文件就會被執行並且輸出內容到瀏覽器上。

但是如果我重命名這個文件會怎樣?如果我把它重命名爲 somefile.txt 或者是 somefile.jpg 呢?我會得到什麼?我會得到它的內容。它不會被執行。它會從硬盤(或者緩存)直接被髮送過來。

在這點上 reddit 社區上的答案是對的。重命名能防止一個文件被非預期的執行,那麼爲什麼我認爲這種解決方法是錯的呢?

我相信你注意到我在 “解決方法” 後面加的問號。這個問號是有意義的。現在大多數網站的 URL 上幾乎看不到單獨的 php 文件。並且就算有,也是人爲故意僞造的,因爲 URL 上需要有 .php 來實現對老版本 URL 的向後兼容。

現在絕大部分 php 代碼是在運行中被引入的,因爲所有請求都被髮送到了網站根目錄的 index.php。這個文件會根據特定的規則引入其他 php 文件。這種規則可能(或者在將來會)被惡意使用。如果你應用的規則允許引入用戶的文件,那麼應用會容易遭到攻擊,你應該立即採取措施防止用戶的文件被執行。

如何防止引入用戶上傳的文件?

*重命名文件名可以嗎? --- *不,辦不到!

PHP解析器不關心文件的後綴名。事實上,所有程序都不關心。雙擊文件,文件會被對應的程序打開。文件後綴名只是幫助操作系統識別用什麼程序打開文件。只要程序有讀取文件的能力,程序就可以打開任何文件。有時程序拒絕打開和操作文件。但那並不是因爲後綴名,是文件內容所致。

服務器通常被設置成執行 .php  文件並將執行結果回覆輸出。如果你請求圖片 .jpg  --- 將從磁盤上原樣的返回。如果你要求服務器以某種方式運行一張 jpeg 圖片,會發生?服務器會執行還是不呢?

 

file

圖片來源: Echo / Cultura / Getty Images

程序不關心文件名。甚至不關心文件是否有名字,也不關心它究竟是不是文件。

從文件執行PHP代碼需要什麼?

有至少兩個情況可以讓PHP執行代碼:

  1. 代碼介於 <?php 和 ?> 標記之間
  2. 代碼介於 <?= 和 ?> 標記之間

即使文件中填充了一些奇怪的二進制數據或一些奇怪的保護名稱,該標記中的代碼仍然會被執行。

這裏有一個圖片給您:

 

file

該圖片沒有問題

它現在很純淨。但是您可能知道 JPEG 格式允許在文件中添加一些註釋。比如,拍攝照片的相機型號或座標地址。如果我們試圖在裏面放一些PHP代碼並嘗試 include 或 require 呢?讓我們來看看吧!

問題! 1

下載這個圖片到你的硬盤上。或者你自己去弄一張 JPEG 圖片也行。你隨便用什麼格式的文件都無所謂。我建議用一個 JPEG 文件來演示,主要是因爲它是一張圖片且易於在其中進行文本編輯。我用的是一個 Windows的筆記本,目前我手頭上沒有 Apple 或 Linux(或其他UNIX系的系統)的筆記本。所以一會我會發一個這個 OS 下的屏幕快照。但是我確信你肯定也能做這個事。

用以下這段 PHP 代碼建個文件:

<h1>Problem?</h1>
<img src="troll-face.jpg">
<?php
include "./troll-face.jpg";
  1. 保存一個圖片命名爲troll-face.jpg
  2. 把圖片和 php 腳本文件都放在同一個文件夾下
  3. 打開瀏覽器請求這個 php 文件

如果你把你的 php 文件命名爲 index.php,然後把它放在文件根目錄或者放在你網站目錄下的任何一個文件目錄中。

如果你準確完成了上述步驟,你就可以看到這個畫面:

 

file

到此這都沒毛病。沒 PHP 代碼展示,也沒有 PHP 代碼被執行。

現在,我們來添加一個問題:

  1. 打開文件屬性對話框或運行一些允許編輯 EXIF 信息的應用程序
  2. 切換到 Details 選項卡或以其他方式編輯該信息
  3. 向下滾動到 camera 參數
  4. 將下面代碼複製到 “camera maker” 字段後面:
<?php echo "<h2>Yep, a problem!</h2>"; phpinfo(); ?>

 

file

刷新頁面!

 

file

很明顯出現了一點問題!

您在頁面上看到了該圖片。相同的圖片還存在頁面的 PHP 代碼中。圖片的代碼也被執行了。

我們該怎麼做?!!1

長話短說: 如果我們不在程序種引入這些不安全的文件,文件中的腳本就不會執行。

仔細看下面的例子。

最終答案?

如果有人在某處看到我錯了 - 請糾正我,這是一個嚴重的問題。

PHP是一種腳本語言。您總是需要引用一些動態組合路徑的文件。因此,爲了保護服務器,您必須檢查路徑並防止混淆您的站點文件和用戶上傳或創建的文件。如果用戶的文件與應用程序文件分開,則可以在使用上傳或創建文件之前檢查文件的路徑。如果它位於您的應用程序腳本允許的文件夾中 - 那麼它可以使用 include_once 或 require 或 require_once 引入這個文件。如果不是--那麼就不引入它。

如何進行檢查?這很簡單。你只需要將 $folder (文件)路徑與一個允許程序引入文件 ( $file ) 的路徑文件夾進行比較。

// 不好的例子,不要用!
if (substr($file, 0, strlen($folder)) === $folder) {
  include $file;
}

如果  $folder 的存放路徑是 /path/to/folder  而且  $file  的存放路徑是  /path/to/folder/and/file , 然後我們在代碼中使用 substr() 函數把他們的路徑都變成字負串進行判斷,如果文件位於不同的文件夾中---這個字符串將不相等。反之則反。

上面的代碼有兩個重要的問題。如果 file 路徑是 /path/to/folderABC/and/file,很明顯,該文件也不在允許引入的文件夾中。通過向兩個路徑添加斜槓可以防止這種情況。我們在這裏向文件路徑添加斜槓並不重要,因爲我們只需要比較兩個字符串。

舉個例子: 如果 folder 路徑是  /path/to/folder  並且 file 路徑是 /path/to/folder/and/file ,那麼從 file 提取和 folder 具有相同數量的字符,那麼 $ folder 將是 /path/to/folder 。

再比如 folder 路徑是 /path/to/folder 並且 file 路徑是 /path/to/folderABC/and/file, 那麼從 file中提取 folder 具有相同數量的字符,和 $folder一樣,並且將再次成爲/path/to/folder,這種都是錯誤的,這不是我們期望的結果。

因此,在 /path/to/folder/ 添加斜槓後,與 /path/to/folder/and/file 的提取部分 /path/to/folder/ 相同就是安全的。

如果將 /path/to/folder/ 與 /path/to/folderABC/and/file 的提取部分 / path/to/folderA ,很明顯二個字符串不一樣。

這就是我們期望得到的。但還有另一個問題。這並不明顯。我敢肯定,如果我問你,你看到這裏有一個災難性的漏洞 - 你不會猜到它在哪裏。你也許已經在經驗中使用過這個東西,甚至可能就在今天。現在,您將看到漏洞是如何隱晦和顯而易見。往下看。

/../

假想一個很常見的場景。

有這麼一個網站。用戶可以上傳文件到該站點。所有的文件都位於一個特定的目錄下。有一個包含用戶文件的腳本。腳本自上而下進行查找是否包含用戶的輸入(直接或間接)路徑---那這個腳本可以通過如下方式進行路徑僞造:

/path/to/folder/../../../../../../../another/path/from/root/

舉例。用戶發起請求,你的腳本中包含了一個基於類似如下用戶輸入路徑的文件:

include $folder . "/" . $_GET['some']; // or $_POST, or whatever

你麻煩大了。有天用戶發送一個 ../../../../../../etc/.passwd 這種或其他請求,你就哭吧。

再不然。假如有人讓你的腳本加載一個他想要的文件,你就廢了。它不一定就只是出現在用戶文件中。它可能是你的CMS或你自己文件的一些插件(別相信任何人),甚至是應用程序邏輯中的錯誤等。

或者

用戶可能會上傳一個名爲 file.php 的文件,你會把它和其他的用戶文件一樣放在一個特定的文件夾裏面:

move_uploaded_file($filename, $folder . '/' . $filename);

用戶的文件就存放在那裏,你必須常常檢查從來沒有包含該文件夾中的文件,目前來看,所有的東西都挺正常的。通常,用戶發給你的文件不會包含斜槓或者其他特殊字符,因爲這是被系統文件系統禁止的。之所以這樣,是因爲通常情況下瀏覽器發給你的文件是在真實文件系統中創建的,同時它的名字是一些真實存在的文件的名字。

但是 http 請求允許用戶發送任何字符。所以如果某人僞造請求創建名爲 ../../../../../../var/www/yoursite.com/index.php 的文件---這行代碼會覆蓋你的 index.php 文件,如果 index.php 處於在上述路徑的話。

所有的初學者都希望通過過濾 「..」或者斜槓來解決這個問題,但是這種做法是錯誤的,由於你在安全方面還缺乏經驗。同時你必須(是的,必須)明白一個簡單的事情:你永遠無法在安全和密碼學方面的獲得足夠的知識。這句話的意思是,如果你懂得了「兩個點和斜槓」的漏洞,但這不代表你知道所有其他的缺陷、攻擊和其他特殊字符,你也不知道在文件寫入文件系統或數據庫時可能發生的代碼轉換。

解決方案和答案

爲了解決這個問題,PHP中內置了一些特殊函數方法,只是爲了在這種情況下使用。

basename()

第一個解決方案 --- basename() 它從路徑結束時提取路徑的一部分,直到它遇到第一個斜槓,但忽略字符串末尾的斜槓,參見示例。無論如何,你會收到一個安全的文件名。如果你覺得安全 - 那麼是的這很安全。如果它被不法上傳利用 - 你可以使用它來校驗文件名是否安全。

realpath()

另一個解決方案 --- realpath()它將上傳文件路徑轉換規範化的絕對路徑名,從根開始,並且根本不包含任何不安全因素。它甚至會將符號鏈接轉換爲此符號鏈接指向的路徑。

因此,您可以使用這兩個函數來檢查上傳文件的路徑。要檢查這個文件路徑到底是否真正屬於此文件夾路徑。

我的代碼

我編寫了一個函數來提供如上的檢查。我並不是專家,所以風險請自行承擔。代碼如下。

<?php
/**
 * Example for the article at medium.com
 * Created by Igor Data.
 * User: igordata
 * Date: 2017-01-23
 * @link https://medium.com/@igordata/php-running-jpg-as-php-or-how-to-prevent-execution-of-user-uploaded-files-6ff021897389 Read the article
 */
/**
 * 檢查某個路徑是否在指定文件夾內。若爲真,返回此路徑,否則返回 false。
 * @param String $path 被檢查的路徑
 * @param String $folder 文件夾的路徑,$path 必須在此文件夾內
 * @return bool|string 失敗返回 false,成功返回 $path
 *
 */
function checkPathIsInFolder($path, $folder) {
        if ($path === '' OR $path === null OR $path === false OR $folder === '' OR $folder === null OR $folder === false) {
            /* 不能使用 empty() 因爲有可能像 "0" 這樣的字符串也是有效的路徑 */
        return false;
    }
    $folderRealpath = realpath($folder);
    $pathRealpath = realpath($path);
    if ($pathRealpath === false OR $folderRealpath === false) {
        // Some of paths is empty
        return false;
    }
    $folderRealpath = rtrim($folderRealpath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
    $pathRealpath = rtrim($pathRealpath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
    if (strlen($pathRealpath) < strlen($folderRealpath)) {
        // 文件路徑比文件夾路徑短,那麼這個文件不可能在此文件夾內。
        return false;
    }
    if (substr($pathRealpath, 0, strlen($folderRealpath)) !== $folderRealpath) {
        // 文件夾的路徑不等於它必須位於的文件夾的路徑。
        return false;
    }
    // OK
    return $path;
}

結語。

  • 必須過濾用戶輸入,文件名也屬於用戶輸入,所以一定要檢查文件名。記得使用 basename() 。
  • 必須檢查你想存放用戶文件的路徑,永遠不要將這個路徑和應用目錄混合在一起。文件路徑必須由某個文件夾的字符串路徑,以及 basename($filename) 組成。文件被寫入之前,一定要檢查最終組成的文件路徑。
  • 在你引用某個文件前,必須檢查路徑,並且是嚴格檢查。
  • 記得使用一些特殊的函數,因爲你可能並不瞭解某些弱點或漏洞。
  • 並且,很明顯,這與文件後綴或 mime-type 無關。JPEG 允許字符串存在於文件內,所以一張合法的 JPEG 圖片能夠同時包含合法的 PHP 腳本。

不要信任用戶。不要信任瀏覽器。構建似乎所有人都在提交病毒的後端。

當然,也不必害怕,這其實比看起來的簡單。只要記住 “不要信任用戶” 以及 “有功能解決此問題” 便可。


Practice makes perfect.

原文地址:https://medium.com/@igordata/php-running...

譯文地址:https://laravel-china.org/topics/19624

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章