This isn’t intended as a “full” walkthrough, I’m basically just going to outline my approach and perhaps mention a few alternatives.

A quick note; this is the first time I’ve participated to any major degree in a CTF contest (Though I have tested and designed a number of levels for the Ruxcon CTF over the past couple of years), I’ve poked at a few in the past but I seem to quickly lose interest. I think the fact that this one was over a defined time period and that the #ctf-support channel was there on slack really helped to keep me engaged.

Recon and initial access

Of course the first tool I reached for out of the gate was nmap, I wasn’t particularly discrete about it because I figured with the objective of this one there wasn’t likely to be any network-level mitigations in place.

nmap -sV -Pn -T4 -p 1-65535 -vv -oX dump 10.0.37.251

I pulled that into metasploit by way of db_import, which gave me the following to work from;

msf > services

Services
========

host         port  proto  name         state   info
----         ----  -----  ----         -----   ----
10.0.37.251  21    tcp    ftp          open    ProFTPD 1.3.5
10.0.37.251  22    tcp    ssh          open    OpenSSH 6.6.1p1 Ubuntu 2ubuntu2 Ubuntu Linux; protocol 2.0
10.0.37.251  80    tcp    http         open    Apache httpd 2.4.7
10.0.37.251  445   tcp    netbios-ssn  open    Samba smbd 3.X - 4.X workgroup: WORKGROUP
10.0.37.251  631   tcp    ipp          open    CUPS 1.7
10.0.37.251  3000  tcp    ppp          closed
10.0.37.251  3306  tcp    mysql        open    MySQL unauthorized
10.0.37.251  3500  tcp    http         open    WEBrick httpd 1.3.1 Ruby 2.3.5 (2017-09-14)
10.0.37.251  6697  tcp    irc          open    UnrealIRCd
10.0.37.251  8181  tcp    http         open    WEBrick httpd 1.3.1 Ruby 2.3.5 (2017-09-14)

Which provided us with quite a few options as far as exploitation goes, but first off the bat I did some sniffing around the various httpd services that were lying around to see if there was anything interesting, at this point I found;

  • phpMyAdmin, drupal, a custom “payroll_app.php” and a custom chatbot running out of apache.
  • A Rails server running on 3500.
  • A custom web app running on 8181.

On to exploitation, had a poke around the exploit database in Metasploit and settled on ‘exploit/unix/irc/unreal_ircd_3281_backdoor’.

This got me a shell on the box as boba_fett, which was enough to get me several of the flag source files, and also lead me to creds for the rest of them.

Getting creds

While I was looking around the system I found a PoC for an SQLi in the payroll_app (Equally could’ve gone direct to the database since the credentials were in the clear in the file, but if it’s there you might as well use it :) ), I’d previously confirmed that it was likely vulnerable to SQLi but parked it for later.

This got me a dump of user creds for the Payroll App which I then fed through ‘auxiliary/scanner/ssh/ssh_login’ for validation.

anakin_skywalker:but_master:(
artoo_detoo:b00p_b33p
ben_kenobi:thats_no_m00n
boba_fett:mandalorian1
c_three_pio:Pr0t0c07
chewbacca:rwaaaaawr8
greedo:hanSh0tF1rst
han_solo:nerf_herder
jabba_hutt:my_kinda_skum
jarjar_binks:mesah_p@ssw0rd
kylo_ren:Daddy_Issues2
lando_calrissian:@dm1n1str8r
leia_organa:help_me_obiwan
luke_skywalker:like_my_father_beforeme

Turns out most of the users on this system (with the notable exception of darth_vader) are guilty of re-using passwords, this got me a bunch of low-privilege user accounts I could directly login with so I could at least switch to a “real” interactive shell ;)

8 of Clubs

This was a quick and easy find due to world-readable home directories, find doesn’t care about depth of directory structures, so;

find /home -iname "*_of_*"

quickly turned this and others up;

./anakin_skywalker/52/37/88/76/24/97/77/22/23/63/19/56/16/27/43/26/82/80/98/73/8_of_clubs.png
./leia_organa/2_of_spades.pcapng
./artoo_detoo/music/10_of_clubs.wav

This one was a very simple, “get file, md5sum file, submit hash”.

8 Clubs Flag

8 Clubs Flag

9 of Diamonds

This one was found by manually hunting through home directories, it was found in a hidden folder under kylo_ren’s home directory;

