PHP文件上傳漏洞
本文用到的代碼地址:https://github.com/Levones/PHP_file_upload_vlun,好評請給小星星,謝謝各位大佬!
0x00 漏洞描述
在實際開發過程中文件上傳的功能時十分常見的,比如博客系統用戶需要文件上傳功能來上傳自己的頭像,寫博客時需要上傳圖片來豐富自己的文章,購物系統在識圖搜索時也需要上傳圖片等,文件上傳功能固然重要,但是如果在實現相應功能時沒有注意安全保護措施,造成的損失可能十分巨大,為了學習和研究文件上傳功能的安全實現方法,我將在下文分析一些常見的文件上傳安全措施和一些繞過方法。
我按照最常見的上傳功能–上傳圖片來分析這個漏洞。為了使漏洞的危害性呈現的清晰明了,我將漏洞防御措施划分為幾個不同的等級來作比較
0x01 前端HTML頁面代碼
<!DOCTYPE html> <html> <meta charset="utf-8"> <title> file_upload_test </title> <body> <form enctype="multipart/form-data" action="upload_1.php" method="POST" /> <input type="hidden" name="MAX_FILE_SIZE" value="100000" /> 選擇你要上傳的圖片: <br /> <input name="uploaded" type="file" /><br /> <br /> <input type="submit" name="Upload" value="上傳" /> </form> </body> </html>
前端的實現代碼均為以上。界面如下圖:
0x01 零防御的PHP上傳代碼
源代碼 upload_0.php
<?php if (isset($_POST['Upload'])) { $target_path = "uploads/"; $target_path = $target_path . basename( $_FILES['uploaded']['name']); if(!move_uploaded_file($_FILES['uploaded']['tmp_name'], $target_path)) { echo '<pre>'; echo '您的圖片上傳失敗.'; echo '</pre>'; } else { echo '<pre>'; echo $target_path . '文件已經成功上傳!'; echo '</pre>'; } } ?>
這段PHP代碼對上傳的文件沒有任何的過濾,只是將上傳的文件直接存儲到了網站uploads文件夾下,此時如果我們上傳一個一句話木馬並通過瀏覽器訪問加上參數的地址或者使用中國菜刀直接連接,就可以為所欲為了。
//一句話木馬 <?php eval($_GET['cmd']);?>
0x01 初級防護-驗證文件類型
源代碼 upload_1.php
<?php if( isset( $_POST[ 'Upload' ] ) ) { $target_path = "uploads/"; $target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] ); //識別文件類型 $uploaded_name = $_FILES[ 'uploaded' ][ 'name' ]; $uploaded_type = $_FILES[ 'uploaded' ][ 'type' ]; $uploaded_size = $_FILES[ 'uploaded' ][ 'size' ]; if( ( $uploaded_type == "image/jpeg" || $uploaded_type == "image/png" ) && ( $uploaded_size < 100000 ) ) { if( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) { echo "<pre>圖片上傳失敗</pre>"; } else { echo "<pre>{$target_path} 圖片上傳成功!</pre>"; } } else { echo "<pre>只允許上傳jpg或者png格式的圖片文件,且文件大小不能超過100k</pre>"; } } ?>
防御方法
初級防御的代碼在審查用戶上傳的文件時加入了“Content-Type”驗證,代碼會自動識別文件類型並將文件類型以表單的形式進行驗證,如果“Content-Type”是image/jpeg或者image/png時文件可以上傳 成功,算是初級防御。
繞過方法
用BurpSuite截斷代理修改數據包的相關字段即可完成繞過,本例上傳的文件時shell.php,代碼會將此文件的Content-Type識別為application/x-php,直接將application/x-php改為mage/jpeg即可繞過驗證,而且對於文件大小的限制也是可以直接修改”MAX_FILE_SIZE”的方式突破限制從而上傳更大的文件。
修改前
修改后
0x02 一般防護-驗證文件后綴
源代碼 upoad_2.php
<?php if( isset( $_POST[ 'Upload' ] ) ) { $target_path = "uploads/"; $target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] ); //記錄文件信息 $uploaded_name = $_FILES[ 'uploaded' ][ 'name' ]; $uploaded_ext = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1); $uploaded_size = $_FILES[ 'uploaded' ][ 'size' ]; $uploaded_tmp = $_FILES[ 'uploaded' ][ 'tmp_name' ]; //識別文件后綴 if( ( strtolower( $uploaded_ext ) == "jpg" || strtolower( $uploaded_ext ) == "jpeg" || strtolower( $uploaded_ext ) == "png" ) && ( $uploaded_size < 100000 ) && getimagesize( $uploaded_tmp ) ) { if( !move_uploaded_file( $uploaded_tmp, $target_path ) ) { echo "<pre>圖片上傳識別.</pre>"; } else { echo "<pre>{$target_path} 圖片上傳成功!</pre>"; } } else { echo "<pre>只能上傳格式為jpg和png的圖片.</pre>"; } } ?>
相比較於前一種比價簡單的驗證content-type的防護方式,一般級別的防護措施換成了驗證文件后綴的方式,順便多說一句,在為了安全性設置一些限制時,使用白名單永遠比設置黑名單要安全的多,因為總會有=各種方式繞過黑名單的方式或者是一些針對不同服務器系統或着服務器的特殊解析原理而造成的一些安全隱患。以下是獲取文件后綴的代碼:
$uploaded_ext = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1)
通過本語句獲取文件名中最后一個“.”后的字符識別上傳的文件名的后綴,並將后綴存儲在一個變量中。
if( ( strtolower( $uploaded_ext ) == "jpg" || strtolower( $uploaded_ext ) == "jpeg" || strtolower( $uploaded_ext ) == "png" ) && ( $uploaded_size < 100000 ) && getimagesize( $uploaded_tmp ) )
而在if的邏輯判斷中,需要上一條語句截取到的文件后綴為“jpg”,“jpeg”或者“png”,切且上傳的文件大小不得大於10000b,如果只有這個限制方法的話,可以直接使用burpsuite進行00截斷,從而使得在文件后綴驗證時通過但是在文件轉儲的時候忽略掉00之后的內容從而實現后綴欺騙,具體方式如下:
假設網站只能上傳圖片文件並在后台歐了后綴的限制
此時你要上傳一個shell.php的一句話木馬
將”shell.php”改為”shell.php 1.png”
使用burpsuite截斷代理,攔截數據包
將”shell.php 1.png”發送至decoder模塊,從text模式轉換為hex編輯模式,找到”shell.php 1.png”中空格對應的hex值“20”,將20改為00
從hex模式恢復為text並將修改過的字符串替換原來報文中的”shell.php 1.png”
發送報文,操作成功后會顯示文件上傳成功
操作成功后會顯示文件上傳成功,在php版本小於5.3.4的版本中,當Magic_quote_gpc選項為off時,可以在文件名中使用%00截斷,所以可以把上傳文件命名a1.php%00.png進行繞過,我們用bp抓包檢測一下文件類型。 可以發現文件類型是png成功繞過前端,並且到服務器文件會被解析成php文件,因為00后面的被截斷了,服務器不解析。
但是在本例中,00截斷的方法不再有效,因為if條件中還有一個getimagesize()函數,此函數會自動識別上傳的圖片的文件頭,長寬,mime類型等信息,因此如果上傳的文件不是圖片將無法上傳。繞過這個限制的方法是制作圖片馬,我是在win環境下制作的,只需准備一個圖片大小較小的jpg或者png格式的圖片,打開cmd使用命令:
copy 1.jpg/b+shell.php 2.jpg
1
來合成一張圖片馬,如果用二進制編輯器打開此文件會發現一句話木馬寫到了文件的后面,把這樣的文件上傳時,由於文件頭仍然是jpg的文件頭,getimagesize()函數也會正確的返回圖片的大小和文類型,因此通這種方式可以繞過getimagesize()函數的限制,再結合00截斷即可上傳木馬並在服務器端將文件解析為php腳本,從而正確執行。
但是如果服務器的PHP版本較高,則無法通過此方法進行漏洞的利用,需要結合文件包含漏洞進行利用。
0x03 無解的防護-全方面限制
當然安全只是相對的,沒有絕對的安全,一下代碼對輸入的文件進行了多種方式的審查並進行了重新編碼,是目前比較完善了安全防御措施。
源代碼 upload_2.php
<?php if( isset( $_POST[ 'Upload' ] ) ) { // 檢查token checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' ); $uploaded_name = $_FILES[ 'uploaded' ][ 'name' ]; $uploaded_ext = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1); $uploaded_size = $_FILES[ 'uploaded' ][ 'size' ]; $uploaded_type = $_FILES[ 'uploaded' ][ 'type' ]; $uploaded_tmp = $_FILES[ 'uploaded' ][ 'tmp_name' ]; $target_path = 'uploads/'; $target_file = md5( uniqid() . $uploaded_name ) . '.' . $uploaded_ext; $temp_file = ( ( ini_get( 'upload_tmp_dir' ) == '' ) ? ( sys_get_temp_dir() ) : ( ini_get( 'upload_tmp_dir' ) ) ); $temp_file .= DIRECTORY_SEPARATOR . md5( uniqid() . $uploaded_name ) . '.' . $uploaded_ext; //判斷是否是一張圖片 if( ( strtolower( $uploaded_ext ) == 'jpg' || strtolower( $uploaded_ext ) == 'jpeg' || strtolower( $uploaded_ext ) == 'png' ) && ( $uploaded_size < 100000 ) && ( $uploaded_type == 'image/jpeg' || $uploaded_type == 'image/png' ) &&getimagesize( $uploaded_tmp ) ) { //重新制作一張圖片,抹去任何可能有危害的數據 if( $uploaded_type == 'image/jpeg' ) { $img = imagecreatefromjpeg( $uploaded_tmp ); imagejpeg( $img, $temp_file, 100); } else { $img = imagecreatefrompng( $uploaded_tmp ); imagepng( $img, $temp_file, 9); } imagedestroy( $img ); //文件轉儲 if( rename( $temp_file, ( getcwd() . DIRECTORY_SEPARATOR . $target_path . $target_file ) ) ) { $html .= "<pre><a href='${target_path}${target_file}'>${target_file}</a> succesfully uploaded!</pre>"; } else { $html .= '<pre>Your image was not uploaded.</pre>'; } //刪除所有暫時文件 if( file_exists( $temp_file ) ) unlink( $temp_file ); } else { //無效文件 $html .= '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>'; } } // 添加抗csrf驗證 generateSessionToken(); ?>
上述代碼的安全措施:
添加了sessionToken,驗證會話身份,用於防止csrf攻擊
使用md5( uniqid() . $uploaded_name )函數,uniqid()函數是根據當前的時間,生成一個唯一的id,跟大多數隨機函數一樣,基於時間的隨機函數在一定條件下也是可以差生碰撞的,因此本例中采用了md5()函數來保證生成id的唯一性,而且由於md5()函數對上傳的文件名進行了重命名,因此無法使用00截斷的方式來上傳php或者其他惡意腳本文件。
以白名單的方式限制上傳的文件后綴
限定上傳的文件大小不得超過10000
通過imagecreatefromjpeg()和imagecreatefrompng()函數將上傳的圖片文件重新寫入到一個新的圖片文件中,這兩個函數會自動將圖片中的有害元數據抹除,因此即使黑客上傳了一張圖片馬也會被這個函數過濾成一個純正的圖片。
imagedestroy( $img )將用戶上傳的源文件刪除
unlink( $temp_file )刪除過濾過程中產生的任何臨時文件
0x04 個人總結
web漏洞種類繁多,利用方法奇葩而有趣,值得研究和學習