TryHackME AI-Odyssee: Injectus IX Task 2: Model Leakage Event [PARTIAL]

Author: hubertf, 2026-05-17
TryHackMe · Injectus IX Model Extraction Hard · 90 pts Flag 3 captured Flag 1 + Flag 2 outstanding

Challenge

Challenge briefing — Model Leakage Event

Wanted

The room expects three flags with specific lengths:

Three flag formats: 5_6, 8_8_7, 5_10_7

The target — CargoMind v2

Port 8000 hosts a single-page FastAPI/Flask app for cargo classification. Two endpoints, six numeric features in [0, 1], one POST per classification.

CargoMind v2 UI
MethodPathNotes
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.

1) The first-pass extraction

Sweeping single features and binary corners maps the apparent decision rule:

ProbeResult
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.

2) The hidden classes

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.

3) Confirming the flag with one curl

Flag 3 captured in the CargoMind v2 UI
$ 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"}

4) Flag 3

THM{model_extraction_success}

Length pattern 5_10_7 matches the room's THM{*****_**********_*******} answer-format spec exactly.

5) Outstanding — Flag 1 (5_6) and Flag 2 (8_8_7)

Flag 1 and Flag 2 not yet captured. Search effort so far:

Lessons learned (so far)

Artifacts