HTTP/2 request smuggling via CRLF injection

9 min read Insane PortSwigger
HTTP request smuggling
Contents

On this page

The lab

here is how portswigger describes it

This lab is vulnerable to request smuggling because the front-end server downgrades HTTP/2 requests and fails to adequately sanitize incoming headers.

To solve the lab, use an HTTP/2-exclusive request smuggling vector to gain access to another user's account. The victim accesses the home page every 15 seconds.

If you're not familiar with Burp's exclusive features for HTTP/2 testing, please refer to the documentation for details on how to use them.

this follows the h2.cl lab. those http/2 labs smuggled by injecting a length header directly. this one is subtler and is genuinely http/2 only, we hide a whole extra header inside another header's value using the carriage return and line feed characters, and it only becomes a real header after the front end downgrades the request to http/1

The underlying idea ;/#

to see why this works you need one fact about how http/1 and http/2 differ and it is the crux of the whole attack.

in http/1 a request is plain text, and every header sits on its own line. the thing that marks the end of one line and the start of the next is a pair of invisible characters, a carriage return followed by a line feed, written \r\n. so in http/1 those two characters are structural, they are literally how the server knows where one header stops and the next begins.

in http/2 a request is not text, it is a binary format where each header is carried as its own separate name and value field. there are no lines and no \r\n acting as separators, the structure is built into the format. that means a \r\n inside a header value in http/2 is just data, two ordinary characters sitting in a value, with no special meaning at all.

now bring in the downgrade. this front end talks http/2 to us but http/1 to the back end, so it has to translate our http/2 request into http/1 text before forwarding it. to do that it writes each of our headers out as a text line. and here is the flaw, if we put a \r\n inside one of our header values and the front end does not strip it out, then when the front end writes that header as a text line, our \r\n gets written too, and in http/1 text a \r\n ends the line. so our single header value is torn into two header lines, and whatever we placed after the \r\n becomes a brand new header that the back end reads as real.

that is the http/2 exclusive part. in plain http/1 we could never have smuggled this, because the \r\n would have been treated as a separator the moment we typed it. only http/2's binary headers let us carry a \r\n as harmless data past the front end's own parsing, so the front end never sees a second header to sanitise, and it only springs into existence during the downgrade.

what we smuggle in this way is a Transfer-Encoding: chunked header. the front end, parsing http/2, never sees a transfer encoding header at all, it just sees one innocent header with a slightly odd value, so it has nothing to strip. but after the downgrade the back end reads a real Transfer-Encoding: chunked, and from there we have an ordinary te desync to exploit.

Step 1 - Understand the search history#

the lab has a search box, and using it a few times we see it keeps a recent search history.

11

we send the search request to repeater and resend it with our session cookie removed, and the history resets. 22

so the search history is tied to our session cookie. that matters for later, whatever gets searched under our session is stored where we, holding that session, can read it back.

Step 2 - Inject the Transfer encoding header with crlf#

in repeater, using the inspector, we add an arbitrary header to the request. into its value we append a \r\n followed by the Transfer-Encoding: chunked header. burp's http/2 tooling lets us put the raw \r\n into the value, which is the whole point.

33

when the front end downgrades this request, our \r\n splits the value and Transfer-Encoding: chunked becomes a genuine header line handed to the back end.

Step 3 - Confirm the Smuggle with a prefix#

in the body we try to smuggle a prefix.

HTTP
0

SMUGGLED
44

this is a te desync now. because of our injected header the back end honours Transfer-Encoding: chunked, so it reads the body as chunks, meets the 0 chunk, ends the request there, and leaves SMUGGLED as the start of the next request on the connection. 55

we send it a few times and get a 404, because our SMUGGLED prefix is prepended to the next request and makes a broken path. that 404 confirms the back end is appending later requests to our smuggled prefix, so the desync is working.

Step 4 - Smuggle search that captures the victim#

now we use the capture trick. we change the body to a smuggled search request that carries our own session cookie and an oversized Content-Length.

HTTP
0

POST / HTTP/1.1
Host: your-lab-id.web-security-academy.net
Cookie: session=your-session-cookie
Content-Length: 800

search=x
66

there are three deliberate pieces here. it is a POST / search request, so whatever ends up as the search term gets recorded in the search history. it carries our own session cookie, so the search is recorded under our history, where we can read it back. and its Content-Length: 800 is far larger than the tiny search=x body, so the back end waits for 800 bytes and fills the rest from the next request that arrives, the victim's, appending the victim's raw request onto our search value.

we send the request, then immediately refresh the home page in the browser. timing matters because the victim visits every 15 seconds, so we may need a couple of tries.

if we refresh and get a 404, we just refresh again. when it lands right, the search results page shows our search term x followed by the captured start of the victim's request, reflected because it was appended to search=x. if the full cookie is cut off, we bump the Content-Length up a little, into the 900 to 950 range, so more of the victim's request fits into the reflected value.

77

inside that reflected text is the victim's GET request with their Cookie header and session value. we copy their session cookie, set it as our own, and refresh.

88

with this, we solved the lab!

how the whole attack worked exactly?#

let me retrace the entire path in plain terms. the front end and back end share connections and pass many requests over them, and they must agree on where each request ends. everything hinges on making them disagree, and the clever bit is how we planted that disagreement. we could not just add a Transfer-Encoding header the normal way, because a careful front end would notice it and strip it during the downgrade. so we smuggled it inside another header's value using a \r\n. in http/2 that \r\n is only data, invisible as structure, so the front end's http/2 parser saw a single harmless header and had nothing to sanitise. but when the front end rewrote our request into http/1 text for the back end, the \r\n did what it always does in text, it ended a line, and our hidden Transfer-Encoding: chunked popped out as a real header. the front end had already decided the request was fine, so it never rechecked, and the back end received a header the front end never knowingly forwarded.

from there it was a transfer encoding desync. the front end measured our request by its true http/2 length, the back end measured it by the smuggled chunked encoding, and the two disagreed on where our request ended. that let us leave a smuggled prefix sitting on the connection for the next request to attach to.

the theft itself was quiet and clever. our smuggled prefix was a search request under our own session with a deliberately oversized Content-Length. when the victim's browser made its next request on that connection, the back end was still waiting to fill our search body, so it swallowed the victim's request, cookie and all, as the value of our search. because the search ran under our session, it was saved to our search history, and we simply opened our own history and read the victim's session cookie out of it. presenting that cookie as our own, the application could not tell us from the victim, and we were in their account.