NextPath
NextPath is a sanitized challenge note from the local HTB archive, organized for quick review by category, difficulty, evidence flow, and reusable operator
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.
Walkthrough flow
Audit the Next.js API route that reads team images...
Identify inconsistent handling of repeated query...
Use a repeated id payload where the first value...
Align the normalized path to 100 bytes so slice(0,...
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.
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 port1337, 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:
- Checks
query.idwithconst ID_REGEX = /^[0-9]+$/m. - Blocks traversal with
query.id.includes("/") || query.id.includes(".."). - Builds
path.join("team", query.id + ".png"). - 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 letspath.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:
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-runRemote solve command:
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.txtAfter 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
| File | Size | SHA256 | Type | Notes |
|---|---|---|---|---|
files/a12c7389-436b-44be-aa8c-160b34857192.zip | 358727 | <hash redacted> | Zip archive data, at least v1.0 to extract, compression method=store | zip entries: 26 shown in artifact inventory JSON |
files/extracted/Dockerfile | 277 | <hash redacted> | ASCII text | |
files/extracted/app/.babelrc | 48 | <hash redacted> | JSON data | |
files/extracted/app/.eslintrc.json | 55 | <hash redacted> | JSON data | |
files/extracted/app/.gitignore | 368 | <hash redacted> | ASCII text | |
files/extracted/app/jsconfig.json | 73 | <hash redacted> | JSON data | |
files/extracted/app/next.config.js | 311 | <hash redacted> | ASCII text | |
files/extracted/app/package.json | 372 | <hash redacted> | JSON data | |
files/extracted/app/pages/_app.js | 170 | <hash redacted> | ASCII text | |
files/extracted/app/pages/_document.js | 231 | <hash redacted> | HTML document text, ASCII text | |
files/extracted/app/pages/api/team.js | 953 | <hash redacted> | Java source, ASCII text | |
files/extracted/app/pages/index.js | 3948 | <hash redacted> | HTML document text, ASCII text | |
files/extracted/app/public/assets/bootstrap.min.css | 232914 | <hash redacted> | Unicode text, UTF-8 text, with very long lines (65342) | |
files/extracted/app/public/favicon.ico | 25931 | <hash redacted> | MS Windows icon resource - 4 icons, 16x16, 32 bits/pixel, 32x32, 32 bits/pixel | |
files/extracted/app/styles/globals.css | 425 | <hash redacted> | ASCII text | |
files/extracted/app/team/1.png | 103652 | <hash redacted> | PNG image data, 256 x 256, 8-bit/color RGB, non-interlaced | |
files/extracted/app/team/2.png | 111529 | <hash redacted> | PNG image data, 256 x 256, 8-bit/color RGB, non-interlaced | |
files/extracted/app/team/3.png | 95815 | <hash redacted> | PNG image data, 256 x 256, 8-bit/color RGB, non-interlaced | |
files/extracted/build-docker.sh | 134 | <hash redacted> | Bourne-Again shell script text executable, ASCII text | |
files/extracted/flag.txt | 27 | <hash redacted> | ASCII text |
Evidence Ledger
| Time | Action | Output/File | Finding | Confidence | Next |
|---|---|---|---|---|---|
| 2026-06-12T07:53:20Z | harness init | challenge-state.json | Workspace initialized with deterministic state file | High | Inventory artifacts |
| 2026-06-12T07:53:32Z | artifact inventory | analysis/artifact-inventory.json | 20 artifact(s) inventoried | High | Build or update hypotheses |
| 2026-06-12T07:53:48Z | hypothesis recorded | hypothesis-board.md | Exploit multiline regex validation and 100-byte path truncation in /api/team?id=... to make fs.readFileSync open the remote flag path. | Medium | Reproduce locally by starting the Next.js app and probing crafted ids that pass regex while manipulating the sliced file path. |
| 2026-06-12T07:54:24Z | local memory search | analysis/research/local-memory-search-20260612T075424577858Z-2cf8f21c.md | Found 8 safe prior-note result(s) | Medium | Record useful result or skip |
| 2026-06-12T07:56:53Z | checkpoint recorded | analysis/checkpoint-triage-20260612T075653576502Z-f88b7e27.md | Checkpoint for TRIAGE | High | Use checkpoint to drive next decision |
| 2026-06-12T07:57:52Z | RAG query | analysis/rag/rag-query-20260612T075727644692Z-75065498.txt | RAG helper exited 0; output saved | Medium | Record retrieval tag and validation |
| 2026-06-12T07:58:11Z | source audit | analysis/source-audit.md | Source audit recorded | High | Gate before exploit |
| 2026-06-12T07:58:36Z | RAG record | analysis/rag-records.md | Retrieved memory tagged GENERIC | Medium | Validate or reject with live evidence |
| 2026-06-12T07:59:14Z | instrumentation plan | analysis/instrumentation-plan.md | Validate 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. | High | Stop 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:37Z | RAG record | analysis/rag-records.md | Retrieved memory tagged PARTIAL | Medium | Validate or reject with live evidence |
| 2026-06-12T08:01:46Z | evaluator | analysis/evaluator-20260612T080146152410Z-d4e35c2e.md | Proceed | High | Run exploit gate and execute the bounded remote solver request. |
| 2026-06-12T08:02:16Z | flag capture | loot/flag.txt | HTB-format flag captured; raw value kept in loot only | High | Write solution and run completion gate |
| 2026-06-12T08:04:43Z | completion gate | challenge-state.json | Completion gate passed; state marked COMPLETE | High | Optional sanitized memory summary approval |
Key Findings
pages/api/team.jsis the only meaningful route and reads image files usingfs.readFileSync(filepath.slice(0, 100)).- Repeated
idquery parameters makequery.idan array in Next.js; this bypasses the substring-style traversal checks becauseArray.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.txtaligns so the first 100 bytes end atflag.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:
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.
- Audit the Next.js API route that reads team images from disk.
- Identify inconsistent handling of repeated query parameters: regex validation coerces arrays to strings, while
Array.includes()checks exact elements. - Use a repeated
idpayload where the first value satisfies the multiline numeric regex and the second value carries traversal content. - Align the normalized path to 100 bytes so
slice(0, 100)removes the forced image extension. - Use
/proc/thread-self/root/...as a symlinked path back to the process root to reach the root-level flag file. - 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.mdanalysis/instrumentation-plan.mdanalysis/local/local-proof-run.txtanalysis/solution-summary.jsonanalysis/rag-records.mdsolve/solve.pyloot/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.
| Rank | Path | Evidence | Missing Proof | Cheapest Validation | Confidence | Status |
|---|---|---|---|---|---|---|
| 1 | Exploit 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. | Medium | Active |
Closed Branches
| Branch | Evidence Tested | Failure Output | Reason Closed | Revisit 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.