PHP編碼安全:請求偽造攻擊
如果Web系統中存在服務器請求偽造漏洞,不僅會影響系統本身,而且會影響到與其相關的其他系統服務。一個被忽視的服務器請求偽造漏洞,很容易引起蝴蝶效應,可能給整個系統帶來長期的巨大危害。
1、服務器請求偽造
服務器請求偽造(Server-Side Request Forgery,SSRF)漏洞是一種由攻擊者利用某服務器請求來獲取內網或外網系統資源,從服務器發起請求的一個安全漏洞。正因為它是由服務器發起的,所以它能夠請求到與它相連而與外網隔離的內部系統。一般情況下,SSRF攻擊的目標是企業的內網系統。
SSRF可以對外網、服務器所在內網、本地進行端口掃描,獲取一些服務的banner信息。攻擊運行于內網或本地的應用程序(比如攻擊內部數據庫系統),并通過掃描默認Web文件對內網Web應用進行指紋識別,同時可以利用file協議讀取本地文件。
SSRF形成的原因大多是由于服務器提供了從其他服務器應用中獲取數據的功能且沒有對目標地址進行過濾與限制,比如從指定URL地址中獲取網頁文本內容、加載指定地址的圖片、下載等。
SSRF漏洞流程如圖1所示,即:

圖1 SSRF攻擊流程
1)攻擊者構造請求;
2)服務器根據攻擊者構造的請求對內網服務器進行請求;
3)內網服務器將請求反饋給服務器;
4)服務器將獲取到的內網資源返回給攻擊者。
2、SSRF漏洞的危害
SSRF漏洞的主要危害是使服務器資源泄露,內網服務任意掃描泄露內網信息。很多網站提供了通過用戶指定的URL上傳圖片和文件的功能,可以將第三方的圖片和文件直接保存在當前的Web系統上,如果用戶輸入的URL是無效的,大部分的Web應用會返回錯誤信息。
攻擊者可以輸入一些不常見但有效的URL,比如以下URL。
http://127.0.0.1:8080/dir/images/
http://127.0.0.1:22/dir/public/image.jpg
http://127.0.0.1:3306/dir/images/
然后根據服務器的返回信息來判斷端口是否開放。大部分應用并不會去判斷端口,只要是有效的URL,就會發出請求。而大部分的TCP服務,在建立socket連接時就會發送banner信息。banner信息是使用ASCII編碼的,能夠作為原始的HTML數據展示。當然,服務端在處理返回信息的時候一般不會直接展示,但是不同的錯誤碼,返回信息的長度以及返回時間都可以作為依據來判斷遠程服務器的端口狀態。
以下是一段未經過安全編碼的代碼,有很大概率被攻擊者利用進行端口掃描。
if(isset($_GET['url']))
{
$url=$_GET['url'];
$filename='/tmp/'.rand().'txt';
$ch=curl_init();
$timeout=5;
curl_setopt($ch,CURLOPT_URL,$url);
curl_setopt($ch,CURLOPT_FOLLOWLOCATION,1);
curl_setopt($ch,CURLOPT_RETURNTRANSFER,1);
curl_setopt($ch,CURLOPT_CONNECTTIMEOUT,$timeout);
$content=curl_exec($ch);
$fp_in=fopen($filename,'w');
fwrite($fp_in,$content);
fclose($fp_in);
$fp_out=fopen($filename,"r");
$result=fread($fp_out,filesize($filename));
fclose($fp_out);
echo $result;
} else {
echo "請輸入資源地址";
}
由于上面不安全編碼的代碼,容易被攻擊者利用對內網Web應用進行指紋識別,識別內網應用所使用的框架、平臺、模塊以及cms,這實際上為潛在的攻擊提供了很多“便利”。大多數Web應用框架有一些獨特的文件和目錄,通過這些文件可以識別出應用的類型,甚至是詳細的版本。根據這些信息就可以有針對性地搜集漏洞進行攻擊。比如可以通過訪問下列文件來判斷phpMyAdmin是否安裝。
http://127.0.0.1:8080/phpMyAdmin/themes/original/img/b_tblimport.png
http://127.0.0.1:8081/wp-content/themes/default/images/audio.jpg
http://127.0.0.1:8082/profiles/minimal/translations/README.txt
3、在PHP中容易引起SSRF的函數
容易出現SSRF漏洞的功能代碼通常是為了獲取遠程或本地內容,例如使用file_get_contents()、fsockopen()、curl()等函數。
如使用file_get_contents()函數從用戶指定的URL中獲取文件,然后把它輸出到瀏覽器端用于下載。
$content=base64_decode(file_get_contents($_GET['url']));
echo $content;
正常情況下,用戶的下載地址是通過服務端提供的base64編碼后的路徑回傳到服務端進行下載操作的,不會造成SSRF漏洞,但是攻擊者很容易識別base64的內容,如果攻擊者將下載地址替換成自己偽造的編碼內容,則可以下載服務器上的任意文件,以及對內網地址進行掃描。如將/etc/password進行base64編碼之后放在URL參數中,會直接將/etc/password的內容展示在頁面上,使得攻擊者能直接獲取服務器的所有用戶列表,從而危害服務器的安全。
使用fsockopen()函數實現獲取用戶指定URL中的數據。這個函數會使用socket與服務器建立tcp連接,傳輸原始數據。在下面的示例代碼中,由于研發人員的疏忽,用戶一旦傳入“主機名+端口+地址”之后,攻擊者可以對內網服務器進行任意掃描。
function GetFile($host,$port,$link)
{
$fp = fsockopen($host, intval($port), $errno, $errstr, 30);
if (!$fp) {
echo "$errstr (error number $errno) ";
} else {
$out = "GET $link HTTP/1.1\r";
$out .= "Host: $host\r";
$out .= "Connection: Close\r\r"; $out .= "\r";
fwrite($fp, $out);
$contents='';
while (!feof($fp)) {
$contents.= fgets($fp, 1024);
}
fclose($fp);
return $contents;
}
}
echo GetFile($_GET['host'],$_GET['port'],$_GET['link']);
?>
curl函數使用不當獲取數據也會造成SSRF漏洞。
if (isset($_POST['url']))
{
$link = $_POST['url'];
$curlobj = curl_init();
curl_setopt($curlobj, CURLOPT_POST, 0);
curl_setopt($curlobj,CURLOPT_URL,$link);
curl_setopt($curlobj, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($curlobj);
curl_close($curlobj);
echo $result;
}
?>
4、容易造成SSRF的功能點
從上面的流程可以看出,SSRF都是由于服務端需要獲取其他服務器的相關服務的功能而造成的,因此可以列舉幾種在Web應用中常見的從服務端獲取其他服務器信息的功能。
(1)頁面分享
如圖2所示,某網站的頁面分享。用戶輸入希望分享的URL,該網站會通過自己的后端服務器獲取URL中的內容返回到用戶頁面中。

圖2 頁面分享
通過目標URL地址獲取了網頁標題、圖片和相關文本內容。如果在此功能中沒有對目標地址的范圍進行過濾與限制,則就存在著SSRF漏洞。國內多家知名互聯網企業曾因為分享功能而被曝出SSRF漏洞。
(2)頁面轉碼
移動互聯網剛剛興起的時候,大部分網站不提供專門的移動端頁面,而手機端的移動瀏覽器不能很好地支持PC瀏覽器的頁面展示(包括昂貴的流量費用因素)。
很多搜索公司為了搶占移動端的市場入口,紛紛推出了頁面轉碼服務,通過搜索,將PC端的頁面轉成適應移動端瀏覽器可查看的頁面。一旦控制不當,很容易產生SSRF漏洞。
(3)翻譯服務
為了不懂外語的用戶可以正常瀏覽外語頁面,翻譯服務通過URL地址翻譯對應頁面中文本的內容,以方便用戶閱讀。如果不做好內網穿透限制,則很容易被攻擊者利用。
(4)圖片加載與下載
通過URL地址加載或下載圖片。比如,加載圖片服務器上的圖片展示給用戶,為了有更好的用戶體驗,通常對圖片進行尺寸比例調整、水印添加、圖片格式轉換、壓縮等,就可能造成SSRF漏洞。
(5)圖片、文章收藏功能
早期,有好的個人博客,可以將其他互聯網服務商的內容,例如圖片、文字內容等,通過爬蟲或者單個URL獲取頁面中的內容,并復制到自己的服務器上。通常不會限制訪問路徑,很容易造成SSRF漏洞。
5、SSRF漏洞防御
防止SSRF漏洞的主要方式是合理控制訪問權限,盡可能地控制PHP的訪問權限,防止穿透到內網以及訪問非授權的資源。通常需要做到以下幾點。
1)無特殊需要的情況下,不要從用戶那里接收要訪問的URL,防止用戶自行構建URL地址進行穿透訪問。
2)在沒有對服務器本身文件訪問需求的情況下,建議開啟PHP的open_basedir配置,將PHP的訪問限制在特定目錄下,禁止PHP隨意訪問服務器任意路徑。
在PHP配置中進行如下設置開啟open_basedir。
open_basedir=/home/web/php/
; PHP配置中限定PHP的訪問目錄為/home/web/php/
或在PHP項目中使用ini_set()函數進行開啟。
ini_set('open_basedir','/home/web/php/');
; PHP項目中限定PHP的訪問目錄為/home/web/php/
開啟open_basedir可以有效地防止file_get_content、curl、fopen等函數對服務器敏感文件進行訪問。
3)如果必須接收用戶傳遞的URL,建議使用白名單機制。如下面的代碼示例中,只允許用戶請求特定的網址,限制請求的端口為HTTP常用的端口,比如80、443,同時只允許用戶訪問特定的文件類型,如只允許訪問靜態文件。并且統一系統返回的錯誤信息,避免請求的錯誤信息直接返回給用戶,避免用戶可以根據錯誤信息來判斷遠端服務器的端口狀態。禁用不需要的協議,僅僅允許HTTP和HTTPS請求,可以防止類似于file:///、gopher://、ftp://等引起的安全問題。
$url=$_GET['url'];
$schemeWhiteList=array("http","https");
$hostWhiteList=array("www.ptpress.com.cn");
$portWhiteList=array("80","443");
$typeWhiteList=array("html","gif","png","jpeg");
$urlInfo=parse_url($url);
// 只允許HTTP和HTTPS請求
if(!in_array($urlInfo['scheme'],$schemeWhiteList)) {
die("訪問地址類型錯誤!");
}
// 只允許訪問白名單內的域名
if(!in_array($urlInfo['host'],$hostWhiteList)) {
die("訪問地址錯誤!");
}
// 只允許訪問白名單內的端口
if(!in_array($urlInfo['port'],$portWhiteList)) {
die("訪問地址端口錯誤!");
}
$type=pathinfo($url,PATHINFO_EXTENSION);
// 只允許訪問白名單內的文件類型
if(!in_array($type,$typeWhiteList)) {
die("訪問文件類型錯誤!");
}
$info=file_get_contents($url);
if(empty($info)) {
die("訪問失敗");
} else {
echo $info;
}
?>
4)如果在項目中需要獲取外網資源,建議使用黑名單屏蔽內網,以避免應用被用來獲取內網數據,攻擊內網。
$url=$_GET['url'];
$schemeWhiteList=array("http","https");
$blackHostList=array("172.","10.","localhost","127.","192.");
$blackIpList=array("172.","10.","127.","192.");
$urlInfo=parse_url($url);
// 只允許HTTP和HTTPS請求
if(!in_array($urlInfo['scheme'],$schemeWhiteList)) {
die("訪問地址類型錯誤!");
}
// 拒絕訪問黑名單內的地址
foreach($blackHostList as $blackHost) {
if(strpos($urlInfo['host'],$blackHost)===0) {
die("訪問地址錯誤!");
}
}
$ip=gethostbyname($urlInfo['host']);
// 拒絕訪問黑名單內的IP
foreach($blackIpList as $ipHost) {
if(strpos($ip,$ipHost)===0) {
die("訪問地址錯誤!");
}
}
$info=file_get_contents($url);
if(empty($info)) {
die("訪問失敗");
} else {
echo $info;
}
?>