han_solo@ip-10-0-37-251:/home/kylo_ren$ ls -laR
...
./.secret_files:
total 680
drw---x--- 2 kylo_ren users   4096 Nov  7 16:45 .
drwxr-xr-x 5 kylo_ren users   4096 Dec  6 05:35 ..
-rw---x--- 1 kylo_ren users 688128 Nov  7 16:45 my_recordings_do_not_open.iso

The flag was then retrieved by mounting the iso and doing the usual “md5sum file, submit hash” dance.

9 Diamonds Flag

9 Diamonds Flag

2 of Spades

This one was also found from the above “find”;

./leia_organa/2_of_spades.pcapng

This one requires a little bit more work to retrieve, scanning through the pcap in Wireshark, we see a bunch of local network traffic which is not terribly interesting, but as you dig further you start to see this;

Section of pcap for 2 Spades

Section of pcap for 2 Spades

So we have a VoIP call, so let’s see what Wireshark has to say about that (Telephony->VoIP Calls);

2 Spades VoIP Call

2 Spades VoIP Call

Playing back the call we find that it’s a very scratchy audio call to an echo test service with a voice reading out a URL for us; https://imgur.com/gmThKFP going to the URL, sure enough, there it is, the 2 of spades.

2 Spades Flag

2 Spades Flag

10 of Spades

This one was another that was found by find, I don’t recall specifically, I think it was probably on /opt.

Another retrieve flag (/opt/readme_app/public/images/10_of_spades.png), md5sum file, submit hash.

10 Spades Flag

10 Spades Flag

5 of Hearts

Was one I’d found earlier, but hadn’t looked into in detail yet, it was openly available on the Drupal install on Apache, but the file wasn’t quite right (each challenge described the flag image);

5 Hearts Original

5 Hearts Original

So I parked it while I went after any other low-hanging fruit.

Once I got back to this one I fed it to identify from ImageMagick, and was greeted by a blob of base64 encoded data in a tEXt chunk in the file, the text was duly extracted and fed to base64 -d to decode it into the original binary, which was the PNG I was looking for;

5 Hearts Flag

5 Hearts Flag

3 of Hearts

It was about here that I started looking for root, the rest of the flags had been acquired with low-privilege accounts.

It took longer than I really care to admit for me to figure out how to escalate to root, I spent a couple of hours slaving over the process list looking for convenient chinks in processes run by root. Classic overthinking though, because at some point I did a “cat /etc/group” and immediately facepalmed when I found this;

...
sudo:x:27:ubuntu,leia_organa,luke_skywalker,han_solo
...

Two of these are accounts I had credentials for, so I switched to han_solo asked “I wonder” and ran “sudo -s” lo and behold, unrestricted root shell. One facepalm later, I continued on with the process.

First port of call was to re-check for low-hanging fruit in case there was something which wasn’t world/users readable, there was;

root@ip-10-0-37-251:~# find / -iname "*_of_*"
...
/lost+found/3_of_hearts.png
...

Winner! Another quick md5sum file, submit hash.

3 Hearts Flag

3 Hearts Flag

5 of Diamonds

Onward and upward, earlier I’d noticed in the process list that knockd was running, but until I got root there wasn’t anything I could do with that because /etc/knockd.conf was owned by root and had 0600 permissions, now I could actually see it.

root@ip-10-0-37-251:/opt/knock_knock# cat /etc/knockd.conf
[options]
        UseSyslog
[openFlag]
        sequence    = 9560,1080,1200
...

Back to the Kali box, install knockd, and;

ec2-user@kali:~/staging$ knock 10.0.37.251 9560 1080 1200"

Quick check with nmap;

PORT     STATE SERVICE REASON         VERSION
8989/tcp open  rtsp    syn-ack ttl 64

We’re golden, hit it with lynx, download the file it presents, do the dance, and done;

5 Diamonds Flag

5 Diamonds Flag

7 of Diamonds

The source file for this one was also discovered in the earlier global find as root, it was an encrypted ZIP file sitting in a docker container, it was duly retrieved and inside it were found; “hint.gif” and another “7_of_diamonds.zip”.

7 Diamonds "Hint"

7 Diamonds "Hint"

Excuse the animated gif.

So it’s an animated gif with a stack of QR codes in it, so presumably we need to extract the frames, read the QR codes and concatenate all the data together.

I used an online service to split the gif into frames (could’ve used ImageMagick but I’d have had to read the manual to remember how to do this operation ;) ), then wrote some code around the ZXing library to parse each of the frames in turn (I went hunting for some pre-existing code but couldn’t find anything quickly), and spit out the binary data (each QR code had a hex string which was part of the larger file), which yielded this;

