Challenge / Web

Desires

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

EasyPublished 2025-09-06Sanitized local writeup

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.

Desires sanitized attack graph

Walkthrough flow

01

Source audit found that the Go login handler writes a...

02

The session middleware ignores the real session...

03

Uploading a normal archive can place attacker-chosen...

04

A failed login using ../../app/service/files/ as the...

05

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.

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/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 -> sessionID pointer before SSO authentication is checked
  • authenticated routes ignore the real session cookie value and resolve the session file using the username cookie

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 archive
  • files/extracted/challenge/service/services/http.go: Go route handlers, login order, upload handler, admin guard
  • files/extracted/challenge/service/services/sessions.go: Redis pointer and file-backed session loader
  • files/extracted/challenge/sso/index.js: Node SSO with user-only registration
  • analysis/source-audit.md: source-backed exploit decision
  • solve/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:

  1. LoginHandler calculates sessionID = sha256(unix_second).
  2. It calls PrepareSession(sessionID, credentials.Username) before validating credentials against the SSO.
  3. A failed login can therefore set a Redis pointer for an arbitrary username string.
  4. SessionMiddleware only checks that session is non-empty, then calls GetSession(username) using the attacker-controlled username cookie.
  5. GetSession(username) reads filepath.Join("/tmp/sessions", username, sessionID).
  6. A traversal username like ../../app/service/files/<normal_user> resolves that read into the normal user's uploaded file directory.
  7. If a file named with the predicted sessionID contains {"username":"admin","id":1,"role":"admin"}, /user/admin renders 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:

bash
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.txt

The solver:

  1. registers a normal user
  2. logs in and keeps the real user cookies
  3. creates a ZIP with many predicted sha256(unix_second) filenames around the server clock
  4. uploads the ZIP through /user/upload
  5. sends a failed login as ../../app/service/files/<normal_user> to poison Redis
  6. requests /user/admin with session: <redacted> and the traversal username
  7. 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

FileSizeSHA256TypeNotes
files/a12c7330-7ca9-4da3-845a-993bcec029e1.zip86236<hash redacted>Zip archive data, at least v2.0 to extract, compression method=deflatezip entries: 30 shown in artifact inventory JSON

Evidence Ledger

TimeActionOutput/FileFindingConfidenceNext
2026-06-08T03:47:11Zharness initchallenge-state.jsonWorkspace initialized with deterministic state fileHighInventory artifacts
2026-06-08T03:47:21Zartifact inventoryanalysis/artifact-inventory.json1 artifact(s) inventoriedHighBuild or update hypotheses
2026-06-08T03:47:43Zhypothesis recordedhypothesis-board.mdSource-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.MediumExtract 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:54Zcheckpoint recordedanalysis/checkpoint-triage-20260608T034854345861Z-bab6bb15.mdCheckpoint for TRIAGEHighUse checkpoint to drive next decision
2026-06-08T03:49:07Zlocal memory recordanalysis/local-memory-records.mdPrior local notes reviewed as fallback/advisory contextMediumValidate against current evidence
2026-06-08T03:53:19Zsource auditanalysis/source-audit.mdSource audit recordedHighGate before exploit
2026-06-08T03:53:35Zhypothesis recordedhypothesis-board.mdPoison Redis for traversal username via failed login, then make GetSession read uploaded admin JSON from /app/service/files/<normal_user>/<sessionID>.HighRun solve/solve.py once after source audit/evaluator/gate.
2026-06-08T03:53:35Zevaluatoranalysis/evaluator-20260608T035335437033Z-9e30d9b1.mdProceedHighRun the solver against the remote instance, then capture the flag through the harness if returned.
2026-06-08T03:54:05Zflag captureloot/flag.txtHTB-format flag captured; raw value kept in loot onlyHighWrite solution and run completion gate
2026-06-08T03:55:10Zcompletion 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: 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.

  1. Source audit found that the Go login handler writes a Redis username-to-session pointer before SSO authentication succeeds.
  2. The session middleware ignores the real session cookie value and loads the session file using the username cookie.
  3. Uploading a normal archive can place attacker-chosen filenames under /app/service/files/<normal_user>.
  4. A failed login using ../../app/service/files/<normal_user> as the username poisons Redis for that traversal key.
  5. GetSession() then resolves the session read into the uploaded files directory and deserializes attacker-controlled admin JSON.
  6. /user/admin renders the flag when the loaded JSON has role admin.

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/admin was closed because github.com/mholt/archiver/v3 performs path traversal checks before ZIP extraction.

Tool Quirks

  • The solver keeps raw flag output in loot/ only and writes a redacted transcript under analysis/remote/.
  • The source archive uses the standard HTB extraction convention.

Evidence Paths

  • analysis/source-audit.md
  • analysis/remote/solve-transcript-redacted.txt
  • solve/solve.py
  • loot/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.

RankPathEvidenceMissing ProofCheapest ValidationConfidenceStatus
1Source-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.MediumActive
1Poison 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.Highactive

Closed Branches

BranchEvidence TestedFailure OutputReason ClosedRevisit 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.

  1. Source audit found that the Go login handler writes a Redis username-to-session pointer before SSO authentication succeeds.
  2. The session middleware ignores the real session cookie value and loads the session file using the username cookie.
  3. Uploading a normal archive can place attacker-chosen filenames under /app/service/files/<normal_user>.
  4. A failed login using ../../app/service/files/<normal_user> as the username poisons Redis for that traversal key.
  5. GetSession() then resolves the session read into the uploaded files directory and deserializes attacker-controlled admin JSON.
  6. /user/admin renders the flag when the loaded JSON has role admin.

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/admin was closed because github.com/mholt/archiver/v3 performs path traversal checks before ZIP extraction.

Tool Quirks

  • The solver keeps raw flag output in loot/ only and writes a redacted transcript under analysis/remote/.
  • The source archive uses the standard HTB extraction convention.

Evidence Paths

  • analysis/source-audit.md
  • analysis/remote/solve-transcript-redacted.txt
  • solve/solve.py
  • loot/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

FileSizeSHA256TypeNotes
files/a12c7330-7ca9-4da3-845a-993bcec029e1.zip86236<hash redacted>Zip archive data, at least v2.0 to extract, compression method=deflatezip entries: 30 shown in artifact inventory JSON

Evidence Ledger

TimeActionOutput/FileFindingConfidenceNext
2026-06-08T03:47:11Zharness initchallenge-state.jsonWorkspace initialized with deterministic state fileHighInventory artifacts
2026-06-08T03:47:21Zartifact inventoryanalysis/artifact-inventory.json1 artifact(s) inventoriedHighBuild or update hypotheses
2026-06-08T03:47:43Zhypothesis recordedhypothesis-board.mdSource-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.MediumExtract 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:54Zcheckpoint recordedanalysis/checkpoint-triage-20260608T034854345861Z-bab6bb15.mdCheckpoint for TRIAGEHighUse checkpoint to drive next decision
2026-06-08T03:49:07Zlocal memory recordanalysis/local-memory-records.mdPrior local notes reviewed as fallback/advisory contextMediumValidate against current evidence
2026-06-08T03:53:19Zsource auditanalysis/source-audit.mdSource audit recordedHighGate before exploit
2026-06-08T03:53:35Zhypothesis recordedhypothesis-board.mdPoison Redis for traversal username via failed login, then make GetSession read uploaded admin JSON from /app/service/files/<normal_user>/<sessionID>.HighRun 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:10Zcompletion 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.

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.