Challenge / Hardware

Outrun

Outrun is a sanitized challenge note from the local HTB archive, organized for quick review by category, difficulty, evidence flow, and reusable operator

MediumPublished 2024-10-07Sanitized local writeup

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.

Outrun sanitized attack graph

Walkthrough flow

01

Decode the provided Saleae-style logic capture to...

02

The speed-zero frame is 402#0000000000340000; live...

03

The live Socket.IO stream exposes unlock frames as...

04

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.

100% coverage
Evidence verdict

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#0000000000340000 drives the dashboard speedometer to 0.
  • 122#6c6f636b3a310000 drives door_status to 0, 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 /data and 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#0000000000340000 sets speedometer to 0;
  • sending 122#6c6f636b3a310000 sets door_status to 0;
  • shorter or longer lock:1 encodings 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#1900800022000000 as visible RPM control: no visible RPM effect.

Solve

No complete reproducible solve is validated yet.

Best next validation after a fresh target restart:

bash
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.txt

If 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

FileSizeSHA256TypeNotes
files/a12c7372-0092-410f-8d3c-70d861a7ec5c.zip76245269<hash redacted>Zip archive data, at least v2.0 to extract, compression method=deflatezip entries: 2 shown in artifact inventory JSON

Evidence Ledger

TimeActionOutput/FileFindingConfidenceNext
2026-06-14T00:07:40Zharness initchallenge-state.jsonWorkspace initialized with deterministic state fileHighInventory artifacts
2026-06-14T00:07:40Zartifact inventoryanalysis/artifact-inventory.json1 artifact(s) inventoriedHighBuild or update hypotheses
2026-06-14T00:08:06Zhypothesis recordedhypothesis-board.mdDecode 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.MediumExtract 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:06Zresearch taskanalysis/research/task-20260614T000806505549Z-05efdffd.mdResearch task created for advisory investigationMediumRecord research output
2026-06-14T00:08:37Zcheckpoint recordedanalysis/checkpoint-analysis-20260614T000837876166Z-be549afa.mdCheckpoint for ANALYSISHighUse checkpoint to drive next decision
2026-06-14T00:10:29Zresearch recordanalysis/research/research-records.mdResearch tagged MISSINGMediumValidate against current evidence
2026-06-14T00:10:29Zresearch recordanalysis/research/research-records.mdResearch tagged PARTIALMediumValidate against current evidence
2026-06-14T00:18:57Zinstrumentation plananalysis/instrumentation-plan.mdDecode the CAN check log and send only validated stop/lock frames to the live car network.HighStop 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:39Zlocal memory recordanalysis/local-memory-records.mdPrior local notes reviewed as fallback/advisory contextMediumValidate against current evidence
2026-06-14T00:19:39Zevaluatoranalysis/evaluator-20260614T001939720645Z-3b2bc806.mdProceedHighFollow evaluator decision
2026-06-14T00:22:40Zbranch closedhypothesis-board.mdSending 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.HighRerank hypotheses
2026-06-14T03:02:38Zbranch closedhypothesis-board.mdValidated speed-zero and lock frames affect state, but repeated single-socket public-loop variants did not emit flag on current instance.HighRerank hypotheses
2026-06-14T03:02:38Zbranch closedhypothesis-board.mdDecoded 612 first byte matched 0x19*32=800 theory, but live /data did not accept 612 as RPM control.HighRerank hypotheses
2026-06-14T03:02:38Zbranch closedhypothesis-board.mdBatching did not produce a flag and higher batch sizes exceed service stability threshold.HighRerank hypotheses
2026-06-14T03:02:39Zcheckpoint recordedanalysis/checkpoint-analysis-20260614T030239029783Z-250dd637.mdCheckpoint for ANALYSISHighUse checkpoint to drive next decision
2026-06-14T03:02:39Zevaluatoranalysis/evaluator-20260614T030239079040Z-beab268a.mdValidate firstHighRestart 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:56Zhypothesis recordedhypothesis-board.mdResume 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.MediumRestart 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:44Zcheckpoint recordedanalysis/checkpoint-analysis-20260614T234844249337Z-4ebdec97.mdCheckpoint for ANALYSISHighUse checkpoint to drive next decision
2026-06-14T23:48:52Zevaluatoranalysis/evaluator-20260614T234852366853Z-e4dc0b44.mdProceedHighRun solve/solve.py once against http://<TARGET>:31876 with polling transport, one thread, 0.001s delay, 90s duration.
2026-06-15T00:05:17Zresearch recordanalysis/research/research-records.mdResearch tagged MISSINGMediumValidate against current evidence
2026-06-15T00:06:48Zbranch closedhypothesis-board.mdSocket.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.HighRerank hypotheses
2026-06-15T00:07:01Zcheckpoint recordedanalysis/checkpoint-analysis-20260615T000701813143Z-3eb34c27.mdCheckpoint for ANALYSISHighUse checkpoint to drive next decision
2026-06-15T00:07:11Zevaluatoranalysis/evaluator-20260615T000711915043Z-93d17742.mdProceedHighRun 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:15Zbranch closedhypothesis-board.mdNo 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.HighRerank hypotheses
2026-06-15T00:40:41Zresearch recordanalysis/research/research-records.mdResearch tagged PARTIALMediumValidate against current evidence
2026-06-15T00:41:03Zcheckpoint recordedanalysis/checkpoint-analysis-20260615T004103171729Z-4d4fbe8d.mdCheckpoint for ANALYSISHighUse checkpoint to drive next decision
2026-06-15T00:41:12Zevaluatoranalysis/evaluator-20260615T004112313353Z-70b971d9.mdProceedHighRun 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:54Zbranch closedhypothesis-board.mdSingle 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.HighRerank hypotheses
2026-06-15T00:43:50Zcheckpoint recordedanalysis/checkpoint-analysis-20260615T004350553618Z-13b020a7.mdCheckpoint for ANALYSISHighUse checkpoint to drive next decision
2026-06-15T00:43:58Zevaluatoranalysis/evaluator-20260615T004358625329Z-2a2d4026.mdProceedHighRun 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:33Zbranch closedhypothesis-board.mdThe 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.HighRerank hypotheses
2026-06-15T00:45:48Zcheckpoint recordedanalysis/checkpoint-analysis-20260615T004548629057Z-8d16e956.mdCheckpoint for ANALYSISHighUse checkpoint to drive next decision
2026-06-15T00:45:48Zevaluatoranalysis/evaluator-20260615T004548700149Z-e4a98c60.mdValidate firstHighSpawn 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:09Zcheckpoint recordedanalysis/checkpoint-analysis-20260615T012409711654Z-42a28ad6.mdCheckpoint for ANALYSISHighUse checkpoint to drive next decision
2026-06-15T01:24:09Zevaluatoranalysis/evaluator-20260615T012409723374Z-cf6875fb.mdProceedHighRun 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:33Zbranch closedhypothesis-board.mdFresh 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.HighRerank hypotheses
2026-06-15T01:25:33Zcheckpoint recordedanalysis/checkpoint-analysis-20260615T012533454760Z-d16b2721.mdCheckpoint for ANALYSISHighUse checkpoint to drive next decision
2026-06-15T01:25:42Zevaluatoranalysis/evaluator-20260615T012542538464Z-d0bfa2df.mdValidate firstHighRequest 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#0000000000340000 is live-validated as the speed-zero frame.
  • 122#6c6f636b3a310000 is 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#1900800022000000 RPM 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:

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

  1. Decode the provided Saleae-style logic capture to recover CAN-like frames.
  2. The speed-zero frame is 402#0000000000340000; live /data validation shows it drives the speedometer to 0.
  3. The live Socket.IO stream exposes unlock frames as ASCII lock:0; sending 122#6c6f636b3a310000 drives door_status to 0.
  4. 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 unrelated socketio package.
  • 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 /data evidence 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#1900800022000000 was tested as a possible rpm=800 packet; 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

  • .venv with python-socketio==4.6.1 and python-engineio==3.14.2 connects cleanly to the live service.
  • The available python-socketio==5.0.0 environment connected and then dropped the namespace before emit in this local setup.
  • The service uses polling only; /socket.io/ reports upgrades: [].

