Challenge / Pwn

ReplaceMe

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

MediumPublished 2025-04-15Sanitized local writeup

Scenario

ReplaceMe attack path

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

ReplaceMe sanitized attack graph

Walkthrough flow

01

Binary triage

02

Memory primitive

03

Control-flow hijack

04

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.

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.

  • Pwn/ReplaceMe/writeup.md
  • htb-challenge/Pwn/ReplaceMe/notes.md
  • htb-challenge/Pwn/ReplaceMe/memory-summary.md
  • htb-challenge/Pwn/ReplaceMe/hypothesis-board.md

Technical Walkthrough

Writeup

Challenge

  • Name: ReplaceMe
  • Category: Pwn
  • Difficulty: Medium
  • Mode: hybrid

Summary

ReplaceMe is a fork-per-connection Linux pwn challenge around a sed-like s/old/new/ replacement utility. The binary reads an input string and a replacement expression into adjacent global buffers, then expands the result into a fixed stack buffer without checking the combined output length.

The exploit uses that unchecked expansion to overwrite the saved return address. Because the program prints the expanded stack buffer before returning, the first stage performs a one-byte saved-RIP overwrite back to main and leaks the high bytes of the PIE return address in the same process. A second stage uses a pop rdi; ret gadget to leak puts from the GOT and loop again. The final stage calls system("/bin/sh") from the provided libc and reads flag.txt.

Artifact Inventory

Reference analysis/artifact-inventory.json and summarize the relevant files or remote surface.

  • files/extracted/replaceme: 64-bit PIE ELF, dynamically linked, not stripped.
  • files/extracted/libc.so.6: matching libc used for ret2libc offsets.
  • files/extracted/Dockerfile: service runs the binary through socat ... EXEC:./replaceme.
  • Remote instance: <TARGET>:30149.

Analysis

Validated local evidence:

  • analysis/source-audit.md records the unchecked stack-copy primitive in do_replacement.
  • analysis/objdump-replaceme.txt shows input at 0x4040, replacement at 0x40c0, the local output area beginning at rbp-0xc0, and leave; ret at the end of do_replacement.
  • analysis/remote/partial-return-onebyte-probe.bin proves that changing only the saved return address low byte loops execution back to main.
  • analysis/remote/partial-return-leak-probe.bin proves the same loop can leak PIE address bytes before returning.
  • analysis/remote/libc-leak-probe.bin proves a ROP stage can leak libc through puts(puts@got) and loop again.

Important implementation details:

  • A two-byte partial overwrite failed because PIE base randomization affects the second byte. The reliable loop changes only the low byte from the return site at main+0x70 back to main.
  • The overflow crosses stack locals used after the memcpy. The payload sets the overwritten tail_len local to -1 so the post-overflow tail copy is skipped.
  • The final chain must call system("/bin/sh") without an extra alignment ret; the extra ret variant crashed, while the no-ret variant produced shell output.

Solve

Run:

bash
python3 solve/solve.py --host <TARGET> --port 30149

The solver:

  1. Leaks PIE and loops to main.
  2. Leaks libc through puts@got and loops to main.
  3. Calls system("/bin/sh"), sends cat flag.txt, and writes the flag candidate to loot/flag-candidate.txt.
  4. Writes only a redacted transcript to analysis/remote/solve-transcript-redacted.bin.

Flag

Raw flag is stored in loot/flag.txt and intentionally not reproduced here.

Lessons

  • For PIE binaries, a one-byte partial overwrite is often more reliable than two bytes when the target and original return site are on the same page.
  • Print-before-return stack overflows can provide an information leak even when the primary bug is a write primitive.
  • When an overflow corrupts locals that are still used, the exploit must neutralize or preserve those locals before focusing on the saved return address.

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: ReplaceMe
  • Category: Pwn
  • Difficulty: Medium
  • Mode: hybrid
  • Remote instance: none
  • Start time: 2026-06-11T23:14:10Z
  • 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/a12c73a5-b9fe-4e10-8b39-f9480f64a9b3.zip863099<hash redacted>Zip archive data, at least v2.0 to extract, compression method=deflatezip entries: 5 shown in artifact inventory JSON

Evidence Ledger

