Over the weekend, I had the opportunity to play in Metasploit CTF 2020 with some friends in the team PrettyBeefy. We managed to come 2nd overall, completing all the challenges - better than I think any of us were expecting, which is always nice.

Below will be a brief writeup on each challenge that I took part in solving. The difficulty of the CTF felt perfect for my skill level, so I hope my walkthroughs can do the challenges some justice!

For any challenges not covered here, have a look over at my teammate stealthcopter's blog to see if he's written them up.

The members of our team - thanks to everyone who took part!

9 of Hearts - Port 53

Performing a UDP Nmap scan with sudo nmap -sU -v --top-ports 20 shows us port 53 (DNS) is open on the target.

Using dig, we can run a reverse DNS lookup on the target with dig -x @ This gives us the hostname 9ofhearts.ctf.

From here, dig can again be used to enumerate the DNS entry. Looking at the TXT entry with dig TXT @ 9ofhearts.ctf returns a big base64 string.

Looking at the string, we can see it starts with iVBOR - this is usually the case with PNG files encoded with base64. We can then take the string, decode it, and save the output in to a file, giving us the 9 of Hearts card.

9 of Hearts

8 of Spades - Port 1080

Scanning port 1080 with Nmap using nmap -sV -p 1080 -vv shows us that there is a socks5 proxy being hosted on the machine.

To connect to this, we can use the free tool Proxychains. We first need to add the proxy to the Proxychains conf file with:

echo "socks5 1080" >> proxychains.conf

After this, we can use nmap through the proxy with proxychains nmap -sV.

This shows that there's a web server running on port 8000. Curling this with proxychains curl gives us the following response:

We can then grab the file from the server using proxychains wget, giving us the 8 of spades card.

8 of Spades

8 of Hearts - Port 4545

Accessing this port through a web browser allows us to download two files, 8_of_hearts.elf and 8_of_hearts.enc:

As 8_of_hearts.enc looks like encrypted data, we open up 8_of_hearts.elf in Ghidra to have a look.

Reading through the code, there are some checks looking for "buffalo" in to stdin. Looking past the checks however, we can see how the file is decrypted if the correct input is given - after renaming some variables, it becomes obvious:

The program simply XORs each byte with 0x41, making it easily reversible. A simple python script should do the trick:

with open("8_of_hearts.enc", "rb") as input:
    with open("8_of_hearts.png","ab") as output:
        while 1:
            byte_s = input.read(1)
            if not byte_s:
            output.write(bytes([byte_s[0] ^ 0x41]))

After running the script, we get the 8 of Hearts card.

8 of Hearts

3 of Spades - Port 8080

Accessing port 8080 through a web browser gives us a login page, hinting us that "guest" is a valid username, and that we need to find the other valid username.

Logging in with "guest" and any password, we see that there's a 5 second load time when accessing the next page:

While for other usernames, the load time is normal:

Knowing this, a python script and username wordlist can be used to bruteforce the correct username.

import requests

