Challenge / Web

Secure Notes

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

EasyPublished 2025-10-18Sanitized local writeup

Scenario

Secure Notes attack path

Secure Notes 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.

Secure Notes sanitized attack graph

Walkthrough flow

01

Source and route audit

02

Trust boundary flaw

03

Exploit request chain

04

Admin or proof proof

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/Secure-Notes/writeup.md
  • htb-challenge/Web/Secure-Notes/notes.md
  • htb-challenge/Web/Secure-Notes/memory-summary.md
  • htb-challenge/Web/Secure-Notes/hypothesis-board.md
  • HTB/_knowledge/exports/ctf-lightrag-latest-203412/documents/challenge__Web__Secure-Notes__memory-summary.md.9722c8fccf.md
  • HTB/_knowledge/exports/ctf-lightrag-latest-203412/documents/challenge__Web__Secure-Notes__notes.md.90a5be11ef.md

Technical Walkthrough

Writeup

Challenge

  • Name: Secure-Notes
  • Category: Web
  • Difficulty: Easy
  • Mode: hybrid

Summary

Secure Notes is a small Express and Mongoose app with a localhost-only /flag route. The intended break is not header spoofing or SSRF. The bug is the unfiltered /update handler: in the shipped mongoose 7.2.4, a crafted $rename can prototype-pollute __proto__._peername.address, which changes the value later read from req.connection.remoteAddress. Once that polluted value becomes <TARGET>, the live /flag request passes.

Artifact Inventory

The relevant local files were analysis/static/app.js.txt, analysis/static/package.json.txt, and analysis/remote/flag-direct.txt. app.js defines four routes: /flag, /create, /get/:noteId, and /update. package.json pins express 4.18.2 and mongoose 7.2.4, which matters because the exploit depends on version-specific update behavior. The remote surface was minimal: the root page served the notes UI and direct GET /flag returned 403.

Analysis

The critical guard is in analysis/static/app.js.txt: /flag reads req.connection.remoteAddress and only accepts loopback strings. The same source file shows /update doing Note.findByIdAndUpdate(noteId, req.body) with the entire attacker-controlled body and then immediately calling Note.find({_id: noteId}). That combination is enough for the vulnerable Mongoose branch.

Local proof is recorded in analysis/local-validation.md. A non-loopback request to the local test instance returned 403 before exploitation. Creating a note with title set to <TARGET> and then sending:

json
{
  "noteId": "<id>",
  "$rename": {
    "title": "__proto__._peername.address"
  }
}

caused the follow-up find() to instantiate the polluted path. After that, the same non-loopback /flag request returned the configured test flag. The remote exploit reused that exact chain, which is summarized in analysis/remote/exploit-summary.md.

Solve

The reproducible solver is solve/solve.py. It performs three requests:

  1. POST /create with title set to <TARGET>
  2. POST /update with $rename: {"title": "__proto__._peername.address"}
  3. GET /flag

The script writes the recovered flag to analysis/flag-candidate.txt. The harness command capture-flag then moves the real value into loot/flag.txt, and the transient candidate file is sanitized.

Flag

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

Lessons

A small trust-boundary check is only as strong as the objects feeding it. Here the route guard itself was simple and correct, but the vulnerable dependency let attacker input rewrite a property that the Node runtime later trusted. When a route passes the full request body into an ORM update call, version-specific operator behavior becomes part of the attack surface.

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: Secure-Notes
  • Category: Web
  • Difficulty: Easy
  • Mode: hybrid
  • Remote instance: <TARGET>:30993
  • Start time: 2026-06-07T17:12:03Z
  • 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/a12c7366-59b8-49ff-bb42-e835e55f1220.zip20377<hash redacted>Zip archive data, at least v1.0 to extract, compression method=storezip entries: 14 shown in artifact inventory JSON
files/extracted/web_secure_notes/Dockerfile740<hash redacted>ASCII text
files/extracted/web_secure_notes/build-docker.sh150<hash redacted>Bourne-Again shell script text executable, ASCII text
files/extracted/web_secure_notes/challenge/conf/supervisord.conf428<hash redacted>ASCII text
files/extracted/web_secure_notes/challenge/src/app.js2173<hash redacted>ASCII text
files/extracted/web_secure_notes/challenge/src/package-lock.json37046<hash redacted>JSON data
files/extracted/web_secure_notes/challenge/src/package.json165<hash redacted>JSON data
files/extracted/web_secure_notes/challenge/src/public/index.html11144<hash redacted>HTML document text, Unicode text, UTF-8 text
files/extracted/web_secure_notes/challenge/src/public/style.css9731<hash redacted>ASCII text
files/extracted/web_secure_notes/challenge/src/public/update.html3867<hash redacted>HTML document text, Unicode text, UTF-8 text

Evidence Ledger

