Post

TryHackMe: El Bandito

Tryhackme Room Link

El Bandito was a room dedicated to request smuggling, where we used two different methods of request smuggling to capture two flags.
First, we abused a SSRF vulnerability to trick a NGINX frontend reverse proxy into believing we established a websocket connection to smuggle requests to endpoints restricted by the proxy and capture the first flag along with a set of credentials.
Second, we will use another method of request smuggling along with found credentials to capture another user’s request and get the flag from the user’s cookies.

Initial Enumeration

Nmap Scan

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ nmap -T4 -n -sC -sV -Pn -p- 10.10.189.186
Nmap scan report for 10.10.189.186
Host is up (0.079s latency).
Not shown: 65400 closed tcp ports (conn-refused), 131 filtered tcp ports (no-response)
PORT     STATE SERVICE  VERSION
22/tcp   open  ssh      OpenSSH 8.2p1 Ubuntu 4ubuntu0.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 86:0f:76:04:77:0f:a8:24:0f:49:a2:1e:04:41:49:9f (RSA)
|   256 6c:ea:de:0c:e9:fd:96:60:c9:10:4f:45:4a:22:d1:01 (ECDSA)
|_  256 21:21:99:f4:7b:bf:6c:dc:e5:59:b4:e1:5d:78:24:74 (ED25519)
80/tcp   open  ssl/http El Bandito Server
|_http-server-header: El Bandito Server
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=localhost
| Subject Alternative Name: DNS:localhost
| Not valid before: 2021-04-10T06:51:56
|_Not valid after:  2031-04-08T06:51:56
|_http-title: Site doesn't have a title (text/html; charset=utf-8).
...
631/tcp  open  ipp      CUPS 2.4
|_http-server-header: CUPS/2.4 IPP/2.1
|_http-title: Forbidden - CUPS v2.4.7
8080/tcp open  http     nginx
|_http-favicon: Spring Java Framework
|_http-title: Site doesn't have a title (application/json;charset=UTF-8).

There are four ports open:

  • 22/SSH
  • 80/HTTPS
  • 631/HTTP
  • 8080/HTTP

Port 80

Checking the source code for https://10.10.189.186:80/ we see a script included: /static/messages.js

Web Server View Source

Looking at https://10.10.189.186:80/static/messages.js, we see it makes a request to two endpoints:

  • A get request to /getMessages to receive messages.
