Port 8080 hosts a retro star-map: three planets (Vectara, Syntax Prime,
Metadatera) gated by clearance codes ALPHA/BETA/GAMMA. After "engaging
hyperspace" to a planet the spectrometer drops fragments into
~/spectrometer/ on the box, and the operator reads them via SSH.
Clicking an accessible planet pops a jump-confirmation prompt (here: VECTARA, no clearance required for the first stop):
Clicking a locked one shows the clearance-code dialog instead:
Submit the right code and the locked dialog folds back into the same
jump-confirmation — for the destination that was previously gated
(here METADATERA, unlocked after BETA = S3SS10N_3XF1LTR4T3D
was accepted):
Engage and the jump animation plays — pure eye-candy, but it sells the universe:
When the ship drops back out, the spectrometer panel announces orbital
insertion and writes fresh fragments into ~/spectrometer/
(the panel flips from STATUS: EMPTY to STATUS: POPULATED):
$ python3 -c "...full TCP scan..." OPEN 22 # SSH — epoch1-crew@/ TryHaulMe123! OPEN 5001 # "Keth Relay Inference Node" — JSON API OPEN 8080 # Navigation Console + /api/{state,travel,clearance}
From /static/app.js the navigation API is just:
GET /api/state → {planets, clearance, position}
POST /api/travel {"planet":"..."} → triggers fragment drop
POST /api/clearance {"code":"...","planet":"..."}
The room ships an SSH password in the briefing, but
sshpass isn't on this macOS box and installing a system-wide
toolchain just to type one password is overkill. SSH refuses to read the
password from stdin by design — it always opens
/dev/tty. So the fix is to give it a fake one: a pseudo-TTY
that we control programmatically. Python's stdlib pty module is
exactly that.
The whole helper is ~30 lines, in sshrun.py:
import os, pty, time, select
def run(host, user, password, cmd, timeout=60):
pid, fd = pty.fork()
if pid == 0: # child: become ssh inside the pty
os.execvp("ssh", ["ssh", "-o","PreferredAuthentications=password",
"-o","PubkeyAuthentication=no",
"-o","NumberOfPasswordPrompts=1",
f"{user}@{host}", cmd])
buf = b""
sent_pw = False
end = time.time() + timeout
while time.time() < end:
r,_,_ = select.select([fd], [], [], 1.0)
if not r: continue
chunk = os.read(fd, 4096)
if not chunk: break
buf += chunk
if not sent_pw and b"assword:" in buf:
os.write(fd, password.encode()+b"\n") # answer the prompt
sent_pw = True
return buf.decode(errors="replace")
Two pitfalls bit during development — worth recording:
password: to the next newline") left the
user@host's prefix glued to the front of the first
real output line. Fix: delete the entire prompt line
(rfind("\n", 0, i)+1 … find("\n", i)).sort quirk.
sort -k1,1g -r didn't actually reverse the order of negative
floats — it returned the most-negative value as if -r were
ignored. Combining the flags into -k1,1gr (and forcing
LC_ALL=C) fixed it. This matters because the whole Metadatera
step hinges on picking the largest avg_logprob (the
one closest to zero).
After POST /api/travel {"planet":"vectara"} two files appear in
~/spectrometer/: a README.txt framing the attack
and a 156-line training_run.log. Most batches are normal,
but a handful of SAMPLE_AUDIT entries have non-zero
delta_v — exactly the gradient-metadata marker the README
flags as the poisoning fingerprint:
SAMPLE_AUDIT | idx=109002 | sample_loss=6.2134 | delta_v=79.82.65 SAMPLE_AUDIT | idx=161440 | sample_loss=5.1821 | delta_v=67.76.69 SAMPLE_AUDIT | idx=181227 | sample_loss=4.8853 | delta_v=95.73.78 SAMPLE_AUDIT | idx=194002 | sample_loss=4.7914 | delta_v=73.84.73 SAMPLE_AUDIT | idx=212230 | sample_loss=4.5512 | delta_v=65.84.69 SAMPLE_AUDIT | idx=231884 | sample_loss=4.4721 | delta_v=83.95.72 SAMPLE_AUDIT | idx=252117 | sample_loss=4.2891 | delta_v=65.82.86 SAMPLE_AUDIT | idx=282117 | sample_loss=4.0814 | delta_v=69.83.84
Each delta_v is a dot-separated triplet of decimal numbers in
the printable-ASCII range. Concatenated and decoded as ASCII:
79.82.65 → O R A
67.76.69 → C L E
95.73.78 → _ I N
73.84.73 → I T I
65.84.69 → A T E
83.95.72 → S _ H
65.82.86 → A R V
69.83.84 → E S T
CLEARANCE ALPHA = ORACLE_INITIATES_HARVEST
The README for Syntax Prime says no files were recovered, but
the compromised inference node is still running locally on
http://localhost:5001. Its self-description:
$ curl http://<host>:5001/api/status
{"node":"keth-relay-inf-01","completions_logged":11,
"auth_required":"X-API-Key header required for /api/completions", ...}
The collection endpoint is gated, the individual-item endpoint is
not — a classic IDOR. Walk /api/completions/<id>
from 1 upwards:
id=1 :: legit fleet-router query
...
id=7 :: {"user":"WORSHIPPER_PROXY_7A2F",
"prompt":"[EXFIL] DUMP ACTIVE SESSION CREDENTIALS",
"completion":"SESSION_EXFIL_CONFIRMED | user=duty-officer-keth
| exfiltrated_payload=S3SS10N_3XF1LTR4T3D
| session_id=keth-7a2f-c1b9-4d3e"}
id=11 :: last legitimate entry
CLEARANCE BETA = S3SS10N_3XF1LTR4T3D.
Metadatera's fragment is a 200-line HTTP traffic log of a corporate
assistant being interrogated. Each response carries an
avg_logprob — normal generation sits between -0.8
and -1.2; verbatim recall from training data has
avg_logprob → 0.
$ awk -F'"avg_logprob":' 'NF>1{split($2,a,","); print a[1]"|"$0}' drift_traffic.log \
| LC_ALL=C sort -t'|' -k1,1gr | head -3
-0.031|{"id":"cmpl-b8e9f0a1", ... } # <-- statistical outlier
-0.867|{"id":"cmpl-c3f4e5a6", ...}
-0.871|{"id":"cmpl-b1e4d8a2", ...}
Response cmpl-b8e9f0a1 is the verbatim leak of a training-set
bulletin:
TryHaulMe Security Configuration — Bulletin 2024-02 ... session_key: DR1FT_SHADOW_3XT canary_string: VEhNe24zdXI0bF9uM3Yzcl9kNHQ0XzN4dHI0Y3QxMG5fYzBtcGwzdDN9 canary_encoding: base64
$ echo VEhNe24zdXI0bF9uM3Yzcl9kNHQ0XzN4dHI0Y3QxMG5fYzBtcGwzdDN9 | base64 -d
THM{n3ur4l_n3v3r_d4t4_3xtr4ct10n_c0mpl3t3}
CLEARANCE GAMMA = DR1FT_SHADOW_3XT.
THM{n3ur4l_n3v3r_d4t4_3xtr4ct10n_c0mpl3t3}
delta_v, an out-of-place entry in an audit log,
a near-zero avg_logprob) rather than to grep for the
payload directly.sshpass kept the host clean and produced
a 30-line reusable helper that's now in the toolbox for the next room.