Challenge / Web

NextPath

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

MediumPublished 2025-09-24Sanitized local writeup

Scenario

NextPath attack path

NextPath 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 Web evidence, validation, and reusable operator lessons.

NextPath sanitized attack graph

Walkthrough flow

01

Audit the Next.js API route that reads team images...

02

Identify inconsistent handling of repeated query...

03

Use a repeated id payload where the first value...

04

Align the normalized path to 100 bytes so slice(0,...

05

Use /proc/thread-self/root/... as a symlinked path...

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.

  • Web/NextPath/writeup.md
  • htb-challenge/Web/NextPath/notes.md
  • htb-challenge/Web/NextPath/memory-summary.md
  • htb-challenge/Web/NextPath/hypothesis-board.md

Technical Walkthrough

Writeup

Challenge

  • Name: NextPath
  • Category: Web
  • Difficulty: Medium
  • Mode: hybrid

Summary

NextPath is a source-backed Next.js web challenge. The vulnerable API endpoint /api/team attempts to restrict image IDs to digits and prevent directory traversal, but the checks use inconsistent JavaScript coercion behavior. By sending repeated id parameters, query.id becomes an array: the regex and includes() checks can be satisfied, while query.id + ".png" converts the array into a path string. A carefully sized traversal path then abuses path.join() normalization plus slice(0, 100) to remove the forced .png suffix and read the root flag file.

Artifact Inventory

  • files/a12c7389-436b-44be-aa8c-160b34857192.zip: original HTB archive.
  • files/extracted/app/pages/api/team.js: vulnerable API route.
  • files/extracted/app/pages/index.js: frontend references /api/team?id=1, /api/team?id=2, and /api/team?id=3.
  • files/extracted/Dockerfile: confirms the app runs under /app, exposes port 1337, and copies the flag to /flag.txt.
  • analysis/artifact-inventory.json: full file inventory and hashes.

Analysis

The full source audit is recorded in analysis/source-audit.md.

The vulnerable code in pages/api/team.js performs these steps:

  1. Checks query.id with const ID_REGEX = /^[0-9]+$/m.
  2. Blocks traversal with query.id.includes("/") || query.id.includes("..").
  3. Builds path.join("team", query.id + ".png").
  4. Reads filepath.slice(0, 100).

The bug is the mismatch between string and array behavior:

  • Repeated query parameters in Next.js can produce an array.
  • RegExp.test(query.id) coerces that array to a comma-joined string; a first value containing a numeric line satisfies the multiline regex.
  • Array.prototype.includes("/") checks for an exact array element, not whether any element contains /.
  • Array.prototype.includes("..") similarly checks exact elements, so an element containing ../../... is not rejected.
  • query.id + ".png" coerces the array into one comma-joined string and lets path.join() normalize traversal.

The path also has to handle the forced .png suffix. The working payload uses 19 ../ segments followed by proc/thread-self/root/proc/thread-self/root/flag.txt. After path.join(), the first 100 bytes end exactly at flag.txt, so slice(0, 100) drops the appended .png. The /proc/thread-self/root/... component resolves through the running process root and reaches the flag at the container root.

The payload was validated locally against the Docker-built app before remote use. The local proof command and non-sensitive solver output are in analysis/local/local-proof-run.txt. RAG was recorded as generic/partial advisory context in analysis/rag-records.md; the exploit evidence is the source audit plus local Docker proof.

Solve

The reproducible solver is solve/solve.py.

Local validation command:

bash
cd <local workspace>
docker build -t nextpath-local Web/NextPath/files/extracted
docker run -d --name nextpath-local-run -p <TARGET>:31337:1337 nextpath-local
/opt/homebrew/bin/python3 Web/NextPath/solve/solve.py --base-url http://<TARGET>:31337 --output loot/local-validation-flag.txt --response-output loot/local-validation-response.bin --show-url
docker rm -f nextpath-local-run

Remote solve command:

bash
cd <local workspace>
python3 scripts/challenge_exec.py Web/NextPath -- sh -lc 'cd Web/NextPath && /opt/homebrew/bin/python3 solve/solve.py --base-url http://<TARGET>:32239 --output loot/flag-candidate.txt --response-output loot/remote-response.bin'
python3 scripts/challenge_harness.py capture-flag Web/NextPath --from loot/flag-candidate.txt

After capture, raw response bodies were removed from analysis/; the final raw flag is stored only in loot/flag.txt. The sanitized solve metadata is in analysis/solution-summary.json.

Flag

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

Lessons

  • In JavaScript web handlers, validation can break when a parameter may be either a string or an array.
  • Array.includes() is not a substring check; it checks for an exact array element.
  • Regex multiline anchors can validate one safe-looking line while leaving other lines uncontrolled.
  • Length truncation after path normalization can remove an appended extension if the attacker controls the normalized path length.
  • For source-backed web challenges, local Docker proof is high-value and reduces blind remote probing.

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: NextPath
  • Category: Web
  • Difficulty: Medium
  • Mode: hybrid
  • Remote instance: none
  • Start time: 2026-06-12T07:53:20Z
  • 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/a12c7389-436b-44be-aa8c-160b34857192.zip358727<hash redacted>Zip archive data, at least v1.0 to extract, compression method=storezip entries: 26 shown in artifact inventory JSON
files/extracted/Dockerfile277<hash redacted>ASCII text
files/extracted/app/.babelrc48<hash redacted>JSON data
files/extracted/app/.eslintrc.json55<hash redacted>JSON data
files/extracted/app/.gitignore368<hash redacted>ASCII text
files/extracted/app/jsconfig.json73<hash redacted>JSON data
files/extracted/app/next.config.js311<hash redacted>ASCII text
files/extracted/app/package.json372<hash redacted>JSON data
files/extracted/app/pages/_app.js170<hash redacted>ASCII text
files/extracted/app/pages/_document.js231<hash redacted>HTML document text, ASCII text
files/extracted/app/pages/api/team.js953<hash redacted>Java source, ASCII text
files/extracted/app/pages/index.js3948<hash redacted>HTML document text, ASCII text
files/extracted/app/public/assets/bootstrap.min.css232914<hash redacted>Unicode text, UTF-8 text, with very long lines (65342)
files/extracted/app/public/favicon.ico25931<hash redacted>MS Windows icon resource - 4 icons, 16x16, 32 bits/pixel, 32x32, 32 bits/pixel
files/extracted/app/styles/globals.css425<hash redacted>ASCII text
files/extracted/app/team/1.png103652<hash redacted>PNG image data, 256 x 256, 8-bit/color RGB, non-interlaced
files/extracted/app/team/2.png111529<hash redacted>PNG image data, 256 x 256, 8-bit/color RGB, non-interlaced
files/extracted/app/team/3.png95815<hash redacted>PNG image data, 256 x 256, 8-bit/color RGB, non-interlaced
files/extracted/build-docker.sh134<hash redacted>Bourne-Again shell script text executable, ASCII text
files/extracted/flag.txt27<hash redacted>ASCII text

Evidence Ledger

TimeActionOutput/FileFindingConfidenceNext
2026-06-12T07:53:20Zharness initchallenge-state.jsonWorkspace initialized with deterministic state fileHighInventory artifacts
2026-06-12T07:53:32Zartifact inventoryanalysis/artifact-inventory.json20 artifact(s) inventoriedHighBuild or update hypotheses
2026-06-12T07:53:48Zhypothesis recordedhypothesis-board.mdExploit multiline regex validation and 100-byte path truncation in /api/team?id=... to make fs.readFileSync open the remote flag path.MediumReproduce locally by starting the Next.js app and probing crafted ids that pass regex while manipulating the sliced file path.
2026-06-12T07:54:24Zlocal memory searchanalysis/research/local-memory-search-20260612T075424577858Z-2cf8f21c.mdFound 8 safe prior-note result(s)MediumRecord useful result or skip
2026-06-12T07:56:53Zcheckpoint recordedanalysis/checkpoint-triage-20260612T075653576502Z-f88b7e27.mdCheckpoint for TRIAGEHighUse checkpoint to drive next decision
2026-06-12T07:57:52ZRAG queryanalysis/rag/rag-query-20260612T075727644692Z-75065498.txtRAG helper exited 0; output savedMediumRecord retrieval tag and validation
2026-06-12T07:58:11Zsource auditanalysis/source-audit.mdSource audit recordedHighGate before exploit
2026-06-12T07:58:36ZRAG recordanalysis/rag-records.mdRetrieved memory tagged GENERICMediumValidate or reject with live evidence
2026-06-12T07:59:14Zinstrumentation plananalysis/instrumentation-plan.mdValidate the /api/team repeated-id array coercion plus 100-byte truncation primitive locally, then use the same single request against the HTB remote to read the challenge flag.HighStop if local Docker proof fails, if remote returns invalid format/timeout, or if path alignment differs; do not fuzz broadly or mutate unrelated routes.
2026-06-12T08:01:37ZRAG recordanalysis/rag-records.mdRetrieved memory tagged PARTIALMediumValidate or reject with live evidence
2026-06-12T08:01:46Zevaluatoranalysis/evaluator-20260612T080146152410Z-d4e35c2e.mdProceedHighRun exploit gate and execute the bounded remote solver request.
2026-06-12T08:02:16Zflag captureloot/flag.txtHTB-format flag captured; raw value kept in loot onlyHighWrite solution and run completion gate
2026-06-12T08:04:43Zcompletion gatechallenge-state.jsonCompletion gate passed; state marked COMPLETEHighOptional sanitized memory summary approval

Key Findings

  • pages/api/team.js is the only meaningful route and reads image files using fs.readFileSync(filepath.slice(0, 100)).
  • Repeated id query parameters make query.id an array in Next.js; this bypasses the substring-style traversal checks because Array.includes() checks exact elements.
  • The multiline numeric regex can be satisfied by the first array value while traversal content is carried in the second array value.
  • A path using 19 traversal segments plus proc/thread-self/root/proc/thread-self/root/flag.txt aligns so the first 100 bytes end at flag.txt, dropping the forced .png.
  • Local Docker proof succeeded before the remote solve; sanitized metadata is in analysis/solution-summary.json.

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: Web
  • Challenge: NextPath
  • 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. Audit the Next.js API route that reads team images from disk.
  2. Identify inconsistent handling of repeated query parameters: regex validation coerces arrays to strings, while Array.includes() checks exact elements.
  3. Use a repeated id payload where the first value satisfies the multiline numeric regex and the second value carries traversal content.
  4. Align the normalized path to 100 bytes so slice(0, 100) removes the forced image extension.
  5. Use /proc/thread-self/root/... as a symlinked path back to the process root to reach the root-level flag file.
  6. Validate locally with the provided Dockerfile, then run the same bounded request against the remote instance and capture the flag.

Reusable Lessons

  • Treat query parameters as union types in Node/Next.js: string, array, or absent.
  • Do not combine regex validation, array coercion, path normalization, and truncation without canonicalizing the input type first.
  • Array.includes("/") does not prove that no array element contains /.
  • Fixed-length truncation can become a suffix-stripping primitive when the attacker controls path length.

Dead Ends

  • No broad fuzzing was needed. The single source-derived route was validated locally and remotely.
  • RAG did not provide NextPath-specific evidence; it was used only as generic advisory context.

Tool Quirks

  • Docker build emitted Next.js/npm warnings, but the application built and ran successfully.
  • Generated response bodies can contain flag-like content; remove or keep them under loot/ only before completion.

Evidence Paths

  • analysis/source-audit.md
  • analysis/instrumentation-plan.md
  • analysis/local/local-proof-run.txt
  • analysis/solution-summary.json
  • analysis/rag-records.md
  • 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
1Exploit multiline regex validation and 100-byte path truncation in /api/team?id=... to make fs.readFileSync open the remote flag path.pages/api/team.js uses /^[0-9]+$/m with RegExp.test, blocks '/' and '..', then reads path.join('team', id + '.png').slice(0, 100); Dockerfile copies flag.txt to /flag.txt.Reproduce locally by starting the Next.js app and probing crafted ids that pass regex while manipulating the sliced file path.MediumActive

Closed Branches

BranchEvidence TestedFailure OutputReason ClosedRevisit Condition

Technical analogy

How to remember this solve

Think of the web app like a building with signs on every door. The solve usually comes from reading the map carefully, finding the door the app forgot to hide, then sending the exact request that proves you understand the route.

For NextPath, 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.