Wild West Hackin' Fest 2024 Badge CTF

Published on 2024-11-19 by boatplugs


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.

Image of the WWHF2024 Badge. It is a white coated PCB with an ESP32 chip at the bottom middle, a screen slightly above that and a button on each side.

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:

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.

Screenshot of dmesg output listing the USB device once it is connected

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

Screenshot of esptool.py flash_id output listing the device flash details

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

Screenshot of esptool.py read_flash output showing the flash being dumped to a file

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

Screenshot of binwalk listing the headers it detects

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

Image of strings output with individual items highlighted

Image of more strings output and more highlighting

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!

Image of the base64 encoded blob crammed into the firmware

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

Screenshot of the nvs json output

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:

Screenshot of a terminal showing hcxpcapngtool output

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...

Screenshot of a terminal showing hashcat output

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...

Image of wihotspot showing the wifi hotspot configuration gui

Image of the badge screen showing full bars of wifi

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.

Screenshot of a wireshark capture with a source filter of our device IP

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.

Screenshot of the strings output with highlights of potential endpoints

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

Screenshot of the script output showing which endpoints returned 404 or 401

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...

Screenshot of firefox showing the api response

/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:

Screenshot of bruno auth field and response

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

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:

Screenshot of the WWHF scoreboard

Screenshot of the WWHF scoreboard

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.

Screenshot of Bruno showing the error POST to the bounties endpoint

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:

Screenshot of a terminal showing the two environment variables and the connection command

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:

{
    "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_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.

Screenshot of a MQTTUI showing a topic called 'blog' and the contents is 'shadylink.lol'


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}

Screenshot of cyberchef showing the flag being rot13'd

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!


References