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
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.
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 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/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:
{
"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:
POST /createwithtitleset to<TARGET>POST /updatewith$rename: {"title": "__proto__._peername.address"}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
| File | Size | SHA256 | Type | Notes |
|---|---|---|---|---|
files/a12c7366-59b8-49ff-bb42-e835e55f1220.zip | 20377 | <hash redacted> | Zip archive data, at least v1.0 to extract, compression method=store | zip entries: 14 shown in artifact inventory JSON |
files/extracted/web_secure_notes/Dockerfile | 740 | <hash redacted> | ASCII text | |
files/extracted/web_secure_notes/build-docker.sh | 150 | <hash redacted> | Bourne-Again shell script text executable, ASCII text | |
files/extracted/web_secure_notes/challenge/conf/supervisord.conf | 428 | <hash redacted> | ASCII text | |
files/extracted/web_secure_notes/challenge/src/app.js | 2173 | <hash redacted> | ASCII text | |
files/extracted/web_secure_notes/challenge/src/package-lock.json | 37046 | <hash redacted> | JSON data | |
files/extracted/web_secure_notes/challenge/src/package.json | 165 | <hash redacted> | JSON data | |
files/extracted/web_secure_notes/challenge/src/public/index.html | 11144 | <hash redacted> | HTML document text, Unicode text, UTF-8 text | |
files/extracted/web_secure_notes/challenge/src/public/style.css | 9731 | <hash redacted> | ASCII text | |
files/extracted/web_secure_notes/challenge/src/public/update.html | 3867 | <hash redacted> | HTML document text, Unicode text, UTF-8 text |
Evidence Ledger
| Time | Action | Output/File | Finding | Confidence | Next |
|---|---|---|---|---|---|
| 2026-06-07T17:12:03Z | harness init | challenge-state.json | Workspace initialized with deterministic state file | High | Inventory artifacts |
| 2026-06-07T17:12:26Z | artifact inventory | analysis/artifact-inventory.json | 10 artifact(s) inventoried | High | Build or update hypotheses |
| 2026-06-07T17:12:26Z | hypothesis recorded | hypothesis-board.md | Audit 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. | Medium | 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. |
| 2026-06-07T17:14:23Z | source audit | analysis/source-audit.md | Source audit recorded | High | Gate before exploit |
| 2026-06-07T17:14:54Z | research task | analysis/research/task-20260607T171454019082Z-1c138e43.md | Research task created for advisory investigation | Medium | Record research output |
| 2026-06-07T17:14:54Z | checkpoint recorded | analysis/checkpoint-hypothesis_ready-20260607T171454022024Z-70367bc5.md | Checkpoint for <secret redacted> | High | Use checkpoint to drive next decision |
| 2026-06-07T17:15:23Z | local memory search | analysis/research/local-memory-search-20260607T171523488190Z-84c112f6.md | Found 8 safe prior-note result(s) | Medium | Record useful result or skip |
| 2026-06-07T17:15:47Z | RAG query | analysis/rag/rag-query-20260607T171531232111Z-6b957736.txt | RAG helper exited 0; output saved | Medium | Record retrieval tag and validation |
| 2026-06-07T17:16:12Z | evaluator | analysis/evaluator-20260607T171612266086Z-690da9a0.md | Validate first | High | Local reproduce before live exploit: build/run challenge, validate update operator/prototype behavior, then gate before remote exploitation. |
| 2026-06-07T17:16:24Z | local memory record | analysis/local-memory-records.md | Prior local notes reviewed as fallback/advisory context | Medium | Validate against current evidence |
| 2026-06-07T17:16:36Z | RAG record | analysis/rag-records.md | Retrieved memory tagged GENERIC | Medium | Validate or reject with live evidence |
| 2026-06-07T17:29:38Z | local validation | analysis/local-validation.md | Clean 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. | High | Record Proceed, pass exploit gate, then replay the same three requests remotely. |
| 2026-06-07T17:32:15Z | local memory record | analysis/local-memory-records.md | Prior local notes reviewed as fallback/advisory context | Medium | Validate against current evidence |
| 2026-06-07T17:32:35Z | evaluator | analysis/evaluator-20260607T173235978144Z-2dc41c39.md | Proceed | High | Run 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:03Z | remote exploit | analysis/remote/exploit-summary.md | The locally validated create -> update -> flag chain worked unchanged against the live target; raw flag was captured only through the harness. | High | Sanitize transient candidate, finish writeup, and run completion checks. |
| 2026-06-07T17:33:09Z | flag capture | loot/flag.txt | HTB-format flag captured; raw value kept in loot only | High | Write solution and run completion gate |
| 2026-06-07T17:34:16Z | completion gate | challenge-state.json | Completion gate passed; state marked COMPLETE | High | Optional sanitized memory summary approval |
Key Findings
/flagtrustsreq.connection.remoteAddressand only permits loopback values, so direct remote access is correctly blocked./updateforwards the full JSON body toNote.findByIdAndUpdate(noteId, req.body)and then immediately executesNote.find({_id: noteId}).- In
mongoose7.2.4,$renamecan 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:
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.
| Rank | Path | Evidence | Missing Proof | Cheapest Validation | Confidence | Status |
|---|---|---|---|---|---|---|
| 1 | Audit 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. | Medium | 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: 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
| File | Size | SHA256 | Type | Notes |
|---|---|---|---|---|
files/a12c7366-59b8-49ff-bb42-e835e55f1220.zip | 20377 | <hash redacted> | Zip archive data, at least v1.0 to extract, compression method=store | zip entries: 14 shown in artifact inventory JSON |
files/extracted/web_secure_notes/Dockerfile | 740 | <hash redacted> | ASCII text | |
files/extracted/web_secure_notes/build-docker.sh | 150 | <hash redacted> | Bourne-Again shell script text executable, ASCII text | |
files/extracted/web_secure_notes/challenge/conf/supervisord.conf | 428 | <hash redacted> | ASCII text | |
files/extracted/web_secure_notes/challenge/src/app.js | 2173 | <hash redacted> | ASCII text | |
files/extracted/web_secure_notes/challenge/src/package-lock.json | 37046 | <hash redacted> | JSON data | |
files/extracted/web_secure_notes/challenge/src/package.json | 165 | <hash redacted> | JSON data | |
files/extracted/web_secure_notes/challenge/src/public/index.html | 11144 | <hash redacted> | HTML document text, Unicode text, UTF-8 text | |
files/extracted/web_secure_notes/challenge/src/public/style.css | 9731 | <hash redacted> | ASCII text | |
files/extracted/web_secure_notes/challenge/src/public/update.html | 3867 | <hash redacted> | HTML document text, Unicode text, UTF-8 text |
Evidence Ledger
| Time | Action | Output/File | Finding | Confidence | Next |
|---|---|---|---|---|---|
| 2026-06-07T17:12:03Z | harness init | challenge-state.json | Workspace initialized with deterministic state file | High | Inventory artifacts |
| 2026-06-07T17:12:26Z | artifact inventory | analysis/artifact-inventory.json | 10 artifact(s) inventoried | High | Build or update hypotheses |
| 2026-06-07T17: <REDACTED>, especially localhost/internal-origin checks and any note/update routes that can trigger server-side requests. | Medium | 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. | |||
| 2026-06-07T17:14:23Z | source audit | analysis/source-audit.md | Source audit recorded | High | Gate before exploit |
| 2026-06-07T17:14:54Z | research task | analysis/research/task-20260607T171454019082Z-1c138e43.md | Research task created for advisory investigation | Medium | Record research output |
| 2026-06-07T17:14:54Z | checkpoint recorded | analysis/checkpoint-hypothesis_ready-20260607T171454022024Z-70367bc5.md | Checkpoint for <secret redacted> | High | Use checkpoint to drive next decision |
| 2026-06-07T17:15:23Z | local memory search | analysis/research/local-memory-search-20260607T171523488190Z-84c112f6.md | Found 8 safe prior-note result(s) | Medium | Record useful result or skip |
| 2026-06-07T17:15:47Z | RAG query | analysis/rag/rag-query-20260607T171531232111Z-6b957736.txt | RAG helper exited 0; output saved | Medium | Record retrieval tag and validation |
| 2026-06-07T17:16:12Z | evaluator | analysis/evaluator-20260607T171612266086Z-690da9a0.md | Validate first | High | Local reproduce before live exploit: build/run challenge, validate update operator/prototype behavior, then gate before remote exploitation. |
| 2026-06-07T17:16:24Z | local memory record | analysis/local-memory-records.md | Prior local notes reviewed as fallback/advisory context | Medium | Validate against current evidence |
| 2026-06-07T17:16:36Z | RAG record | analysis/rag-records.md | Retrieved memory tagged GENERIC | Medium | Validate or reject with live evidence |
| 2026-06-07T17: <REDACTED>, pass exploit gate, then replay the same three requests remotely. | |||||
| 2026-06-07T17:32:15Z | local memory record | analysis/local-memory-records.md | Prior local notes reviewed as fallback/advisory context | Medium | Validate 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:16Z | completion gate | challenge-state.json | Completion gate passed; state marked COMPLETE | High | Optional sanitized memory summary approval |
Key Findings
/flagtrustsreq.connection.remoteAddressand only permits loopback values, so direct remote access is correctly blocked./updateforwards the full JSON body toNote.findByIdAndUpdate(noteId, req.body)and then immediately executesNote.find({_id: noteId}).- In
mongoose7.2.4,$renamecan 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:
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.