DoxPit
DoxPit is a sanitized challenge note from the local HTB archive, organized for quick review by category, difficulty, evidence flow, and reusable operator
Scenario
DoxPit attack path
DoxPit 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
Source and route audit
Trust boundary flaw
Exploit request chain
Admin or proof proof
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/DoxPit/writeup.md
- htb-challenge/Web/DoxPit/notes.md
- htb-challenge/Web/DoxPit/memory-summary.md
- htb-challenge/Web/DoxPit/hypothesis-board.md
Technical Walkthrough
Writeup
Challenge
- Name: DoxPit
- Category: Web
- Difficulty: Medium
- Mode: hybrid
Summary
DoxPit is a hybrid Web challenge with a public Next.js frontend and an internal Flask "AV" service. The solved chain was:
- Abuse a vulnerable Next.js Server Actions redirect path to SSRF into the internal Flask service.
- Use the internal
/registerroute to create a token-bearing user. - Use that token against
/home. - Trigger Jinja SSTI through the
directoryparameter and read the randomized/flag*path.
Artifact Inventory
Reference analysis/artifact-inventory.json and summarize the relevant files or remote surface.
Relevant artifacts:
files/extracted/challenge/front-end/package.json: pinsnextto14.1.0.files/extracted/challenge/front-end/app/serverActions.tsx: exports a server action that callsredirect("/error").files/extracted/challenge/av/application/blueprints/routes.py: exposes/registerand token-authenticated/home.files/extracted/challenge/av/application/util/general.py: defines the weak blacklist fordirectory.files/extracted/entrypoint.sh: renames/flag.txtto/flag<10 hex>.txt.analysis/ssrf-register-nextaction-response.txt: live proof that the Next-Action request reached internal Flask/register.analysis/remote-solve-transcript.txt: sanitized execution transcript for the final solver run.
Analysis
The Next source and package metadata matched the Server Actions redirect SSRF class:
nextis14.1.0.doRedirect()callsredirect("/error").- The live root page exposed a
$<secret redacted>...value for the action.
The browser-style form request only produced a normal redirect. The server-action request shape that worked was:
POST /Host: <attacker tunnel host>Next-Action: <live action id>Content-Type: text/plain;charset=UTF-8- body
[]
That returned the internal Flask registration response through the Next response, proving SSRF into <TARGET>:3000.
The Flask backend then supplied the second primitive. /register returns a token, and /home accepts either a logged-in Flask session or a valid token query parameter. /home rejects only a short blacklist in directory, then replaces {{ results.scanned_directory }} with the raw directory value before calling render_template_string(). That creates a Jinja SSTI sink.
The final payload avoided the blacklist by using Jinja statement delimiters, attr(), and generated underscores via format(95,95,95,95), then executed cat /flag* to handle the randomized filename.
Public research used:
- Assetnote advisory on <secret redacted>:
https://www.assetnote.io/resources/research/advisory-next-js-ssrf-cve-2024-34351/ - Assetnote Server Actions SSRF background:
https://www.assetnote.io/resources/research/digging-for-ssrf-in-nextjs-apps/
The public research was treated only as a lead. The exploit decision came from the provided source and the live SSRF proof.
Solve
The reproducible solver is solve/solve.py. It:
- Fetches the live root page and extracts the server-action ID.
- Starts a local HTTP redirect helper.
- Uses a Cloudflare tunnel, or a supplied stable tunnel, as the attacker-controlled
Host. - Sends the validated Next-Action request to SSRF into internal Flask
/register. - Parses the returned token.
- Sends a second SSRF request to the authenticated
/homescan route with the returned bearer value and the encoded directory payload. - Writes only the flag candidate to the requested output file.
Final tracked execution used:
python3 scripts/challenge_exec.py Web/DoxPit -- python3 Web/DoxPit/solve/solve.py \
--base-url http://<TARGET>:31944 \
--tunnel-url https://unable-names-upgrades-flying.trycloudflare.com \
--helper-port 8123 \
--output Web/DoxPit/analysis/flag-candidate.txt \
--transcript Web/DoxPit/analysis/remote-solve-transcript.txtThen the harness captured the candidate into loot/flag.txt.
Flag
Raw flag is stored in loot/flag.txt and intentionally not reproduced here.
Lessons
- For source-backed Medium Web challenges, source audit plus one high-signal live validation is stronger than broad fuzzing.
- Next.js Server Actions requests are not always equivalent to browser form submissions; the
Next-Actionheader and request body shape mattered here. - Blacklists that block literal Jinja expression delimiters and attribute syntax can still be bypassed with statement delimiters plus dynamic attribute access.
- Randomized flag filenames should be read with a glob such as
/flag*when the entrypoint proves the naming convention.
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: DoxPit
- Category: Web
- Difficulty: Medium
- Mode: hybrid
- Remote instance: <TARGET>:31944
- Start time: 2026-06-12T13:54:35Z
- 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/a12c7371-7866-481d-81ef-4747c2fff58c.zip | 70557 | <hash redacted> | Zip archive data, at least v2.0 to extract, compression method=deflate | zip entries: 55 shown in artifact inventory JSON |
files/extracted/Dockerfile | 1621 | <hash redacted> | ASCII text | |
files/extracted/build_docker.sh | 134 | <hash redacted> | Bourne-Again shell script text executable, ASCII text | |
files/extracted/challenge/av/application/app.py | 645 | <hash redacted> | Python script text executable, ASCII text | |
files/extracted/challenge/av/application/blueprints/routes.py | 2889 | <hash redacted> | Python script text executable, ASCII text | |
files/extracted/challenge/av/application/config.py | 261 | <hash redacted> | Python script text executable, ASCII text | |
files/extracted/challenge/av/application/static/css/styles.css | 1858 | <hash redacted> | ASCII text | |
files/extracted/challenge/av/application/static/images/icon.png | 2387 | <hash redacted> | PNG image data, 64 x 64, 8-bit/color RGBA, non-interlaced | |
files/extracted/challenge/av/application/templates/error.html | 415 | <hash redacted> | HTML document text, ASCII text | |
files/extracted/challenge/av/application/templates/index.html | 918 | <hash redacted> | HTML document text, ASCII text | |
files/extracted/challenge/av/application/templates/layouts/base.html | 144 | <hash redacted> | HTML document text, ASCII text | |
files/extracted/challenge/av/application/templates/layouts/head.html | 245 | <hash redacted> | HTML document text, ASCII text | |
files/extracted/challenge/av/application/templates/login.html | 716 | <hash redacted> | HTML document text, ASCII text | |
files/extracted/challenge/av/application/templates/register.html | 682 | <hash redacted> | HTML document text, ASCII text | |
files/extracted/challenge/av/application/templates/scan.html | 1446 | <hash redacted> | HTML document text, ASCII text | |
files/extracted/challenge/av/application/util/database.py | 2086 | <hash redacted> | Python script text executable, ASCII text | |
files/extracted/challenge/av/application/util/general.py | 202 | <hash redacted> | Python script text executable, ASCII text | |
files/extracted/challenge/av/application/util/scanner.py | 1858 | <hash redacted> | Python script text executable, ASCII text | |
files/extracted/challenge/av/requirements.txt | 86 | <hash redacted> | ASCII text | |
files/extracted/challenge/av/run.py | 198 | <hash redacted> | Python script text executable, ASCII text | |
files/extracted/challenge/front-end/.eslintrc.json | 40 | <hash redacted> | JSON data | |
files/extracted/challenge/front-end/.gitignore | 391 | <hash redacted> | ASCII text | |
files/extracted/challenge/front-end/app/asciiart.tsx | 8961 | <hash redacted> | Java source, Unicode text, UTF-8 text | |
files/extracted/challenge/front-end/app/error/page.tsx | 545 | <hash redacted> | Java source, ASCII text | |
files/extracted/challenge/front-end/app/globals.css | 3776 | <hash redacted> | ASCII text | |
files/extracted/challenge/front-end/app/hof/page.tsx | 2562 | <hash redacted> | Java source, Unicode text, UTF-8 text | |
files/extracted/challenge/front-end/app/layout.tsx | 623 | <hash redacted> | HTML document text, ASCII text | |
files/extracted/challenge/front-end/app/navbar.tsx | 2040 | <hash redacted> | ASCII text | |
files/extracted/challenge/front-end/app/page.tsx | 5161 | <hash redacted> | Java source, Unicode text, UTF-8 text | |
files/extracted/challenge/front-end/app/pastetable.tsx | 1731 | <hash redacted> | HTML document text, ASCII text | |
files/extracted/challenge/front-end/app/serverActions.tsx | 120 | <hash redacted> | Java source, ASCII text | |
files/extracted/challenge/front-end/next-env.d.ts | 201 | <hash redacted> | ASCII text | |
files/extracted/challenge/front-end/next.config.mjs | 92 | <hash redacted> | ASCII text | |
files/extracted/challenge/front-end/package.json | 530 | <hash redacted> | JSON data | |
files/extracted/challenge/front-end/public/doxpit-ascii.png | 11592 | <hash redacted> | PNG image data, 555 x 277, 8-bit/color RGBA, non-interlaced | |
files/extracted/challenge/front-end/public/oni-ascii.png | 40868 | <hash redacted> | PNG image data, 555 x 526, 8-bit/color RGBA, non-interlaced | |
files/extracted/challenge/front-end/tsconfig.json | 574 | <hash redacted> | JSON data | |
files/extracted/config/supervisord.conf | 419 | <hash redacted> | ASCII text | |
files/extracted/entrypoint.sh | 215 | <hash redacted> | POSIX shell script text executable, ASCII text | |
files/extracted/flag.txt | 26 | <hash redacted> | ASCII text, with no line terminators |
Evidence Ledger
| Time | Action | Output/File | Finding | Confidence | Next |
|---|---|---|---|---|---|
| 2026-06-12T13:54:35Z | harness init | challenge-state.json | Workspace initialized with deterministic state file | High | Inventory artifacts |
| 2026-06-12T13:54:45Z | artifact inventory | analysis/artifact-inventory.json | 40 artifact(s) inventoried | High | Build or update hypotheses |
| 2026-06-12T14:04:48Z | hypothesis recorded | hypothesis-board.md | Next.js 14.1.0 Server Actions SSRF reaches internal Flask AV, registers a token, then authenticated /home directory SSTI reads randomized /flag*.txt | High | Run solve/solve.py through challenge_exec; it starts a tunnel helper, obtains token via SSRF, sends SSTI payload via SSRF, and writes only a flag candidate file. |
| 2026-06-12T14:04:59Z | source audit | analysis/source-audit.md | Source audit recorded | High | Gate before exploit |
| 2026-06-12T14:04:59Z | research record | analysis/research/research-records.md | Research tagged MATCHED | Medium | Validate against current evidence |
| 2026-06-12T14:05:10Z | checkpoint recorded | analysis/checkpoint-analysis-20260612T140510771810Z-24ca7f0a.md | Checkpoint for ANALYSIS | High | Use checkpoint to drive next decision |
| 2026-06-12T14:05:32Z | RAG query | analysis/rag/rag-query-20260612T140519857091Z-f0660a97.txt | RAG helper exited 0; output saved | Medium | Record retrieval tag and validation |
| 2026-06-12T14:05:50Z | RAG record | analysis/rag-records.md | Retrieved memory tagged MISSING | Medium | Validate or reject with live evidence |
| 2026-06-12T14:06:07Z | instrumentation plan | analysis/instrumentation-plan.md | Capture the flag through a bounded Next.js SSRF to internal Flask token creation followed by token-authenticated Jinja SSTI against /home. | High | Stop after two failed SSRF/SSTI attempts without a new response difference; record failure and do not continue blind remote probing. |
| 2026-06-12T14:06:24Z | local memory search | analysis/research/local-memory-search-20260612T140624922497Z-e5672713.md | Found 8 safe prior-note result(s) | Medium | Record useful result or skip |
| 2026-06-12T14:06:41Z | local memory record | analysis/local-memory-records.md | Prior local notes reviewed as fallback/advisory context | Medium | Validate against current evidence |
| 2026-06-12T14:06:53Z | evaluator | analysis/evaluator-20260612T140653692969Z-dd8b471f.md | Proceed | High | Run gated solver through scripts/challenge_exec.py Web/DoxPit -- python3 Web/DoxPit/solve/solve.py --base-url http://<TARGET>:31944 --output Web/DoxPit/analysis/flag-candidate.txt --transcript Web/DoxPit/analysis/remote-solve-transcript.txt |
| 2026-06-12T14:11:37Z | flag capture | loot/flag.txt | HTB-format flag captured; raw value kept in loot only | High | Write solution and run completion gate |
| 2026-06-12T14:12:35Z | completion gate | challenge-state.json | Completion gate passed; state marked COMPLETE | High | Optional sanitized memory summary approval |
| 2026-06-12T14:13:24Z | completion gate | challenge-state.json | Completion gate passed; state marked COMPLETE | High | Optional sanitized memory summary approval |
Key Findings
-
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: DoxPit
- 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 | Next.js 14.1.0 Server Actions SSRF reaches internal Flask AV, registers a token, then authenticated /home directory SSTI reads randomized /flag*.txt | Source pins next 14.1.0 and has server action redirect("/error"); Flask /register returns tokens; /home accepts token auth and inserts raw directory into render_template_string after a weak blacklist; live Next-Action Host-header request returned internal Flask registration HTML and token. | Need final bounded SSRF request to /home with validated blacklist-safe Jinja payload and harness flag capture. | Run solve/solve.py through challenge_exec; it starts a tunnel helper, obtains token via SSRF, sends SSTI payload via SSRF, and writes only a flag candidate file. | High | 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 DoxPit, 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.