Evidence Paths

  • analysis/can-decoded-ch0-crc.txt
  • analysis/remote/lock-speed-oracle.txt
  • analysis/remote/hold-rpm800-speed0-lock1-20260614.jsonl
  • analysis/remote/raw-batch-moderate-speed-lock-20260614.txt
  • analysis/remote/final-tail-ordered-lock-20260614.txt
  • analysis/checkpoint-analysis-20260614T030239029783Z-250dd637.md
  • analysis/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.

RankPathEvidenceMissing ProofCheapest ValidationConfidenceStatus
1Decode 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.MediumActive
1Resume 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.MediumActive

Closed Branches

BranchEvidence TestedFailure OutputReason ClosedRevisit Condition
minimal guessed lock/speed packet sequenceanalysis/remote/send-minimal-sequence.txtSending 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 onlyanalysis/remote/final-instrumented-no-slash.txt; exact v4 loop no_flag sends=116592; solve.py no_flag sends=96292no flag emittedValidated 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#1900800022000000analysis/remote/hold-rpm800-speed0-lock1-20260614.jsonldoor locked and speed sometimes zero; rpm remained natural around 4.6k-5.5kDecoded 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 batchinganalysis/remote/raw-batch-moderate-speed-lock-20260614.txt; raw high-rate/lock-variant attempts reset connectionmoderate no_flag; aggressive batches reset polling connectionBatching 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 loopanalysis/remote/current-sio5-speed-lock-<TARGET>-31876.txtSocket.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 oracleanalysis/remote/current-rpm-sweep-612-403-<TARGET>-31876.jsonNo 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 targetanalysis/remote/bridge-cadence-speed-lock-<TARGET>-31876.jsonlSingle 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 targetanalysis/remote/staged-speed-then-lock-<TARGET>-31876.jsonlThe 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 transitionanalysis/remote/staged-speed-then-lock-<TARGET>-30423.jsonlFresh 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.