Challenge / Pwn

Under The Web

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

MediumPublished 2025-05-06Sanitized local writeup

Scenario

Under The Web attack path

Under The Web 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.

Under The Web sanitized attack graph

Walkthrough flow

01

Audited PHP routes and found view.php arbitrary local...

02

Used LFI to read process files such as...

03

Audited metadata_reader.so and found strcpy into...

04

Locally reproduced the boundary: 56-byte metadata...

05

Built a raw-PNG metadata payload that corrupts Zend...

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/Under-the-Web/writeup.md
  • htb-challenge/Pwn/Under-the-Web/notes.md
  • htb-challenge/Pwn/Under-the-Web/memory-summary.md
  • htb-challenge/Pwn/Under-the-Web/hypothesis-board.md

Technical Walkthrough

Writeup

Challenge

  • Name: Under-the-Web
  • Category: Pwn
  • Difficulty: Medium
  • Mode: hybrid

Summary

Under-the-Web is a PHP web challenge backed by a vulnerable native PHP extension. The web layer has an arbitrary local file read in view.php, and the extension metadata_reader.so has a heap overflow while copying PNG text metadata with strcpy.

The final chain uses the LFI to leak /proc/self/maps, computes live libc and extension bases, crafts a PNG that corrupts Zend heap allocation state, overwrites _efree@GOT with system, and makes a metadata string execute a short command that copies the randomized flag to a known upload path.

Artifact Inventory

  • files/a12c7385-1413-45ca-99f9-03c9b443445b.zip: original HTB archive.
  • files/extracted/pwn_under_the_web/view.php: arbitrary file read via image= and file_get_contents.
  • files/extracted/pwn_under_the_web/upload.php: PNG upload route, magic-byte and extension checked.
  • files/extracted/pwn_under_the_web/metadata_reader.so: vulnerable native extension.
  • files/extracted/pwn_under_the_web/Dockerfile: renames flag.txt to a random hash-like filename during image build.
  • analysis/artifact-inventory.json: hashes and file inventory.

Analysis

Source audit is recorded in analysis/source-audit.md.

view.php URL-decodes image, checks only file_exists, and returns base64_encode(file_get_contents($image)). This gives a read primitive for process files such as /proc/self/maps, confirmed in analysis/remote/proc-self-maps.txt.

metadata_reader.so reads PNG tEXt chunks and copies Title, Artist, and Copyright values into fixed 56-byte heap chunks with strcpy. Static evidence is in analysis/local/metadata_reader.objdump.txt. Runtime tests showed 56-byte metadata corrupts adjacent output and 57-byte metadata crashes the process, recorded in analysis/local/php-cli-crash-repro.txt and the upload response artifacts.

The initial shortcut ideas were closed:

  • Overlay lowerdir /app/flag.txt recovery failed: analysis/remote/lowerdir-flag-probe.txt.
  • PHP wrapper bypasses for file_exists failed: analysis/remote/wrapper-probe-summary.txt.

Docker local reproduction was eventually enabled. The local exploit first failed because the local rebuilt libc had system at 0x4c490, while the remote leaked libc had system at 0x4c3a0. After leaking the remote libc and checking its symbols in analysis/remote/remote-libc-symbols.txt, the final remote run used the correct offset.

Solve

The reproducible solver is solve/solve.py.

High-level flow:

  1. Read /proc/self/maps through view.php.
  2. Parse the live libc base and metadata_reader.so base.
  3. Compute:

- system = libc_base + 0x4c3a0 for the leaked remote libc.

- _efree@GOT = metadata_reader_base + 0x4090.

  1. Build a PNG with raw tEXt chunks:

- Artist: heap overflow padding plus _efree@GOT.

- Title: short shell command copying /app/[0-9a-f]* to /app/uploads/.underweb_flag.png.

- Copyright: system address.

  1. Upload the PNG. When the extension later frees metadata strings, _efree resolves to system, executing the copied Title command.
  2. Read /app/uploads/.underweb_flag.png through view.php.
  3. Store the matched HTB flag candidate in loot/ and capture it with the harness.

The successful remote evidence is analysis/remote/final-exploit-remote-correct-libc.txt.

Flag

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

Lessons

  • When LFI leaks /proc/self/maps, prefer live base addresses over assumptions from local rebuilds.
  • Local Docker reproduction is still valuable for heap exploit behavior, but exact libc offsets may differ from the spawned remote instance.
  • A crash boundary can hide a useful primitive: here, 56-byte text metadata gave controlled heap corruption, while 57+ bytes produced visible crashes.
  • For native PHP extension challenges, GOT overwrite can be cleaner than full ROP when the extension has writable relocation slots and calls a controllable function pointer path.

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: Under-the-Web
  • Category: Pwn
  • Difficulty: Medium
  • Mode: hybrid
  • Remote instance: none
  • Start time: 2026-06-12T06:47:04Z
  • 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/a12c7385-1413-45ca-99f9-03c9b443445b.zip2096589<hash redacted>Zip archive data, at least v1.0 to extract, compression method=storezip entries: 11 shown in artifact inventory JSON