7 Diamonds Password

7 Diamonds Password

Which allowed me to extract the actual flag file.

7 Diamonds Flag

7 Diamonds Flag

8 of Hearts

I’d retrieved the source for this one pretty much immediately after getting into the box, the root credentials for MySQL were in the clear in the payroll_app.php file, I logged in to MySQL via phpMyAdmin and found the following;

8 Hearts phpMyAdmin

8 Hearts phpMyAdmin

The blob was duly extracted, and found to be an encrypted zip file. The reason this took me so long to resolve was basically down to using the wrong tool, I’d grabbed a current copy of John The Ripper, and fed the file to that along with several wordlists including rockyou, but I was unable to get a positive result, I resorted to pure brute force, by feeding the output of Mask Processor with a 13 character ASCII mask into JTR to try to crack the file (it’s a big keyspace but at 4.5Mc/s it’s not too bad), I left it running overnight.

The following day I still didn’t have a result, but I happened to have a look at the #ctf-support channel scrollback and found suggestions that the password WAS in fact in rockyou, some time later I saw reference to fcrackzip which is not a tool I’d used, so I killed my MP/JTR job and instead went for fcrackzip+rockyou, the password was found in about 30 seconds…I thought requiring an outright brute force was a bit OTT but couldn’t think of another option, live and learn.

Turns out the password was ‘vagrant’, extracted ZIP, did the dance and got the flag;

8 Hearts Flag

8 Hearts Flag

Ace of Clubs

This flag was hidden in the chatbot which was running out of Apache.

I gather it’s possible to get the bot to divulge the flag (it says as much, but you have to “ask the right question”, it’s also vulnerable to command injection so there’s an alternative path to root for you), but I dislike these types of challenges, never can seem to get the “right” question so instead I went straight for the source.

A bit of fossicking around in the files in /opt/chatbot lead me to two NodeJS processes in the process list;

 1255 ?        Sl     0:00 nodejs /opt/chatbot/papa_smurf/functions.js
 1256 ?        Sl    54:56 nodejs /opt/chatbot/papa_smurf/chat_client.js

Looking inside chat_client we find a base64 encoded blob, which upon extraction and decoding is “surprise” another PNG file;

Ace Clubs Flag

Ace Clubs Flag

6 of Clubs

This one is getting a bit more involved and seemed to cause the most “technical” trouble for contestants, I coached about 6 other people through solving this one one way or the other.

There are two pathways to it, either you can modify and re-sign a cookie, or you can recover the source. Of the people I coached, I think it was about a 50:50 split.

I’m not much of a web hacker so I went straight for the source, but for some variety here is the broad strokes of how to do it through the “front door”;

The web app for this one is running on 8181, when you access it it tells you what you need to do, “The /flag route can give you a flag if the _metasploitable cookie has the name of the flag.” but the cookies it sets are signed.

The cookie signing secret is in the session cookie that is set when you connect to the service, so you need to extract that, then add “6 of clubs” into the _metasploitable field within the cookie, then re-sign it and replace the cookie, it took quite a while for all the people who took this approach successfully to get the cookie “just right”.

I took the “other” approach, a bit of hunting around lead me to two ruby processes in the process list;

...
 9592 ?        S      0:00 /bin/sh -c cd /opt/sinatra && ruby -e "require 'obfuscate'; Obfuscate.setup { |c| c.salt = 'sinatra'; c.mode = :string }; cr = Obfuscate.clarify(File.read('.raIhUJTLEMAfUW3GmynyFySPw')); File.delete('.raIhUJTLEMAfUW3GmynyFySPw') if File.exists?('.raIhUJTLEMAfUW3GmynyFySPw'); eval(cr)" --
 9593 ?        Sl     0:07 ruby -e require 'obfuscate'; Obfuscate.setup { |c| c.salt = 'sinatra'; c.mode = :string }; cr = Obfuscate.clarify(File.read('.raIhUJTLEMAfUW3GmynyFySPw')); File.delete('.raIhUJTLEMAfUW3GmynyFySPw') if File.exists?('.raIhUJTLEMAfUW3GmynyFySPw'); eval(cr)
...

What we can see here is that something is changing to the directory /opt/sinatra, and executing some ruby code which picks up a hidden file ‘.raIhUJTLEMAfUW3GmynyFySPw’, reads the contents, deobfuscates it, stores it in a variable, deletes the original file then “eval”s the code to execute it. Logically something must be creating that file on startup because otherwise this service would be broken on reboot.

