PowerGrid
PowerGrid is a sanitized challenge note from the local HTB archive, organized for quick review by category, difficulty, evidence flow, and reusable operator
Scenario
PowerGrid attack path
PowerGrid 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 SecureCoding evidence, validation, and reusable operator lessons.
Walkthrough flow
Artifact review
Hypothesis
Validated solve path
Proof captured
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.
- SecureCoding/PowerGrid/writeup.md
- htb-challenge/SecureCoding/PowerGrid/notes.md
- htb-challenge/SecureCoding/PowerGrid/memory-summary.md
- htb-challenge/SecureCoding/PowerGrid/hypothesis-board.md
- HTB/_knowledge/exports/ctf-lightrag-latest-203412/documents/challenge__SecureCoding__PowerGrid__memory-summary.md.9183d64cf8.md
- HTB/_knowledge/exports/ctf-lightrag-latest-203412/documents/challenge__SecureCoding__PowerGrid__notes.md.a5693dc448.md
Technical Walkthrough
Writeup
Challenge
- Name: PowerGrid
- Category: SecureCoding
- Difficulty: Easy
- Mode: remote
Summary
PowerGrid exposes the challenge application source through an outer HTB editor service. The bug is in utils/db.js: user records are serialized as username|<password redacted>|role, but registration accepts usernames containing |, carriage return, and newline. That lets an attacker inject an additional admin record into users.txt. The intended solve is to patch the storage layer, restart the service, and verify that the vulnerability is fixed.
Artifact Inventory
Relevant local and remote artifacts:
analysis/remote/api-readonly.txt: confirms the live editor endpoints/api/directory,/api/file,/api/restart, and/api/verifyfiles/exposed-source/utils/db.js: vulnerable user storage and parsing logicfiles/exposed-source/routes/auth.js: registration path that passes the supplied username intoaddUser(...)files/exposed-source/exploit/solver.py: bundled proof-of-concept for record injectionsolve/patched-db.js: prepared remediationsolve/solve.py: reproducible remote patch/restart/verify helperanalysis/remote/verify-after-patch.json: sanitized proof that verification passed after patching
Analysis
The live editor API enumerated the remote project tree and made utils/db.js readable. In that file, readUsers() and writeUsers() use newline-separated username|<password redacted>|role records, while addUser(username, password, role) writes the caller-provided username directly into that format.
That is the core bug: because the username field is serialized without rejecting |, \r, or \n, a crafted username can terminate the current record and inject a second one. The mirrored routes/auth.js shows public registration reaches addUser(...), and the bundled files/exposed-source/exploit/solver.py demonstrates the exploit shape by creating an extra admin entry in users.txt.
The remediation in solve/patched-db.js closes the bug at the right layer:
- usernames must match a strict allowlist
- serialized fields cannot contain pipe or line-break delimiters
- public registration is restricted to
operator - malformed stored records are skipped on read and filtered on write
- authentication and lookup reject invalid usernames early
During execution I also had to correct the automation helper: the editor save protocol expects the MD5 of the currently loaded file as a save token, not the MD5 of the new content. The final solve/solve.py fetches the current utils/db.js, computes that MD5, sends the patch over Socket.IO, restarts the service, and then calls /api/verify.
Solve
From the workspace root:
cd <local workspace>
python3 -m venv .venv
. .venv/bin/activate
python -m pip install requests 'python-socketio[client]'
python solve/solve.py --base-url http://<TARGET>:32453 --output loot/flag-candidate.txtWhat the helper does:
GET /api/file?path=utils/db.jsto read the current file and derive the editor’s MD5 save token.- Connect to the editor’s Socket.IO endpoint and send a
messageevent of typesaveforutils/db.js. POST /api/restart.GET /api/verify.- Store the returned flag candidate only in
loot/flag-candidate.txt.
After that, the harness capture path is:
cd <local workspace>
python3 scripts/challenge_harness.py capture-flag SecureCoding/PowerGrid --from loot/flag-candidate.txt
python3 scripts/challenge_harness.py complete SecureCoding/PowerGrid
python3 scripts/challenge_harness.py validate-state SecureCoding/PowerGridFlag
Raw flag is stored in loot/flag.txt and intentionally not reproduced here.
Lessons
- When a challenge is wrapped in an editor shell, inspect the outer tooling as well as the exposed app source.
- Delimiter-based flat-file storage becomes privilege escalation as soon as untrusted fields can cross record boundaries.
- For editor-backed challenges, save semantics matter: optimistic concurrency tokens can break otherwise correct automation if you send the wrong hash.
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: PowerGrid
- Category: SecureCoding
- Difficulty: Easy
- Mode: remote
- Remote instance: <TARGET>:32453
- Start time: 2026-06-07T22:54:29Z
- 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/exposed-source/directory.json | 1621 | <hash redacted> | JSON data | |
files/exposed-source/exploit/solver.py | 1802 | <hash redacted> | Python script text executable, ASCII text | |
files/exposed-source/index.js | 1667 | <hash redacted> | Java source, ASCII text | |
files/exposed-source/package-lock.json | 32014 | <hash redacted> | JSON data | |
files/exposed-source/package.json | 332 | <hash redacted> | JSON data | |
files/exposed-source/public/css/admin.css | 2181 | <hash redacted> | ASCII text | |
files/exposed-source/public/css/dashboard.css | 1398 | <hash redacted> | ASCII text | |
files/exposed-source/public/css/index.css | 1834 | <hash redacted> | ASCII text | |
files/exposed-source/public/css/main.css | 8828 | <hash redacted> | ASCII text | |
files/exposed-source/public/js/admin.js | 3879 | <hash redacted> | ASCII text | |
files/exposed-source/public/js/charts.js | 7805 | <hash redacted> | ASCII text | |
files/exposed-source/public/js/common.js | 2451 | <hash redacted> | ASCII text | |
files/exposed-source/public/js/dashboard.js | 1918 | <hash redacted> | ASCII text | |
files/exposed-source/public/js/index.js | 2750 | <hash redacted> | ASCII text | |
files/exposed-source/routes/auth.js | 2094 | <hash redacted> | Java source, ASCII text | |
files/exposed-source/routes/views.js | 904 | <hash redacted> | Java source, ASCII text | |
files/exposed-source/templates/admin.html | 14838 | <hash redacted> | HTML document text, Unicode text, UTF-8 text | |
files/exposed-source/templates/dashboard.html | 6844 | <hash redacted> | HTML document text, Unicode text, UTF-8 text | |
files/exposed-source/templates/index.html | 4901 | <hash redacted> | HTML document text, Unicode text, UTF-8 text | |
files/exposed-source/users.txt | 329 | <hash redacted> | ASCII text | |
files/exposed-source/utils/db.js | 3142 | <hash redacted> | Java source, ASCII text, with very long lines (319) |
Evidence Ledger
| Time | Action | Output/File | Finding | Confidence | Next |
|---|---|---|---|---|---|
| 2026-06-07T22:54:29Z | harness init | challenge-state.json | Workspace initialized with deterministic state file | High | Inventory artifacts |
| 2026-06-07T22:54:35Z | artifact inventory | analysis/artifact-inventory.json | 0 artifact(s) inventoried | High | Build or update hypotheses |
| 2026-06-07T22:54:46Z | hypothesis recorded | hypothesis-board.md | Remote SecureCoding service likely exposes source/review target or HTTP workflow; fingerprint routes and public assets before mutation. | Medium | GET /, inspect headers/body/assets, then enumerate only linked or clearly discoverable endpoints. |
| 2026-06-07T22:54:46Z | checkpoint recorded | analysis/checkpoint-triage-20260607T225446983052Z-fda6541a.md | Checkpoint for TRIAGE | High | Use checkpoint to drive next decision |
| 2026-06-07T22:57:17Z | source audit | analysis/source-audit.md | Source audit recorded | High | Gate before exploit |
| 2026-06-07T22:57:17Z | local memory record | analysis/local-memory-records.md | Prior local notes reviewed as fallback/advisory context | Medium | Validate against current evidence |
| 2026-06-07T22:57:17Z | source audit | analysis/source-audit.md | Source audit recorded | High | Gate before exploit |
| 2026-06-07T22:57:17Z | hypothesis recorded | hypothesis-board.md | Patch users.txt record injection by rejecting CR/LF and pipe delimiters in username/password/role, enforcing a strict username allowlist, and constraining public registration to operator role. | Medium | Apply patch to utils/db.js, restart service, call /api/verify, capture returned flag only through harness. |
| 2026-06-07T22:58:36Z | artifact inventory | analysis/artifact-inventory.json | 21 artifact(s) inventoried | High | Build or update hypotheses |
| 2026-06-07T22:58:36Z | evaluator | analysis/evaluator-20260607T225836646727Z-7756ca51.md | Proceed | High | Apply prepared patch, restart, verify, capture flag through harness. |
| 2026-06-07T23:08:34Z | flag capture | loot/flag.txt | HTB-format flag captured; raw value kept in loot only | High | Write solution and run completion gate |
| 2026-06-07T23:10:03Z | completion gate | challenge-state.json | Completion gate passed; state marked COMPLETE | High | Optional sanitized memory summary approval |
Key Findings
- Service fingerprint:
GET /serves an HTB Editor frontend;GET /challenge/serves the PowerGrid app. - Real editor endpoints use
/api/*, including/api/directory,/api/file,/api/restart, and/api/verify. GET /api/directoryexposed the challenge source tree; mirrored copy is stored underfiles/exposed-source/.GET /api/verifyconfirmed the current service is still vulnerable before patching.- Root cause:
utils/db.jsserializes users asusername|<password redacted>|roleand accepts user-controlled registration usernames without rejecting pipes or newlines. - The included
exploit/solver.pyproves a username can inject an extraadminrecord. - Prepared remediation artifacts:
- solve/patched-db.js
- solve/solve.py
- <secret redacted>md
- Remote execution succeeded after patching
utils/db.js, restarting the service, and verifying through/api/verify. - Editor save protocol note: the Socket.IO
saveevent must carry the MD5 of the currently loaded file as its save token; sending the MD5 of the new content prevents the save acknowledgement.
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: SecureCoding
- Challenge: PowerGrid
- 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 | Remote SecureCoding service likely exposes source/review target or HTTP workflow; fingerprint routes and public assets before mutation. | Challenge provides only remote host <TARGET>:32453 and SecureCoding category; no archive has been provided. | GET /, inspect headers/body/assets, then enumerate only linked or clearly discoverable endpoints. | Medium | Active | |
| 1 | Patch users.txt record injection by rejecting CR/LF and pipe delimiters in username/password/role, enforcing a strict username allowlist, and constraining public registration to operator role. | files/exposed-source/utils/db.js serializes users as username\ | password\ | role; files/exposed-source/exploit/solver.py injects a newline-delimited admin record through username. | Apply patch to utils/db.js, restart service, call /api/verify, capture returned flag only through harness. |
Closed Branches
| Branch | Evidence Tested | Failure Output | Reason Closed | Revisit Condition |
|---|
Memory Summary
approval_required: true
Sanitized Memory Summary
Metadata
- Platform: HackTheBox Challenges
- Category: SecureCoding
- Challenge: PowerGrid
- 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: PowerGrid
- Category: SecureCoding
- Difficulty: Easy
- Mode: remote
- Remote instance: <TARGET>:32453
- Start time: 2026-06-07T22:54:29Z
- 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/exposed-source/directory.json | 1621 | <hash redacted> | JSON data | |
files/exposed-source/exploit/solver.py | 1802 | <hash redacted> | Python script text executable, ASCII text | |
files/exposed-source/index.js | 1667 | <hash redacted> | Java source, ASCII text | |
files/exposed-source/package-lock.json | 32014 | <hash redacted> | JSON data | |
files/exposed-source/package.json | 332 | <hash redacted> | JSON data | |
files/exposed-source/public/css/admin.css | 2181 | <hash redacted> | ASCII text | |
files/exposed-source/public/css/dashboard.css | 1398 | <hash redacted> | ASCII text | |
files/exposed-source/public/css/index.css | 1834 | <hash redacted> | ASCII text | |
files/exposed-source/public/css/main.css | 8828 | <hash redacted> | ASCII text | |
files/exposed-source/public/js/admin.js | 3879 | <hash redacted> | ASCII text | |
files/exposed-source/public/js/charts.js | 7805 | <hash redacted> | ASCII text | |
files/exposed-source/public/js/common.js | 2451 | <hash redacted> | ASCII text | |
files/exposed-source/public/js/dashboard.js | 1918 | <hash redacted> | ASCII text | |
files/exposed-source/public/js/index.js | 2750 | <hash redacted> | ASCII text | |
files/exposed-source/routes/auth.js | 2094 | <hash redacted> | Java source, ASCII text | |
files/exposed-source/routes/views.js | 904 | <hash redacted> | Java source, ASCII text | |
files/exposed-source/templates/admin.html | 14838 | <hash redacted> | HTML document text, Unicode text, UTF-8 text | |
files/exposed-source/templates/dashboard.html | 6844 | <hash redacted> | HTML document text, Unicode text, UTF-8 text | |
files/exposed-source/templates/index.html | 4901 | <hash redacted> | HTML document text, Unicode text, UTF-8 text | |
files/exposed-source/users.txt | 329 | <hash redacted> | ASCII text | |
files/exposed-source/utils/db.js | 3142 | <hash redacted> | Java source, ASCII text, with very long lines (319) |
Evidence Ledger
| Time | Action | Output/File | Finding | Confidence | Next |
|---|---|---|---|---|---|
| 2026-06-07T22:54:29Z | harness init | challenge-state.json | Workspace initialized with deterministic state file | High | Inventory artifacts |
| 2026-06-07T22:54:35Z | artifact inventory | analysis/artifact-inventory.json | 0 artifact(s) inventoried | High | Build or update hypotheses |
| 2026-06-07T22:54:46Z | hypothesis recorded | hypothesis-board.md | Remote SecureCoding service likely exposes source/review target or HTTP workflow; fingerprint routes and public assets before mutation. | Medium | GET /, inspect headers/body/assets, then enumerate only linked or clearly discoverable endpoints. |
| 2026-06-07T22:54:46Z | checkpoint recorded | analysis/checkpoint-triage-20260607T225446983052Z-fda6541a.md | Checkpoint for TRIAGE | High | Use checkpoint to drive next decision |
| 2026-06-07T22:57:17Z | source audit | analysis/source-audit.md | Source audit recorded | High | Gate before exploit |
| 2026-06-07T22:57:17Z | local memory record | analysis/local-memory-records.md | Prior local notes reviewed as fallback/advisory context | Medium | Validate against current evidence |
| 2026-06-07T22:57:17Z | source audit | analysis/source-audit.md | Source audit recorded | High | Gate before exploit |
| 2026-06-07T22: <REDACTED>, enforcing a strict username allowlist, and constraining public registration to operator role. | Medium | Apply patch to utils/db.js, restart service, call /api/verify, capture returned flag only through harness. | |||
| 2026-06-07T22:58:36Z | artifact inventory | analysis/artifact-inventory.json | 21 artifact(s) inventoried | High | Build or update hypotheses |
| 2026-06-07T22: <REDACTED>, restart, verify, capture flag through harness. | |||||
| 2026-06-07T23: <REDACTED> | |||||
| 2026-06-07T23:10:03Z | completion gate | challenge-state.json | Completion gate passed; state marked COMPLETE | High | Optional sanitized memory summary approval |
Key Findings
- Service fingerprint:
GET /serves an HTB Editor frontend;GET /challenge/serves the PowerGrid app. - Real editor endpoints use
/api/*, including/api/directory,/api/file,/api/restart, and/api/verify. GET /api/directoryexposed the challenge source tree; mirrored copy is stored underfiles/exposed-source/.GET /api/verifyconfirmed the current service is still vulnerable before patching.- Root cause:
utils/db.jsserializes users asusername|<password redacted>|roleand accepts user-controlled registration usernames without rejecting pipes or newlines. - The included
exploit/solver.pyproves a username can inject an extraadminrecord. - Prepared remediation artifacts:
- solve/patched-db.js
- solve/solve.py
- <secret redacted>md
- Remote execution succeeded after patching
utils/db.js, restarting the service, and verifying through/api/verify. - Editor save protocol note: <REDACTED>
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 it like inspecting a building plan for missing guardrails. You follow each trust boundary and ask what happens if a user controls more input than the developer expected.
For PowerGrid, 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.