每個(gè)專(zhuān)業(yè)的 PHP 開(kāi)發(fā)者都知道用戶(hù)上傳的文件都是極其危險(xiǎn)的。不論是后端和前端的黑客都可以利用它們搞事情。
大約在一個(gè)月前,我在 reddit 上看了一篇 PHP 上傳漏洞檢測(cè) ,因此, 我決定寫(xiě)一篇文章。用戶(hù) darpernter 問(wèn)了一個(gè)棘手的問(wèn)題:
盡管我將其重命名為 helloworld.txt , 攻擊者是否仍然能夠運(yùn)行他的php 腳本?置頂?shù)拇饛?fù)是:
如果文件后綴修改為 .txt ,那么它不會(huì)被當(dāng)做php文件執(zhí)行,這樣你安心了吧,不過(guò)再三確保不是 .php.txt 的后綴上傳。不好意思,問(wèn)題的正確答案并非如此 . 雖然上面的答復(fù)并非全部錯(cuò)誤,但顯然不全面。讓人驚訝的是,大多數(shù)的答案都非常相似。
我想解釋清楚這個(gè)問(wèn)題。所以我要討論的東西變得有點(diǎn)大,我決定讓它變得更大。
問(wèn)題
人們?cè)试S用戶(hù)上傳文件,但是擔(dān)心用戶(hù)上傳的文件在服務(wù)器上被執(zhí)行。
從 php 文件如何被執(zhí)行開(kāi)始看。假設(shè)一個(gè)有 php 環(huán)境的服務(wù)器,那么它通常有兩種方法在外部執(zhí)行 php 文件。一是直接用 URL 請(qǐng)求文件,像 http://example.com/somefile.php 。第二種是 php 現(xiàn)在常用的,將所有請(qǐng)求轉(zhuǎn)發(fā)到 index.php ,并在這個(gè)文件中以某種方式引入其他文件。所以,從 php 文件中運(yùn)行代碼有兩種方式:執(zhí)行文件或用 include/include_once/require/require_once 的方法引入其他需要運(yùn)行的文件。
其實(shí)還有第三種方法:eval() 函數(shù)。它能將傳入的字符串當(dāng)做 php 代碼執(zhí)行。這個(gè)函數(shù)在大多數(shù) CMS 系統(tǒng)中被用來(lái)執(zhí)行存儲(chǔ)在數(shù)據(jù)庫(kù)里的代碼。eval() 函數(shù)非常危險(xiǎn),但如果你用了它,通常就意味著你確認(rèn)自己在做危險(xiǎn)的操作,并確認(rèn)你已經(jīng)沒(méi)有其他選擇。實(shí)際上, eval() 有它的用途,并且在某些情況下非常有用。但如果你是新手的話(huà),我不推薦你使用它。請(qǐng)看 這篇在 OWASP 的文章。我在上面寫(xiě)了很多。
所以,有兩種方法執(zhí)行文件里的代碼:直接執(zhí)行或者在被執(zhí)行的文件中引入它。那么如何避免這種事情發(fā)生呢?
解決方法?
我們?cè)鯓硬拍苤酪粋€(gè)文件包含 php 代碼呢?看拓展名,如果以 .php 結(jié)尾的,像 somefile.php 我們就認(rèn)為它里面有 php 代碼。
如果在網(wǎng)站根目錄下有一個(gè) somefile.php 文件,那么在瀏覽器訪問(wèn) http://example.com/somefile.php ,這個(gè)文件就會(huì)被執(zhí)行并且輸出內(nèi)容到瀏覽器上。
但是如果我重命名這個(gè)文件會(huì)怎樣?如果我把它重命名為 somefile.txt 或者是 somefile.jpg 呢?我會(huì)得到什么?我會(huì)得到它的內(nèi)容。它不會(huì)被執(zhí)行。它會(huì)從硬盤(pán)(或者緩存)直接被發(fā)送過(guò)來(lái)。
在這點(diǎn)上 reddit 社區(qū)上的答案是對(duì)的。重命名能防止一個(gè)文件被非預(yù)期的執(zhí)行,那么為什么我認(rèn)為這種解決方法是錯(cuò)的呢?
我相信你注意到我在 “解決方法” 后面加的問(wèn)號(hào)。這個(gè)問(wèn)號(hào)是有意義的。現(xiàn)在大多數(shù)網(wǎng)站的 URL 上幾乎看不到單獨(dú)的 php 文件。并且就算有,也是人為故意偽造的,因?yàn)?URL 上需要有 .php 來(lái)實(shí)現(xiàn)對(duì)老版本 URL 的向后兼容。
現(xiàn)在絕大部分 php 代碼是在運(yùn)行中被引入的,因?yàn)樗姓?qǐng)求都被發(fā)送到了網(wǎng)站根目錄的 index.php。這個(gè)文件會(huì)根據(jù)特定的規(guī)則引入其他 php 文件。這種規(guī)則可能(或者在將來(lái)會(huì))被惡意使用。如果你應(yīng)用的規(guī)則允許引入用戶(hù)的文件,那么應(yīng)用會(huì)容易遭到攻擊,你應(yīng)該立即采取措施防止用戶(hù)的文件被執(zhí)行。
如何防止引入用戶(hù)上傳的文件?重命名文件名可以嗎??---?不,辦不到!
PHP解析器不關(guān)心文件的后綴名。事實(shí)上,所有程序都不關(guān)心。雙擊文件,文件會(huì)被對(duì)應(yīng)的程序打開(kāi)。文件后綴名只是幫助操作系統(tǒng)識(shí)別用什么程序打開(kāi)文件。只要程序有讀取文件的能力,程序就可以打開(kāi)任何文件。有時(shí)程序拒絕打開(kāi)和操作文件。但那并不是因?yàn)楹缶Y名,是文件內(nèi)容所致。
服務(wù)器通常被設(shè)置成執(zhí)行 .php 文件并將執(zhí)行結(jié)果回復(fù)輸出。如果你請(qǐng)求圖片 .jpg ?---?將從磁盤(pán)上原樣的返回。如果你要求服務(wù)器以某種方式運(yùn)行一張 jpeg 圖片,會(huì)發(fā)生?服務(wù)器會(huì)執(zhí)行還是不呢?
程序不關(guān)心文件名。甚至不關(guān)心文件是否有名字,也不關(guān)心它究竟是不是文件。
從文件執(zhí)行PHP代碼需要什么?有至少兩個(gè)情況可以讓PHP執(zhí)行代碼:
代碼介于 ?php 和 ? 標(biāo)記之間
代碼介于 ?= 和 ? 標(biāo)記之間
即使文件中填充了一些奇怪的二進(jìn)制數(shù)據(jù)或一些奇怪的保護(hù)名稱(chēng),該標(biāo)記中的代碼仍然會(huì)被執(zhí)行。
該圖片沒(méi)有問(wèn)題
它現(xiàn)在很純凈。但是您可能知道 JPEG 格式允許在文件中添加一些注釋。比如,拍攝照片的相機(jī)型號(hào)或坐標(biāo)地址。如果我們?cè)噲D在里面放一些PHP代碼并嘗試 include 或 require 呢?讓我們來(lái)看看吧!
問(wèn)題! 1下載這個(gè)圖片到你的硬盤(pán)上。或者你自己去弄一張 JPEG 圖片也行。你隨便用什么格式的文件都無(wú)所謂。我建議用一個(gè) JPEG 文件來(lái)演示,主要是因?yàn)樗且粡垐D片且易于在其中進(jìn)行文本編輯。我用的是一個(gè) Windows的筆記本,目前我手頭上沒(méi)有 Apple 或 Linux(或其他UNIX系的系統(tǒng))的筆記本。所以一會(huì)我會(huì)發(fā)一個(gè)這個(gè) OS 下的屏幕快照。但是我確信你肯定也能做這個(gè)事。
用以下這段 PHP 代碼建個(gè)文件:
h1 Problem? /h1 img src= troll-face.jpg ?phpinclude ./troll-face.jpg
保存一個(gè)圖片命名為troll-face.jpg
把圖片和 php 腳本文件都放在同一個(gè)文件夾下
打開(kāi)瀏覽器請(qǐng)求這個(gè) php 文件
如果你把你的 php 文件命名為 index.php,然后把它放在文件根目錄或者放在你網(wǎng)站目錄下的任何一個(gè)文件目錄中。
如果你準(zhǔn)確完成了上述步驟,你就可以看到這個(gè)畫(huà)面:
到此這都沒(méi)毛病。沒(méi) PHP 代碼展示,也沒(méi)有 PHP 代碼被執(zhí)行。
現(xiàn)在,我們來(lái)添加一個(gè)問(wèn)題:
打開(kāi)文件屬性對(duì)話(huà)框或運(yùn)行一些允許編輯 EXIF 信息的html' target='_blank'>應(yīng)用程序
切換到 Details 選項(xiàng)卡或以其他方式編輯該信息
向下滾動(dòng)到 camera 參數(shù)
將下面代碼復(fù)制到 “camera maker” 字段后面:
?php echo h2 Yep, a problem! /h2 phpinfo(); ?
刷新頁(yè)面!
很明顯出現(xiàn)了一點(diǎn)問(wèn)題!
您在頁(yè)面上看到了該圖片。相同的圖片還存在頁(yè)面的 PHP 代碼中。圖片的代碼也被執(zhí)行了。
我們?cè)撛趺醋?!!1長(zhǎng)話(huà)短說(shuō): 如果我們不在程序種引入這些不安全的文件,文件中的腳本就不會(huì)執(zhí)行。
仔細(xì)看下面的例子。
最終答案?如果有人在某處看到我錯(cuò)了 - 請(qǐng)糾正我,這是一個(gè)嚴(yán)重的問(wèn)題。
PHP是一種腳本語(yǔ)言。您總是需要引用一些動(dòng)態(tài)組合路徑的文件。因此,為了保護(hù)服務(wù)器,您必須檢查路徑并防止混淆您的站點(diǎn)文件和用戶(hù)上傳或創(chuàng)建的文件。如果用戶(hù)的文件與應(yīng)用程序文件分開(kāi),則可以在使用上傳或創(chuàng)建文件之前檢查文件的路徑。如果它位于您的應(yīng)用程序腳本允許的文件夾中 - 那么它可以使用 include_once 或 require 或 require_once 引入這個(gè)文件。如果不是--那么就不引入它。
如何進(jìn)行檢查?這很簡(jiǎn)單。你只需要將 $folder (文件)路徑與一個(gè)允許程序引入文件 ( $file ) 的路徑文件夾進(jìn)行比較。
// 不好的例子,不要用!if (substr($file, 0, strlen($folder)) === $folder) { include $file;}
如果 $folder 的存放路徑是 /path/to/folder 而且 $file 的存放路徑是 /path/to/folder/and/file , 然后我們?cè)诖a中使用 substr() 函數(shù)把他們的路徑都變成字負(fù)串進(jìn)行判斷,如果文件位于不同的文件夾中---這個(gè)字符串將不相等。反之則反。
上面的代碼有兩個(gè)重要的問(wèn)題。如果 file 路徑是 /path/to/folderABC/and/file,很明顯,該文件也不在允許引入的文件夾中。通過(guò)向兩個(gè)路徑添加斜杠可以防止這種情況。我們?cè)谶@里向文件路徑添加斜杠并不重要,因?yàn)槲覀冎恍枰容^兩個(gè)字符串。
舉個(gè)例子: 如果 folder 路徑是 /path/to/folder 并且 file 路徑是 /path/to/folder/and/file ,那么從 file 提取和 folder 具有相同數(shù)量的字符,那么 $ folder 將是 /path/to/folder 。
再比如 folder 路徑是 /path/to/folder 并且 file 路徑是 /path/to/folderABC/and/file, 那么從 file 中提取 folder 具有相同數(shù)量的字符,和 $folder一樣,并且將再次成為/path/to/folder,這種都是錯(cuò)誤的,這不是我們期望的結(jié)果。
因此,在 /path/to/folder/ 添加斜杠后,與 /path/to/folder/and/file 的提取部分 /path/to/folder/ 相同就是安全的。
如果將 /path/to/folder/ 與 /path/to/folderABC/and/file 的提取部分 / path/to/folderA ,很明顯二個(gè)字符串不一樣。
這就是我們期望得到的。但還有另一個(gè)問(wèn)題。這并不明顯。我敢肯定,如果我問(wèn)你,你看到這里有一個(gè)災(zāi)難性的漏洞 - 你不會(huì)猜到它在哪里。你也許已經(jīng)在經(jīng)驗(yàn)中使用過(guò)這個(gè)東西,甚至可能就在今天。現(xiàn)在,您將看到漏洞是如何隱晦和顯而易見(jiàn)。往下看。
/../假想一個(gè)很常見(jiàn)的場(chǎng)景。
有這么一個(gè)網(wǎng)站。用戶(hù)可以上傳文件到該站點(diǎn)。所有的文件都位于一個(gè)特定的目錄下。有一個(gè)包含用戶(hù)文件的腳本。腳本自上而下進(jìn)行查找是否包含用戶(hù)的輸入(直接或間接)路徑---那這個(gè)腳本可以通過(guò)如下方式進(jìn)行路徑偽造:
/path/to/folder/../../../../../../../another/path/from/root/
舉例。用戶(hù)發(fā)起請(qǐng)求,你的腳本中包含了一個(gè)基于類(lèi)似如下用戶(hù)輸入路徑的文件:
include $folder . / . $_GET[ some // or $_POST, or whatever
你麻煩大了。有天用戶(hù)發(fā)送一個(gè) ../../../../../../etc/.passwd 這種或其他請(qǐng)求,你就哭吧。
再不然。假如有人讓你的腳本加載一個(gè)他想要的文件,你就廢了。它不一定就只是出現(xiàn)在用戶(hù)文件中。它可能是你的CMS或你自己文件的一些插件(別相信任何人),甚至是應(yīng)用程序邏輯中的錯(cuò)誤等。
或者用戶(hù)可能會(huì)上傳一個(gè)名為 file.php 的文件,你會(huì)把它和其他的用戶(hù)文件一樣放在一個(gè)特定的文件夾里面:
move_uploaded_file($filename, $folder . / . $filename);
用戶(hù)的文件就存放在那里,你必須常常檢查從來(lái)沒(méi)有包含該文件夾中的文件,目前來(lái)看,所有的東西都挺正常的。通常,用戶(hù)發(fā)給你的文件不會(huì)包含斜杠或者其他特殊字符,因?yàn)檫@是被系統(tǒng)文件系統(tǒng)禁止的。之所以這樣,是因?yàn)橥ǔG闆r下瀏覽器發(fā)給你的文件是在真實(shí)文件系統(tǒng)中創(chuàng)建的,同時(shí)它的名字是一些真實(shí)存在的文件的名字。
但是 http 請(qǐng)求允許用戶(hù)發(fā)送任何字符。所以如果某人偽造請(qǐng)求創(chuàng)建名為 ../../../../../../var/www/yoursite.com/index.php 的文件---這行代碼會(huì)覆蓋你的 index.php 文件,如果 index.php 處于在上述路徑的話(huà)。
所有的初學(xué)者都希望通過(guò)過(guò)濾 「..」或者斜杠來(lái)解決這個(gè)問(wèn)題,但是這種做法是錯(cuò)誤的,由于你在安全方面還缺乏經(jīng)驗(yàn)。同時(shí)你必須(是的,必須)明白一個(gè)簡(jiǎn)單的事情:你永遠(yuǎn)無(wú)法在安全和密碼學(xué)方面的獲得足夠的知識(shí)。這句話(huà)的意思是,如果你懂得了「兩個(gè)點(diǎn)和斜杠」的漏洞,但這不代表你知道所有其他的缺陷、攻擊和其他特殊字符,你也不知道在文件寫(xiě)入文件系統(tǒng)或數(shù)據(jù)庫(kù)時(shí)可能發(fā)生的代碼轉(zhuǎn)換。
解決方案和答案為了解決這個(gè)問(wèn)題,PHP中內(nèi)置了一些特殊函數(shù)方法,只是為了在這種情況下使用。
basename()第一個(gè)解決方案?---?basename() 它從路徑結(jié)束時(shí)提取路徑的一部分,直到它遇到第一個(gè)斜杠,但忽略字符串末尾的斜杠,參見(jiàn)示例。無(wú)論如何,你會(huì)收到一個(gè)安全的文件名。如果你覺(jué)得安全 - 那么是的這很安全。如果它被不法上傳利用 - 你可以使用它來(lái)校驗(yàn)文件名是否安全。
realpath()另一個(gè)解決方案?---?realpath()它將上傳文件路徑轉(zhuǎn)換規(guī)范化的絕對(duì)路徑名,從根開(kāi)始,并且根本不包含任何不安全因素。它甚至?xí)⒎?hào)鏈接轉(zhuǎn)換為此符號(hào)鏈接指向的路徑。
因此,您可以使用這兩個(gè)函數(shù)來(lái)檢查上傳文件的路徑。要檢查這個(gè)文件路徑到底是否真正屬于此文件夾路徑。
我的代碼我編寫(xiě)了一個(gè)函數(shù)來(lái)提供如上的檢查。我并不是專(zhuān)家,所以風(fēng)險(xiǎn)請(qǐng)自行承擔(dān)。代碼如下。
?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 * 檢查某個(gè)路徑是否在指定文件夾內(nèi)。若為真,返回此路徑,否則返回 false。 * @param String $path 被檢查的路徑 * @param String $folder 文件夾的路徑,$path 必須在此文件夾內(nèi) * @return bool|string 失敗返回 false,成功返回 $pathfunction checkPathIsInFolder($path, $folder) { if ($path === OR $path === null OR $path === false OR $folder === OR $folder === null OR $folder === false) { /* 不能使用 empty() 因?yàn)橛锌赡芟?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)) { // 文件路徑比文件夾路徑短,那么這個(gè)文件不可能在此文件夾內(nèi)。 return false; if (substr($pathRealpath, 0, strlen($folderRealpath)) !== $folderRealpath) { // 文件夾的路徑不等于它必須位于的文件夾的路徑。 return false; // OK return $path;}
結(jié)語(yǔ)。
必須過(guò)濾用戶(hù)輸入,文件名也屬于用戶(hù)輸入,所以一定要檢查文件名。記得使用 basename() 。
必須檢查你想存放用戶(hù)文件的路徑,永遠(yuǎn)不要將這個(gè)路徑和應(yīng)用目錄混合在一起。文件路徑必須由某個(gè)文件夾的字符串路徑,以及 basename($filename) 組成。文件被寫(xiě)入之前,一定要檢查最終組成的文件路徑。
在你引用某個(gè)文件前,必須檢查路徑,并且是嚴(yán)格檢查。
記得使用一些特殊的函數(shù),因?yàn)槟憧赡懿⒉涣私饽承┤觞c(diǎn)或漏洞。
并且,很明顯,這與文件后綴或 mime-type 無(wú)關(guān)。JPEG 允許字符串存在于文件內(nèi),所以一張合法的 JPEG 圖片能夠同時(shí)包含合法的 PHP 腳本。
不要信任用戶(hù)。不要信任瀏覽器。構(gòu)建似乎所有人都在提交病毒的后端。
當(dāng)然,也不必害怕,這其實(shí)比看起來(lái)的簡(jiǎn)單。只要記住 “不要信任用戶(hù)” 以及 “有功能解決此問(wèn)題” 便可。
以上就是如何防范用戶(hù)上傳PHP可執(zhí)行文件(附示例)的詳細(xì)內(nèi)容,PHP教程
鄭重聲明:本文版權(quán)歸原作者所有,轉(zhuǎn)載文章僅為傳播更多信息之目的,如作者信息標(biāo)記有誤,請(qǐng)第一時(shí)間聯(lián)系我們修改或刪除,多謝。
新聞熱點(diǎn)
疑難解答
圖片精選