TimeActionOutput/FileFindingConfidenceNext
2026-06-07T17:12:03Zharness initchallenge-state.jsonWorkspace initialized with deterministic state fileHighInventory artifacts
2026-06-07T17:12:26Zartifact inventoryanalysis/artifact-inventory.json10 artifact(s) inventoriedHighBuild or update hypotheses
2026-06-07T17:12:26Zhypothesis recordedhypothesis-board.mdAudit the Express source for trust-boundary issues around the flag door, especially localhost/internal-origin checks and any note/update routes that can trigger server-side requests.MediumRead app.js and public client code, map routes, then validate whether localhost-only flag access can be reached via SSRF/proxy/header/origin confusion.
2026-06-07T17:14:23Zsource auditanalysis/source-audit.mdSource audit recordedHighGate before exploit
2026-06-07T17:14:54Zresearch taskanalysis/research/task-20260607T171454019082Z-1c138e43.mdResearch task created for advisory investigationMediumRecord research output
2026-06-07T17:14:54Zcheckpoint recordedanalysis/checkpoint-hypothesis_ready-20260607T171454022024Z-70367bc5.mdCheckpoint for <secret redacted>HighUse checkpoint to drive next decision
2026-06-07T17:15:23Zlocal memory searchanalysis/research/local-memory-search-20260607T171523488190Z-84c112f6.mdFound 8 safe prior-note result(s)MediumRecord useful result or skip
2026-06-07T17:15:47ZRAG queryanalysis/rag/rag-query-20260607T171531232111Z-6b957736.txtRAG helper exited 0; output savedMediumRecord retrieval tag and validation
2026-06-07T17:16:12Zevaluatoranalysis/evaluator-20260607T171612266086Z-690da9a0.mdValidate firstHighLocal reproduce before live exploit: build/run challenge, validate update operator/prototype behavior, then gate before remote exploitation.
2026-06-07T17:16:24Zlocal memory recordanalysis/local-memory-records.mdPrior local notes reviewed as fallback/advisory contextMediumValidate against current evidence
2026-06-07T17:16:36ZRAG recordanalysis/rag-records.mdRetrieved memory tagged GENERICMediumValidate or reject with live evidence
2026-06-07T17:29:38Zlocal validationanalysis/local-validation.mdClean local reproduction proved that $rename into __proto__._peername.address flips non-loopback /flag from 403 to the configured test flag in the vulnerable Mongoose path.HighRecord Proceed, pass exploit gate, then replay the same three requests remotely.
2026-06-07T17:32:15Zlocal memory recordanalysis/local-memory-records.mdPrior local notes reviewed as fallback/advisory contextMediumValidate against current evidence
2026-06-07T17:32:35Zevaluatoranalysis/evaluator-20260607T173235978144Z-2dc41c39.mdProceedHighRun the three-request remote chain: create note, title into __proto__._peername.address, then fetch /flag and capture it through the harness.
2026-06-07T17:33:03Zremote exploitanalysis/remote/exploit-summary.mdThe locally validated create -> update -> flag chain worked unchanged against the live target; raw flag was captured only through the harness.HighSanitize transient candidate, finish writeup, and run completion checks.
2026-06-07T17:33:09Zflag captureloot/flag.txtHTB-format flag captured; raw value kept in loot onlyHighWrite solution and run completion gate
2026-06-07T17:34:16Zcompletion gatechallenge-state.jsonCompletion gate passed; state marked COMPLETEHighOptional sanitized memory summary approval

Key Findings

  • /flag trusts req.connection.remoteAddress and only permits loopback values, so direct remote access is correctly blocked.
  • /update forwards the full JSON body to Note.findByIdAndUpdate(noteId, req.body) and then immediately executes Note.find({_id: noteId}).
  • In mongoose 7.2.4, $rename can move attacker-controlled note data into __proto__._peername.address, which is enough to change the address returned by the Node socket getter used in /flag.
  • The exploit requires only three remote requests and no header spoofing, SSRF, or client-side behavior.

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: Secure-Notes
  • 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.

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.

RankPathEvidenceMissing ProofCheapest ValidationConfidenceStatus
1Audit the Express source for trust-boundary issues around the flag door, especially localhost/internal-origin checks and any note/update routes that can trigger server-side requests.Scenario says only those who knock from inside may enter; source-heavy Web app exposes public pages and a live target.Read app.js and public client code, map routes, then validate whether localhost-only flag access can be reached via SSRF/proxy/header/origin confusion.MediumActive

Closed Branches

BranchEvidence TestedFailure OutputReason ClosedRevisit Condition

Memory Summary

approval_required: true

Sanitized Memory Summary

Metadata

  • Platform: HackTheBox Challenges
  • Category: Web
  • Challenge: Secure-Notes
  • 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.

Reusable Lessons

-

Dead Ends

-

Tool Quirks

-

Evidence Paths

-

