首頁 >後端開發 >php教程 >如何防範用戶上傳PHP可執行檔(附範例)

如何防範用戶上傳PHP可執行檔(附範例)

不言
不言轉載
2019-01-16 10:08:044062瀏覽

這篇文章帶給大家的內容是關於如何防範使用者上傳PHP可執行文件,有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。

每個專業的 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 圖片,會發生?伺服器會執行還是不呢?

程式不關心檔案名稱。甚至不關心文件是否有名字,也不關心它究竟是不是文件。

從檔案執行PHP程式碼需要什麼?

##有至少兩個情況可以讓PHP執行程式碼:

  1. 程式碼介於 

    < ;?php 和 ?> 標記之間

  2. #程式碼介於 

    = 和 ?> ; 標記之間

即使檔案中填入了一些奇怪的二進位資料或一些奇怪的保護名稱,該標記中的程式碼仍然會被執行。

如何防範用戶上傳PHP可執行檔(附範例)

該圖片沒有問題

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

問題! 1

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

用以下這段PHP 程式碼建立個檔案:

<h1>Problem?</h1>
<img  alt="如何防範用戶上傳PHP可執行檔(附範例)" >
<?php include "./troll-face.jpg";
  1. #儲存一個圖片命名為

    troll-face.jpg

  2. 把圖片和php 腳本檔案都放在同一個資料夾下

  3. #開啟瀏覽器請求這個php 檔案

#如果你把你的php 檔案命名為 

index.php,然後把它放在檔案根目錄或放在你網站目錄下的任何一個檔案目錄中。

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

如何防範用戶上傳PHP可執行檔(附範例)

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

現在,我們來新增一個問題:

  1. 開啟檔案屬性對話方塊或執行一些允許編輯EXIF 資訊的應用程式

  2. 切換到Details 標籤或以其他方式編輯該資訊

  3. 向下捲動到camera 參數

  4. 將下面程式碼複製到“camera maker” 欄位後面:

  5. <?php  echo "<h2>Yep, a problem!"; phpinfo(); ?>

如何防範用戶上傳PHP可執行檔(附範例)

#刷新頁面!

如何防範用戶上傳PHP可執行檔(附範例)

很明顯出現了一點問題!

您在頁面上看到了該圖片。相同的圖片也存在頁面的 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 === &#39;&#39; OR $path === null OR $path === false OR $folder === &#39;&#39; 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可執行檔(附範例)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:segmentfault.com。如有侵權,請聯絡admin@php.cn刪除