1
2
3
4
5
6
7
8
9
10
11
...
// Function to fetch messages from the server
function fetchMessages() {
  fetch("/getMessages")
    .then((response) => {
      if (!response.ok) {
        throw new Error("Failed to fetch messages");
      }
      return response.json();
    }
...
  • A post request to /send_message with the data parameter to send messages.
1
2
3
4
5
6
7
8
9
...
fetch("/send_message", {
  method: "POST",
  headers: {
    "Content-Type": "application/x-www-form-urlencoded",
  },
  body: "data="+messageText
}
...

Visiting /getMessages, we get a login page.

Web Server Login Page

Trying to send a message using /send_message, we also get the same login page.

Web Server Send Message Login Page

Port 8080

Looking at the web server on port 8080, we get a page about Bandit-Coin.

Web Server Index Page

There are two interesting endpoints.

  • /burn.html

Web Server Burn Page

The form does not seem to be doing anything.

  • /services.html

Web Server Services Page

It seems to be printing the status of different web servers.

Checking the source code for the page, we see that it does this by making a request to the /isOnline endpoint with the url parameter.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const serviceURLs = [
  "http://bandito.websocket.thm",
  "http://bandito.public.thm"
];

async function checkServiceStatus() {
  for (let serviceUrl of serviceURLs) {
    try {
       const response = await fetch(`/isOnline?url=${serviceUrl}`, {
        method: 'GET', 
      });

      if (response.ok) {
        let existingContent = document.getElementById("output").innerHTML;
        document.getElementById("output").innerHTML = `${existingContent}<br/>${serviceUrl}: <strong>ONLINE</strong>`;
      } else {
        throw new Error('Service response not OK');
      }
    } catch (error) {
      let existingContent = document.getElementById("output").innerHTML;
      document.getElementById("output").innerHTML = `${existingContent}<br/>${serviceUrl}: <strong>OFFLINE</strong>`;
    }
  }
}

One interesting thing to note here is that from both endoints’ favicon, we can see that the application uses the Spring Java Framework. Nmap also reports this.

We can also see this from the distinct 404 page.

Web Server 404 Page

First Web Flag

Since we know that the application on port 8080 uses Spring Java Framework, we can try to access Spring Actuators like /env or /mappings.

Trying to reach /env, we get the 403 Forbidden message, but this forbidden response comes from the NGINX frontend reverse proxy instead of the backend server.

Web Server 404 Page

While we are not able to access /env, we are able to access /mappings actuator.

Web Server Mappings Actuator

From there, we discover two interesting endpoints.

  • /admin-flag
  • /admin-creds

Unfortunately, when we try to access these endpoints, we get the same forbidden message as before.

So, if we can figure out a way to bypass the proxy, we might be able to access these endpoints.

Testing the /isOnline endpoint we discovered before. We notice an SSRF vulnerability, and the server also returns the status of the response it received from the URL we supplied.

Web Server SSRF for Existent Page Web Server SSRF for Non-Existent Page

1
2
3
4
5
$ python3 -m http.server 80                                                  
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.189.186 - - [23/Mar/2024 13:48:57] "GET /exists HTTP/1.1" 200 -
10.10.189.186 - - [23/Mar/2024 13:50:50] code 404, message File not found
10.10.189.186 - - [23/Mar/2024 13:50:50] "GET /doesnotexist HTTP/1.1" 404 -

Naturally, we are able to control the request, but this SSRF vulnerability also allows us to control the status code of the response. We can use this ability to trick NGINX into believing we established a websocket connection and smuggle requests to endpoints we could not reach before.

This method is detailed here and also covered in another TryHackMe room.

To perform this exploit, we need to add the Upgrade: WebSocket header to our requests to make the proxy think we are performing a Websocket Upgrade. While just this is enough to trick some other proxies, NGINX also validates the response’s status code before establishing a tunnel between the client and the backend server. So, when we send the request with the websocket upgrade header, the response to this request must have a valid status code (101 Switching Protocols), and to make the server return the valid status code, we will use the SSRF vulnerability. After that, NGINX will establish the tunnel and send anything after our request using this tunnel without checking, believing it to be a part of websocket communication. Since we did not perform a valid websocket upgrade, the backend server will interpret the rest of our request as another HTTP request.

So, we need to an HTTP server that will return the 101 Switching Protocols response to our SSRF payload on the /isOnline endpoint.

We can use this Python code for that.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import sys
from http.server import HTTPServer, BaseHTTPRequestHandler

if len(sys.argv) != 2:
    print(f"Usage: {sys.argv[0]} <port>")
    exit()

class Redirect(BaseHTTPRequestHandler):
   def do_GET(self):
       self.protocol_version = "HTTP/1.1"
       self.send_response(101)
       self.end_headers()

HTTPServer(("", int(sys.argv[1])), Redirect).serve_forever()

Now, after running the server and making the request, we see that we are able to smuggle requests.

Do not forget to disable the Update Content-Length option in Repeater while dealing with request smuggling.

Web Server WebSocket Request Smuggling

1
2
$ python3 101_server.py 80
10.10.189.186 - - [23/Mar/2024 15:39:00] "GET / HTTP/1.1" 101 -

Now that we are able to bypass the proxy and smuggle requests to the backend, we can make requests to /admin-creds and /admin-flag endpoints.

From /admin-flag, we get our first flag.

Web Server First Flag

And from /admin-creds, we get a set of credentials.

Web Server Admin Credentials

Second Web Flag

Now that we have a set of credentials, we can login to the web application on port 80.

After logging in, we see a chat and are able to send and receive messages.

Web Server Chat Page

Also from the headers, we notice the server uses a frontend reverse proxy for caching.

Trying different payloads for request smuggling, we have success with using Content-Length: 0.

Web Server Request Smuggling Payload

As we can see from the below response, we were able to cause a desync, and our next request was appended to our smuggled request from before, and it was interpreted like this: 

1
2
3
4
GET /doesnotexist HTTP/1.1
Foo: GET / HTTP/1.1
Host: 10.10.189.186:80
...

Hence, we are getting 404 Not Found for a request to the / endpoint.

Web Server Request Smuggling Payload Response

Since the application allows us to store and retrieve text data via messages, we can use this to capture the requests of other users.

For this, we will smuggle an incomplete request to the /send_message endpoint with an overly long Content-Length header and our cookie, since authorization is needed for sending messages.

With this request, any other request that follows ours will be appended to our smuggled request and will be interpreted as the data parameter in a request to the /send_message endpoint.

Web Server Request Smuggling Capture Payload

After sending the request and receiving the messages after a bit, we see that another user’s request was indeed appended to our payload and stored as a message.

Web Server Request Smuggling Captured Request

We get the flag from the cookies in the user’s request.

This might take a couple of attempts, and also, do not forget to unescape unicode encoding in the flag when submitting it.

This post is licensed under CC BY 4.0 by the author.