在閱讀本文前,大家要有一個概念,在實現正常的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 ,更不可能更改應用層信息,故不存在此問題。如果有任何疑惑或需要幫助,請聯系筆者信箱 zhangxugg@163.com。
存在此問題的程序:
所有版本的discuz, phpcms, phpwind, dedeCMS 。以及其它可能未知的程序。
例如使用Modify Headers 進行 IP 偽裝之后再登錄 bbs.phpchina.com ,我們查看自己的個人資料中的“上次訪問IP ”就發現就是我們偽裝的數據。
可以說,互聯網上存在此漏洞的網站實在是太多了。試試便知。那么對於存在此漏洞,並且使用IP 作限制的網站,一定要小心。