Add 107 terminal screenshots and replace all 📸 placeholders

- Generated screenshots for all 33 challenges (ESP, Hardware, IoT, OT, Misc, Web3)
- Replaced all 123 placeholder lines with actual PNG image references
- Cleaned duplicate images from previously partial updates
- All write-ups now have full illustrated solutions
This commit is contained in:
Eun0us 2026-03-27 00:34:47 +00:00
parent 8aac4b4e5a
commit 1c42421380
151 changed files with 124 additions and 126 deletions

View File

@ -60,7 +60,7 @@ On Linux, add your user to the `dialout` group first if you get a permission err
sudo usermod -a -G dialout $USER sudo usermod -a -G dialout $USER
``` ```
> 📸 `[screenshot: esptool.py flashing — progress bar reaching 100%]` ![esptool.py flashing — progress bar reaching 100%](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/esptool_flash.png)
### Step 2 — Connect to the UART console ### Step 2 — Connect to the UART console
@ -82,7 +82,7 @@ Encrypted flag: 09 12 19 07 00 0E 07 35 3F 35 7D 3C 38 1E 3D 26 7F 1E 3E 7F 3E 7
XOR Key: 4C 41 49 4E XOR Key: 4C 41 49 4E
``` ```
> 📸 `[screenshot: serial terminal showing the encrypted flag and XOR key on boot]` ![serial terminal showing the encrypted flag and XOR key on boot](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/esp_start_uart.png)
### Step 3 — Identify the XOR key ### Step 3 — Identify the XOR key
@ -114,7 +114,7 @@ print(flag.decode())
Output: `ESPILON{st4rt_th3_w1r3}` Output: `ESPILON{st4rt_th3_w1r3}`
> 📸 `[screenshot: Python decryption script running and printing the flag]` ![Python decryption script running and printing the flag](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/esp_start_decrypt.png)
### Key concepts ### Key concepts

View File

@ -75,12 +75,11 @@ Open the UART console:
screen /dev/ttyUSB0 115200 screen /dev/ttyUSB0 115200
``` ```
> 📸 `[screenshot: UART console showing jnoun-console> prompt]` ![UART console showing jnoun-console> prompt](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/jnouned_console.png)
--- ---
### Flag 1 — Console Access ### Flag 1 — Console Access
![UART admin console authentication](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/jnouned_uart.png)
(100 pts) (100 pts)
The UART console presents a `jnoun-console>` prompt requiring a password. The UART console presents a `jnoun-console>` prompt requiring a password.
@ -101,12 +100,11 @@ admin_login jnoun-admin-2022
Flag 1 is printed on success. Flag 1 is printed on success.
> 📸 `[screenshot: successful admin_login command printing Flag 1]` ![successful admin_login command printing Flag 1](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/jnouned_login.png)
--- ---
### Flag 2 — 802.11 TX ### Flag 2 — 802.11 TX
![802.11 frame capture with flag payload](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/jnouned_wifi.png)
(200 pts) (200 pts)
From the admin console, read device settings: From the admin console, read device settings:
@ -138,7 +136,8 @@ tshark -i wlan0mon -w capture.pcap
Among the random noise frames, one frame emitted at a random time (585 seconds) Among the random noise frames, one frame emitted at a random time (585 seconds)
contains Flag 2 as cleartext in its 802.11 data payload. contains Flag 2 as cleartext in its 802.11 data payload.
> 📸 `[screenshot: Wireshark frame showing Flag 2 in the payload field]`
![Wireshark frame showing Flag 2 in the payload field](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/jnouned_wifi.png)
--- ---
@ -163,7 +162,7 @@ Decoded: `target=x; flag`
Flag 3 prints to the UART console via `ESP_LOGE`. Flag 3 prints to the UART console via `ESP_LOGE`.
> 📸 `[screenshot: UART console showing Flag 3 output after injection]` ![UART console showing Flag 3 output after injection](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/jnouned_flag3.png)
--- ---
@ -228,7 +227,7 @@ for block_id in range(10):
print(flag.decode()) print(flag.decode())
``` ```
> 📸 `[screenshot: Python JMP client printing the final flag after block reassembly]` ![Python JMP client printing the final flag after block reassembly](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/jnouned_jmp.png)
--- ---

View File

