from:http://www.skeletonscribe.net/2013/05/practical-http-host-header-attacks.html
一般通用web程序是如果想知道網站域名不是一件簡單的事情,如果用一個固定的URI來作為域名會有各種麻煩。開發人員一般是依賴HTTP Host header(比如在php里是_SERVER["HTTP_HOST"]
),而這個header很多情況下是靠不住的。而很多應用是直接把這個值不做html編碼便輸出到了頁面中,比如:
<link href="http://_SERVER['HOST']" (Joomla)
還有的地方還包含有secret key和token,
<a href="http://_SERVER['HOST']?token=topsecret"> (Django, Gallery, others)
這樣處理問題一般會很容易遭遇到兩種常見的攻擊:緩存污染和密碼重置。緩存污染是指攻擊者通過控制一個緩存系統來將一個惡意站點的頁面返回給用戶。密碼重置這種攻擊主要是因為發送給用戶的內容是可以污染的,也就是說可以間接的劫持郵件發送內容。
拿 Gallery 這個站來做例子。當我們進行密碼重置的時候,網站會給我們發送一個隨機的key:
#!php
$user -> hash = random::hash() ;
$message -> confirm_url = url::abs_site("password/do_reset?key=$user->hash") ;
當用戶點擊重置密碼的鏈接時,肯定可以說明點的是自己的賬戶。
這個地方的漏洞是: url::abs_site
這一部分使用的Host header是來自用戶重置密碼的請求,那么攻擊者可以通過一個受他控制的鏈接來污染密碼重置的郵件。
> POST /password/reset HTTP/1.1
> Host: evil.com
> ...
> csrf=1e8d5c9bceb16667b1b330cc5fd48663&name=admin
這個漏洞在Django,Piwik 和Joomla中都存在,還有一些其他的應用,框架和類庫。
當然這種攻擊方式一定要能騙取用戶點擊訪問這個受污染的鏈接,如果用戶警覺了沒有點擊,那么攻擊就會失敗。當然你自己也可以配合一些社會工程學的方法來保證攻擊的成功率。
還有一些情況,Host可能會被url編碼后直接放到email的header里面造成header注入。通過這個,攻擊者可以很容易的就能劫持用戶的賬戶。
通過Host header來污染緩存的攻擊方法最初是Carlos Beuno 在2008年提出來的。但是在現在的網絡架構中,這種攻擊還是比較困難的,因為現在的緩存設備都能夠識別Host。比如對于下面的這兩種情況他們絕對不會弄混淆:
> GET /index.html HTTP/1.1 > GET /index.html HTTP/1.1
> Host: example.com > Host: evil.com
因此為了能使緩存能將污染后的response返回給用戶,我們還必須讓緩存服務器看到的host header 和應用看到的host header 不一樣。比如說對于Varnish(一個很有名的緩存服務軟件),可以使用一個復制的Host header。Varnish是通過最先到達的請求的host header來辨別host的,而Apache則是看所有請求的host,Nginx則只是看最后一個請求的host。這就意味著你可以通過下面這個請求來欺騙Varnish達到污染的目的:
> GET / HTTP/1.1
> Host: example.com
> Host: evil.com
應用本身的緩存也可能受到污染。比如Joomla就將取得的host值不經html編碼便寫進任意頁面,而它的緩存則對這些沒有任何處理。比如可以通過下面的請求來寫入一個存儲型的xss:
curl -H "Host: cow\"onerror='alert(1)'rel='stylesheet'" http://example.com/ | fgrep cow\"
實際上的請求是這樣的:
> GET / HTTP/1.1
> Host: cow"onerror='alert(1)'rel='stylesheet'
響應其實已經受到污染:
<link href="http://cow"onerror='alert(1)'rel='stylesheet'/" rel="canonical"/>
這時只需要瀏覽首頁看是否有彈窗就知道緩存是否已經被污染了。
在這里我假設你可以通過任何類型的應用來發起一個http請求,而host header也是可以任意編輯的。雖然在一個http請求里,host header是用來告訴webserver該請求應該轉發給哪個站點,但是事實上,這個header的作用或者說風險并不止如此。
比如如果Apache接收到一個帶有非法host header的請求,它會將此請求轉發給在 httpd.conf 里定義的第一個虛擬主機。因此,Apache很有可能將帶有任意host header的請求轉發給應用。而Django已經意識到了這個缺陷,所以它建議用戶另外建立一個默認的虛擬主機,用來接受這些帶有非法host header的請求,以保證Django自己的應用不接受到這些請求。
不過可以通過X-Forwarded-Host 這個header就可以繞過。Django非常清楚緩存污染的風險,并且在2011年的9月份就通過默認禁用X-Forwarded-Host這個header來修復此問題。Mozilla卻在addons.mozilla.org站點忽視了此問題,我在2012年的4月發現了此問題:
> POST /en-US/firefox/user/pwreset HTTP/1.1
> Host: addons.mozilla.org
> X-Forwarded-Host: evil.com
即使Django給出了補丁,但是依然存在風險。Webserver允許在host header里面指定端口,但是它并不能通過端口來識別請求是對應的哪個虛擬主機。可以通過下面的方法來繞過:
> POST /en-US/firefox/user/pwreset HTTP/1.1
> Host: addons.mozilla.org:@passwordreset.net
這直接會導致生成一個密碼重置鏈接: https://addons.mozilla.org:@passwordreset.net/users/pwreset/3f6hp/3ab-9ae3db614fc0d0d036d4
當用戶點擊這個鏈接的時候就會發現,其實這個key已經被發送到passwordreset.net這個站點了。在我報告了此問題后,Django又推出了一個補丁:https://www.djangoproject.com/weblog/2012/oct/17/security/
不幸的是,這個補丁只是簡單的通過黑名單方式來簡單的過濾[email protected]??文本而不是html的方式發送的,所以此補丁只需要添加一個空格就可以繞過:
> POST /en-US/firefox/users/pwreset HTTP/1.1
> Host: addons.mozilla.org: www.securepasswordreset.com
Django的后續補丁規定了host header的端口部分只能是含有數字,以規避此問題。但是在RFC2616文檔中規定了,如果請求URI是一個絕對的URI,那么host是Request-URI的一部分。在請求中的任何Host header值必須被忽略。
也就是說,在Apache和Nginx(只要是遵守此文檔的webserver)中,可以通過絕對uri向任意應用發送一個包含有任意host header的請求:
> POST https://addons.mozilla.org/en-US/firefox/users/pwreset HTTP/1.1
> Host: evil.com
這個請求在SERVER_NAME里面的值是addons.mozilla.org,而不是host里的evil.com。應用可以通過使用SERVER_NAME而不是host header來規避此風險,但是如果沒有配合特殊配置的webserver,這個風險依然存在。可以在這里http://stackoverflow.com/questions/2297403/http-host-vs-server-name/2297421#2297421看看 HTTP_HOST 和SERVER_NAME 的區別。Django官方在2013年的二月通過強制使用一個host白名單來修復了此問題。盡管如此,在很多其他的wen應用上,這種攻擊方式依然屢試不爽。
由于http請求的特點,host header的值其實是不可信的。唯一可信的只有SERVER_NAME,這個在Apache和Nginx里可以通過設置一個虛擬機來記錄所有的非法host header。在Nginx里還可以通過指定一個SERVER_NAME名單,Apache也可以通過指定一個SERVER_NAME名單并開啟UseCanonicalName選項。建議兩種方法同時使用。
Varnish很快會發布一個補丁。在官方補丁出來前,可以通過在配置文件里加入:
import std;
sub vcl_recv {
std.collect(req.http.host);
}
來防護。
解決這個問題其實是很困難的,因為沒有完全自動化的方法來幫助站長識別哪些host 的值是值得信任的。雖然做起來有點麻煩,但是最安全的做法是:效仿Django的方法,在網站安裝和初始化的時候,要求管理員提供一個可信任的域名白名單。如果這個實現起來比較困難,那至少也要保證使用SERVER_NAME而不是host header,并且鼓勵用戶使用安全配置做的比較好的站點。