構造HTTP請求Header實現“偽造來源IP”
構造 HTTP請求 Header 實現“偽造來源 IP ”
在閱讀本文前,大家要有一個概念,在實現正常的TCP/IP 雙方通訊情況下,是無法偽造來源 IP 的,也就是說,在 TCP/IP 協議中,可以偽造資料包來源 IP ,但這會讓傳送出去的資料包有去無回,無法實現正常的通訊。這就像我們給對方寫信時,如果寫出錯誤的發信人地址,而收信人按信封上的發信人地址回信時,原發信人是無法收到回信的。
一些DDoS 攻擊,如 SYN flood, 就是利用了 TCP/ip 的此缺陷而實現攻擊的。《計算機網路》教材一書上,對這種行為定義為“發射出去就不管”。
因此,本文標題中的偽造來源IP 是帶引號的。並非是所有 HTTP 應用程式中存在此漏洞。
那麼在HTTP 中, " 偽造來源 IP", 又是如何造成的?如何防禦之?
在理解這個原理之前,讀者有必要對HTTP 協議有所瞭解。 HTTP 是一個應用層協議,基於請求 / 響應模型。客戶端(往往是瀏覽器)請求與伺服器端響應一一對應。
請求資訊由請求頭和請求正文構成(在GET 請求時,可視請求正文為空)。請求頭類似我們寫信時信封上的基本資訊,對於描述本次請求的一些雙方約定。而請求正文就類似於信件的正文。伺服器的響應格式,也是類似的,由響應頭資訊和響應正文構成。
為了解這個原理,可使用Firefox Firebug, 或 IE 瀏覽器外掛 HTTPwatch 來跟蹤 HTTP 請求 / 響應資料。
本文中,以HTTPwatch 為例說明之。安裝 httpwatch 並重啟 IE 瀏覽器後, IE 的工具欄上出現其圖示,點選並執行 Httpwatch, 就會在瀏覽器下方顯示出 HTTPWatch 的主介面。
點選左下角紅色的“Record ”按鈕,並在位址列輸入 www.baidu.com, 等頁面開啟後,選中一個請求,並在下方的 tab 按鈕中選擇“ Stream ”,如圖:
左邊即是請求資料,右邊即是伺服器響應資料。左邊的請求頭均以回車換行結束,即“\r\n ” , 最後是一個空行(內容為 \r\n ) , 表示請求 header 結束。而請求 header 中除第一行外,其它行均由 header 名稱, header 值組成,如 Accept-Encoding: gzip, deflate , header 名稱與值之間有冒號相隔,之間的空格是可有可無的。
那麼,在HTTP 應用程式中,如何取得指定的請求 header 資訊呢?這裡使用 PHP 語言為例說明。對所有客戶端請求 header, PHP 程式中取得其值的方式如下:
$_SERVER['HTTP_ HEADER_NAME ']
HEADER_NAME應該以換成對應的 header 名稱,此項的規律是:全大寫,連線線變成下劃線。比如要取得客戶端的 User-Agent 請求頭,則使用 $_SERVER['HTTP_USER_AGENT'], 掌握這個規律,即可達到舉一反三的效果。如要取得 COOKIE 資訊,則使用 $_SERVER['HTTP_COOKIE'] 即可。也就是說, $_SERVER 陣列中,以HTTP 開頭的項均屬於客戶端發出的資訊。
迴歸到HTTP 應用程式層,來源 IP 的重要性不言而語,例如表單提交限制,頻率等等均需要客戶端 IP 資訊。使用流行的 Discuz X2.5 的檔案 source/class/discuz/discuz_application.php 中的程式碼片斷:
private function _get_client_ip() {
$ip = $_SERVER['REMOTE_ADDR'];
if (isset($_SERVER['HTTP_CLIENT_IP']) && preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
如以下的JSP程式碼片段:
public String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
以上程式碼片段即是獲取客戶端IP ,這段程式會嘗試檢查 HTTP_CLIENT_IP, HTTP_X_FORWARDED_FOR, 根據之前的原理說明,以 HTTP_ 開頭的 header, 均屬於客戶端傳送的內容。那麼,如果客戶端偽造 Client-Ip, X-Forward-For ,不就可以欺騙此程式,達到“偽造 IP ”之目的?
那麼如何偽造這項值?如果你會寫程式,並瞭解HTTP 協議,直接偽造請求 header 即可。或者使用 Firefox 的Moify Headers 外掛即可。
按圖示順序號輸入或點選相應按鈕。Start 按鈕這裡變為紅色 Stop ,說明設定成功。
這時,如果我們使用Firefox 訪問其它網站,網站伺服器就針接收到我們偽造的 X-Forward-For, 值為 1.1.1.1 。
嚴格意義上講,這並不是程式中的漏洞。Discuz 為了保持較好的環境相容性 ( 包含有反向代理的 web 伺服器環境,如 nginx 作為 php fastcgi 的前端代理 ) ,如此處理是可以理解的。那麼如何處理,才能杜絕這個問題呢?
伺服器重新配置X-Forward-For 為正確的值。
如對典型的nginx + php fastcgi 環境( nginx 與 php fastcgi 是否位於同一機器,並不妨礙此問題的產生) , nginx和 php fastcig 程序直接通訊:
切記,$_SERVER['REMOTE_ADDR'] 是由 nginx 傳遞給 php 的引數,就代表了與當前 nginx 直接通訊的客戶端的 IP (是不能偽造的)。
再比如,存在中間層代理伺服器的環境:
這種情況下,後端的HTTP 檔案伺服器上獲取取的 REMOTE_ADDR 永遠是前端的 squid/varnish cache 伺服器的通訊 IP 。
伺服器叢集之間的通訊,是可以信任的。我們要做的就是在離使用者最近的前端代理上,強制設定X-Forward-For的值,後端所有機器不作任何設定,直接信任並使用前端機器傳遞過來的 X-Forward-For 值即可。
即在最前端的Nginx 上設定:
location ~ ^/static {
proxy_pass ....;
proxy_set_header X-Forward-For $remote_addr ;
}
如果最前端(與使用者直接通訊)代理伺服器是與php fastcgi 直接通訊,則需要在其上設定:
location ~ "\.+\.php$" {
fastcgi_pass localhost:9000;
fastcgi_param HTTP_X_FORWARD_FOR $remote_addr;
}
記住,$remote_addr 是 nginx 的內建變數,代表了客戶端真實(網路傳輸層) IP 。通過此項措施,強行將 X-Forward-For 設定為客戶端 ip, 使客戶端無法通過本文所述方式“偽造 IP ”。
LVS轉發環境下,是否存在此問題?
LVS工作在網路層,不改變來源及目標 IP ,更不可能更改應用層資訊,故不存在此問題。如果有任何疑惑或需要幫助,請聯絡筆者信箱 [email protected]。
存在此問題的程式:
所有版本的discuz, phpcms, phpwind, dedeCMS 。以及其它可能未知的程式。
例如使用Modify Headers 進行 IP 偽裝之後再登入 bbs.phpchina.com ,我們檢視自己的個人資料中的“上次訪問IP ”就發現就是我們偽裝的資料。
可以說,網際網路上存在此漏洞的網站實在是太多了。試試便知。那麼對於存在此漏洞,並且使用IP 作限制的網站,一定要小心。