Ingestion Decision

  • Proposed for LightRAG: yes/no
  • Requires user approval before ingestion: yes

Notes

Notes

Scope

  • Challenge: Secure-Notes
  • Category: Web
  • Difficulty: Easy
  • Mode: hybrid
  • Remote instance: <TARGET>:30993
  • Start time: 2026-06-07T17:12:03Z
  • 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/a12c7366-59b8-49ff-bb42-e835e55f1220.zip20377<hash redacted>Zip archive data, at least v1.0 to extract, compression method=storezip entries: 14 shown in artifact inventory JSON
files/extracted/web_secure_notes/Dockerfile740<hash redacted>ASCII text
files/extracted/web_secure_notes/build-docker.sh150<hash redacted>Bourne-Again shell script text executable, ASCII text
files/extracted/web_secure_notes/challenge/conf/supervisord.conf428<hash redacted>ASCII text
files/extracted/web_secure_notes/challenge/src/app.js2173<hash redacted>ASCII text
files/extracted/web_secure_notes/challenge/src/package-lock.json37046<hash redacted>JSON data
files/extracted/web_secure_notes/challenge/src/package.json165<hash redacted>JSON data
files/extracted/web_secure_notes/challenge/src/public/index.html11144<hash redacted>HTML document text, Unicode text, UTF-8 text
files/extracted/web_secure_notes/challenge/src/public/style.css9731<hash redacted>ASCII text
files/extracted/web_secure_notes/challenge/src/public/update.html3867<hash redacted>HTML document text, Unicode text, UTF-8 text

Evidence Ledger

TimeActionOutput/FileFindingConfidenceNext
2026-06-07T17:12:03Zharness initchallenge-state.jsonWorkspace initialized with deterministic state fileHighInventory artifacts
2026-06-07T17:12:26Zartifact inventoryanalysis/artifact-inventory.json10 artifact(s) inventoriedHighBuild or update hypotheses
2026-06-07T17: <REDACTED>, especially localhost/internal-origin checks and any note/update routes that can trigger server-side requests.MediumRead app.js and public client code, map routes, then validate whether localhost-only flag access can be reached via SSRF/proxy/header/origin confusion.
2026-06-07T17:14:23Zsource auditanalysis/source-audit.mdSource audit recordedHighGate before exploit
2026-06-07T17:14:54Zresearch taskanalysis/research/task-20260607T171454019082Z-1c138e43.mdResearch task created for advisory investigationMediumRecord research output
2026-06-07T17:14:54Zcheckpoint recordedanalysis/checkpoint-hypothesis_ready-20260607T171454022024Z-70367bc5.mdCheckpoint for <secret redacted>HighUse checkpoint to drive next decision
2026-06-07T17:15:23Zlocal memory searchanalysis/research/local-memory-search-20260607T171523488190Z-84c112f6.mdFound 8 safe prior-note result(s)MediumRecord useful result or skip
2026-06-07T17:15:47ZRAG queryanalysis/rag/rag-query-20260607T171531232111Z-6b957736.txtRAG helper exited 0; output savedMediumRecord retrieval tag and validation
2026-06-07T17:16:12Zevaluatoranalysis/evaluator-20260607T171612266086Z-690da9a0.mdValidate firstHighLocal reproduce before live exploit: build/run challenge, validate update operator/prototype behavior, then gate before remote exploitation.
2026-06-07T17:16:24Zlocal memory recordanalysis/local-memory-records.mdPrior local notes reviewed as fallback/advisory contextMediumValidate against current evidence
2026-06-07T17:16:36ZRAG recordanalysis/rag-records.mdRetrieved memory tagged GENERICMediumValidate or reject with live evidence
2026-06-07T17: <REDACTED>, pass exploit gate, then replay the same three requests remotely.
2026-06-07T17:32:15Zlocal memory recordanalysis/local-memory-records.mdPrior local notes reviewed as fallback/advisory contextMediumValidate against current evidence
2026-06-07T17: <REDACTED>, title into __proto__._peername.address, then fetch /flag and capture it through the harness.
2026-06-07T17: <REDACTED>, finish writeup, and run completion checks.
2026-06-07T17: <REDACTED>
2026-06-07T17:34:16Zcompletion gatechallenge-state.jsonCompletion gate passed; state marked COMPLETEHighOptional sanitized memory summary approval

Key Findings

  • /flag trusts req.connection.remoteAddress and only permits loopback values, so direct remote access is correctly blocked.
  • /update forwards the full JSON body to Note.findByIdAndUpdate(noteId, req.body) and then immediately executes Note.find({_id: noteId}).
  • In mongoose 7.2.4, $rename can move attacker-controlled note data into __proto__._peername.address, which is enough to change the address returned by the Node socket getter used in /flag.
  • The exploit requires only three remote requests and no header spoofing, SSRF, or client-side behavior.

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 Secure Notes, 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.