文件上傳漏洞可以說是日常滲透測試用得最多的一個漏洞,因為用它獲得服務器權限最快最直接。但是想真正把這個漏洞利用好卻不那么容易,其中有很多技巧,也有很多需要掌握的知識。俗話說,知己知彼方能百戰不殆,因此想要研究怎么防護漏洞,就要了解怎么去利用。
特點
1、利用簡單
2、危害大
產生原因
缺少必要的校驗
代碼審計
基礎
關於 PHP 中 $_FILES 數組的使用方法
$_FILES[‘file’][‘name’] 客戶端文件名稱
$_FILES[‘file’][‘type’] 文件的 MIME 類型
$_FILES[‘file’][‘size’] 文件大小單位字節
$_FILES[‘file’][‘tmp_name’] 文件被上傳后再服務器端臨時文件名,可以在 php.ini 中指定
需要注意的是在文件上傳結束后,默認的被儲存在臨時文件夾中,這時必須把他從臨時目錄中刪除或移動到其他地方,否則,腳本運行完畢后,自動刪除臨時文件,可以使用 copy
或者 *move_uploaded_file
兩個函數
程序員對某些常用函數的錯誤認識
這些函數有: empty()、isset()、strpos()、rename()
等,如下面的代碼:
#!php
if($operateId == 1){ $date = date("Ymd"); $dest = $CONFIG->basePath."data/files/".$date."/"; $COMMON->createDir($dest); //if (!is_dir($dest)) mkdir($dest, 0777); $nameExt = strtolower($COMMON->getFileExtName($_FILES['Filedata']['name'])); $allowedType = array('jpg', 'gif', 'bmp', 'png', 'jpeg'); if(!in_array($nameExt, $allowedType)){ $msg = 0; } if(empty($msg)){ $filename = getmicrotime().'.'.$nameExt; $file_url = urlencode($CONFIG->baseUrl.'data/files/'.$date."/".$filename); $filename = $dest.$filename; if(empty($_FILES['Filedata']['error'])){ move_uploaded_file($_FILES['Filedata']['tmp_name'],$filename); } if (file_exists($filename)){ //$msg = 1; $msg = $file_url; @chmod($filename, 0444); }else{ $msg = 0; } } $outMsg = "fileUrl=".$msg; $_SESSION["eoutmsg"] = $outMsg; exit; }
我們來看上面的這段代碼,要想文件成功的上傳, if(empty($msg))
必須為 True 才能進入 if 的分支,接下來我們來看 empty 函數何時返回 True,看看 PHP Manual 怎么說,如圖:

很明顯,""、0、"0"、NULL、FALSE、array()、var $var;
以及沒有任何屬性的對象都將被認為是空的,如果 var 為空,則返回 True。 非常好,接下來我們往回看,有這樣的幾行代碼
#!php
$allowedType = array('jpg', 'gif', 'bmp', 'png', 'jpeg'); if(!in_array($nameExt, $allowedType)){ $msg = 0; }
看見沒有,即使我們上傳類似 shell.php 的文件,雖然程序的安全檢查把 $msg 賦值為 0,經 empty($msg) 后,仍然返回 True,於是我們利用這個邏輯缺陷即可成功的上傳 shell.php。
程序員對某些常用函數的錯誤使用
這些函數有 iconv()、copy() 等,如下面的這段代碼(摘自 SiteStar)
#!php
public function img_create(){ $file_info =& ParamHolder::get('img_name', array(), PS_FILES); if($file_info['error'] > 0){ Notice::set('mod_marquee/msg', __('Invalid post file data!')); Content::redirect(Html::uriquery('mod_tool', 'upload_img')); } if(!preg_match('/\.('.PIC_ALLOW_EXT.')$/i', $file_info["name"])){ Notice::set('mod_marquee/msg', __('File type error!')); Content::redirect(Html::uriquery('mod_marquee', 'upload_img')); } if(file_exists(ROOT.'/upload/image/'.$file_info["name"])){ $file_info["name"] = Toolkit::randomStr(8).strrchr($file_info["name"],"."); } if(!$this->_savelinkimg($file_info)){ Notice::set('mod_marquee/msg', __('Link image upload failed!')); Content::redirect(Html::uriquery('mod_marquee', 'upload_img')); } //... } private function _savelinkimg($struct_file){ $struct_file['name'] = iconv("UTF-8", "gb2312", $struct_file['name']); move_uploaded_file($struct_file['tmp_name'], ROOT.'/upload/image/'.$struct_file['name']); return ParamParser::fire_virus(ROOT.'/upload/image/'.$struct_file['name']); }
我們再來看看這段代碼, img_create()
函數的邏輯非常嚴密,安全檢查做的很到位。然而問題出在了 _savelinkimg()
函數,即在保存文件的前面程序員錯誤的使用了 iconv()
函數,並且文件名經過了此函數,為什么是錯用了呢?
因為啊 iconv
函數在轉碼過程中,可能存在字符串截斷的問題:
在 iconv
轉碼的過程中, utf->gb2312 (其他部分編碼之間轉換同樣存在這個問題)會導致字符串被截斷,如:
$filename="shell.php(hex).jpg";
(hex 為 0x80-0x99),經過 iconv 轉碼后會變成 $filename="shell.php ";
所以,經過 iconv 后 $struct_file['name']) 為 shell.php,於是我們利用這個邏輯缺陷可以成功的上傳 shell.php (前提是上傳的文件名為 shell.php{%80-%99}.jpg)
。
歷史經典漏洞再次爆發
條件競爭漏洞,這類歷史經典漏洞在逐漸淡出人們視線的時候,再次爆發..
接着看下面這段代碼(摘自某 VPN 系統)
#!php
<? if($_POST['realfile']){ copy($_POST['realfile'],$_POST['path']); } $file = mb_convert_encoding($_POST[file],"GBK","UTF-8"); header("Pragma:"); header("Cache-Control:"); header("Content-type:application/octet-stream"); header("Content-Length:".filesize($_POST[path])); header("Content-Disposition:attachment;filename=\"$file\""); readfile($_POST[path]); if($_POST['realfile']){ unlink($_POST["path"]); } ?>
上述代碼的邏輯表面上看起來是這樣的(對於攻擊者來說):
利用 copy 函數,將 realfile 生成 shell.php 然后刪除掉 shell.php
這樣初看起來沒辦法利用,但是仔細一想, 這段代碼其實是存在邏輯問題的,所以我們可以利用這個邏輯缺陷達到 GetShell 的目的。
具體利用方法:
copy 成 temp.php --> 不斷訪問 temp.php --> temp.php 生成 shell.php --> 刪除 temp.php --> 留下 shell.php
校驗方式分類&總結
客戶端 javascript 校驗(一般只校驗后綴名)
服務端校驗
1、文件頭 content-type 字段校驗(image/gif)
2、文件內容頭校驗(GIF89a)
3、后綴名黑名單校驗
4、后綴名白名單校驗
5、自定義正則校驗
6、WAF 設備校驗(根據不同的 WAF 產品而定)
校驗方式溯源
通常一個文件以 HTTP 協議進行上傳時,將以 POST 請求發送至 Web 服務器,Web 服務器接收到請求並同意后,用戶與 Web 服務器將建立連接,並傳輸數據。一般文件上傳過程中將會經過如下幾個檢測步驟:

校驗方式&繞過姿勢
PUT 方法
WebDAV 是一種基於 HTTP 1.1 協議的通信協議.它擴展了 HTTP 1.1,在 GET、POST、HEAD 等幾個 HTTP 標准方法以外添加了一些新的方法。使應用程序可直接對 Web Server 直接讀寫,並支持寫文件鎖定 (Locking) 及解鎖 (Unlock),還可以支持文件的版本控制。當 WebDAV 開啟 PUT,MOVE,COPY,DELETE 方法時,攻擊者就可以向服務器上傳危險腳本文件。
此時可以使用 OPTIONS 探測服務器支持的 http 方法,如果支持 PUT,就進行上傳腳本文件,在通過 MOVE 或 COPY 方法改名。
當開啟 DELETE 時還可以刪除文件。
參考:
http://wiki.wooyun.org/server:httpput
客戶端校驗
JavaScript 校驗
驗證代碼如下:
<?php //文件上傳漏洞演示腳本之js驗證 $uploaddir = 'uploads/'; if (isset($_POST['submit'])) { if (file_exists($uploaddir)) { if (move_uploaded_file($_FILES['upfile']['tmp_name'], $uploaddir . '/' . $_FILES['upfile']['name'])) { echo '文件上傳成功,保存於:' . $uploaddir . $_FILES['upfile']['name'] . "\n"; } } else { exit($uploaddir . '文件夾不存在,請手工創建!'); } //print_r($_FILES); } ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html;charset=gbk"/> <meta http-equiv="content-language" content="zh-CN"/> <title>文件上傳漏洞演示腳本--JS驗證實例</title> <script type="text/javascript"> function checkFile() { var file = document.getElementsByName('upfile')[0].value; if (file == null || file == "") { alert("你還沒有選擇任何文件,不能上傳!"); return false; } //定義允許上傳的文件類型 var allow_ext = ".jpg|.jpeg|.png|.gif|.bmp|"; //提取上傳文件的類型 var ext_name = file.substring(file.lastIndexOf(".")); //alert(ext_name); //alert(ext_name + "|"); //判斷上傳文件類型是否允許上傳 if (allow_ext.indexOf(ext_name + "|") == -1) { var errMsg = "該文件不允許上傳,請上傳" + allow_ext + "類型的文件,當前文件類型為:" + ext_name; alert(errMsg); return false; } } </script> <body> <h3>文件上傳漏洞演示腳本--JS驗證實例</h3> <form action="" method="post" enctype="multipart/form-data" name="upload" onsubmit="return checkFile()"> <input type="hidden" name="MAX_FILE_SIZE" value="204800"/> 請選擇要上傳的文件:<input type="file" name="upfile"/> <input type="submit" name="submit" value="上傳"/> </form> </body