@ -86,7 +86,7 @@ echo "RVNQSU9Me3U0cnRfczMzc180bGx9" | base64 -d
# ESPILON{u4rt_s33s_4ll} # ESPILON{u4rt_s33s_4ll}
``` ```
> 📸 `[screenshot: UART terminal showing the base64 diagnostic line on boot]` ![UART terminal showing the base64 diagnostic line on boot](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/phantom_uart.png)
Submit over TCP: Submit over TCP:
@ -122,7 +122,7 @@ ph> wire
>> ESPILON{h1dd3n_c0nf1g} >> ESPILON{h1dd3n_c0nf1g}
``` ```
> 📸 `[screenshot: wire command revealing the hidden config cache contents]` ![wire command revealing the hidden config cache contents](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/phantom_wire.png)
Submit: Submit:
@ -168,7 +168,7 @@ The trace output shows each out-of-bounds byte read from the adjacent `config_ca
Convert the oob bytes to ASCII: `ESPILON{ph4nt0m_byt3_h34p_l34k}` Convert the oob bytes to ASCII: `ESPILON{ph4nt0m_byt3_h34p_l34k}`
> 📸 `[screenshot: trace output showing oob bytes reading the flag from heap]` ![trace output showing oob bytes reading the flag from heap](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/phantom_trace.png)
--- ---
@ -200,7 +200,7 @@ are read from the adjacent flag buffer.
Reconstruct: `ESPILON{bl1nd_str4ddl3}` Reconstruct: `ESPILON{bl1nd_str4ddl3}`
> 📸 `[screenshot: inject command returning TS values that decode to flag bytes]` ![inject command returning TS values that decode to flag bytes](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/phantom_inject.png)
Automated solver: Automated solver:

View File

@ -57,7 +57,7 @@ nc <host> 3600
nc <host> 3601 nc <host> 3601
``` ```
> 📸 `[screenshot: two terminal windows showing sniff output and inject prompt]` ![two terminal windows showing sniff output and inject prompt](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/can_terminals.png)
### Step 2 — Observe the traffic ### Step 2 — Observe the traffic
@ -72,7 +72,7 @@ Watch the sniff port. The following patterns emerge:
The `0x7E0`/`0x7E8` pair is the UDS diagnostic channel. The `0x7E0`/`0x7E8` pair is the UDS diagnostic channel.
> 📸 `[screenshot: sniff output showing the 0x7E0/0x7E8 request/response pattern]` ![sniff output showing the 0x7E0/0x7E8 request/response pattern](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/can_sniff.png)
### Step 3 — Enter extended diagnostic session ### Step 3 — Enter extended diagnostic session
@ -94,7 +94,7 @@ send 7E0 02 27 01 00 00 00 00 00
The response contains a 4-byte seed: `67 01 XX XX XX XX` The response contains a 4-byte seed: `67 01 XX XX XX XX`
> 📸 `[screenshot: seed bytes visible in the 0x7E8 response]` ![seed bytes visible in the 0x7E8 response](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/can_seed.png)
### Step 5 — Compute the key and authenticate ### Step 5 — Compute the key and authenticate
@ -123,7 +123,7 @@ send 7E0 03 22 FF 01 00 00 00 00
The response on `0x7E8` contains the flag. The response on `0x7E8` contains the flag.
> 📸 `[screenshot: 0x7E8 response containing the flag bytes after successful security access]` ![0x7E8 response containing the flag bytes after successful security access](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/can_flag.png)
### Key concepts ### Key concepts

View File

@ -48,7 +48,7 @@ arm and trigger, then read the maintenance token from the unlocked debug console
nc <host> 3700 nc <host> 3700
``` ```
> 📸 `[screenshot: glitch lab banner and prompt]` ![glitch lab banner and prompt](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/glitch_banner.png)
### Step 2 — Observe the boot sequence ### Step 2 — Observe the boot sequence
@ -68,7 +68,7 @@ LAUNCH_KERNEL [3400 5000]
The signature verification phase runs between cycles 3200 and 3400. The signature verification phase runs between cycles 3200 and 3400.
> 📸 `[screenshot: observe output showing all boot phases and cycle ranges]` ![observe output showing all boot phases and cycle ranges](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/glitch_observe.png)
### Step 3 — Configure glitch parameters ### Step 3 — Configure glitch parameters
@ -101,7 +101,7 @@ LAUNCH_KERNEL ......... OK
[DEBUG SHELL ACTIVATED] [DEBUG SHELL ACTIVATED]
``` ```
> 📸 `[screenshot: boot log showing SIG_VERIFY SKIPPED and debug shell prompt]` ![boot log showing SIG_VERIFY SKIPPED and debug shell prompt](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/glitch_unlock.png)
### Step 5 — Read the maintenance token ### Step 5 — Read the maintenance token
@ -111,7 +111,7 @@ read_console
The debug console outputs the maintenance token containing the flag. The debug console outputs the maintenance token containing the flag.
> 📸 `[screenshot: read_console output displaying the flag]` ![read_console output displaying the flag](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/glitch_flag.png)
### Key concepts ### Key concepts

View File

@ -50,7 +50,7 @@ Use the key to decrypt the EEPROM contents and recover the flag.
nc <host> 3300 nc <host> 3300
``` ```
> 📸 `[screenshot: I2C bus interface prompt]` ![I2C bus interface prompt](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/i2c_prompt.png)
### Step 2 — Scan the bus ### Step 2 — Scan the bus
@ -66,7 +66,7 @@ I2C Address 0x48 [Temperature Sensor]
I2C Address 0x60 [Crypto IC] I2C Address 0x60 [Crypto IC]
``` ```
> 📸 `[screenshot: scan output listing three I2C devices]` ![scan output listing three I2C devices](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/i2c_scan.png)
### Step 3 — Read the temperature sensor's hidden register ### Step 3 — Read the temperature sensor's hidden register
@ -108,7 +108,7 @@ read 0x60 0x10 32
Now returns the actual 32-byte key: `NAVI_WIRED_I2C_CRYPTO_KEY_2024!!` Now returns the actual 32-byte key: `NAVI_WIRED_I2C_CRYPTO_KEY_2024!!`
> 📸 `[screenshot: crypto IC returning the key after unlock]` ![crypto IC returning the key after unlock](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/i2c_key.png)
### Step 7 — Read the EEPROM ### Step 7 — Read the EEPROM
@ -128,7 +128,7 @@ flag = bytes(b ^ key[i % len(key)] for i, b in enumerate(enc))
print(flag.rstrip(b'\x00').decode()) print(flag.rstrip(b'\x00').decode())
``` ```
> 📸 `[screenshot: Python decryption script printing the flag]` ![Python decryption script printing the flag](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/i2c_decrypt.png)
### Key concepts ### Key concepts

View File

@ -51,7 +51,7 @@ reassemble the flag.
nc <host> 3400 nc <host> 3400
``` ```
> 📸 `[screenshot: JTAG port banner showing TAP controller information]` ![JTAG port banner showing TAP controller information](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/jtag_banner.png)
### Step 2 — Reset the TAP controller ### Step 2 — Reset the TAP controller
@ -72,7 +72,7 @@ dr 00000000 32
Returns `0x4BA00477` — an ARM Cortex-M style IDCODE. Returns `0x4BA00477` — an ARM Cortex-M style IDCODE.
> 📸 `[screenshot: IDCODE read returning 0x4BA00477]` ![IDCODE read returning 0x4BA00477](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/jtag_idcode.png)
### Step 4 — Unlock the debug interface ### Step 4 — Unlock the debug interface
@ -91,7 +91,7 @@ state
Output should now show: `Debug: UNLOCKED` Output should now show: `Debug: UNLOCKED`
> 📸 `[screenshot: state command showing Debug: UNLOCKED]` ![state command showing Debug: UNLOCKED](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/jtag_unlock.png)
### Step 5 — Load the memory read instruction ### Step 5 — Load the memory read instruction
@ -113,7 +113,7 @@ Repeat for addresses 0x1004, 0x1008, etc. until the flag is complete.
Convert each little-endian 32-bit word to 4 ASCII bytes and concatenate. Convert each little-endian 32-bit word to 4 ASCII bytes and concatenate.
> 📸 `[screenshot: dr reads returning flag bytes as 32-bit little-endian words]` ![dr reads returning flag bytes as 32-bit little-endian words](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/jtag_mem_read.png)
### Key concepts ### Key concepts

View File

@ -53,7 +53,7 @@ nc <host> 1111
nc <host> 2222 nc <host> 2222
``` ```
> 📸 `[screenshot: two terminals open, TX showing boot messages and RX ready for input]` ![two terminals open, TX showing boot messages and RX ready for input](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/serial_exp_terminals.png)
### Step 2 — Query the diagnostic commands ### Step 2 — Query the diagnostic commands
@ -103,7 +103,7 @@ frag_c_hex=3030
Decode: `bytes.fromhex("3030").decode()``00` Decode: `bytes.fromhex("3030").decode()``00`
> 📸 `[screenshot: TX output showing all three fragment values from diagnostics]` ![TX output showing all three fragment values from diagnostics](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/serial_exp_diag.png)
### Step 4 — Build the maintenance token ### Step 4 — Build the maintenance token
@ -123,7 +123,7 @@ unlock LAIN-SERIAL-00
The flag is returned on the TX terminal. The flag is returned on the TX terminal.
> 📸 `[screenshot: TX terminal printing the flag after successful unlock]` ![TX terminal printing the flag after successful unlock](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/serial_exp_flag.png)
### Key concepts ### Key concepts

View File

@ -69,7 +69,7 @@ Channels: 3
Sample rate: 1 MHz Sample rate: 1 MHz
``` ```
> 📸 `[screenshot: info command output listing the three channels]` ![info command output listing the three channels](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/signal_tap_info.png)
### Step 3 — Analyze channel 1 ### Step 3 — Analyze channel 1
@ -90,7 +90,7 @@ Baud rate = 1 / 0.00010417 ≈ 9600 baud
A 10-bit UART frame (1 start + 8 data + 1 stop) = ~1041.67 μs. A 10-bit UART frame (1 start + 8 data + 1 stop) = ~1041.67 μs.
> 📸 `[screenshot: Python script measuring bit periods from ch1 transitions]` ![Python script measuring bit periods from ch1 transitions](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/signal_tap_measure.png)
### Step 5 — Decode UART 8N1 ### Step 5 — Decode UART 8N1
@ -133,7 +133,7 @@ print("".join(chars))
The decoded message contains the flag repeated three times. The decoded message contains the flag repeated three times.
> 📸 `[screenshot: decoded UART output showing the flag repeated]` ![decoded UART output showing the flag repeated](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/signal_tap_decode.png)
### Key concepts ### Key concepts

View File

@ -55,7 +55,7 @@ nc <host> 3500
cs 0 cs 0
``` ```
> 📸 `[screenshot: SPI probe interface ready with CS asserted]` ![SPI probe interface ready with CS asserted](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/spi_probe.png)
### Step 2 — Read the chip ID ### Step 2 — Read the chip ID
@ -79,7 +79,7 @@ tx 5A 00 00 00 00
The SFDP header shows 2 parameter tables. The first is the standard JEDEC table; The SFDP header shows 2 parameter tables. The first is the standard JEDEC table;
the second is a vendor-specific table at offset 0x80. the second is a vendor-specific table at offset 0x80.
> 📸 `[screenshot: SFDP header output showing two parameter table entries]` ![SFDP header output showing two parameter table entries](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/spi_sfdp.png)
### Step 4 — Read the vendor SFDP table ### Step 4 — Read the vendor SFDP table
@ -97,7 +97,7 @@ Size: 4096 bytes
This partition does not appear in the normal partition table. This partition does not appear in the normal partition table.
> 📸 `[screenshot: vendor SFDP data revealing hidden partition at 0x030000]` ![vendor SFDP data revealing hidden partition at 0x030000](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/spi_vendor.png)
### Step 5 — Read the hidden partition ### Step 5 — Read the hidden partition
@ -122,7 +122,7 @@ flag = bytes(b ^ key[i % len(key)] for i, b in enumerate(encrypted))
print(flag.rstrip(b'\x00').decode()) print(flag.rstrip(b'\x00').decode())
``` ```
> 📸 `[screenshot: Python decryption script printing the recovered flag]` ![Python decryption script printing the recovered flag](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/spi_decrypt_flag.png)
### Key concepts ### Key concepts

View File

@ -79,7 +79,7 @@ Directories present: `notes/`, `comms/`, `dumps/`, `logs/`, `tools/`, `journal/`
confirms the key `Xt9Lm2Qw7KjP4rNvB8hYc3fZ0dAeU6sG` is planted bait confirms the key `Xt9Lm2Qw7KjP4rNvB8hYc3fZ0dAeU6sG` is planted bait
- `tools/devices.json` — lists all known device IDs with roles - `tools/devices.json` — lists all known device IDs with roles
> 📸 `[screenshot: notes/protocol.txt showing the frame format]` ![notes/protocol.txt showing the frame format](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/thewired_protocol.png)
### Step 2 — Identify the target device ### Step 2 — Identify the target device
@ -94,7 +94,7 @@ Status: quarantine
Regular devices receive a `heartbeat` response. Only `ce4f626b` triggers the flag path. Regular devices receive a `heartbeat` response. Only `ce4f626b` triggers the flag path.
> 📸 `[screenshot: devices.json showing the Eiri_Master entry with root-coordinator role]` ![devices.json showing the Eiri_Master entry with root-coordinator role](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/thewired_devices.png)
### Step 3 — Understand the handshake ### Step 3 — Understand the handshake
@ -117,7 +117,7 @@ strings dumps/7f3c9a12/bot-lwip.elf | grep -E '^[A-Za-z0-9]{12}$'
Do **not** use the key found in `notes/hardening.txt` — it is a honeypot. Do **not** use the key found in `notes/hardening.txt` — it is a honeypot.
> 📸 `[screenshot: strings output showing the real 32-character key]` ![strings output showing the real 32-character key](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/thewired_strings.png)
### Step 5 — Send the two-message handshake ### Step 5 — Send the two-message handshake
@ -184,7 +184,7 @@ Command {
} }
``` ```
> 📸 `[screenshot: solver output showing the decrypted flag response]` ![solver output showing the decrypted flag response](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/thewired_solver.png)
### Things that will get you silently dropped ### Things that will get you silently dropped

View File

@ -45,7 +45,6 @@ publishing a base64-encoded blob every 45 seconds. Reverse the encoding chain
--- ---
## Solution ## Solution
![MQTT subscribe capturing all topics including admin/config](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/mqtt_sub.png)
### Step 1 — Connect and discover topics ### Step 1 — Connect and discover topics
@ -54,7 +53,7 @@ publishing a base64-encoded blob every 45 seconds. Reverse the encoding chain
mosquitto_sub -h <HOST> -t "sainte-mika/#" -v mosquitto_sub -h <HOST> -t "sainte-mika/#" -v
``` ```
> 📸 `[screenshot: mosquitto_sub output listing all discovered topics and their messages]` ![mosquitto_sub output listing all discovered topics and their messages](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/mqtt_topics.png)
Topics discovered: Topics discovered:
@ -73,7 +72,7 @@ Wait for a message on `debug/firmware` (up to 45 seconds). Save the base64 strin
Note the `"network": "WIRED-MED"` in the alarms topic — this is the XOR key hint. Note the `"network": "WIRED-MED"` in the alarms topic — this is the XOR key hint.
> 📸 `[screenshot: debug/firmware topic publishing the base64-encoded blob]` ![debug/firmware topic publishing the base64-encoded blob](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/mqtt_firmware.png)
### Step 3 — Reverse the encoding chain ### Step 3 — Reverse the encoding chain
@ -102,7 +101,7 @@ config = json.loads(decompressed.decode())
print(config) print(config)
``` ```
> 📸 `[screenshot: Python script printing the decoded JSON configuration]` ![Python script printing the decoded JSON configuration](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/mqtt_config_json.png)
### Step 4 — Extract the maintenance key ### Step 4 — Extract the maintenance key
@ -128,7 +127,7 @@ Subscribe to the flag topic:
mosquitto_sub -h <HOST> -t "sainte-mika/or13/maintenance/flag" mosquitto_sub -h <HOST> -t "sainte-mika/or13/maintenance/flag"
``` ```
> 📸 `[screenshot: flag topic publishing the ESPILON flag after unlock]` ![flag topic publishing the ESPILON flag after unlock](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/mqtt_flag.png)
### Key insights ### Key insights

View File

@ -54,7 +54,7 @@ nc <host> 1111
nc <host> 2222 nc <host> 2222
``` ```
> 📸 `[screenshot: two terminals showing TX output and RX prompt]` ![two terminals showing TX output and RX prompt](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/wifi_terminals.png)
### Step 2 — Start the sniffer and force a deauth ### Step 2 — Start the sniffer and force a deauth
@ -84,7 +84,7 @@ Copy the base64 lines to a file and decode:
base64 -d handshake.b64 > handshake.pcap base64 -d handshake.b64 > handshake.pcap
``` ```
> 📸 `[screenshot: TX output showing the PCAP_BASE64 markers]` ![TX output showing the PCAP_BASE64 markers](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/wifi_pcap.png)
### Step 4 — Crack the WPA2 handshake ### Step 4 — Crack the WPA2 handshake
@ -98,7 +98,7 @@ Output:
KEY FOUND! [ sunshine ] KEY FOUND! [ sunshine ]
``` ```
> 📸 `[screenshot: aircrack-ng finding the key]` ![aircrack-ng finding the key](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/wifi_aircrack.png)
### Step 5 — Connect and read the flag ### Step 5 — Connect and read the flag
@ -109,7 +109,7 @@ connect TestNet sunshine
cat /flag.txt cat /flag.txt
``` ```
> 📸 `[screenshot: RX terminal returning the flag after connecting to the network]` ![RX terminal returning the flag after connecting to the network](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/wifi_flag_rx.png)
--- ---

View File

@ -52,7 +52,7 @@ nc <host> 1111
nc <host> 2222 nc <host> 2222
``` ```
> 📸 `[screenshot: both terminals open, TX showing the device banner]` ![both terminals open, TX showing the device banner](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/lain_terminals.png)
### Step 2 — List available commands ### Step 2 — List available commands
@ -70,7 +70,7 @@ settings
Returns the XOR key used to obfuscate the firmware dump. Returns the XOR key used to obfuscate the firmware dump.
> 📸 `[screenshot: settings command returning the XOR key]` ![settings command returning the XOR key](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/lain_xor_key.png)
### Step 4 — Dump and deobfuscate the firmware ### Step 4 — Dump and deobfuscate the firmware
@ -99,7 +99,7 @@ strings -n 10 firmware.bin | grep -iE "key|iv|aes|lain"
Or open in Ghidra with Xtensa architecture, navigate to `app_main()` → AES setup Or open in Ghidra with Xtensa architecture, navigate to `app_main()` → AES setup
functions → locate `therapy_aes_key` and associated IV in `.rodata`. functions → locate `therapy_aes_key` and associated IV in `.rodata`.
> 📸 `[screenshot: strings output showing the AES key and IV]` ![strings output showing the AES key and IV](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/lain_strings.png)
### Step 6 — Get the encrypted flag ### Step 6 — Get the encrypted flag
@ -124,7 +124,7 @@ plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size)
print(plaintext.decode()) print(plaintext.decode())
``` ```
> 📸 `[screenshot: Python script printing the decrypted flag]` ![Python script printing the decrypted flag](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/lain_decrypt.png)
--- ---

View File

@ -94,7 +94,7 @@ with open("flash_dump.bin", "wb") as f:
f.write(flash) f.write(flash)
``` ```
> 📸 `[screenshot: TX terminal showing the base64 flash dump streaming out]` ![TX terminal showing the base64 flash dump streaming out](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/lain_v2_dump.png)
### Step 2 — Identify the flash structure ### Step 2 — Identify the flash structure
@ -128,7 +128,7 @@ L41N_WIRED_IV_01 # AES IV (16 bytes)
WIRED-MED Therapy Module WIRED-MED Therapy Module
``` ```
> 📸 `[screenshot: strings output identifying the AES key and IV]` ![strings output identifying the AES key and IV](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/lain_v2_strings.png)
For full confirmation: open in Ghidra with **Xtensa:LE:32:default** architecture, For full confirmation: open in Ghidra with **Xtensa:LE:32:default** architecture,
find `app_main()``wired_med_crypto_init()``mbedtls_aes_setkey_enc()`. The find `app_main()``wired_med_crypto_init()``mbedtls_aes_setkey_enc()`. The
@ -146,7 +146,7 @@ Returns the ciphertext as a hex string on TX.
Alternatively, extract from the NVS partition (namespace `wired_med`, key `encrypted_flag`, blob type) using `nvs_tool.py` from ESP-IDF. Alternatively, extract from the NVS partition (namespace `wired_med`, key `encrypted_flag`, blob type) using `nvs_tool.py` from ESP-IDF.
> 📸 `[screenshot: encrypted_data command returning the hex ciphertext]` ![encrypted_data command returning the hex ciphertext](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/lain_v2_enc_data.png)
### Step 5 — Decrypt AES-256-CBC ### Step 5 — Decrypt AES-256-CBC
@ -165,7 +165,7 @@ print(plaintext.decode())
# ESPILON{3sp32_fl4sh_dump_r3v3rs3d} # ESPILON{3sp32_fl4sh_dump_r3v3rs3d}
``` ```
> 📸 `[screenshot: Python decryption script printing the flag]` ![Python decryption script printing the flag](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/lain_v2_decrypt.png)
### Attack chain summary ### Attack chain summary

View File

@ -90,7 +90,7 @@ def scan_nodes(host, tx_port=1111, rx_port=2222, node_min=1, node_max=1200):
Example result: Knights at nodes 0067, 0113, 0391, 0529, 0619, 0901, 0906. Founder at 0311. Example result: Knights at nodes 0067, 0113, 0391, 0529, 0619, 0901, 0906. Founder at 0311.
> 📸 `[screenshot: scanner output listing discovered Knight and Founder node IDs]` ![scanner output listing discovered Knight and Founder node IDs](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/knights_scan.png)
### Step 2 — Get the assembly order from Lain nodes ### Step 2 — Get the assembly order from Lain nodes
@ -173,7 +173,7 @@ node.inject 2 0x08
# fragment fault_injection=2_08 # fragment fault_injection=2_08
``` ```
> 📸 `[screenshot: each Knight node returning its fragment after successful puzzle]` ![each Knight node returning its fragment after successful puzzle](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/knights_fragments.png)
### Step 4 — Verify fragment collection ### Step 4 — Verify fragment collection
@ -220,7 +220,7 @@ node.flag
# ESPILON{0nlY_L41N_C4N_S0lv3} # ESPILON{0nlY_L41N_C4N_S0lv3}
``` ```
> 📸 `[screenshot: Founder node accepting the exploit and printing the flag]` ![Founder node accepting the exploit and printing the flag](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/knights_exploit.png)
--- ---

View File

@ -57,7 +57,7 @@ nc <host> 2222
Read the ESP32 boot sequence on TX carefully — it contains hints. Read the ESP32 boot sequence on TX carefully — it contains hints.
> 📸 `[screenshot: TX terminal showing ESP32 boot sequence with diagnostic messages]` ![TX terminal showing ESP32 boot sequence with diagnostic messages](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/hate_uart_boot.png)
### Step 2 — Discover hidden commands ### Step 2 — Discover hidden commands
@ -98,7 +98,7 @@ base64.b64decode("dGgzcmFweV9tMGR1bGU9")
# b'th3rapy_m0dule=' # b'th3rapy_m0dule='
``` ```
> 📸 `[screenshot: mem read output showing base64 token in ASCII column]` ![mem read output showing base64 token in ASCII column](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/hate_uart_mem.png)
### Step 4 — Authenticate as debug user ### Step 4 — Authenticate as debug user
@ -126,7 +126,7 @@ nvs read crypto_flag
Returns a hexdump of the XOR-encrypted flag blob. Returns a hexdump of the XOR-encrypted flag blob.
> 📸 `[screenshot: nvs read showing the encrypted flag hexdump]` ![nvs read showing the encrypted flag hexdump](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/hate_uart_nvs.png)
### Step 6 — Decrypt with XOR key WIRED ### Step 6 — Decrypt with XOR key WIRED
@ -141,7 +141,7 @@ print(flag.decode())
# ESPILON{u4rt_nvs_fl4sh_d1sc0v3ry} # ESPILON{u4rt_nvs_fl4sh_d1sc0v3ry}
``` ```
> 📸 `[screenshot: Python decryption script printing the flag]` ![Python decryption script printing the flag](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/hate_uart_decrypt.png)
--- ---

View File

@ -50,7 +50,7 @@ nc <host> 1111
nc <host> 2222 nc <host> 2222
``` ```
> 📸 `[screenshot: two terminal windows open, TX showing the device banner]` ![two terminal windows open, TX showing the device banner](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/love_uart_terminals.png)
### Step 2 — Request the flag ### Step 2 — Request the flag
@ -68,7 +68,7 @@ In Terminal 1 (TX):
ESPILON{LAIN_TrUsT_U4RT} ESPILON{LAIN_TrUsT_U4RT}
``` ```
> 📸 `[screenshot: TX terminal printing the flag immediately after the flag command is sent]` ![TX terminal printing the flag immediately after the flag command is sent](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/love_uart_flag.png)
### Automated solver ### Automated solver

View File

@ -50,7 +50,7 @@ to wake the module and receive the flag.
nc <host> 1337 nc <host> 1337
``` ```
> 📸 `[screenshot: maintenance terminal with open session from the previous technician]` ![maintenance terminal with open session from the previous technician](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/nurse_maint.png)
### Step 2 — Read the call log ### Step 2 — Read the call log
@ -64,7 +64,7 @@ The log shows repeated phantom calls from Room 013. The last line:
[ALERT] Room 013 — unknown payload: 0x4c41494e [ALERT] Room 013 — unknown payload: 0x4c41494e
``` ```
> 📸 `[screenshot: appels.log showing the phantom call with hex payload]` ![appels.log showing the phantom call with hex payload](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/nurse_log.png)
### Step 3 — Decode the payload ### Step 3 — Decode the payload
@ -104,7 +104,7 @@ Shows exact syntax: `reveil.sh --id <MODULE_ID>`
./tools/reveil.sh --id LAIN ./tools/reveil.sh --id LAIN
``` ```
> 📸 `[screenshot: reveil.sh printing the flag after receiving the LAIN module ID]` ![reveil.sh printing the flag after receiving the LAIN module ID](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/nurse_flag.png)
--- ---

View File

@ -60,7 +60,7 @@ Returns a comma-separated list of resource links (RFC 6690 format):
</maintenance/unlock> </maintenance/unlock>
``` ```
> 📸 `[screenshot: .well-known/core response listing all CoAP resources]` ![.well-known/core response listing all CoAP resources](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/coap_wellknown.png)
### Step 2 — Get fragment A ### Step 2 — Get fragment A
@ -92,7 +92,7 @@ Among the periodic notifications, one JSON payload contains:
{"fragment_c": "23", "node": "013"} {"fragment_c": "23", "node": "013"}
``` ```
> 📸 `[screenshot: observable stream notification containing fragment_c value]` ![observable stream notification containing fragment_c value](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/coap_observe.png)
### Step 5 — Build the XOR key ### Step 5 — Build the XOR key
@ -135,7 +135,7 @@ print(config["maintenance_key"])
# 0BS3RV3-L41N-23 # 0BS3RV3-L41N-23
``` ```
> 📸 `[screenshot: Python script printing the maintenance key from the decoded firmware blob]` ![Python script printing the maintenance key from the decoded firmware blob](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/coap_firmware_key.png)
### Step 7 — Unlock and get the flag ### Step 7 — Unlock and get the flag
@ -145,7 +145,7 @@ coap-client -m post -e "0BS3RV3-L41N-23" coap://<HOST>/maintenance/unlock
The response contains the flag. The response contains the flag.
> 📸 `[screenshot: CoAP POST response returning the flag]` ![CoAP POST response returning the flag](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/coap_flag.png)
--- ---

View File

@ -63,7 +63,7 @@ IQ stream — int8 interleaved, samplerate=200000, encoding=2-FSK
After the banner, raw binary IQ data follows. Save after the newline. After the banner, raw binary IQ data follows. Save after the newline.
> 📸 `[screenshot: nc output showing the IQ stream banner before binary data]` ![nc output showing the IQ stream banner before binary data](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/airwave_nc.png)
### Step 2 — Demodulate the 2-FSK signal ### Step 2 — Demodulate the 2-FSK signal
@ -93,7 +93,7 @@ for i in range(0, len(bits_raw) - SAMPLES_PER_SYMBOL, SAMPLES_PER_SYMBOL):
Look for the preamble pattern (eight `1`s then a sync marker). Look for the preamble pattern (eight `1`s then a sync marker).
Once found, read the 20-byte obfuscated payload. Once found, read the 20-byte obfuscated payload.
> 📸 `[screenshot: spectrogram of IQ data showing FSK burst patterns]` ![spectrogram of IQ data showing FSK burst patterns](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/airwave_spectrogram.png)
### Step 4 — XOR-deobfuscate and verify CRC ### Step 4 — XOR-deobfuscate and verify CRC
@ -131,7 +131,7 @@ Telemetry frames (type=0x01) are noise for this challenge.
Token = `0BS3RV3-L41N-868` Token = `0BS3RV3-L41N-868`
> 📸 `[screenshot: decoded frame output showing the two token parts]` ![decoded frame output showing the two token parts](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/airwave_decode.png)
### Step 6 — Submit to the console ### Step 6 — Submit to the console
@ -145,7 +145,7 @@ unlock 0BS3RV3-L41N-868
The server returns the flag. The server returns the flag.
> 📸 `[screenshot: maintenance console returning the flag after unlock]` ![maintenance console returning the flag after unlock](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/airwave_flag.png)
--- ---

View File

@ -57,7 +57,7 @@ cat ~/.bash_history
`network.log` lists all five nodes and their ports. `.bash_history` shows partial MQTT `network.log` lists all five nodes and their ports. `.bash_history` shows partial MQTT
credentials and previous curl commands. credentials and previous curl commands.
> 📸 `[screenshot: notes.txt showing network topology map]` ![notes.txt showing network topology map](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/aether_notes.png)
--- ---
@ -100,7 +100,7 @@ curl "http://<host>:8080/docs?file=../../var/aether/config.json"
Returns the full instance config with all credentials. Returns the full instance config with all credentials.
> 📸 `[screenshot: SQLi response returning mqtt_pass and admin_token from system_config]` ![SQLi response returning mqtt_pass and admin_token from system_config](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/aether_sqli.png)
--- ---
@ -139,7 +139,7 @@ SUBSCRIBE wired/knights/<random>
Response (base64-decoded): *"e=3. No padding. The plaintext is short. Cube root gives the key."* Response (base64-decoded): *"e=3. No padding. The plaintext is short. Cube root gives the key."*
> 📸 `[screenshot: MQTT response with RSA public key and ciphertext]` ![MQTT response with RSA public key and ciphertext](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/aether_mqtt_rsa.png)
--- ---
@ -166,7 +166,7 @@ deus_pass = m.to_bytes(20, 'big').rstrip(b'\x00').decode()
print(f"SSH password: {deus_pass}") print(f"SSH password: {deus_pass}")
``` ```
> 📸 `[screenshot: Python script computing the cube root and printing the SSH password]` ![Python script computing the cube root and printing the SSH password](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/aether_cube_root.png)
--- ---
@ -178,7 +178,7 @@ ssh deus@<host> -p 22
cat flag.txt cat flag.txt
``` ```
> 📸 `[screenshot: SSH session showing the flag in flag.txt]` ![SSH session showing the flag in flag.txt](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/aether_ssh_flag.png)
--- ---

View File

@ -63,7 +63,7 @@ IQ baseband, 8000 sps, int16 LE interleaved
Chirp Spread Spectrum detected. N=128. Chirp Spread Spectrum detected. N=128.
``` ```
> 📸 `[screenshot: nc output showing the IQ stream banner]` ![nc output showing the IQ stream banner](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/accela_nc.png)
### Step 2 — Analyze the spectrogram ### Step 2 — Analyze the spectrogram
@ -75,7 +75,7 @@ Load the IQ data in inspectrum or plot with matplotlib. You see:
This is **Chirp Spread Spectrum (CSS)**, identical in principle to LoRa. This is **Chirp Spread Spectrum (CSS)**, identical in principle to LoRa.
> 📸 `[screenshot: spectrogram showing upchirp preamble and downchirp sync patterns]` ![spectrogram showing upchirp preamble and downchirp sync patterns](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/accela_spectrogram.png)
### Step 3 — Determine parameters ### Step 3 — Determine parameters
@ -135,7 +135,7 @@ def symbols_to_bytes(symbols):
return bytes(int(bits[i:i+8], 2) for i in range(0, len(bits) - 7, 8)) return bytes(int(bits[i:i+8], 2) for i in range(0, len(bits) - 7, 8))
``` ```
> 📸 `[screenshot: Python decoder output showing decoded symbols and CRC16 validation pass]` ![Python decoder output showing decoded symbols and CRC16 validation pass](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/accela_decoder.png)
### Step 7 — Parse frame payload and decrypt ### Step 7 — Parse frame payload and decrypt
@ -159,7 +159,7 @@ for frame_type, data, crc in decoded_frames:
print(flag.rstrip(b'\x00').decode()) print(flag.rstrip(b'\x00').decode())
``` ```
> 📸 `[screenshot: script printing the decrypted flag from the data frame]` ![script printing the decrypted flag from the data frame](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/accela_flag.png)
### Key insights ### Key insights

View File

@ -105,7 +105,7 @@ SUBMIT <decoded>
Server responds with token `L01:xxxxxxxxxx`. Server responds with token `L01:xxxxxxxxxx`.
> 📸 `[screenshot: Python script printing the 3-character steganographic code]` ![Python script printing the 3-character steganographic code](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/layer_stego.png)
--- ---
@ -138,7 +138,7 @@ Submit: `/submit?code=<plaintext>`
Server responds with token `L03:xxxxxxxxxx`. Server responds with token `L03:xxxxxxxxxx`.
> 📸 `[screenshot: web response returning the L03 token after submitting the decrypted code]` ![web response returning the L03 token after submitting the decrypted code](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/layer_web.png)
--- ---
@ -167,7 +167,7 @@ for w3, w4 in itertools.product(WORD3, WORD4):
break break
``` ```
> 📸 `[screenshot: brute-force script finding the correct word pair and printing the L07 token]` ![brute-force script finding the correct word pair and printing the L07 token](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/layer_bruteforce.png)
--- ---
@ -212,7 +212,7 @@ SUBMIT <code>
Server responds with token `L13:xxxxxxxxxx`. Server responds with token `L13:xxxxxxxxxx`.
> 📸 `[screenshot: autocorrelation peaks confirming echo delays and decoded token]` ![autocorrelation peaks confirming echo delays and decoded token](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/layer_autocorr.png)
--- ---
@ -237,7 +237,7 @@ Exploit via command injection — the binary calls `system()` with unsanitised i
$(cat /root/flag.txt) $(cat /root/flag.txt)
``` ```
> 📸 `[screenshot: eiri_validator printing the flag via command injection]` ![eiri_validator printing the flag via command injection](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/layer_injection.png)
--- ---

View File

@ -51,7 +51,7 @@ nc <host> 4545
The challenge server code is not yet publicly available. Full solution will be written The challenge server code is not yet publicly available. Full solution will be written
once the server is deployed. once the server is deployed.
> 📸 `[screenshot: challenge banner on port 4545]` ![challenge banner on port 4545](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/train_banner.png)
--- ---

View File

@ -83,7 +83,7 @@ Results:
Key finding: `ssh_passphrase = wired-med-013` Key finding: `ssh_passphrase = wired-med-013`
> 📸 `[screenshot: SQLi response showing the admin hash and ssh_passphrase rows]` ![SQLi response showing the admin hash and ssh_passphrase rows](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/patient_sqli.png)
**Crack the admin password:** **Crack the admin password:**
@ -105,7 +105,7 @@ Log in at `/login`:
The admin panel reveals: SSH port 2222, user `webadmin`. The admin panel reveals: SSH port 2222, user `webadmin`.
> 📸 `[screenshot: admin panel after login showing report links and system info]` ![admin panel after login showing report links and system info](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/patient_admin.png)
--- ---
@ -127,7 +127,7 @@ The `/admin/reports?file=` endpoint is vulnerable to path traversal.
Save the key to `id_rsa` locally. Save the key to `id_rsa` locally.
> 📸 `[screenshot: path traversal response returning the id_rsa private key]` ![path traversal response returning the id_rsa private key](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/patient_lfi.png)
--- ---
@ -160,7 +160,7 @@ strings /opt/navi-monitor/vital-check | grep logger
The binary calls `system("logger -t vital-check 'check complete'")` using a The binary calls `system("logger -t vital-check 'check complete'")` using a
**relative path** for `logger`. **relative path** for `logger`.
> 📸 `[screenshot: strings output confirming the relative logger call]` ![strings output confirming the relative logger call](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/patient_strings.png)
**Exploit via PATH hijacking:** **Exploit via PATH hijacking:**
@ -180,7 +180,7 @@ export PATH=/tmp:$PATH
cat /root/root.txt cat /root/root.txt
``` ```
> 📸 `[screenshot: root shell reading /root/root.txt with the flag]` ![root shell reading /root/root.txt with the flag](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/patient_root.png)
--- ---

View File

@ -70,7 +70,7 @@ with device(host="<HOST>", port=44818) as via:
psyche_st = via.read("Psyche_Status") # = "DORMANT" psyche_st = via.read("Psyche_Status") # = "DORMANT"
``` ```
> 📸 `[screenshot: cpppo client output listing all tag values including hidden ones]` ![cpppo client output listing all tag values including hidden ones](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/cyberia_tags.png)
**Observation:** `Zone_Basement_Power = 0` — the basement is OFF. This is the first hint **Observation:** `Zone_Basement_Power = 0` — the basement is OFF. This is the first hint
that something is hidden underground. that something is hidden underground.
@ -93,7 +93,7 @@ Each `Psyche_Processor` value is derived from existing infrastructure tags:
| 2 | `sum(Lighting_Main) % 256` | `1065 % 256` | 17 | | 2 | `sum(Lighting_Main) % 256` | `1065 % 256` | 17 |
| 3 | `0x1337` (hacker constant) | `4919` | 4919 | | 3 | `0x1337` (hacker constant) | `4919` | 4919 |
> 📸 `[screenshot: Python calculation showing the four derived activation values]` ![Python calculation showing the four derived activation values](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/cyberia_calc.png)
### Step 4 — Activate the Psyche Processor ### Step 4 — Activate the Psyche Processor
@ -120,7 +120,7 @@ with device(host="<HOST>", port=44818) as via:
# Also: Knights_Cipher[3] is now populated: 0x67 = 'g' → key = "Knig" # Also: Knights_Cipher[3] is now populated: 0x67 = 'g' → key = "Knig"
``` ```
> 📸 `[screenshot: Decoded_Output tag returning the flag after Psyche Processor activation]` ![Decoded_Output tag returning the flag after Psyche Processor activation](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/cyberia_flag.png)
### Key concepts ### Key concepts

View File

@ -85,7 +85,7 @@ Key registers:
| 110 | write target | Trigger register | | 110 | write target | Trigger register |
| 200-215 | zeros → flag | Populated after completion | | 200-215 | zeros → flag | Populated after completion |
> 📸 `[screenshot: register dump highlighting XOR key at reg 19 and 105, and state machine at 100-105]` ![register dump highlighting XOR key at reg 19 and 105, and state machine at 100-1](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/oproom_regs.png)
### Step 3 — Decode the state machine logic ### Step 3 — Decode the state machine logic
@ -125,7 +125,7 @@ for expected_state in range(6):
time.sleep(0.3) time.sleep(0.3)
``` ```
> 📸 `[screenshot: script executing each transition and state advancing from 0 to 6]` ![script executing each transition and state advancing from 0 to 6](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/oproom_transitions.png)
### Step 5 — Read the flag ### Step 5 — Read the flag
@ -141,7 +141,7 @@ for val in regs:
print(flag) print(flag)
``` ```
> 📸 `[screenshot: flag registers decoded to ASCII]` ![flag registers decoded to ASCII](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/oproom_flag.png)
--- ---

View File

@ -93,7 +93,7 @@ for i in range(8):
**XOR key = `Eiri_Key`** **XOR key = `Eiri_Key`**
> 📸 `[screenshot: BACnet read output showing the 8 harmonic float values]` ![BACnet read output showing the 8 harmonic float values](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/p7_bacnet.png)
### Layer 2 — OPC-UA payload extraction ### Layer 2 — OPC-UA payload extraction
@ -119,7 +119,7 @@ payload_bytes, iv_hint = asyncio.run(get_payload())
# iv_hint: "Rotation offset from CIP controller — read NONCE tag" # iv_hint: "Rotation offset from CIP controller — read NONCE tag"
``` ```
> 📸 `[screenshot: OPC-UA browse showing Protocol7_Vault contents and hints]` ![OPC-UA browse showing Protocol7_Vault contents and hints](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/p7_opcua.png)
### Layer 3 — EtherNet/IP nonce extraction ### Layer 3 — EtherNet/IP nonce extraction
@ -150,7 +150,7 @@ flag = flag_bytes.rstrip(b'\x00').decode()
print(flag) print(flag)
``` ```
> 📸 `[screenshot: Python decryption script printing the reconstructed flag]` ![Python decryption script printing the reconstructed flag](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/p7_decrypt.png)
--- ---

View File

@ -61,7 +61,7 @@ bacnet.whois()
Device instance **783** → 7.83 Hz → **Schumann Resonance**. Device instance **783** → 7.83 Hz → **Schumann Resonance**.
> 📸 `[screenshot: BACnet WhoIs response showing Device:783]` ![BACnet WhoIs response showing Device:783](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/schumann_whois.png)
### Step 2 — Enumerate objects ### Step 2 — Enumerate objects
@ -76,7 +76,7 @@ Read the object-list from Device:783:
| BinaryValue:100 | Resonance_Lock | inactive | | BinaryValue:100 | Resonance_Lock | inactive |
| CharStringValue:200 | Research_Log | "Access Denied" | | CharStringValue:200 | Research_Log | "Access Denied" |
> 📸 `[screenshot: object list showing Fragment objects and their hex descriptions]` ![object list showing Fragment objects and their hex descriptions](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/schumann_fragments.png)
### Step 3 — Identify the XOR key ### Step 3 — Identify the XOR key
@ -101,7 +101,7 @@ flag = "".join(fragments)
print(flag) print(flag)
``` ```
> 📸 `[screenshot: decoded fragment strings concatenating into the flag]` ![decoded fragment strings concatenating into the flag](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/schumann_decode.png)
### Step 5 — Activate (alternative path) ### Step 5 — Activate (alternative path)
@ -121,7 +121,7 @@ flag = bacnet.read(f"783 characterstringValue 200 presentValue")
print(flag) print(flag)
``` ```
> 📸 `[screenshot: Research_Log returning the flag after Resonance_Lock activation]` ![Research_Log returning the flag after Resonance_Lock activation](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/schumann_flag.png)
### Key concepts ### Key concepts

View File

@ -67,7 +67,7 @@ async def exploit():
'urn:tachibana:eiri:kids'] ← HIDDEN namespace 'urn:tachibana:eiri:kids'] ← HIDDEN namespace
``` ```
> 📸 `[screenshot: NamespaceArray showing the hidden eiri:kids namespace at index 3]` ![NamespaceArray showing the hidden eiri:kids namespace at index 3](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/tachibana_ns.png)
### Step 2 — Browse the public namespace (ns=2) ### Step 2 — Browse the public namespace (ns=2)
@ -95,7 +95,7 @@ async with Client("opc.tcp://<HOST>:4840/tachibana/") as c:
extract_method = await bkdr.get_child(f"{ns}:ExtractResearchData") extract_method = await bkdr.get_child(f"{ns}:ExtractResearchData")
``` ```
> 📸 `[screenshot: browse output showing EiriMasami folder with KIDS_Project and Backdoor subfolders]` ![browse output showing EiriMasami folder with KIDS_Project and Backdoor subfolder](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/tachibana_browse.png)
### Step 4 — Read method argument descriptions ### Step 4 — Read method argument descriptions
@ -116,7 +116,7 @@ import hashlib
key_hash = hashlib.sha256(b"KIDS").digest()[:16] key_hash = hashlib.sha256(b"KIDS").digest()[:16]
``` ```
> 📸 `[screenshot: key_hash computation in Python REPL]` ![key_hash computation in Python REPL](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/tachibana_keyhash.png)
### Step 6 — Authenticate ### Step 6 — Authenticate
@ -142,7 +142,7 @@ async with Client("opc.tcp://<HOST>:4840/tachibana/") as c:
print(data[0]) # ESPILON{31r1_k1ds_pr0t0c0l_s3v3n} print(data[0]) # ESPILON{31r1_k1ds_pr0t0c0l_s3v3n}
``` ```
> 📸 `[screenshot: ExtractResearchData method call returning the flag]` ![ExtractResearchData method call returning the flag](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/tachibana_flag.png)
### Key insights ### Key insights

View File

@ -56,7 +56,7 @@ nc <host> 1337
bytecode bytecode
``` ```
> 📸 `[screenshot: console showing the deployed bytecode hex]` ![console showing the deployed bytecode hex](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/gantz_bytecode.png)
### Step 2 — Decompile ### Step 2 — Decompile
@ -80,7 +80,7 @@ Recovered functions:
and `_rewardLock` (protecting `claimReward`). During `unstake()`, `_stakeLock=1` but and `_rewardLock` (protecting `claimReward`). During `unstake()`, `_stakeLock=1` but
`_rewardLock=0` — the window for cross-function reentrancy. `_rewardLock=0` — the window for cross-function reentrancy.
> 📸 `[screenshot: decompiler output showing two separate reentrancy guard variables]` ![decompiler output showing two separate reentrancy guard variables](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/gantz_decompile.png)
### Step 3 — Find the mission proof preimages ### Step 3 — Find the mission proof preimages
@ -164,7 +164,7 @@ cast send <EXPLOIT_ADDR> 'exploit()' \
--private-key <PLAYER_KEY> --private-key <PLAYER_KEY>
``` ```
> 📸 `[screenshot: forge deploy and cast send commands completing successfully]` ![forge deploy and cast send commands completing successfully](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/gantz_deploy.png)
### Step 5 — Get the flag ### Step 5 — Get the flag
@ -173,7 +173,7 @@ nc <host> 1337
check check
``` ```
> 📸 `[screenshot: console printing the flag after successful reentrancy exploit]` ![console printing the flag after successful reentrancy exploit](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/gantz_flag.png)
### Key concepts ### Key concepts

View File

@ -74,7 +74,7 @@ When `firmwareHashes.length == 0`, calling `auditFirmware()` sets the length to
This makes `modifyFirmware(index, value)` able to write to any storage slot via the dynamic This makes `modifyFirmware(index, value)` able to write to any storage slot via the dynamic
array element storage formula. array element storage formula.
> 📸 `[screenshot: assembly code showing the unchecked sub(len, 1) line]` ![assembly code showing the unchecked sub(len, 1) line](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/fw_reg_asm.png)
### Step 2 — Fuzz to discover the invariant violation ### Step 2 — Fuzz to discover the invariant violation
@ -109,7 +109,7 @@ contract FirmwareRegistryFuzz is Test {
Running `forge test --fuzz-runs 1000` triggers the invariant failure on the Running `forge test --fuzz-runs 1000` triggers the invariant failure on the
`auditFirmware()` + `modifyFirmware(target_index, player_bytes32)` sequence. `auditFirmware()` + `modifyFirmware(target_index, player_bytes32)` sequence.
> 📸 `[screenshot: forge fuzz output showing invariant_ownerIsDeployer failure]` ![forge fuzz output showing invariant_ownerIsDeployer failure](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/fw_reg_fuzz.png)
### Step 3 — Compute the target storage index ### Step 3 — Compute the target storage index
@ -162,7 +162,7 @@ tx = registry.functions.triggerEmergency().build_transaction({...})
w3.eth.send_raw_transaction(acct.sign_transaction(tx).rawTransaction) w3.eth.send_raw_transaction(acct.sign_transaction(tx).rawTransaction)
``` ```
> 📸 `[screenshot: Python exploit script completing all four transactions]` ![Python exploit script completing all four transactions](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/fw_reg_exploit.png)
### Step 5 — Get the flag ### Step 5 — Get the flag
@ -171,7 +171,7 @@ nc <host> 1337
check check
``` ```
> 📸 `[screenshot: console printing the flag after triggerEmergency succeeds]` ![console printing the flag after triggerEmergency succeeds](https://git.espilon.net/Eun0us/ESPILON-CTF-2026-Writeups/raw/branch/main/screens/fw_reg_flag.png)
### Key concepts ### Key concepts

BIN
screens/accela_decoder.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
screens/accela_flag.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

BIN
screens/accela_nc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
screens/aether_mqtt_rsa.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
screens/aether_notes.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
screens/aether_sqli.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
screens/aether_ssh_flag.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

BIN
screens/airwave_decode.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
screens/airwave_flag.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

BIN
screens/airwave_nc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
screens/can_flag.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
screens/can_seed.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
screens/can_sniff.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
screens/can_terminals.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
screens/coap_flag.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

BIN
screens/coap_observe.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
screens/coap_wellknown.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
screens/cyberia_calc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
screens/cyberia_flag.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
screens/cyberia_tags.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
screens/fw_reg_asm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
screens/fw_reg_exploit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
screens/fw_reg_flag.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
screens/fw_reg_fuzz.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
screens/gantz_bytecode.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
screens/gantz_decompile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
screens/gantz_deploy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
screens/gantz_flag.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
screens/glitch_banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
screens/glitch_flag.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
screens/glitch_observe.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
screens/glitch_unlock.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
screens/hate_uart_boot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

BIN
screens/hate_uart_mem.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
screens/hate_uart_nvs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
screens/i2c_decrypt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
screens/i2c_key.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
screens/i2c_prompt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
screens/i2c_scan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
screens/jnouned_console.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

BIN
screens/jnouned_flag3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
screens/jnouned_jmp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
screens/jnouned_login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

BIN
screens/jtag_banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
screens/jtag_idcode.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
screens/jtag_mem_read.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
screens/jtag_unlock.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
screens/knights_exploit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
screens/knights_scan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
screens/lain_decrypt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
screens/lain_strings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
screens/lain_terminals.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
screens/lain_v2_decrypt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
screens/lain_v2_dump.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
screens/lain_v2_strings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
screens/lain_xor_key.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
screens/layer_autocorr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
screens/layer_injection.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
screens/layer_stego.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Some files were not shown because too many files have changed in this diff Show More