Vulnerable-by-design multi-protocol security testing platform built on two stacked ESP32-C6 boards.
The master board transmits intentionally weak payloads across 8 communication protocols. The slave board receives, decodes, and reports everything back to a web dashboard served by the master.
| Protocol | Transport | Vulnerability by Design |
|---|---|---|
| UART | GPIO16 β GPIO17, 115 200 baud | Plain-text serial, no framing |
| IΒ²C | SDA GPIO22 / SCL GPIO23, 100 kHz | Unencrypted bus, fixed slave address 0x28 |
| SPI | MOSI 18 / MISO 20 / SCLK 19 / CS 21, 1 MHz | No authentication on the bus |
| ESP-NOW | WiFi broadcast, channel 1 | Broadcast MAC, no encryption |
| BLE Open | GATT service 0000aa00-β¦ |
Characteristic readable without pairing |
| BLE Auth | GATT service 0000bb00-β¦ |
Static 6-digit PIN (001234), Legacy Pairing |
| Thread | IEEE 802.15.4, channel 15, PAN 0xFACE |
Well-known network key, UDP multicast |
| HTTP | WiFi TCP port 80 | Payload in plaintext over open SoftAP |
The master exposes a GET /system endpoint returning real-time telemetry:
- Heap usage (total / free / min-free)
- Internal temperature (Β°C)
- Uptime, FreeRTOS task count
- ESP-IDF version
- Per-protocol running state & custom payload
The master boots with WiFi AP + HTTP server only. Each protocol is started
or stopped at runtime through the web dashboard (POST /control).
Heavy initializations (OpenThread, NimBLE) run in dedicated FreeRTOS tasks
so the HTTP server stays responsive.
Each protocol's payload can be overridden from the dashboard
(POST /payload). Clear the value to revert to the auto-generated default.
Each CTF service maps to a specific capability in the Vandal security analysis platform. The table below shows which Vandal command or module targets each service.
| CTF Service | Vandal Module / Command | What to Demonstrate |
|---|---|---|
| UART | Passive serial capture / protocol analysis | Plain-text payload recovery |
| IΒ²C | IΒ²C bus monitor / logic analyser | Unencrypted sensor data capture |
| SPI | SPI bus capture | No-auth bus snooping |
| ESP-NOW | esp_now sniffer (WiFi monitor mode) |
Broadcast frame capture, no encryption |
| BLE Open | bt_scanner (GATT enumeration) |
Discover open characteristic, read flag without pairing |
| BLE Auth | bt_probe_start (PIN dictionary attack) |
Identify PASSKEY_REQUIRED class, crack PIN 001234 via dictionary, read protected characteristic |
| Thread | Roadmap β OpenThread network analysis | Capture 802.15.4 frames, known network key extraction |
| HTTP | wifi_sniffer (probe capture) + HTTP sniff |
Intercept HTTP payload on open WiFi |
bt_probe_start targets=[<slave_mac>] pins=["0000","1234","001234"]
β preflight: BT_AUTH_CLASS_PASSKEY_REQUIRED
β dictionary attempt "001234" β pairing_success: true
β post-pairing: bt_write or GATT read on service 0000bb00-β¦
ββββββββββββββββββββ MASTER βββββββββββββββββββββββββ
β WiFi AP ("Vandal CTF") β
β HTTP Server (port 80) β
β ββ GET / β React SPA (FATFS) β
β ββ GET /status β merged payload report β
β ββ GET /system β system telemetry β
β ββ POST /control β start/stop protocol β
β ββ POST /payload β custom payload β
β ββ POST /messages β slave JSON report β
β ββ POST /http-payloadβ HTTP service endpoint β
β β
β Protocol TX (on demand, 8 services) β
β UART Β· IΒ²C Β· SPI Β· ESP-NOW Β· BLE Β· Thread Β· HTTP β
ββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββ
wired + wireless
ββββββββββββββββ΄βββββββββββββββββββββββββββββββββββββ
β SLAVE β
β All RX modules started at boot β
β Event bus β HTTP Client POST to master β
βββββββββββββββββββββββββββββββββββββββββββββββββββββ
Master TX event
β esp_event_post(VANDAL_EVT_PAYLOAD_RECEIVED)
β master_payload_event_handler() updates s_master_payloads[]
β GET /status returns merged view (master + slave data)
Slave RX event
β http_event_handler() stores in s_payloads[]
β POST /messages every 5 s to master
β messages_post_handler() updates s_last_report
β GET /status merges s_last_report with s_master_payloads[]
components/
βββ vandal_common/ # Types, event bus, payload generator, running-state API
βββ vandal_wifi/ # SoftAP (master) + STA (slave) setup
βββ vandal_http/ # HTTP server (master) / HTTP client (slave)
βββ vandal_uart/ # UART1 master TX / slave RX
βββ vandal_i2c/ # IΒ²C master write / slave v2 driver
βββ vandal_spi/ # SPI master TX / slave RX
βββ vandal_espnow/ # ESP-NOW broadcast TX / RX
βββ vandal_ble/ # NimBLE GATT server (master) / client (slave)
βββ vandal_thread/ # OpenThread FTD, UDP multicast TX / RX
| Tool | Version |
|---|---|
| ESP-IDF | v5.5.2 |
| Target | ESP32-C6 |
| pnpm | β₯ 10 (for the React dashboard build) |
The dashboard is pre-built in the website/ folder. You only need pnpm if
you want to modify the React source (see vandal-monitor).
# 1. Source the ESP-IDF environment
source ~/.espressif/v5.5.2/esp-idf/export.sh
# 2. Configure the role (default = MASTER)
idf.py menuconfig
# β Vandal CTF Configuration β Board role β Master / Slave
# 3. Build
idf.py build
# 4. Flash (USB Serial/JTAG)
idf.py -p /dev/ttyACM0 flash
# 5. Monitor
idf.py -p /dev/ttyACM0 monitorImportant: Erase NVS before first deployment (or after changing Thread network credentials) to avoid stale OpenThread datasets:
idf.py -p /dev/ttyACM0 erase-flash && idf.py -p /dev/ttyACM0 flash
Tip: Flash the master first, then change the role to Slave, rebuild, and flash the second board.
| Name | Type | Offset | Size |
|---|---|---|---|
nvs |
NVS | 0x9000 | 24 KB |
phy_init |
PHY | 0xF000 | 4 KB |
factory |
App | 0x10000 | 3 MB |
website |
FAT | 0x310000 | 960 KB |
The website partition is auto-generated from the website/ directory at
build time via fatfs_create_spiflash_image().
All tunables are under Vandal CTF Configuration in menuconfig:
| Menu | Key Options |
|---|---|
| Board role | Master / Slave |
| UART | TX/RX pins, baud rate |
| IΒ²C | SDA/SCL pins, slave address, clock |
| SPI | MOSI/MISO/SCLK/CS pins, clock speed |
| ESP-NOW | Channel, primary master key |
| BLE | Service UUIDs, PIN, device name |
| WiFi AP | SSID, password, channel, max connections |
| HTTP | Port, master IP, slave POST interval |
| General | Payload send interval, LED GPIO |
| Function | GPIO |
|---|---|
| UART TX / RX | 16 / 17 |
| IΒ²C SDA / SCL | 22 / 23 |
| SPI MOSI / MISO / SCLK / CS | 18 / 20 / 19 / 21 |
| User LED | 15 |
| Console | USB Serial/JTAG (native) |
All JSON endpoints. CORS enabled (Access-Control-Allow-Origin: *).
Returns a merged view: slave-received payloads from POST /messages merged
with any master-originated payloads (Thread TX, HTTP TX self-reported via the
event bus).
{
"UART": "VANDAL payload #0042 sent through UART",
"I2C": "VANDAL payload #0042 sent through I2C",
"SPI": null,
"ESP-NOW": "VANDAL payload #0042 sent through ESP-NOW",
"BLE-OPEN": "VANDAL payload #0042 sent through BLE-OPEN",
"BLE-AUTH": "VANDAL payload #0042 sent through BLE-AUTH",
"Thread": "VANDAL payload #0042 sent through Thread",
"HTTP": "VANDAL payload #0042 sent through HTTP"
}{
"heap_total": 327680,
"heap_free": 215040,
"heap_min_free": 198000,
"temperature": 32.5,
"uptime_s": 1234,
"task_count": 14,
"idf_version": "v5.5.2",
"protocols": {
"UART": { "running": true, "custom_payload": null },
"BLE-AUTH": { "running": true, "custom_payload": null },
"Thread": { "running": false, "custom_payload": null },
...
}
}{ "protocol": "Thread", "action": "start" }Response: { "status": "ok", "protocol": "Thread", "running": true }
{ "protocol": "UART", "payload": "MY-CUSTOM-DATA" }Send "payload": null or "" to clear.
vandal-ctf/
βββ CMakeLists.txt # Top-level project file
βββ partitions.csv # Custom partition table
βββ sdkconfig.defaults # Default KConfig values
βββ main/
β βββ main.c # Entry point & boot orchestration
β βββ Kconfig.projbuild # All KConfig menus
β βββ CMakeLists.txt
βββ components/ # Modular protocol drivers (see above)
βββ website/ # Pre-built React dashboard (FATFS image)
Educational / security research project β vulnerable by design. Do not deploy in production.