構造 HTTP請求 Header 實現「僞造來源 IP 」 php
在閱讀本文前,你們要有一個概念,在實現正常的TCP/IP 雙方通訊狀況下,是沒法僞造來源 IP 的,也就是說,在 TCP/IP 協議中,能夠僞造數據包來源 IP ,但這會讓發送出去的數據包有去無回,沒法實現正常的通訊。這就像咱們給對方寫信時,若是寫出錯誤的發信人地址,而收信人按信封上的發信人地址回信時,原發信人是沒法收到回信的。 前端
一些DDoS 攻擊,如 SYN flood, 就是利用了 TCP/ip 的此缺陷而實現攻擊的。《計算機網絡》教材一書上,對這種行爲定義爲「發射出去就無論」。 nginx
所以,本文標題中的僞造來源IP 是帶引號的。並不是是全部 HTTP 應用程序中存在此漏洞。 web
那麼在HTTP 中, " 僞造來源 IP", 又是如何形成的?如何防護之? 後端
在理解這個原理以前,讀者有必要對HTTP 協議有所瞭解。 HTTP 是一個應用層協議,基於請求 / 響應模型。客戶端(每每是瀏覽器)請求與服務器端響應一一對應。 數組
請求信息由請求頭和請求正文構成(在GET 請求時,可視請求正文爲空)。請求頭相似咱們寫信時信封上的基本信息,對於描述本次請求的一些雙方約定。而請求正文就相似於信件的正文。服務器的響應格式,也是相似的,由響應頭信息和響應正文構成。 瀏覽器
爲了解這個原理,可以使用Firefox Firebug, 或 IE 瀏覽器插件 HTTPwatch 來跟蹤 HTTP 請求 / 響應數據。 服務器
本文中,以HTTPwatch 爲例說明之。安裝 httpwatch 並重啓 IE 瀏覽器後, IE 的工具欄上出現其圖標,點擊並運行 Httpwatch, 就會在瀏覽器下方顯示出 HTTPWatch 的主界面。 網絡
點擊左下角紅色的「Record 」按鈕,並在地址欄輸入 www.baidu.com, 等頁面打開後,選中一個請求,並在下方的 tab 按鈕中選擇「 Stream 」,如圖: app
左邊便是請求數據,右邊便是服務器響應數據。左邊的請求頭均以回車換行結束,即「\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 做限制的網站,必定要當心。