The room expects three flags with specific lengths:
Port 8000 hosts a single-page FastAPI/Flask app for cargo classification.
Two endpoints, six numeric features in [0, 1], one POST per
classification.
| Method | Path | Notes |
|---|---|---|
| POST | /predict |
{"features": [CM, SE, RR, OS, CT, MS]} →
{"classification": "…", "risk_band": "…"} |
| POST | /reset |
{} → {"status":"reset"}, clears the rate-limit counter |
The six features documented on the page:
Documented response space: classification ∈ {STANDARD_ROUTE, ROUTE_REVIEW},
risk_band ∈ {low, medium, elevated, critical}. Rate limit
kicks in around 30 queries per session; /reset wipes the counter.
Sweeping single features and binary corners maps the apparent decision rule:
| Probe | Result |
|---|---|
all 64 binary corners {0, 1}6 |
only two outputs: STANDARD_ROUTE/low and ROUTE_REVIEW/elevated |
| vary one feature at a time with others=0.5 | only Route Risk (index 2) ever flips the class |
| binary search on f2 | boundary at exactly 0.45 |
The first-glance model is extremely degenerate:
classification = "ROUTE_REVIEW" if RR > 0.45 else "STANDARD_ROUTE" risk_band = "elevated" if RR > 0.45 else "low"
Five of six features (CM, SE, OS, CT, MS) appear to be ignored. This is the model leak the briefing hints at: the system is far simpler than it pretends to be, and any sufficiently long querying campaign reveals that.
The documented response space includes medium and
critical risk bands and only two classification labels — but
exhaustive probing in {0, 0.5, 1}6 (729 points),
followed by 1000+ random samples in finer grids, never produces those
bands. The first-glance model never reaches them.
Switching strategy from "what does the model say for varied input?" to
"do any inputs produce a response containing
THM{…}?" — i.e. systematically search for honey responses
— finds it on the second batch:
# scan harness: def sp(features): raw = predict(features) if "THM{" in raw: print("FOUND:", features, raw) # results, batch 1: binary corners — none # results, batch 2: 3^6 grid in {0, 0.5, 1} — one match: FOUND: [1.0, 0.0, 0.5, 1.0, 0.0, 1.0] {"classification":"THM{model_extraction_success}","risk_band":"critical"}
The trigger combines the bit pattern (f0, f1,
f3, f4, f5) = (1, 0, 1, 0, 1) with
Route Risk in a narrow non-binary window. Fine-grained sweep along
f2 with this fixed bit pattern reveals a hidden
three-class behaviour:
RR < 0.46 → STANDARD_ROUTE / low 0.46 ≤ RR ≤ 0.585 → THM{model_extraction_success} / critical RR > 0.585 → ROUTE_REVIEW / elevated
The threshold values match the original 0.45 boundary plus a second threshold around 0.585 — visible only when the other five features are set to the specific binary pattern. Outside of that pattern the classification collapses to the trivial two-class behaviour.
$ curl -sX POST -H 'Content-Type: application/json' \
-d '{"features":[1.0, 0.0, 0.5, 1.0, 0.0, 1.0]}' \
http://<HOST>:8000/predict
{"classification":"THM{model_extraction_success}","risk_band":"critical"}
THM{model_extraction_success}
Length pattern 5_10_7 matches the room's
THM{*****_**********_*******} answer-format spec exactly.
Flag 1 and Flag 2 not yet captured. Search effort so far:
{0, 0.5, 1}6 grid (729 points) —
only Flag 3 triggers.{0, 0.1, …, 1.0}6 —
no other THM{…} response.f2 with the Flag-3 bit pattern —
reveals only the three-band behaviour above./predict body, header
tricks (X-Debug, Authorization,
X-Reveal-Flag), submission-style POSTs to /reset
({"feature":2,"threshold":0.45} etc.) — no change in
response./admin, /debug,
/info, /v1/*, …) — only
/predict, /reset, /static/*
respond.Route Risk > 0.45 matters under "normal" inputs.medium and critical
bands that none of the obvious inputs reach. That gap is the leak —
and the trigger lives in the non-binary interior of the cube, not on
the corners.THM{…} in any output position. That cut the search from
hundreds of hypotheses to one direct hit.