def make_req(user):
        data = {
                "username" : user,
                "password" : "test"
        headers = {
                "Content-Type": "application/x-www-form-urlencoded"
        r = requests.post("", data=data)
        time = r.elapsed.total_seconds()
        if time > 3:
                print("Username: {} | Response time: {}".format(user,time))

with open("/opt/SecLists/Usernames/xato-net-10-million-usernames.txt") as f:
        for line in f:
                line = line.strip()

Running this for a while gives us the username "demo", and after entering it on the home page we get a success message.

Following this link gives us the 3 of Spades card.

3 of Spades

4 of Clubs - Port 8092

Accessing port 8092 from a web browser returns a page with a login form and some source code:

Reading the source, it looks like it wants us to generate a valid comparison between a given password's hash and a hash we provide. The issue is that the hash is salted, and we don't know what the salt is, making it almost impossible for us to do this legitimately.

One vulnerability can be spotted in the source code straight away: a loose comparison operator (==) is used to compare the generated hash with the user's hash. PHP allows for type juggling, making the comparison insecure - meaning we can potentially have two slightly different inputs pass the check, as seen in the table below:

One thing we know we can provide is an empty string. Looking at the table, we can see this will pass the comparison when compared with FALSE, 0, NULL, and another empty string.

Another vulnerability lies within unsanitised input being put through the password_hash function - if anything that can't be cast in to a string (such as an array or object) is passed in to the function, it will throw a warning and return NULL. This would allow us to pass the check, as seen in the test script below:

Now we can make a request and intercept it with burp. We first change the password=test param to password[]=test, converting it in to an array. Then, we change hash=test to hash=, sending an empty string. We can then forward the request as normal.

Visiting this page gives us the 4 of Clubs.

4 of Clubs

9 of Diamonds - Port 8201

Navigating to the web page gives a "site not found" error. Looking at the error message, we can see it's trying to redirect to the subdomain intranet.metasploit.ctf.

Adding this subdomain to our /etc/hosts file and trying to access the site again, we get a webpage hinting that there are other subdomains available.

A tool like ffuf can be used to try and fuzz the subdomains. We can use the following command to do so:

ffuf -w directory-list-2.3-medium.txt -u -H "Host: FUZZ.intranet.metasploit.ctf" -fs 145

This returns a list of subdomains:

"hidden" sticks out here as the response size is different. Adding it to our /etc/hosts and nagivating to it gives us a page with the 9 of Diamonds card.

9 of Diamonds

Queen of Spades - Port 8202

Accessing port 8202 in a web browser returns a login page:

Looking at the login data sent, we can see that it is using graphQL queries to communicate with the database.

Using the graphQL client "altair", we can dump the database schema and look for anything interesting.

Finding a "users" list holding the "User" type data, we can check and see if there are passwords stored. Unfortunately, there are only fields for id, username, createdAt, and updatedAt:

Checking the "posts" definition, we can see there are some potentially interesting fields such as content and media. We can then grab the data from the first post:

Visiting the URL in the media field gives us the Queen of Spades card.

Queen of Spades

2 of Hearts - Port 9000

Opening the webpage on port 9000 shows a "PC Game Library" search box, allowing us to enter search queries and get results back.

After some initial testing, we can make a few assumptions:

  • The ./Games entry is odd, it may suggest that we're working with the file system directly
  • Entering a $ returns all results, suggesting we may be doing something with regex

Playing with the search request in burp, we find that we can cause a crash by altering the search parameter from a string to an array.

With access to the sinatra error console, we can look a bit in to the source code. In particular, this block inside app.rb is interesting:

It looks like it's using backticks to execute a shell command without escaping our input. This means we should be able to alter the shell command to gain command execution. Trying out the input ";ls -lahR;" shows that we are able to do so:

We can then base64 encode the 2_of_hearts.png file in the hidden directory to retrieve it from the server.

Decoding this locally gives us the 2 of Hearts card.

2 of Hearts

2 of Spades - Port 9001

Similarly to port 9000, this page contains a search box allowing the user to search through game reviews.

Entering a quote mark in to the search box causes a sinatra exception to be thrown:

We can see from this that the DB software being used is sqlite3, and we can also see the query being executed. Our input is being passed unfiltered in to the query, allowing for SQL injection.

This query should be vulnerable to a UNION attack, leaking data from the database. The first step with this is finding out how many columns we need to fill. This is mostly trial and error, and inputting this query gives us a valid result:

' UNION SELECT 1,2,3-- -

From this, we can also see that only rows 2 and 3 are displayed. Table names can be extracted with the query:

' UNION SELECT 1,tbl_name,3 FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%'-- -

The "hidden" table looks interesting - let's grab its columns next with this query:

'UNION SELECT 1,tbl_name,sql FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%'-- -

We can then grab the "flag" and "link" values from the table using this query:

' UNION SELECT 1,flag,link FROM hidden-- -

Visiting the page in the "link" column gives us the 2 of Spades card.

2 of Spades