So we need to A) figure out under what circumstances the file is being (re)created, and B) retrieve it before it gets deleted.

Some hunting in /etc/init reveals that “sinatra” (which is referred to in the first command above) is a service on the system (it doesn’t appear in ‘service –status-all’ presumably “because systemd”).

Issuing a “service sinatra stop” we observe that the two ruby processes above have disappeared, so evidently this is the service responsible for the creation of the file and executing the code which removes the file.

Excellent, a quick shell script later;

#!/bin/bash
while :
do
  if [ -f /opt/sinatra/.raIhUJTLEMAfUW3GmynyFySPw ]
  then
    cp /opt/sinatra/.raIhUJTLEMAfUW3GmynyFySPw /tmp
    echo "We got one!"
    exit 0
  fi
done

Run the script in the background, bounce the service;

root@ip-10-0-37-251:/tmp# ./copy.sh &
[1] 16772
root@ip-10-0-37-251:/tmp# service sinatra stop && service sinatra start
sinatra stop/waiting
sinatra start/running, process 16786
root@ip-10-0-37-251:/tmp# We got one!
ls -a .*
.raIhUJTLEMAfUW3GmynyFySPw

Ta Da! We have our file. From the commands above we can tell that it’s going to be obfuscated so we need to do something about that, the command above with a slight twist will do the trick;

ruby -e "require 'obfuscate'; Obfuscate.setup { |c| c.salt = 'sinatra'; c.mode = :string }; cr = Obfuscate.clarify(File.read('.raIhUJTLEMAfUW3GmynyFySPw')); File.open('file.rb', 'w') { |file| file.write(cr) }; "

Which gives us our cleartext Ruby code, once again the flag is in there as a base64 encoded blob, which is duly extracted, decoded, md5sum’d and submitted;

6 Clubs Flag

6 Clubs Flag

King of Spades

Another one which took longer than was reasonable to do…

In the early stages of the contest I connected to everything on the server, or almost everything… When I originally attempted to connect to the ircd on the box I ran into trouble, the IRC server was running on 6697 which caused me to assume it was using SSL, it wasn’t…

I’d gone through most of the rest of the flags before coming back to this one, and I’ll admit I needed a nudge (thanks @Fullname) to move me in the right direction, when you connect to the ircd you get the following;

...
02:40 -!- - G2dXn2DJ5MuUiul38wHYu8rFwYfxAzhkm/S5oUCRJkPbcw3n4uOhvTmYEbGEjpWdvH3SbZmke5A9LkU00
02:40 -!- - jE03jLWA3LmKmec6G6eIXnGr6l/IsXUNrYfEJfhq3P2J4uvgD+2BRnKUC8b/GL2kyl8bLfC617xFzfip8
02:40 -!- - G92Q4rRzOd81dZ6LHBnV51FKLQ00UGc2lEzkO7xOkDALeZQDzPN8HJOzcmnT1kHiebyd4vexyTzAKu1yI
02:40 -!- - 05GHjfjsvOYIUmTDgME6eMNzrJvrHQ6JvAjOgFXSvdgvmLOwe/h0OCknGIKKWUc4+w5Qhlf4hFe4jbOoI
02:40 -!- - x8s/WmJisF2beKP50XU/xG1vNDs6F/Ulbez430AEHChNI6OiFpGKCfXl7mhCxJk0cdV9MBdLA6hc/gnxy
...

Initially I logged the output, cleaned it up and fed it to ‘base64 -d’ but that didn’t work because irssi truncates the lines. Never mind, we now know that the flag is in the irc message of the day so we can extract it directly from ‘/opt/unrealircd/Unreal3.2/ircd.motd’

Content was decoded and found to be a PNG image, but hang on… This doesn’t look like the description…

King Spades Original

King Spades Original

Looking a bit deeper into the file with a hex editor (binwalk would’ve done it directly though) we notice that something’s not quite right here, PNG files aren’t supposed to have PKZIP trailers…

