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}