Wild West Hackin' Fest 2024 Badge CTF
Initial Recon
The theme this year is a fun take on intergalactic bounty hunting. The badge is intended to be scanned at the various speaker and vendor booths at which point the badge will update and show the total number of captured bounties; the display indicates that there were twenty five total bounties to collect.
I'll start off like I do with most hardware projects which is performing recon of the functions, hardware, and software.
The device has the following components:
- a small oled display
- two push buttons
- a power switch
- an NFC tag
- ESP32-S3-wroom-1 chip
When powered on the display shows a battery indicator and a wireless RSSI indicator, below that there is a MAC address (presumably this device) and a canonical name: FoalMoon2
. There is a second screen that can be accessed by pressing the right-most button which shows the bounties that have been 'captured'.
In the conference area there are WiFi Hotspots configured with the name 'WWHF Badges' which the badge connects to in order to receive events.
There are some web services that the badge works with but we'll dive into those later.
Let's start off with the hardware and see what can be discovered with some static analysis.
Static Analysis
Firmware Dumping
My goto tool for dumping ESP32 chips is esptool.py. First we'll want to connect our device to USB and see if it even appears on our host.
I like to use dmesg -wH
and watch the log as I plug a device in.
Okay, so it looks like the device is recognized as a serial terminal over USB and it's located at /dev/ttyACM0
. That saves a lot of work!
Now that we know what port the device is on we can ask esptool.py for information about the flash chip. Specifically we need to know how large it is so we can dump the entire thing.
esptool.py -p /dev/ttyACM0 flash_id
According to the output our device has a total of 8MB flash storage, we can use that in our next command to read the contents of the flash.
esptool.py -p /dev/ttyACM0 read_flash 0x0 0x800000 ../fw.bin
Note the parameters being passed to esptool.py and how we specified 8MB of data with 0x800000
as our size param.
This will dump the .bin file from the hardware into the specified file.
Once I've got the firmware on disk I can use a binary analysis tool like binwalk
to extract the filesystem. This unfortunately did not yield much as the results were all mangled but it's good to verify our flash dump with!
binwalk ./wwhf2024.bin
Another classic tool to use is strings
, so I'll go ahead and run strings
against this binary to see if there's anything juicy...
Scrolling through I can see a couple of notable strings:
strings ./wwhf2024.bin > ./strings.txt
https://wwhf2024.s3.amazonaws.com/v2/
- this is most likely an API endpoint for updates/beaconing
WWHF Badges
- the SSID name that the badge looks to connect to
77HackTheEsp3too@
- SSID password, this is found littered throughout the strings output
o_0?/
- o_0?/
[MQTT]
- This tells us that MQTT is involved somewhere, maybe that's what our AWS url is for?
https://hardwarelabs.io/
- This is a known endpoint that was disclosed during the event, the MQTT server may live here as well
Authorization: Bearer W8nqEekeZ2a4L**********
- Sweet, a particularly juicy tidbit! This is an auth bearer token that is used in an HTTP request, I can probably use this later to poke at the web side of things.
Later down in the firmware strings we've got a blob of base64. If you've done any amount of this type of work you become immediately intrigued by a blob of base64. I'll decode this later. Don't forget!
NVS JSON
Dumping the NVS partition into a JSON format is a fun way to glean secrets and configuration data for the device. To do this I can use esp32_image_parser again with the following command
./esp32_image_parser.py dump_nvs ./wwhf2024.bin -partition nvs -nvs_output_type json > ./nvs.json
This should give you a good idea of what I'm looking for, in the case of our badge it seems I'm able to confirm the URLs found earlier in the firmware along with some credentials, connection details, and topics for the MQTT services.
"entry_ns": "badge_config", "entry_key": "MQTTBroker", "entry_data": "w6e1deed.ala.us-east-1.emqxsl.com" "entry_ns": "badge_config", "entry_key": "MQTTBroadcast", "entry_data": "broadcast/action" "entry_ns": "badge_config", "entry_key": "MQTTEvents", "entry_data": "events/device" "entry_ns": "badge_config", "entry_key": "MQTTUsername", "entry_data": "badges" "entry_ns": "badge_config", "entry_key": "MQTTPassword", "entry_data": "TPY_net1pvg*******" "entry_ns": "badge_config", "entry_key": "MQTTPort", "entry_data": 8883
NFC tag
NFC tags can be read with a variety of devices, in my case I'll scan it really quick with a Flipper Zero and save the .nfc file in-case I need it later
Filetype: Flipper NFC device Version: 4 # Device type can be ISO14443-3A, ISO14443-3B, ISO14443-4A, ISO14443-4B, ISO15693-3, FeliCa, NTAG/Ultralight, Mifare Classic, Mifare Plus, Mifare DESFire, SLIX, ST25TB Device type: Mifare Classic # UID is common for all formats UID: 9A 35 D9 40 # ISO14443-3A specific data ATQA: 00 04 SAK: 08 # Mifare Classic specific data Mifare Classic type: 1K Data format version: 2 # Mifare Classic blocks, '??' means unknown data Block 0: 9A 35 D9 40 36 08 04 00 62 63 64 65 66 67 68 69 Block 1: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 Block 2: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 Block 3: FF FF FF FF FF FF FF 07 80 69 FF FF FF FF FF FF
Blinkenlights
A keen observer will notice that when the device is powered on the lights at the top left will blink in a pattern. Obviously, the next step would be to record the sequence and decode the data, it really helped to have a slow-motion video for this part.
01010011 01001001 01000111 01111011 01100011 00110100 01101110 01110100 01011111 01110011 01110100 00110000 01110000 01011111 01110100 01101000 00110011 01011111 01110011 00110001 01100111 01101110 00110100 01101100 01111101
SIG{c4nt_st0p_th3_s1gn4l}`
One flag down, four to go!
There is another light at the lower right that seems to indicate status. During boot it remains blue, turning to white for a moment and then turning either green or red depending on whether or not it made a connection. Occaisionally during the event I would notice this LED flash between red and blue almost in a morse code esque pattern. I wasn't able to successfully capture the entire sequence but it was in-fact morse code leading to another flag.
Behavioral Analysis
WiFi Connectivity
Studying the badge behavior will be tricky unless I can convince the badge to connect to our own network, plus I'll be stuck sitting near the access point all day long otherwise. If I want to stand up my own network I'll need the SSID (known) and the password (unknown), thankfully there are a few ways to recover the PSK...
In my case I simply walked around the conference with a pwnagotchi configured for passive sniffing and since there were so many badges authenticating I was able to capture a dozen handshakes within minutes without the need for deauth attacks.
Recover the pcaps from the pwnagotchi and pass them hcxpcapngtool to convert them into .hc22000
hashes:
If your pcaps contain enough key material you should end up with a .hc22000 file that you can then pass to hashcat. Hashcat can occupy multiple blog posts in itself so I won't go in-depth during this post but if you want more you can continue reading about that in a future post of mine.
Initially I tried some simple attacks like best64 with rockyou but those didn't work so we're dealing with something a bit more complex. I'm sure it's a no-brainer to some of you readers but in this scenario what worked great was using the strings output as a simple wordlist attack against this hash.
I definitely felt cool when that idea popped into my head and worked...
In the case of our hardware you can glean the PSK from the NVS dump or the firmware .bin itself so capturing handshakes and cracking them is completely unecessary. But it is fun and a useful skill to have when attacking IoT devices.
Anyway... Once I've recovered the wireless network password I can attempt to set up an ad-hoc network that fits the details and see if our device connects.
Setting up the hotspot is relatively simple in linux with wihotspot but depending on how picky our badge is I may need to get more complicated with it. Once the network is up I can turn our badge on and see if it connects...
Suhweet! Looks like the badge is connected properly, the wifi status is showing full bars and the indicator light is green for connectivity.
network capture
Now that I have the badge connected to a wifi controlled by us, within the comfort of our hotel room, I can start capturing some packets to see if there is anything interesting to glean.
To be honest, most of this looks to be encrypted with TLS and control frames so I doubt I'm going to see much. The most important thing I can gain from this are the DNS requests which happen to align with the data I pulled from the firmware with strings
and the NVS partition.
Infrastructure Analysis
Web Pentesting
Most of the time my process for 'black box' hacking is iterative, meaning I loop through analyzing what is available and looping back when new pertinent information is available.
For instance, after dumping the firmware and analyzing it you may find credentials for a web service, from there you might investigate the web service to see what the device can interact with, which further informs your firmware analysis and so on peeling back layer after layer until you eventually reach your goal.
During analysis of the firmware I discovered credentials for the MQTT server along with an HTTP bearer token, I can refer to the DNS requests seen in wireshark to inform our web service testing and narrow down the hosts our device is talking to.
Let's focus on hardwarelabs.io
for a moment.
If I look in our pcap at the destination port I can see that hardwarelabs.io is talking over port 443, so this is probably not our endpoint for MQTT (I can also verify that with the MQTT details dumped from NVS) but if you remember back to our strings output there was an authorization bearer token right? I can investigate the area around that string to see if there is anything related to the token and this endpoint.
Sure enough, you can see hardwarelabs.io
packed nicely prior to the data required for the request so I can safely assume these are related.
It's reasonable to assume that the strings tossed among this HTTP request data might actually be API endpoints or somehow related to the request as well, so I slapped together a quick bash script to iterate these with curl and see if any are actually endpoints or if they're just parameters/junk:
#!/bin/bash endpoints=('vendors' 'staff' 'tracks' 'badge' 'special_events' 'mac_address' 'rfid' 'bounties' 'version.txt') for i in "${endpoints[@]}"; do curl -s -o /dev/null -w "%{http_code}" "https://hardwarelabs.io/$i" echo " $i" done
With that script output it looks like both the /vendors and /bounties are the only two that return 401 (unauthorized) to our client so I've identified some potential API endpoints. I can request those endpoints in our browser just to take a quick peek...
/bounties
aha! Looks like an API endpoint and it has conveniently informed us of what we're missing. Let's cram the Authorization Bearer token into Bruno and send it again:
Seems it appreciated that request and returned a mahoosiva json array. The array looks to contain each badge scan event which is represented by a dictionary containing the
bounty_hardware_id
- ID of the bounty
captured_dt
- timestamp of the bounty capture
hardware_id
- ID of the badge that captured the bounty
Using the cononical name of my badge I can correlate this data to the scoreboard data and learn the hardware ID from the URL params:
Our hardware ID looks to be 713
and it shows I currently have 14/40 bounty total, doing a ctrl+f through the response json data for our hardware_id yields 14 results so that confirms our theory about what this data is.
/vendors
The initial response on vendors
is the same as the bounty API so we'll repeat the steps of adding our request to Bruno with the Auth header.
After doing this I get json response content that contains what looks like the vendor data:
[ { "hardware_id": 1257, "sponsor_status": "Gold", "vendor_id": 6, "vendor_name": "ATTACKD", "venue_location": 6 }, { "hardware_id": 1255, "sponsor_status": "Gold", "vendor_id": 8, "vendor_name": "Wolf & Company", "venue_location": 8 }, { "hardware_id": 1247, "sponsor_status": "Gold", "vendor_id": 13, "vendor_name": "PlexTrac", "venue_location": 13 }, .....
Cool, so it looks like I have a way to gather all of the vendor IDs for later use when I try to unlock the badge. I did tell you that's what I'm trying to do right? Anyway, if you go back to the strings output and look around the request data you'll notice that the hardware is expected to send a POST request to an endpoint:
https://hardwarelabs.io/ https:// POST HTTP/1.1 Authorization: Bearer W8nqEekeZ2a4********** Content-Type: application/json Content-Length: mac_address rfid /bounties version.txt [OTA] Checking for new firmware
If I'm going to send a POST request then surely I'll need some information to send along with it, given context clues around the request data from strings
it looks like I might need a mac_address and rfid?
First let's identify the POST api endpoint, if I go back to Bruno and send an empty POST request to both endpoints I only get an Internal Server Error when sending it to /bounties only so this might be what I want to send our data to. I'll fill out the params and see what that does..
After it a little testing it looks like the endpoint is happier if I send it json (noted by content-type header) with the mac_address gleaned from the badge screen and rfid pulled from the NFC tag on the badge.
Cool beans, the good news is that it looks like I'm getting closer to what the server is expecting, the problem is I have no idea what the server means by "Badge not enrolled"... While that stews in the back of my brain let's poke at some of the MQTT infrastructure.
MQTT Snooping
MQTT is a message queue transport protocol with a pub/sub model, during the conference the device will connect to the MQTT server to subscribe to certain topics and receive events related to... whatever.
I can utilize the credentials pilfered from the firmware to connect to the MQTT server and have a snoop around, a simple CLI tool for doing this is MQTTUI.
I'll go ahead and set the environment variables so I won't have to keep typing creds in while exploring and then run the command to connect:
On the command line you can specify which topics you would like to subscribe to when connecting. We can refer back to the NVS partition data to see which topics I can expect to see data for
"entry_ns": "badge_config", "entry_key": "MQTTBroadcast", "entry_data": "broadcast/action" "entry_ns": "badge_config", "entry_key": "MQTTEvents", "entry_data": "events/device"
After subscribing to the broadcast and events topics I have a little data to analyze:
- events
{ "charge_level": "97%", "charge_volts": 4.18, "connected_time": 6, "current_version": "1.26", "data": "ZrVaRWy46ObK0f3A3XZqZBkCOVEzcS22DJJKUjYtGPW+ioZTrub8jeVg0suzjsQY", "device": "EC:DA:3B:5E:4A:D4", "hardware_event": 1, "hardware_type": 1, "powered_time": 7, "voltage_checks": 1 } { "charge_level": "97%", "charge_volts": 4.18, "connected_time": 50, "current_version": "1.26", "data": "H6xLDceFGPnLNtDuMnMZAA==", "device": "EC:DA:3B:5E:4A:D4", "hardware_event": 3, "hardware_type": 1, "powered_time": 51, "voltage_checks": 1 }
- broadcast
{ "broadcast_event": 10, "data": "qH/2hhZVZaWD2tMwzLmgC8TxGh/pzckIvSS+GQJsXWd2Lso1UIcJGjAK2orc4ydTrsxuOH3f3gcfydrpeCC4sza3f2zoxtuq/VsxNSw8AbU=", "date": "10/11/24", "device": "30:30:F9:7B:90:D0", "message": "{\"vendors\": 20, \"staff\": 0, \"tracks\": 2, \"badge\": 4, \"special_events\": 4}", "time": "03:18PM UTC" } { "broadcast_event": 10, "data": "31b0sAfoFpYnUwaueFXyR8TxGh/pzckIvSS+GQJsXWd2Lso1UIcJGjAK2orc4ydTrsxuOH3f3gcfydrpeCC4sza3f2zoxtuq/VsxNSw8AbU=", "date": "10/11/24", "device": "30:30:F9:7B:90:D0", "message": "{\"vendors\": 21, \"staff\": 0, \"tracks\": 2, \"badge\": 4, \"special_events\": 4}", "time": "03:18PM UTC" }
There's some interesting data in here!
In the events topic I have what looks like device data that gets sent by the badges each time a keep alive event is received. It shows the hardware type, version and status.
From the broadcast topic I receive some events that look like they occur each time a badge is reported scanned, along with the number of device bounties. There is a data field in the events from both topics which contains base64 data but it's not plaintext once decoded so the purpose eludes me.
I can still have some fun while I'm here, MQTT has a thing called retained messages where normally messages would be discarded after being sent, retained messages will be broadcast until they are updated.
blobby rot
So about that base64 blob... You remembered that right?
Any seasoned analyst will see a blob of base64 and their mouths should start watering, well maybe not that extreme... but it's always fun to find juicy things hidden behind a simple layer of encoding. If I take that blob I pulled from the firmware...
I2luY2x1ZGUgPHN0ZGlvLmg+CiNpbmNsdWRlIDxzdHJpbmcuaD4KI2luY2x1ZGUgPHN0ZGxpYi5oPgoKdm9pZCByKGNoYXIgKnMsIGludCBzaGlmdCkgewogICAgZm9yIChpbnQgaSA9IDA7IHNbaV07IGkrKykgewogICAgICAgIGlmIChz W2ldID49ICdBJyAmJiBzW2ldIDw9ICdaJykgc1tpXSA9ICgoc1tpXSAtICdBJyArIHNoaWZ0KSAlIDI2KSArICdBJzsKICAgICAgICBlbHNlIGlmIChzW2ldID49ICdhJyAmJiBzW2ldIDw9ICd6Jykgc1tpXSA9ICgoc1tpXSAtICdhJyAr IHNoaWZ0KSAlIDI2KSArICdhJzsKICAgIH0KfQoKaW50IG1haW4oaW50IGFyZ2MsIGNoYXIgKmFyZ3ZbXSkgewogICAgaWYgKGFyZ2MgIT0gMikgcmV0dXJuIDE7CiAgICBpbnQgc2hpZnQgPSBhdG9pKGFyZ3ZbMV0pOwogICAgY2hhciBm W10gPSAiRlZUe3JMcl90MF8zaTNlbGp1M2UzfSI7CiAgICByKGYsIHNoaWZ0KTsKICAgIHByaW50ZigiJXNcbiIsIGYpOwogICAgcmV0dXJuIDA7Cn0K
and feed it into cyberchef base64 I get this:
#include <stdio.h> #include <string.h> #include <stdlib.h> void r(char *s, int shift) { for (int i = 0; s[i]; i++) { if (s[i] >= 'A' && s[i] <= 'Z') s[i] = ((s[i] - 'A' + shift) % 26) + 'A'; else if (s[i] >= 'a' && s[i] <= 'z') s[i] = ((s[i] - 'a' + shift) % 26) + 'a'; } } int main(int argc, char *argv[]) { if (argc != 2) return 1; int shift = atoi(argv[1]); char f[] = "FVT{rLr_t0_3i3elju3e3}"; r(f, shift); printf("%s\n", f); return 0; }
Neat! Now I'm not a wizard at C or anything but if I was a betting gal I'd say this is probably a rotation or bitshift. I can probably figure it out quickly by extracting the string and feeding it through a few patterns in cyberchef:
FVT{rLr_t0_3i3elju3e3}
Of course, looks like it was a simple ROT13 encoding aaaaand that's another flag! For a total of 2/4 badge bounty flags.
SIG{eYe_g0_3v3rywh3r3}
Conclusion
I ran out of time! That's about the extent of what I was able to uncover during the course of the 3-day Wild West Hackin' Fest conference. Overall the conference was a ton of fun and I learned a handful of things that I can apply to other aspects of my hobby and career.
Performing black box analysis on a piece of hardware can be pretty daunting but we can see that sometimes all it takes is a firmware dump and strings
to find out plenty of information to begin with. Approach your analysis in an iterative and cyclical manner so you can peel back each layer until you are satisfied, when you get stuck you can circle back to previous avenues of investigation to find more juice to keep you going.
If I get around to messing with this device again in the future a good goal would be to create a tool for unlocking the badge either through emulating the infrastructure or manipulating the firmware. Anyway, I hope you learned something, happy hacking!