root@ip-10-0-37-251:/tmp# xxd king.bin
...
0024ba0: e947 05f8 5b34 549e 28bf 57b4 0af9 1f50  .G..[4T.(.W....P
0024bb0: 4b01 0234 0314 0000 0008 00e1 03fc 4ac8  K..4..........J.
0024bc0: 7a3e 4fa7 2202 00c0 2302 0012 0000 0000  z>O."...#.......
0024bd0: 0000 0001 0000 00a4 8100 0000 006b 696e  .............kin
0024be0: 675f 6f66 5f73 7061 6465 732e 706e 6750  g_of_spades.pngP
0024bf0: 4b05 0600 0000 0001 0001 0040 0000 00d7  K..........@....
0024c00: 2202 0000 00                             "....

The PNG was stripped off the top of the ZIP archive and the archive was extracted revealing “the real king of spades”;

King Spades Flag

King Spades Flag

10 of Clubs

This one is another that took me WAAAAY too long to solve, I got there after a nudge from another contestant (thanks @Fullname). The problem here was basically a failure to abide pretty much the first commandment in binary file analysis “thou shalt binwalk”.

I’d spent some time manually pouring over the data in the file looking for headers and such but didn’t find any, which lead me to the rabbit hole of overthinking, I’d basically tunnel visioned onto the idea that the data must be somehow encoded into the audio, spent a lot of time with stego tools and GNU Radio Companion trying to work it out.

After a comment about “not overthinking challenges” in #ctf-support, and the aforementioned nudge I was back on the path.

In the end, this one was as simple as “binwalk -e 10_of_clubs.wav” which revealed that there was a chunk of ZLIB compressed data after the WAV file header, upon extraction we find that said ZLIB compressed data is a PNG file and the flag;

10 Clubs Flag

10 Clubs Flag

Joker

This is the only challenge within the contest that I have a gripe with, it was very difficult to get right not because it was technically challenging but because it has ONE VERY specific solution (well there was also a code-based solution but I’d expect that that was written “after the fact” to replicate the results, because the chances of “stumbling” onto the “right” code is basically nil, I’ll explain further below).

So this one looks pretty straight forward, there’s a file in /etc called “joker.png” owned by root with 0600 permissions so once you’ve got root it’s trivial, right… Not quite…

The file looks like this;

Joker Original

Joker Original

So obviously the colours are inverted, that’s easy enough to fix and Rapid7 provided the following hint; “@channel A little hint for you: The Joker is tricky; it’s easy to calculate the wrong MD5 hash. In fact, you might notice the hash changes on you (this is the Joker, after all). Pay attention to why and when the hash changes. Working with an image editing tool? Watch the metadata. Using a custom script? Focus on pixels. Good luck!” Which I interpret as “make sure that nothing except the pixels change in the file”.

I then proceeded to try about a dozen different approaches to generate a file that had ONLY the pixels inverted, I eventually settled on this chunk of Ruby;

require 'chunky_png'

module ChunkyPNG::Color
  def invert(value)
    rgba(MAX - r(value), MAX - g(value), MAX - b(value), a(value))
  end
end

image = ChunkyPNG::Image.from_file('joker.png')

image.pixels.map! do |pixel|
  ChunkyPNG::Color.invert(pixel)
end

image.save('joker_inverted.png')

Which resulted in the desired result;

Joker File Structures

Joker File Structures

Note that the IHDR and IEND CRCs are identical, the only difference here is in the IDAT section, to further confirm I also extracted the IDAT sections from the two files and did a direct comparison, and verified that the pixel RGB values were, in fact inverted between them.

"My" Joker Flag

"My" Joker Flag

But that didn’t work… The only conclusion I could come to at that point that although compressed data ought to be deterministic, that there must be some quirk in the LZ77 implementation in chunky_png versus whatever was used to generate the “master” image, in desperation I reached out to one of the contestants who had already solved all the challenges and I was told “all the solves I’m aware of used GIMP” (thanks @mubix)…

I’d already tried that a couple of days before, but discounted it because when I looked at the file structure it was different… But lo and behold I exported from GIMP with “Save creation time” unset, and the flag was accepted.

"Correct" Joker Flag

"Correct" Joker Flag

My problem with this one is that every tool I used to perform the changes produced a file with a different structure, which basically meant the ONLY way to solve this was to use GIMP, and considering that the file structure of the “right” file looks like this;

"Right" Joker Structure

"Right" Joker Structure

You can probably see why I doubt that the code which produced the “right” answer was produced without having seen the file structure first (Furthermore it used chunky_png and what I have above is pretty much the simplest case for inverting pixel colours with that.).

Summary

Overall barring that one minor gripe it was a thoroughly enjoyable competition.

I thank Rapid7/Metasploit and in particular juggernot and sinn3r for their efforts in putting the competition together and catc0n for her ongoing encouragement throughout the competition.

Also, if you want to read a WAAAAAY more comprehensive and detailed write up have a look at mubix’s (Second place in the CTF) Metasploitable 3 - A Walk-through: Linux Edition