TimeActionOutput/FileFindingConfidenceNext
2026-06-11T23:14:10Zharness initchallenge-state.jsonWorkspace initialized with deterministic state fileHighInventory artifacts
2026-06-11T23:14:11Zartifact inventoryanalysis/artifact-inventory.json1 artifact(s) inventoriedHighBuild or update hypotheses
2026-06-11T23:18:34Zhypothesis recordedhypothesis-board.mdStack overflow in do_replacement from expansion of s/old/new/ into fixed stack output bufferHighUse a bounded socket probe that overwrites only the low two bytes of the saved return address to return to main and observe a second prompt
2026-06-11T23:18:34Zhypothesis recordedhypothesis-board.mdfind/strlen out-of-bounds read can extend copied tail across adjacent globalsMediumProbe controlled output length changes using shorter input versus full input
2026-06-11T23:19:02Zsource auditanalysis/source-audit.mdSource audit recordedHighGate before exploit
2026-06-11T23:19:02Zcheckpoint recordedanalysis/checkpoint-analysis-20260611T231902490353Z-fa3ef7db.mdCheckpoint for ANALYSISHighUse checkpoint to drive next decision
2026-06-11T23:19:04Zlocal memory searchanalysis/research/local-memory-search-20260611T231904085750Z-d0c25ce0.mdFound 5 safe prior-note result(s)MediumRecord useful result or skip
2026-06-11T23:19:26Zresearch recordanalysis/research/research-records.mdResearch tagged GENERICMediumValidate against current evidence
2026-06-11T23:19:43ZRAG queryanalysis/rag/rag-query-20260611T231926170450Z-7d77b349.txtRAG helper exited 0; output savedMediumRecord retrieval tag and validation
2026-06-11T23:19:53Zlocal memory recordanalysis/local-memory-records.mdPrior local notes reviewed as fallback/advisory contextMediumValidate against current evidence
2026-06-11T23:20:10Zinstrumentation plananalysis/instrumentation-plan.mdValidate and exploit the unchecked replacement expansion without blind remote attemptsHighStop after two failed primitive probes without a new fact, or immediately if the service stops responding, the partial return does not produce a second prompt, or the exploit requires unvalidated brute force.
2026-06-11T23:20:10ZRAG recordanalysis/rag-records.mdRetrieved memory tagged PARTIALMediumValidate or reject with live evidence
2026-06-11T23:20:21Zevaluatoranalysis/evaluator-20260611T232021059853Z-afb03626.mdProceedHighRun challenge_exec-wrapped Python probe against <TARGET>:30149 and save transcript in analysis/remote/.
2026-06-11T23:35:48Zflag captureloot/flag.txtHTB-format flag captured; raw value kept in loot onlyHighWrite solution and run completion gate
2026-06-11T23:36:42Zcompletion gatechallenge-state.jsonCompletion gate passed; state marked COMPLETEHighOptional sanitized memory summary approval

Key Findings

  • do_replacement expands s/old/new/ into a stack output area without a bounds check.
  • PIE/NX/full RELRO are enabled, but no stack canary was observed.
  • The reliable control primitive is a one-byte saved-RIP overwrite back to main, not a two-byte overwrite.
  • Payloads must neutralize overwritten stack locals, especially tail_len, before the function reaches fputs and leave; ret.
  • Final solve chain: PIE leak -> libc leak through puts@got -> system("/bin/sh") without an extra alignment ret -> read flag.txt.
  • Raw flag is stored only in loot/flag.txt.

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: Pwn
  • Challenge: ReplaceMe
  • 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.

RankPathEvidenceMissing ProofCheapest ValidationConfidenceStatus
1Stack overflow in do_replacement from expansion of s/old/new/ into fixed stack output bufferobjdump shows do_replacement allocates 0xc0 bytes, zeroes an 0x80 output area at rbp-0xc0, then memcpy copies prefix + replacement + tail without checking output length before fputs and returnNeed live signal that the overflow can redirect control under PIE/ASLRUse a bounded socket probe that overwrites only the low two bytes of the saved return address to return to main and observe a second promptHighActive
2find/strlen out-of-bounds read can extend copied tail across adjacent globalsinput and replacement are adjacent globals at 0x4040 and 0x40c0; reads do not append NUL; find scans fixed 0x80 windows without length-plus-needle bounds; strlen(match+old_len) can consume adjacent bytes until a NULMay be useful for sizing/leaks but not sufficient alone for flag captureProbe controlled output length changes using shorter input versus full inputMediumActive

Closed Branches

BranchEvidence TestedFailure OutputReason ClosedRevisit 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 ReplaceMe, 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.