Outrun
Outrun is a sanitized challenge note from the local HTB archive, organized for quick review by category, difficulty, evidence flow, and reusable operator
Scenario
Outrun attack path
Outrun is a sanitized challenge note from the local HTB archive, organized for quick review by category, difficulty, evidence flow, and reusable operator
Objective
Challenge walkthrough focused on Hardware evidence, validation, and reusable operator lessons.
Walkthrough flow
Decode the provided Saleae-style logic capture to...
The speed-zero frame is 402#0000000000340000; live...
The live Socket.IO stream exposes unlock frames as...
The current workspace did not capture the proof...
Source coverage
High source coverage
Status: complete. This article is generated from 4 sanitized Markdown sources and keeps raw flags, credentials, keys, cookies, and reusable secrets out of the rendered blog.
High confidence: the page is reconstructed from a primary walkthrough plus multiple supporting notes or evidence sources. Treat the chain as source-backed, while still checking the listed source files for sensitive values.
- Hardware/Outrun/writeup.md
- htb-challenge/Hardware/Outrun/notes.md
- htb-challenge/Hardware/Outrun/memory-summary.md
- htb-challenge/Hardware/Outrun/hypothesis-board.md
Technical Walkthrough
Writeup
Challenge
- Name: Outrun
- Category: Hardware
- Difficulty: Medium
- Mode: hybrid
Summary
Outrun is not solved in this workspace yet. The local and live work did identify the two visible control frames:
402#0000000000340000drives the dashboard speedometer to0.122#6c6f636b3a310000drivesdoor_statusto0, interpreted as locked.
Those effects are validated against the live /data endpoint and Socket.IO stream, but the current target instance did not emit a flag after multiple controlled single-session attempts. The workspace is therefore parked at <secret redacted>, not COMPLETE.
Artifact Inventory
files/a12c7372-0092-410f-8d3c-70d861a7ec5c.zip: original challenge archive.analysis/extracted/PCS_checklog.logicdata: Saleae-style logic capture used to recover CAN frames.analysis/extracted/bridge.py: Socket.IO helper supplied by the challenge.- Remote service
<TARGET>:31022: Flask/Werkzeug dashboard with/dataand Engine.IO v3 Socket.IO endpoint.
Analysis
The official HTB discussion confirms the intended workflow: use python-socketio, decode the .logicdata capture, and send the necessary packets over one Socket.IO connection. It also documents that the challenge was reworked after an earlier RPM-related guessing step.
Local decoder output in analysis/can-decoded-ch0-crc.txt recovered the stop-speed frame:
024053316 402#0000000000340000
Live state validation in analysis/remote/lock-speed-oracle.txt confirmed:
- sending
402#0000000000340000setsspeedometerto0; - sending
122#6c6f636b3a310000setsdoor_statusto0; - shorter or longer
lock:1encodings did not produce the same visible lock effect.
The old forum near-solve clue suggested rpm=800, so 612#1900800022000000 was tested because its first byte is 0x19 and 0x19 * 32 = 800. Evidence in analysis/remote/hold-rpm800-speed0-lock1-20260614.jsonl shows that this did not control visible RPM on the live instance.
Failed branches are recorded in hypothesis-board.md. The important closed branches are:
- exact/public two-packet loops: no flag;
- moderate raw Engine.IO batching: no flag;
- aggressive raw batching / lock variants: connection reset by peer;
- final decoded tail sequence plus lock: no flag;
612#1900800022000000as visible RPM control: no visible RPM effect.
Solve
No complete reproducible solve is validated yet.
Best next validation after a fresh target restart:
cd <local workspace>
sleep 65
.venv/bin/python solve/solve.py \
--url http://<TARGET>:31022 \
--duration 90 \
--delay 0.001 \
--threads 1 \
--transports polling \
--flag-output loot/flag-candidate.txtIf that does not write loot/flag-candidate.txt, do not continue timing guesses. Get server source or a narrow hint for the remaining predicate/event channel.
Flag
No live flag has been captured. If captured later, store it under loot/ and complete through the harness.
Lessons
- Treat public writeups and leaked flag lists as leads only. This workspace did not mark complete from public flag material.
- The service is sensitive to Socket.IO client version and traffic rate. Aggressive raw Engine.IO batching can reset the connection.
- The dashboard state can show speed-zero and locked without a flag, so the remaining blocker is either target freshness, a hidden predicate, or a missed event/channel condition.
Source-Backed Dossier
The sections below are merged from companion Markdown notes for the same case. They are rendered after sanitization so the article stays precise without publishing raw flags, credentials, or target-specific secrets.
Notes
Scope
- Challenge: Outrun
- Category: Hardware
- Difficulty: Medium
- Mode: hybrid
- Remote instance: none
- Start time: 2026-06-14T00:07:40Z
- Operator: harness
- State file:
challenge-state.json
Harness Status
- Current phase: see
challenge-state.json - Next allowed actions: see
next-action.json - Raw flags and sensitive material stay in
loot/only. Do not paste them here.
Artifact Inventory
| File | Size | SHA256 | Type | Notes |
|---|---|---|---|---|
files/a12c7372-0092-410f-8d3c-70d861a7ec5c.zip | 76245269 | <hash redacted> | Zip archive data, at least v2.0 to extract, compression method=deflate | zip entries: 2 shown in artifact inventory JSON |
Evidence Ledger
| Time | Action | Output/File | Finding | Confidence | Next |
|---|---|---|---|---|---|
| 2026-06-14T00:07:40Z | harness init | challenge-state.json | Workspace initialized with deterministic state file | High | Inventory artifacts |
| 2026-06-14T00:07:40Z | artifact inventory | analysis/artifact-inventory.json | 1 artifact(s) inventoried | High | Build or update hypotheses |
| 2026-06-14T00:08:06Z | hypothesis recorded | hypothesis-board.md | Decode the provided logic analyzer CAN capture, infer the required stop/lock arbitration IDs and payloads, then use bridge.py against <TARGET>:31022 to send only the validated frames. | Medium | Extract bridge.py, identify its protocol, inspect the logicdata archive with sigrok/Saleae-style parsing, and recover candidate CAN frames from the check log before any remote send. |
| 2026-06-14T00:08:06Z | research task | analysis/research/task-20260614T000806505549Z-05efdffd.md | Research task created for advisory investigation | Medium | Record research output |
| 2026-06-14T00:08:37Z | checkpoint recorded | analysis/checkpoint-analysis-20260614T000837876166Z-be549afa.md | Checkpoint for ANALYSIS | High | Use checkpoint to drive next decision |
| 2026-06-14T00:10:29Z | research record | analysis/research/research-records.md | Research tagged MISSING | Medium | Validate against current evidence |
| 2026-06-14T00:10:29Z | research record | analysis/research/research-records.md | Research tagged PARTIAL | Medium | Validate against current evidence |
| 2026-06-14T00:18:57Z | instrumentation plan | analysis/instrumentation-plan.md | Decode the CAN check log and send only validated stop/lock frames to the live car network. | High | Stop after two remote packet sequences without new facts, if service packet format conflicts with bridge.py, or if local CAN decode cannot identify stop/lock candidates. |
| 2026-06-14T00:19:39Z | local memory record | analysis/local-memory-records.md | Prior local notes reviewed as fallback/advisory context | Medium | Validate against current evidence |
| 2026-06-14T00:19:39Z | evaluator | analysis/evaluator-20260614T001939720645Z-3b2bc806.md | Proceed | High | Follow evaluator decision |
| 2026-06-14T00:22:40Z | branch closed | hypothesis-board.md | Sending zeroed 402 speed-like frame and ASCII lock:1 variant on one Socket.IO connection produced no flag and passive stream continued to show lock:0 frames. | High | Rerank hypotheses |
| 2026-06-14T03:02:38Z | branch closed | hypothesis-board.md | Validated speed-zero and lock frames affect state, but repeated single-socket public-loop variants did not emit flag on current instance. | High | Rerank hypotheses |
| 2026-06-14T03:02:38Z | branch closed | hypothesis-board.md | Decoded 612 first byte matched 0x19*32=800 theory, but live /data did not accept 612 as RPM control. | High | Rerank hypotheses |
| 2026-06-14T03:02:38Z | branch closed | hypothesis-board.md | Batching did not produce a flag and higher batch sizes exceed service stability threshold. | High | Rerank hypotheses |
| 2026-06-14T03:02:39Z | checkpoint recorded | analysis/checkpoint-analysis-20260614T030239029783Z-250dd637.md | Checkpoint for ANALYSIS | High | Use checkpoint to drive next decision |
| 2026-06-14T03:02:39Z | evaluator | analysis/evaluator-20260614T030239079040Z-beab268a.md | Validate first | High | Restart target if possible and run the exact two-packet loop first; otherwise obtain source or a narrow hint for the remaining predicate/channel. |
| 2026-06-14T03:02:56Z | hypothesis recorded | hypothesis-board.md | Resume only after a clean target restart or source/narrow hint; first validation should be exact single-socket speed-zero plus lock loop before any extra probes. | Medium | Restart target container if available, wait for no stale Socket.IO sessions, then run solve/solve.py once with polling for 90s; if no flag, obtain source or hint for remaining predicate. |
| 2026-06-14T23:48:44Z | checkpoint recorded | analysis/checkpoint-analysis-20260614T234844249337Z-4ebdec97.md | Checkpoint for ANALYSIS | High | Use checkpoint to drive next decision |
| 2026-06-14T23:48:52Z | evaluator | analysis/evaluator-20260614T234852366853Z-e4dc0b44.md | Proceed | High | Run solve/solve.py once against http://<TARGET>:31876 with polling transport, one thread, 0.001s delay, 90s duration. |
| 2026-06-15T00:05:17Z | research record | analysis/research/research-records.md | Research tagged MISSING | Medium | Validate against current evidence |
| 2026-06-15T00:06:48Z | branch closed | hypothesis-board.md | Socket.IO 5.0.0 connected and transmitted the same validated 402/122 loop, but no flag was emitted; one sender thread later hit BadNamespace after disconnect, but packets were sent and the branch did not produce new success signal. | High | Rerank hypotheses |
| 2026-06-15T00:07:01Z | checkpoint recorded | analysis/checkpoint-analysis-20260615T000701813143Z-3eb34c27.md | Checkpoint for ANALYSIS | High | Use checkpoint to drive next decision |
| 2026-06-15T00:07:11Z | evaluator | analysis/evaluator-20260615T000711915043Z-93d17742.md | Proceed | High | Run a bounded state oracle over decoded candidate IDs/payload positions to identify packets that move /data.rpm or hidden state, stopping if no packet family produces a repeatable delta. |
| 2026-06-15T00:13:15Z | branch closed | hypothesis-board.md | No tested 612 or 403 byte/value combination produced a repeatable RPM control signal, speed-zero signal, lock-state signal, or flag; all visible states stayed in natural RPM/speed ranges. | High | Rerank hypotheses |
| 2026-06-15T00:40:41Z | research record | analysis/research/research-records.md | Research tagged PARTIAL | Medium | Validate against current evidence |
| 2026-06-15T00:41:03Z | checkpoint recorded | analysis/checkpoint-analysis-20260615T004103171729Z-4d4fbe8d.md | Checkpoint for ANALYSIS | High | Use checkpoint to drive next decision |
| 2026-06-15T00:41:12Z | evaluator | analysis/evaluator-20260615T004112313353Z-70b971d9.md | Proceed | High | Run hold_and_poll.py against the responsive target with SPEED0 then LOCK, send-delay 0.1, duration 60, and stop after one attempt if no flag. |
| 2026-06-15T00:42:54Z | branch closed | hypothesis-board.md | Single Socket.IO connection with python-socketio 5.0.0, Payload.max_decode_packets=100, SPEED0 then LOCK, and 0.1s sleep between individual packet emissions produced no flag. Evidence logged 584 sends and 20 simultaneous door_status=0/speedometer=0 states. | High | Rerank hypotheses |
| 2026-06-15T00:43:50Z | checkpoint recorded | analysis/checkpoint-analysis-20260615T004350553618Z-13b020a7.md | Checkpoint for ANALYSIS | High | Use checkpoint to drive next decision |
| 2026-06-15T00:43:58Z | evaluator | analysis/evaluator-20260615T004358625329Z-2a2d4026.md | Proceed | High | Run staged_speed_then_lock.py once; if no flag, close current-instance transition/cadence path and require fresh instance or narrow hint. |
| 2026-06-15T00:45:33Z | branch closed | hypothesis-board.md | The staged runner observed speedometer=0, sent the lock frame on the same Socket.IO connection, continued holding SPEED0 and LOCK with 0.1s per-packet delay, and later observed repeated simultaneous door_status=0/speedometer=0 states, but no flag was emitted or written. | High | Rerank hypotheses |
| 2026-06-15T00:45:48Z | checkpoint recorded | analysis/checkpoint-analysis-20260615T004548629057Z-8d16e956.md | Checkpoint for ANALYSIS | High | Use checkpoint to drive next decision |
| 2026-06-15T00:45:48Z | evaluator | analysis/evaluator-20260615T004548700149Z-e4a98c60.md | Validate first | High | Spawn a clean Outrun instance and run staged_speed_then_lock.py once as the first action, or ask for a narrow hint: is a third CAN frame/state required beyond speed zero and locked door? |
| 2026-06-15T01:24:09Z | checkpoint recorded | analysis/checkpoint-analysis-20260615T012409711654Z-42a28ad6.md | Checkpoint for ANALYSIS | High | Use checkpoint to drive next decision |
| 2026-06-15T01:24:09Z | evaluator | analysis/evaluator-20260615T012409723374Z-cf6875fb.md | Proceed | High | Run staged_speed_then_lock.py once against http://<TARGET>:30423 and capture flag if emitted; if no flag, stop and request a narrow third-frame/state hint. |
| 2026-06-15T01:25:33Z | branch closed | hypothesis-board.md | Fresh target <TARGET>:30423 was responsive and used as the first action. The staged runner observed speedometer=0, sent LOCK on the same Socket.IO connection, continued holding SPEED0 and LOCK with 0.1s per-packet cadence, and reached repeated simultaneous door_status=0/speedometer=0 states, but no flag was emitted or written. | High | Rerank hypotheses |
| 2026-06-15T01:25:33Z | checkpoint recorded | analysis/checkpoint-analysis-20260615T012533454760Z-d16b2721.md | Checkpoint for ANALYSIS | High | Use checkpoint to drive next decision |
| 2026-06-15T01:25:42Z | evaluator | analysis/evaluator-20260615T012542538464Z-d0bfa2df.md | Validate first | High | Request or discover the missing third CAN frame/state/server predicate before any further remote attempt. Ask: besides speedometer=0 and door_status=0, what additional frame or state must be present for flag emission? |
Key Findings
- Remote service is a Flask/Werkzeug dashboard with Engine.IO v3 Socket.IO polling and
/data. - Official HTB discussion confirms the challenge expects decoded CAN packets over one Socket.IO connection; research is advisory only.
402#0000000000340000is live-validated as the speed-zero frame.122#6c6f636b3a310000is live-validated as the door-lock frame.- The current instance did not emit a flag after exact two-packet loops, final-tail replay, moderate raw batching,
612#1900800022000000RPM testing, or lock encoding variants. - Latest evaluator is
Validate first; do not continue blind remote timing attempts without a fresh target restart, source, or a narrow hint for the remaining predicate/channel.
RAG / Advisory Memory
RAG output is advisory only. Record evaluated retrievals with:
scripts/challenge_harness.py rag-record <workspace> --query "..." --tag MATCHED|PARTIAL|MISSING|<secret redacted>|GENERIC --validation "..."Secrets/Flags
Raw flags and sensitive material stay in loot/ only. Use scripts/challenge_harness.py capture-flag to validate and record flag capture without printing the value.
Memory Summary
Metadata
- Platform: HackTheBox Challenges
- Category: Hardware
- Challenge: Outrun
- Difficulty: Medium
- Source workspace:
<local workspace>
Validated Solve Chain
Concepts only. Do not include raw flags, reusable credentials, tokens, cookies, private keys, or live secrets.
- Decode the provided Saleae-style logic capture to recover CAN-like frames.
- The speed-zero frame is
402#0000000000340000; live/datavalidation shows it drives the speedometer to0. - The live Socket.IO stream exposes unlock frames as ASCII
lock:0; sending122#6c6f636b3a310000drivesdoor_statusto0. - The current workspace did not capture the flag despite validating both visible state controls.
Reusable Lessons
- For Socket.IO based HTB hardware challenges, use the challenge-supplied helper pattern and install
python-socketio, not the unrelatedsocketiopackage. - Treat Engine.IO version mismatches as a first-class blocker. This instance speaks Engine.IO v3 polling.
- A visible dashboard state is not always proof that the flag predicate fired; preserve both
/dataevidence and raw Socket.IO event evidence. - After two no-new-fact remote attempts, stop and update the harness instead of continuing blind timing retries.
Dead Ends
- Exact public-style speed-zero plus lock loops did not emit a flag on the current instance.
612#1900800022000000was tested as a possiblerpm=800packet; it did not control visible RPM.- Moderate raw Engine.IO batching did not emit a flag.
- Aggressive raw Engine.IO batching and broad lock variant batches reset the server connection.
- Final decoded tail replay plus the lock packet did not emit a flag.
Tool Quirks
.venvwithpython-socketio==4.6.1andpython-engineio==3.14.2connects cleanly to the live service.- The available
python-socketio==5.0.0environment connected and then dropped the namespace before emit in this local setup. - The service uses polling only;
/socket.io/reportsupgrades: [].
Evidence Paths
analysis/can-decoded-ch0-crc.txtanalysis/remote/lock-speed-oracle.txtanalysis/remote/hold-rpm800-speed0-lock1-20260614.jsonlanalysis/remote/raw-batch-moderate-speed-lock-20260614.txtanalysis/remote/final-tail-ordered-lock-20260614.txtanalysis/checkpoint-analysis-20260614T030239029783Z-250dd637.mdanalysis/evaluator-20260614T030239079040Z-beab268a.md
Ingestion Decision
- Proposed for LightRAG: yes, after solving or after user approval for partial blocked-state memory
- Requires user approval before ingestion: yes
Hypothesis Board
Keep no more than 3 active hypotheses on Easy/Medium and 5 on Hard unless the user explicitly asks for breadth.
| Rank | Path | Evidence | Missing Proof | Cheapest Validation | Confidence | Status |
|---|---|---|---|---|---|---|
| 1 | Decode the provided logic analyzer CAN capture, infer the required stop/lock arbitration IDs and payloads, then use bridge.py against <TARGET>:31022 to send only the validated frames. | Archive contains PCS_checklog.logicdata and bridge.py; scenario says a system check log and network connection wrapper were provided to stop the prototype vehicle and lock doors. | Extract bridge.py, identify its protocol, inspect the logicdata archive with sigrok/Saleae-style parsing, and recover candidate CAN frames from the check log before any remote send. | Medium | Active | |
| 1 | Resume only after a clean target restart or source/narrow hint; first validation should be exact single-socket speed-zero plus lock loop before any extra probes. | Speed and lock frames are live-validated, but current instance emitted no flag after exact public loop, final-tail replay, rpm800 612 test, moderate raw batches, and lock variants. | Restart target container if available, wait for no stale Socket.IO sessions, then run solve/solve.py once with polling for 90s; if no flag, obtain source or hint for remaining predicate. | Medium | Active |
Closed Branches
| Branch | Evidence Tested | Failure Output | Reason Closed | Revisit Condition |
|---|---|---|---|---|
| minimal guessed lock/speed packet sequence | analysis/remote/send-minimal-sequence.txt | Sending zeroed 402 speed-like frame and ASCII lock:1 variant on one Socket.IO connection produced no flag and passive stream continued to show lock:0 frames. | Only revisit if local capture decode proves required checksum, alternate arbitration ID, or exact payload format. | |
| two-packet public loop only | analysis/remote/final-instrumented-no-slash.txt; exact v4 loop no_flag sends=116592; solve.py no_flag sends=96292 | no flag emitted | Validated speed-zero and lock frames affect state, but repeated single-socket public-loop variants did not emit flag on current instance. | Restart target and run exact loop as first action before any probes, or obtain direct source/hint confirming predicate. |
| visible rpm=800 via 612#1900800022000000 | analysis/remote/hold-rpm800-speed0-lock1-20260614.jsonl | door locked and speed sometimes zero; rpm remained natural around 4.6k-5.5k | Decoded 612 first byte matched 0x19*32=800 theory, but live /data did not accept 612 as RPM control. | Only revisit if source confirms hidden RPM predicate uses 612 despite /data not showing it. |
| raw high-rate batching | analysis/remote/raw-batch-moderate-speed-lock-20260614.txt; raw high-rate/lock-variant attempts reset connection | moderate no_flag; aggressive batches reset polling connection | Batching did not produce a flag and higher batch sizes exceed service stability threshold. | Use a fresh target instance or server source to set a safe exact send cadence. |
| Socket.IO 5 client retry of validated speed-zero plus lock loop | analysis/remote/current-sio5-speed-lock-<TARGET>-31876.txt | Socket.IO 5.0.0 connected and transmitted the same validated 402/122 loop, but no flag was emitted; one sender thread later hit BadNamespace after disconnect, but packets were sent and the branch did not produce new success signal. | Only revisit if server source or official hint says the Python 4.x client specifically drops a required event; otherwise continue protocol/state inference. | |
| Bounded 612/403 RPM/control oracle | analysis/remote/current-rpm-sweep-612-403-<TARGET>-31876.json | No tested 612 or 403 byte/value combination produced a repeatable RPM control signal, speed-zero signal, lock-state signal, or flag; all visible states stayed in natural RPM/speed ranges. | Only revisit if server source or an official/narrow hint confirms RPM or the remaining predicate is controlled through 612/403 with a specific payload shape not covered by this sweep. | |
| True bridge.py per-packet cadence speed-zero plus lock on current target | analysis/remote/bridge-cadence-speed-lock-<TARGET>-31876.jsonl | Single Socket.IO connection with python-socketio 5.0.0, Payload.max_decode_packets=100, SPEED0 then LOCK, and 0.1s sleep between individual packet emissions produced no flag. Evidence logged 584 sends and 20 simultaneous door_status=0/speedometer=0 states. | Revisit only on a newly spawned clean instance as the first action, or if a narrow hint confirms cadence/order is the intended missing condition. | |
| Same-socket speed-zero then lock transition on current target | analysis/remote/staged-speed-then-lock-<TARGET>-31876.jsonl | The staged runner observed speedometer=0, sent the lock frame on the same Socket.IO connection, continued holding SPEED0 and LOCK with 0.1s per-packet delay, and later observed repeated simultaneous door_status=0/speedometer=0 states, but no flag was emitted or written. | Only revisit on a newly spawned clean target as the first action, or if a narrow hint confirms a specific transition/order/state that differs from speed-zero then lock. | |
| Fresh-instance same-socket speed-zero then lock transition | analysis/remote/staged-speed-then-lock-<TARGET>-30423.jsonl | Fresh target <TARGET>:30423 was responsive and used as the first action. The staged runner observed speedometer=0, sent LOCK on the same Socket.IO connection, continued holding SPEED0 and LOCK with 0.1s per-packet cadence, and reached repeated simultaneous door_status=0/speedometer=0 states, but no flag was emitted or written. | Only revisit if a narrow hint identifies the missing third CAN frame/state or if source reveals a different flag predicate. |
Technical analogy
How to remember this solve
Think of the hardware challenge like following copper tracks on a circuit board. The useful clue is usually where signals enter, where they are transformed, and which debug or storage path exposes hidden state.
For Outrun, keep the mental model simple: identify the trusted assumption, prove it with the smallest safe test, then automate or repeat only the part that directly leads to the flag.