PHP 安全:如何防範使用者上傳 PHP 可執行檔案

Summer__發表於2019-01-15

file

每個專業的 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 指令碼。

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

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

轉自 PHP / Laravel 開發者社群 laravel-china.org/topics/1962…

相關文章