files/extracted/pwn_under_the_web/Dockerfile483<hash redacted>ASCII text
files/extracted/pwn_under_the_web/flag.txt27<hash redacted>ASCII text
files/extracted/pwn_under_the_web/index.php2371<hash redacted>HTML document text, ASCII text
files/extracted/pwn_under_the_web/metadata_reader.so45152<hash redacted>ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=<hash redacted>, with debug_info, not stripped
files/extracted/pwn_under_the_web/start.sh93<hash redacted>Bourne-Again shell script text executable, ASCII text
files/extracted/pwn_under_the_web/upload.php3300<hash redacted>PHP script text, ASCII text
files/extracted/pwn_under_the_web/uploads/starry_night.png1112785<hash redacted>PNG image data, 757 x 599, 8-bit/color RGB, non-interlaced
files/extracted/pwn_under_the_web/uploads/the_potato_eaters.png960809<hash redacted>PNG image data, 794 x 599, 8-bit/color RGBA, non-interlaced
files/extracted/pwn_under_the_web/view.php1672<hash redacted>HTML document text, ASCII text

Evidence Ledger

TimeActionOutput/FileFindingConfidenceNext
2026-06-12T06:47:04Zharness initchallenge-state.jsonWorkspace initialized with deterministic state fileHighInventory artifacts
2026-06-12T06:47:04Zartifact inventoryanalysis/artifact-inventory.json10 artifact(s) inventoriedHighBuild or update hypotheses
2026-06-12T06:48:16Zhypothesis recordedhypothesis-board.mdLFI leaks runtime/process state, then crafted PNG metadata triggers metadata_reader.so heap overflow to reach the randomized flagMediumRun a local Docker service, confirm LFI against /proc/self/maps, upload a long-text PNG, and compare local crash/leak behavior with remote only after instrumentation
2026-06-12T06:48:16Zsource auditanalysis/source-audit.mdSource audit recordedHighGate before exploit
2026-06-12T06:48:35Zlocal memory searchanalysis/research/local-memory-search-20260612T064835012551Z-693ea52d.mdFound 8 safe prior-note result(s)MediumRecord useful result or skip
2026-06-12T06:48:35Zcheckpoint recordedanalysis/checkpoint-triage-20260612T064835014322Z-b1bcc52d.mdCheckpoint for TRIAGEHighUse checkpoint to drive next decision
2026-06-12T06:48:49Zinstrumentation plananalysis/instrumentation-plan.mdValidate the chain from arbitrary file read plus crafted PNG metadata into a usable flag-read primitiveHighStop after two crafted PNG crashes without new control/leak evidence, if LFI cannot read useful process files, or if local reproduction diverges from remote behavior
2026-06-12T06:49:29ZRAG queryanalysis/rag/rag-query-20260612T064849675838Z-6d9c2e0c.txtRAG helper exited 0; output savedMediumRecord retrieval tag and validation
2026-06-12T06:50:25ZRAG recordanalysis/rag-records.mdRetrieved memory tagged GENERICMediumValidate or reject with live evidence
2026-06-12T06:50:25Zlocal memory recordanalysis/local-memory-records.mdPrior local notes reviewed as fallback/advisory contextMediumValidate against current evidence
2026-06-12T06:51:51Zevaluatoranalysis/evaluator-20260612T065151158557Z-112613ee.mdProceedHighRun gated probe.py LFI reads for /proc/self/cmdline and /proc/self/maps
2026-06-12T06:55:38Zbranch closedhypothesis-board.mdAll lowerdir and upperdir /app/flag.txt candidates from /proc/self/mountinfo were missing or unreadable through view.php LFIHighRerank hypotheses
2026-06-12T06:57:45Zcheckpoint recordedanalysis/checkpoint-analysis-20260612T065745753453Z-a6064f0b.mdCheckpoint for ANALYSISHighUse checkpoint to drive next decision
2026-06-12T06:58:32Zbranch closedhypothesis-board.mdTitle length 57+ causes truncated response or connection reset even when the Title is the only metadata field; no leak or controlled output observedHighRerank hypotheses
2026-06-12T06:59:05Zbranch closedhypothesis-board.mdphp://filter, glob://, and compress.zlib paths all returned Image not found because file_exists() blocked the wrapper pathsHighRerank hypotheses
2026-06-12T07:07:51Zresearch recordanalysis/research/research-records.mdResearch tagged MATCHEDMediumValidate against current evidence
2026-06-12T07:07:51Zevaluatoranalysis/evaluator-20260612T070751086055Z-6091524c.mdProceedHighExecute solve.py remotely and capture the flag candidate
2026-06-12T07:09:10Zflag captureloot/flag.txtHTB-format flag captured; raw value kept in loot onlyHighWrite solution and run completion gate
2026-06-12T07:10:47Zcompletion gatechallenge-state.jsonCompletion gate passed; state marked COMPLETEHighOptional sanitized memory summary approval

