Scanner
Scanner is a sanitized challenge note from the local HTB archive, organized for quick review by category, difficulty, evidence flow, and reusable operator
Scenario
Scanner attack path
Scanner 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 Pwn evidence, validation, and reusable operator lessons.
Walkthrough flow
Binary triage
Memory primitive
Control-flow hijack
Proof captured
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.
- Pwn/Scanner/writeup.md
- htb-challenge/Pwn/Scanner/notes.md
- htb-challenge/Pwn/Scanner/memory-summary.md
- htb-challenge/Pwn/Scanner/hypothesis-board.md
Technical Walkthrough
Writeup
Challenge
- Name: Scanner
- Category: Pwn
- Difficulty: Medium
- Mode: hybrid
Summary
scanner implements three memory-search backends. The optimized naive2 path does not bound the candidate start position by buffer_len - needle_len, so a long needle turns the scanner into an out-of-bounds stack read oracle. A separate %16s read into a 16-byte stack buffer gives a one-byte frame-pointer poison. Chaining those bugs gives a leak and then a ret2libc shell.
Artifact Inventory
files/extracted/pwn_scanner/scanner: PIE ELF, not stripped, interpreter./ld-2.31.so.files/extracted/pwn_scanner/libc.so.6: bundled glibc used for libc offsets.files/extracted/pwn_scanner/ld-2.31.so: bundled loader.- Remote service:
<TARGET>:32084.
Analysis
read_parametersusesscanf("%16s %u", ...)into a 16-byte local name buffer. A 16-byte invalid scanner token writes the trailing NUL into the saved frame pointer byte.naive2callsmemcmp(buffer + i, needle, needle_len)while iteratingi <= 0xfff, regardless ofneedle_len.- A deterministic newline-free buffer suffix plus fixed needle length turns
Found at i=Ninto a byte oracle for stack bytes above the buffer. - The first remote version leaked too slowly one guess at a time. The final solver pipelines all 256 guesses for each unknown byte and parses the one response that returns the expected index.
- The final exploit leaks only the active heap pointer and a libc return pointer. It then writes fake post-poison locals for every viable aligned frame-pointer delta and uses a libc-only ret sled, avoiding a PIE leak and exact delta calculation.
Solve
Run:
python3 solve/solve.py --host <TARGET> --port 32084 --output loot/flag.txtThe harness run used scripts/challenge_exec.py so the exploit gate was checked first. The script stores the flag in loot/flag.txt.
Flag
Raw flag is stored in loot/flag.txt and intentionally not reproduced here.
Lessons
- For high-latency remotes, equality oracles need batching. The first working byte oracle was correct but impractically slow over the network.
- If an exact stack-pivot offset is expensive to recover, placing fake locals for all aligned deltas and using a ret sled can remove the dependency.
- RAG and public search were non-contributory here; direct binary audit and local validation were the deciding evidence.
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: Scanner
- Category: Pwn
- Difficulty: Medium
- Mode: hybrid
- Remote instance: none
- Start time: 2026-06-12T22:07:15Z
- 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/a12c732c-bf6b-4f21-a202-5a85bd8807a9.zip | 5979460 | <hash redacted> | Zip archive data, at least v1.0 to extract, compression method=store | zip entries: 5 shown in artifact inventory JSON |
files/extracted/pwn_scanner/flag.txt | 27 | <hash redacted> | ASCII text | |
files/extracted/pwn_scanner/ld-2.31.so | 191504 | <hash redacted> | ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=<hash redacted>, stripped | |
files/extracted/pwn_scanner/libc.so.6 | 7459456 | <hash redacted> | ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=<hash redacted>, for GNU/Linux 3.2.0, with debug_info, not stripped | |
files/extracted/pwn_scanner/scanner | 29440 | <hash redacted> | ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter ./ld-2.31.so, for GNU/Linux 3.2.0, BuildID[sha1]=<hash redacted>, not stripped |
Evidence Ledger
| Time | Action | Output/File | Finding | Confidence | Next |
|---|---|---|---|---|---|
| 2026-06-12T22:07:15Z | harness init | challenge-state.json | Workspace initialized with deterministic state file | High | Inventory artifacts |
| 2026-06-12T22:07:16Z | artifact inventory | analysis/artifact-inventory.json | 5 artifact(s) inventoried | High | Build or update hypotheses |
| 2026-06-12T22:32:55Z | hypothesis recorded | hypothesis-board.md | OOB stack byte oracle from naive2 plus %16s frame-pointer poison yields single-stage ret2libc | Medium | Use solve/solve.py locally against bundled libc/ld, then gated remote run against <TARGET>:32084. |
| 2026-06-12T22:33:13Z | source audit | analysis/source-audit.md | Source audit recorded | High | Gate before exploit |
| 2026-06-12T22:33:13Z | instrumentation plan | analysis/instrumentation-plan.md | Exploit Scanner via OOB stack leak plus frame-pointer poison to capture the remote flag. | High | Stop on flag capture, retry exhaustion, missing PIE/libc-shaped leaks, unusable frame delta, or remote unavailability. |
| 2026-06-12T22:33:29Z | research record | analysis/research/research-records.md | Research tagged MISSING | Medium | Validate against current evidence |
| 2026-06-12T22:34:22Z | RAG query | analysis/rag/rag-query-20260612T223403556726Z-33c36cd4.txt | RAG helper exited 0; output saved | Medium | Record retrieval tag and validation |
| 2026-06-12T22:34:33Z | RAG record | analysis/rag-records.md | Retrieved memory tagged MISSING | Medium | Validate or reject with live evidence |
| 2026-06-12T22:34:57Z | checkpoint recorded | analysis/checkpoint-analysis-20260612T223457078032Z-e2343762.md | Checkpoint for ANALYSIS | High | Use checkpoint to drive next decision |
| 2026-06-12T22:35:45Z | local memory record | analysis/local-memory-records.md | Prior local notes reviewed as fallback/advisory context | Medium | Validate against current evidence |
| 2026-06-12T22:35:53Z | evaluator | analysis/evaluator-20260612T223553443386Z-21f303ea.md | Proceed | High | Gated remote exploit attempt and capture flag. |
| 2026-06-12T22:53:04Z | flag capture | loot/flag.txt | HTB-format flag captured; raw value kept in loot only | High | Write solution and run completion gate |
| 2026-06-12T22:54:11Z | completion gate | challenge-state.json | Completion gate passed; state marked COMPLETE | High | Optional sanitized memory summary approval |
Key Findings
- Direct binary audit identified two cooperating vulnerabilities:
naive2performs an out-of-bounds stack read when the search length reaches past the 4096-byte stack buffer, andread_parametersuses%16sinto a 16-byte stack buffer, allowing a one-byte frame-pointer poison. - The OOB scanner behavior was converted into a byte oracle by using a fixed search length and a deterministic newline-free buffer suffix.
- Local validation first recovered heap, PIE, libc, and stack-delta values from the stack oracle, then used the frame-pointer poison to overlap the next
fgetsreturn address and execute a single-stage ret2libc chain. - The final remote exploit was optimized to avoid PIE and exact-delta leakage: it leaks only the heap pointer and a libc return pointer, prepares fake locals for all viable poisoned frame-pointer deltas, and uses a libc-only ret sled into
system("/bin/sh"). solve/solve.pyimplements the optimized path and writes the remote flag toloot/flag.txtwithout printing it.
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: Pwn
- Challenge: Scanner
- 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.
Reusable Lessons
-
Dead Ends
-
Tool Quirks
-
Evidence Paths
-
Ingestion Decision
- Proposed for LightRAG: yes/no
- 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 | OOB stack byte oracle from naive2 plus %16s frame-pointer poison yields single-stage ret2libc | Disassembly shows naive2 reads past the 4096-byte stack buffer and read_parameters uses %16s into a 16-byte local scanner-name buffer; local Docker validation reached system('/bin/sh'). | Use solve/solve.py locally against bundled libc/ld, then gated remote run against <TARGET>:32084. | Medium | Active |
Closed Branches
| Branch | Evidence Tested | Failure Output | Reason Closed | Revisit Condition |
|---|
Technical analogy
How to remember this solve
Think of the challenge as a small system with one rule that matters more than the rest. The solve is finding that rule, validating it, and using it carefully enough to reach the final proof.
For Scanner, 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.