2020 Metasploit Community CTF Write-up

Steve Walker
13 min readDec 9, 2020


This past weekend I organized a team of coworkers for a little time… ok a lot of time… team building and fun with technical hacking challenges. I’ve enjoyed past editions and looked forward to this one. They feel more real world than some of the other CTFs I’ve participated in the past.

Metasploit Community CTF

Post-CTF write-ups are great to know what little thing that might have been missed. So here are a few of the ones I was able to solve. Rest assured I’ll be searching for write-ups for those challenges that escaped us too.

Note that these challenges required SSH tunneling into a provisioned Kali system, as a result many of the web requests below reference localhost

The SETUID Ace of Clubs (port 9009)

This challenge started with an unknown login to a server. Login as admin toget started. Turns out, everyone’s favorite admin/password worked. Not tricks, I just guessed. Once logged in, I looked around manually for anything interesting. Later i found some useful CTF and Linux escalation scripts that will help in the future. I soon found that we have an ace_of_clubs.png in the /etc folder, and in interesting vpn_connect executable in the /opt directory.

Our prize awaits with root privileges

The vpn_connect program has rwsr-xr-x permissions, and I skipped over the s at first, but that key bit is the SETUID bit that means the program executes with its owner’s privileges — root. The program accepts 3 arguments: username, password, and log file location. With the SETUID bit, this means it can touch ANY file on the system. What’s more, the created log files therefore write their log at the start of any file you point them at. I started trying to write commands into logs through the username field and executing them like so:

Bash-based redirection
Bash reverse shell — but only as our user. :(

As you can see, this reverse shell is only running as our current user since I had to execute it myself. So any escalation or execution has to be done using vpn_connect. At this point I broke out the local gdb debugger to walk the code, try to find the built-in username/password, and see if there are any buffer overflow vulnerabilities that would let us execute code as root. I spent hours here stepping through execution to pull out username/securePass only to see it doesn’t actually do anything and all string variables are properly handled. If I had only remembered Ghidra’s amazing decompilation capabilities it would have saved me all that time.

Correct User/Pass… overwritten flag. Time to revert!
Ghidra reversed it straight to source code. Beautiful.

With no executable exploitation found it’s back to leveraging the SETUID bit. Lots of searching for ways to use the SETUID rights to escalate. I tried changing my PATH variable to point at /tmp, copying bash to /tmp/umask and hoping the reference would be executed only to realize my PATH variable had to impact on the PATH for root. Eventually I found a reference to overwrite /etc/passwd with new credentials and group membership to give us root privileges.

Overwriting /etc/passwd

And it works!

Confirmed root privileges

SpyHunter — Eight of Diamonds (port 5555)

Switching gears. This one is a type of challenge I’ve always wanted to take the time to solve: game automation. When you connect to the port you’re entered into what reminds me of SpyHunter — a driving game where you have to dodge other cars. If your ^ car is hit the game is over and the server tells you you're not as fast as a computer!

A quick search for tools that would help in building a socket based program and I came across Pwntools.

Rapid prototyping made easy

Using the pwntools python socket library I wrote a quick script to read the gameboard as the server sends it, store the 2 lines above our car, check for a 0 obstacle in front of us, and send a move command based on relative position.

drive.py test run
Ready to Drive

As you see, if the space in the line above the car prev[pos] is a 0 (or code48) it prints move! for effect then decides to move left or right. I found that late in the sequence we may encounter a 00, two adjacent obstacles, so I added additional checks for that too.

I was ready to add some more logic to check lines above the immediate next line, but it wasn’t needed.

It was so rewarding and amazing to watch the self-driving car fly through the late stages of the game to victory.

Watching it move!

Password Functions — 4 of Clubs

This will be a short write-up, but it took me a long time. Everything you need for the challenge is right here:

The Challenge

Take a bit and see if you can spot the way to login…

I’m sure some of you thought of it quickly, but I spent a lot of time digging into php manuals on password_hash and its arguments. Like how PASSWORD_DEFAULT is BCRYPT and if you give it a passsword over 72 characters anything beyond that is truncated. I tried POSTing a hash with wlidcards, guessing password salts like Clover Tail Login Page which is 22 characters, the required length of a salt, etc.

Guessing a hash

So we can’t brute force the salt, and even if we know the password and the algorithm that unknown salt will prevent us from ever guessing the right hash.

I was on the right track when I looked up source-code for password_hash to see if it ever returns False or errors out. In the code there’s no returning False, and only exception clauses. Eventually I found that we could pass password as an array by sending password[]= as a POST parameter instead of password=test.


Game Review — 2 of Spades

This was a straight forward SQL injection challenge with a twist in that the database is SQLLite based vs the more well-known MySQL exploit paths.

At least for this write-up it can be shown through straight screenshots. The infamous ' or 1==1;-- gets us started and just prints out a list of all games. Then trying to enumerate the database table structure through a traditional MySQL approach of ' or SELECT * FROM information_schema.tables;--throws an error. An we can see the database is SQLite3 based, which doesn’t have the same schema tables.

Ah, this is SQLite

Let’s go back to enumerating number of columns and where we can get them to show up in our returned data. ' AND 1=2 UNION SELECT 1,2,3; — It looks like columns 2 and 3 are our ticket.

Enumerate column numbers and output location

Now we can use a SQLite based query, which took some searching and trial and error, to figure out what tables are in this database ' AND 1=2 UNION SELECT 1,group_concat(name),3 from sqlite_master;--

Well hidden stands out for sure! Another call guessing at a field value of id to confirm we can get at the data ' AND 1=2 UNION SELECT 1,group_concat(id),3 from hidden;--

Great! Now let’s grab all the columns from hidden!

Oops, group_concat() didn’t like that. Ok, taking that out of the picture with ' AND 1=2 UNION SELECT * from hidden;-- and we have the entry with the URL for our flag!

Moose Gallery — 6 of Diamonds

File upload bypass and web shell

This one was an interesting exercise in bypassing a somewhat restrictive file upload form.

Don’t mind if I do!

A quick look at the code for the gallery and we find that the gallery.php page links to each of the referenced images, which the client’s browser then loads through individual requests. This is an important point because it means that my early attempts at pushing files that had reverse shells in them only proved to reverse myself.

Of course we immediately find that the file upload is restricted to only a few image formats, and it’s also doing some amount of content validation. Typically you would try

  1. Send a web shell like<?php system( $_GET['cmd']); ?> as content of the uploaded file, and name the file something.jpg.php. This double-extension filename often gets by basic filename filtering as it sees the first ., and ignores the second extension. If successful, and the site isn’t looking at file content it would give us a shell we could do any thing with through requests to the page such as/page.php?cmd=ls ← obviously what we want to happen here! No joy.
  2. The next level is to use a simple GIF image header GIF89a1 in front of your php shell payload, but that didn’t work either.
  3. Exif content fields in jpg images are known to work as well. Let’s go that route.

During attempts 1&2 I found that the upload.php page was definitely vulnerable to cross-site scripting (XSS) by adding a \ character in the filename, and totally jacked up the page a couple times in trial and error hoping for in-page PHP code rendering by the server.

Exif-based shells have been around for awhile. Really interesting reading:


Using the exiftool we add a comment to a random jpg file we are able to upload it, but attempts to reference the shell threw errors on execution. With some troubleshooting I found that by removing everything after the PHP shell still passed the filter test and still worked.

This is not the moose you’re looking for!

And while the page doesn’t load here, we can reference the image directly with our ls command:

And here we go!

That directory looks interesting
There’s our flag!

Black Joker — I Hate Salt Password Cracking

This was a fun test of observation and having access to a password cracking setup. It probably wouldn’t take much time for even an underpowered system to solve this one.

So here’s our entry page:

The create account page tells us that new accounts aren’t being accepted right now. And the source code has embedded regex check to make sure the submitted password is between 9 and 14 characters using only lower-case a-z and 0-9. That’s a pretty small keyspace outside of the passwor length.

Knowing the admin from our front page we can ask for a password hint:

Isn’t that special! We have the first 9 of a potential 14 character password… but that’d still be a lot of requests to throw at this webpage. Fortunately, a closer inspection of that password hint reveals something else:

The password hash for the admin user was included in the JSON response! Armed with the hash, the initial 9 characters of the password, and a password scheme it’s off to create a hashcat ruleset for this password. I made a file listing out each of the characters as a rule option, and then set it in motion:

hashcat 8123.txt -wordlist ./wordlist.txt -r addon.rule -r addon.rule -r addon.rule -r addon.rule -r addon.rule

With the password in hand we go to the easily missed Admin login link at the bottom of the main page, an we enter our newly found creds into the Basic login prompt

Subdomain Brute-Force — 9 of Diamonds

This challenge took alittle Burp guesswork and Host header configuration. When you visit the initial page by IP address you get a redirection to intranet.metasploit.ctf which of course would fail in our browser as we don’t have an entry for that.

It also tells you that the intranet page is still under construction, and to use the direct subdomains already sent to you to connect.

What subdomains?!

There aren’t any obvious other ports open around this one, and what few tools are out there for enumerating DNS sub-domains are intended for open internet usage, not this challenge situatoin.

So I’m thinking this server is also serving the subdomain(s) pages. However, in order for the server to answer we have to make a request for the name not using just an IP as we have for all the other challenges.

Alrighty then, with an entry in our hosts table we can send a request to intranet:

The next step is to use Burp’s great Intruder module to mark where we want to fuzz some input on web requests and see what the server gives us in return. Burp can certainly make short work of a subdomain list.

I let it run sorted on Status response, and the game masters were nice enough to give a carrot to the players with assets.metasploit.ctf being valid. The page said Good job, but this isn't the right site! or similar. So we knew we just needed to let it run it’s course and soon hidden jumped out with this response:

At this point we could download the flag directly from our Kali system which has the intranet entry.

curl -H 'hidden.metasploit.ctf http://intranet.metasploit.ctf:8123/9_of_diamonds.png

Answer Guessing — 3 of Spades

This was one of the first ones solved, but it’s a good example of how quickly brute forced queries can find things. Security through obscurity does not work.

Standard login page tells us that guest is a valid username, and we need to guess at another.

Looking at the page source we see it’s a PHP posting form.

Capturing the request our browser sends out shows it’s a basic query.

Let’s automate these queries with a word list through Burp Suite:

After awhile we hit paydirt: demo returned a successful result:

We can now grab the flag!

Thanks for reading. I learned a ton about various techniques, and if nothing else I hope to use this write-up in the future to help remember what worked for me in previous CTFs.