Key Findings

-

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: Under-the-Web
  • 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. Audited PHP routes and found view.php arbitrary local file read guarded only by file_exists.
  2. Used LFI to read process files such as /proc/self/maps and leak live libc plus metadata_reader.so bases.
  3. Audited metadata_reader.so and found strcpy into fixed 56-byte heap chunks for PNG text metadata.
  4. Locally reproduced the boundary: 56-byte metadata corrupts adjacent heap/output; 57+ bytes crashes the PHP process.
  5. Built a raw-PNG metadata payload that corrupts Zend heap allocation state so a later metadata allocation writes to _efree@GOT.
  6. Overwrote _efree@GOT with system, then used a short metadata string command to copy the randomized flag to a known upload path.
  7. Retrieved the copied flag through view.php and captured it with the harness.

Reusable Lessons

  • LFI of /proc/self/maps can remove the need for a separate address leak in native extension exploitation.
  • Validate libc offsets from the exact target libc. A local Docker rebuild can differ from the remote instance.
  • If a heap overflow seems to only crash, test exact boundary lengths; off-by-one or fixed-chunk overflows can still create allocation-control primitives.
  • For PHP extension pwn, writable GOT plus a later _efree call can provide a compact system(command) path.

Dead Ends

  • Overlay lowerdir /app/flag.txt recovery from /proc/self/mountinfo was negative.
  • PHP wrapper bypasses such as php://filter, glob://, and compress.zlib:// did not pass the file_exists check.
  • Simple long-metadata layouts gave crashes but no direct leak or flag without the GOT overwrite chain.

Tool Quirks

  • Docker Desktop was initially stopped; starting it enabled exact local PHP container reproduction.
  • GDB inside the amd64 container under Apple Silicon Docker emulation could not obtain registers reliably.
  • The local rebuilt libc had system at 0x4c490; the remote leaked libc had system at 0x4c3a0.

Evidence Paths

  • analysis/source-audit.md
  • analysis/remote/proc-self-maps.txt
  • analysis/local/php-cli-crash-repro.txt
  • analysis/local/final-exploit-local-spaces.txt
  • analysis/remote/remote-libc-symbols.txt
  • analysis/remote/final-exploit-remote-correct-libc.txt
  • solve/solve.py
  • loot/flag.txt

Ingestion Decision

  • Proposed for LightRAG: yes
  • 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
1LFI leaks runtime/process state, then crafted PNG metadata triggers metadata_reader.so heap overflow to reach the randomized flagview.php reads arbitrary existing paths; Dockerfile randomizes the flag filename in /app; metadata_reader.so uses 56-byte heap buffers and strcpy on PNG Title/Artist/Copyright text chunksRun a local Docker service, confirm LFI against /proc/self/maps, upload a long-text PNG, and compare local crash/leak behavior with remote only after instrumentationMediumActive

Closed Branches

BranchEvidence TestedFailure OutputReason ClosedRevisit Condition
Overlay lowerdir /app/flag.txt recoveryanalysis/remote/lowerdir-flag-probe.txtAll lowerdir and upperdir /app/flag.txt candidates from /proc/self/mountinfo were missing or unreadable through view.php LFIOnly revisit if a new mountinfo/source clue identifies an accessible layer path or exact randomized flag filename
Simple long tEXt metadata layouts as leak primitiveanalysis/remote/upload-title57-only-response.htmlTitle length 57+ causes truncated response or connection reset even when the Title is the only metadata field; no leak or controlled output observedRevisit only with local Docker/gdb/ZendMM instrumentation or a payload structure that predicts allocator behavior
PHP wrapper bypass for file_exists LFI guardanalysis/remote/wrapper-probe-summary.txtphp://filter, glob://, and compress.zlib paths all returned Image not found because file_exists() blocked the wrapper pathsOnly revisit if a PHP wrapper is found that returns true for file_exists() in PHP 8.2 and can list or transform target files

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 Under The Web, 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.