Desires
Desires is a sanitized challenge note from the local HTB archive, organized for quick review by category, difficulty, evidence flow, and reusable operator
Scenario
Desires attack path
Desires 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 audit found that the Go login handler writes a...
The session middleware ignores the real session...
Uploading a normal archive can place attacker-chosen...
A failed login using ../../app/service/files/ as the...
GetSession() then resolves the session read into the...
Source coverage
High source coverage
Status: complete. This article is generated from 6 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/Desires/writeup.md
- htb-challenge/Web/Desires/notes.md
- htb-challenge/Web/Desires/memory-summary.md
- htb-challenge/Web/Desires/hypothesis-board.md
- HTB/_knowledge/exports/ctf-lightrag-latest-203412/documents/challenge__Web__Desires__memory-summary.md.6464f0ee4b.md
- HTB/_knowledge/exports/ctf-lightrag-latest-203412/documents/challenge__Web__Desires__notes.md.b789497f95.md
Technical Walkthrough
Writeup
Challenge
- Name: Desires
- Category: Web
- Difficulty: Easy
- Mode: hybrid
Summary
Desires is a source-provided web challenge built from a Go Fiber frontend, a local Node SSO service, and Redis-backed file sessions. The SSO correctly creates only normal users, but the Go frontend trusts attacker-controlled session metadata in two places:
- failed logins still write a Redis
username -> sessionIDpointer before SSO authentication is checked - authenticated routes ignore the real
sessioncookie value and resolve the session file using theusernamecookie
The solve uploads a fake admin session file into a normal user's upload directory, poisons Redis with a traversal username, and then makes /user/admin load that uploaded JSON as the current session.
Artifact Inventory
Key artifacts:
files/a12c7330-7ca9-4da3-845a-993bcec029e1.zip: challenge archivefiles/extracted/challenge/service/services/http.go: Go route handlers, login order, upload handler, admin guardfiles/extracted/challenge/service/services/sessions.go: Redis pointer and file-backed session loaderfiles/extracted/challenge/sso/index.js: Node SSO with user-only registrationanalysis/source-audit.md: source-backed exploit decisionsolve/solve.py: reproducible remote solver
Analysis
The first candidate path was Zip Slip into /tmp/sessions/admin, because the upload route extracts an archive with archiver.Unarchive(tempFile, userFolder). That was closed after checking github.com/mholt/archiver/v3 v3.5.0: ZIP extraction calls CheckPath() before writing entries and rejects traversal outside the destination directory.
The stronger path is application-native path traversal through session lookup:
LoginHandlercalculatessessionID = sha256(unix_second).- It calls
PrepareSession(sessionID, credentials.Username)before validating credentials against the SSO. - A failed login can therefore set a Redis pointer for an arbitrary username string.
SessionMiddlewareonly checks thatsessionis non-empty, then callsGetSession(username)using the attacker-controlledusernamecookie.GetSession(username)readsfilepath.Join("/tmp/sessions", username, sessionID).- A traversal username like
../../app/service/files/<normal_user>resolves that read into the normal user's uploaded file directory. - If a file named with the predicted
sessionIDcontains{"username":"admin","id":1,"role":"admin"},/user/adminrenders the flag.
The remote run confirmed the chain in analysis/remote/solve-transcript-redacted.txt: registration and normal login succeeded, archive upload returned 202, the failed traversal login returned 400 as expected, and the first admin request returned 200 with an HTB-format flag captured by the harness.
Solve
Run:
python3 Web/Desires/solve/solve.py \
--base-url http://<TARGET>:30575 \
--output Web/Desires/loot/flag-candidate.txt \
--transcript Web/Desires/analysis/remote/solve-transcript-redacted.txt
python3 scripts/challenge_harness.py capture-flag Web/Desires --from loot/flag-candidate.txtThe solver:
- registers a normal user
- logs in and keeps the real user cookies
- creates a ZIP with many predicted
sha256(unix_second)filenames around the server clock - uploads the ZIP through
/user/upload - sends a failed login as
../../app/service/files/<normal_user>to poison Redis - requests
/user/adminwithsession: <redacted>and the traversalusername - writes the flag candidate under
loot/without printing it
Flag
Raw flag is stored in loot/flag.txt and intentionally not reproduced here.
Lessons
- For source-provided web challenges, check operation ordering before looking for complex primitives. Here,
PrepareSession()before authentication was the key bug. - Do not assume archive upload means Zip Slip. Validate the extraction library before committing to that path.
- Cookie trust bugs can combine with filesystem joins even when account registration filters path characters.
- Predictable session IDs are especially dangerous when the server reads session files from attacker-influenced paths.
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: Desires
- Category: Web
- Difficulty: Easy
- Mode: hybrid
- Remote instance: <TARGET>:30575
- Start time: 2026-06-08T03:47:11Z
- 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/a12c7330-7ca9-4da3-845a-993bcec029e1.zip | 86236 | <hash redacted> | Zip archive data, at least v2.0 to extract, compression method=deflate | zip entries: 30 shown in artifact inventory JSON |
Evidence Ledger
| Time | Action | Output/File | Finding | Confidence | Next |
|---|---|---|---|---|---|
| 2026-06-08T03:47:11Z | harness init | challenge-state.json | Workspace initialized with deterministic state file | High | Inventory artifacts |
| 2026-06-08T03:47:21Z | artifact inventory | analysis/artifact-inventory.json | 1 artifact(s) inventoried | High | Build or update hypotheses |
| 2026-06-08T03:47:43Z | hypothesis recorded | hypothesis-board.md | Source-first web audit: map Go service routes, Node SSO behavior, Redis/session trust boundary, and file upload/admin paths; exploit only a source-backed auth/session or upload flaw against the remote. | Medium | Extract source, inspect route handlers and SSO/session code, identify the precise trust boundary, then test one source-backed request chain on the remote. |
| 2026-06-08T03:48:54Z | checkpoint recorded | analysis/checkpoint-triage-20260608T034854345861Z-bab6bb15.md | Checkpoint for TRIAGE | High | Use checkpoint to drive next decision |
| 2026-06-08T03:49:07Z | local memory record | analysis/local-memory-records.md | Prior local notes reviewed as fallback/advisory context | Medium | Validate against current evidence |
| 2026-06-08T03:53:19Z | source audit | analysis/source-audit.md | Source audit recorded | High | Gate before exploit |
| 2026-06-08T03:53:35Z | hypothesis recorded | hypothesis-board.md | Poison Redis for traversal username via failed login, then make GetSession read uploaded admin JSON from /app/service/files/<normal_user>/<sessionID>. | High | Run solve/solve.py once after source audit/evaluator/gate. |
| 2026-06-08T03:53:35Z | evaluator | analysis/evaluator-20260608T035335437033Z-9e30d9b1.md | Proceed | High | Run the solver against the remote instance, then capture the flag through the harness if returned. |
| 2026-06-08T03:54:05Z | flag capture | loot/flag.txt | HTB-format flag captured; raw value kept in loot only | High | Write solution and run completion gate |
| 2026-06-08T03:55:10Z | 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: Desires
- Difficulty: Easy
- Source workspace:
<local workspace>
Validated Solve Chain
Concepts only. Do not include raw flags, reusable credentials, tokens, cookies, private keys, or live secrets.
- Source audit found that the Go login handler writes a Redis username-to-session pointer before SSO authentication succeeds.
- The session middleware ignores the real session cookie value and loads the session file using the username cookie.
- Uploading a normal archive can place attacker-chosen filenames under
/app/service/files/<normal_user>. - A failed login using
../../app/service/files/<normal_user>as the username poisons Redis for that traversal key. GetSession()then resolves the session read into the uploaded files directory and deserializes attacker-controlled admin JSON./user/adminrenders the flag when the loaded JSON has roleadmin.
Reusable Lessons
- Validate library protections before pursuing an expected primitive. In this case, the archive library blocked ZIP traversal, but the application session loader still provided a traversal path.
- Look for pre-auth state mutation. Failed authentication can still leave useful server-side state.
- If a session cookie is only checked for presence and not used for lookup, another cookie or parameter may become the real authority.
- Time-derived session IDs can be handled by uploading a window of predicted hashes rather than racing a single value.
Dead Ends
- Direct Zip Slip to
/tmp/sessions/adminwas closed becausegithub.com/mholt/archiver/v3performs path traversal checks before ZIP extraction.
Tool Quirks
- The solver keeps raw flag output in
loot/only and writes a redacted transcript underanalysis/remote/. - The source archive uses the standard HTB extraction convention.
Evidence Paths
analysis/source-audit.mdanalysis/remote/solve-transcript-redacted.txtsolve/solve.pyloot/flag.txt
Ingestion Decision
- Proposed for LightRAG: yes, after user approval
- 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 | Source-first web audit: map Go service routes, Node SSO behavior, Redis/session trust boundary, and file upload/admin paths; exploit only a source-backed auth/session or upload flaw against the remote. | Provided source archive contains Go service, Node SSO, Redis helper, views for register/login/upload/admin, Docker and supervisor configuration. | Extract source, inspect route handlers and SSO/session code, identify the precise trust boundary, then test one source-backed request chain on the remote. | Medium | Active | |
| 1 | Poison Redis for traversal username via failed login, then make GetSession read uploaded admin JSON from /app/service/files/<normal_user>/<sessionID>. | LoginHandler calls PrepareSession before SSO validation; SessionMiddleware ignores session cookie value; GetSession joins /tmp/sessions, username cookie, and Redis sessionID; upload extracts normal files under /app/service/files/<normal_user>. | Remote flag render from /user/admin. | Run solve/solve.py once after source audit/evaluator/gate. | High | active |
Closed Branches
| Branch | Evidence Tested | Failure Output | Reason Closed | Revisit Condition |
|---|
Memory Summary
approval_required: true
Sanitized Memory Summary
Metadata
- Platform: HackTheBox Challenges
- Category: Web
- Challenge: Desires
- Difficulty: Easy
- Source workspace:
<local workspace>
Validated Solve Chain
Concepts only. Do not include raw flags, reusable credentials, tokens, cookies, private keys, or live secrets.
- Source audit found that the Go login handler writes a Redis username-to-session pointer before SSO authentication succeeds.
- The session middleware ignores the real session cookie value and loads the session file using the username cookie.
- Uploading a normal archive can place attacker-chosen filenames under
/app/service/files/<normal_user>. - A failed login using
../../app/service/files/<normal_user>as the username poisons Redis for that traversal key. GetSession()then resolves the session read into the uploaded files directory and deserializes attacker-controlled admin JSON./user/adminrenders the flag when the loaded JSON has roleadmin.
Reusable Lessons
- Validate library protections before pursuing an expected primitive. In this case, the archive library blocked ZIP traversal, but the application session loader still provided a traversal path.
- Look for pre-auth state mutation. Failed authentication can still leave useful server-side state.
- If a session cookie is only checked for presence and not used for lookup, another cookie or parameter may become the real authority.
- Time-derived session IDs can be handled by uploading a window of predicted hashes rather than racing a single value.
Dead Ends
- Direct Zip Slip to
/tmp/sessions/adminwas closed becausegithub.com/mholt/archiver/v3performs path traversal checks before ZIP extraction.
Tool Quirks
- The solver keeps raw flag output in
loot/only and writes a redacted transcript underanalysis/remote/. - The source archive uses the standard HTB extraction convention.
Evidence Paths
analysis/source-audit.mdanalysis/remote/solve-transcript-redacted.txtsolve/solve.pyloot/flag.txt
Ingestion Decision
- Proposed for LightRAG: yes, after user approval
- Requires user approval before ingestion: yes
Notes
Notes
Scope
- Challenge: Desires
- Category: Web
- Difficulty: Easy
- Mode: hybrid
- Remote instance: <TARGET>:30575
- Start time: 2026-06-08T03:47:11Z
- 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/a12c7330-7ca9-4da3-845a-993bcec029e1.zip | 86236 | <hash redacted> | Zip archive data, at least v2.0 to extract, compression method=deflate | zip entries: 30 shown in artifact inventory JSON |
Evidence Ledger
| Time | Action | Output/File | Finding | Confidence | Next |
|---|---|---|---|---|---|
| 2026-06-08T03:47:11Z | harness init | challenge-state.json | Workspace initialized with deterministic state file | High | Inventory artifacts |
| 2026-06-08T03:47:21Z | artifact inventory | analysis/artifact-inventory.json | 1 artifact(s) inventoried | High | Build or update hypotheses |
| 2026-06-08T03:47:43Z | hypothesis recorded | hypothesis-board.md | Source-first web audit: map Go service routes, Node SSO behavior, Redis/session trust boundary, and file upload/admin paths; exploit only a source-backed auth/session or upload flaw against the remote. | Medium | Extract source, inspect route handlers and SSO/session code, identify the precise trust boundary, then test one source-backed request chain on the remote. |
| 2026-06-08T03:48:54Z | checkpoint recorded | analysis/checkpoint-triage-20260608T034854345861Z-bab6bb15.md | Checkpoint for TRIAGE | High | Use checkpoint to drive next decision |
| 2026-06-08T03:49:07Z | local memory record | analysis/local-memory-records.md | Prior local notes reviewed as fallback/advisory context | Medium | Validate against current evidence |
| 2026-06-08T03:53:19Z | source audit | analysis/source-audit.md | Source audit recorded | High | Gate before exploit |
| 2026-06-08T03:53:35Z | hypothesis recorded | hypothesis-board.md | Poison Redis for traversal username via failed login, then make GetSession read uploaded admin JSON from /app/service/files/<normal_user>/<sessionID>. | High | Run solve/solve.py once after source audit/evaluator/gate. |
| 2026-06-08T03: <REDACTED>, then capture the flag through the harness if returned. | |||||
| 2026-06-08T03: <REDACTED> | |||||
| 2026-06-08T03:55:10Z | 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.
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 Desires, 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.