Agriweb
Agriweb is a sanitized challenge note from the local HTB archive, organized for quick review by category, difficulty, evidence flow, and reusable operator
Scenario
Agriweb attack path
Agriweb 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/Agriweb/writeup.md
- htb-challenge/SecureCoding/Agriweb/notes.md
- htb-challenge/SecureCoding/Agriweb/memory-summary.md
- htb-challenge/SecureCoding/Agriweb/hypothesis-board.md
- HTB/_knowledge/exports/ctf-lightrag-latest-203412/documents/challenge__SecureCoding__Agriweb__memory-summary.md.6b5b543cda.md
- HTB/_knowledge/exports/ctf-lightrag-latest-203412/documents/challenge__SecureCoding__Agriweb__notes.md.b6542c363a.md
Technical Walkthrough
Writeup
Challenge
- Name: Agriweb
- Category: SecureCoding
- Difficulty: Easy
- Mode: remote
Summary
Agriweb exposes its source through an outer HTB editor service. The actual bug is in routes/profile.js: attacker-controlled profile JSON is recursively merged with an unsafe deepMerge(...) that does not block __proto__, prototype, or constructor. That allows prototype pollution, and because the admin middleware trusts req.user.isAdmin, a polluted Object.prototype.isAdmin turns ordinary authenticated users into admins. The intended fix is to harden the merge logic, restart the service, and verify the patch.
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/routes/profile.js: vulnerable recursive merge logicfiles/exposed-source/app.js: admin guard that trustsreq.user.isAdminfiles/exposed-source/utils/jwt.js: normal JWT handling, useful to rule out weaker bypass pathsfiles/exposed-source/exploit/solver.py: bundled proof-of-concept for the prototype-pollution payload classsolve/patched-profile.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 service root is an HTB editor frontend, while /challenge/ serves the Agriweb application. The editor API exposed the remote project tree, and routes/profile.js contained the vulnerable merge code.
updateProfile() reads the stored profile JSON and feeds attacker-controlled profileData into a recursive deepMerge(target, source). In the vulnerable version, the merge walks nested objects without rejecting special keys, so payloads containing __proto__, prototype, or constructor can poison object prototypes.
The included bundled files/exposed-source/exploit/solver.py proves the intended bug class by sending exactly those pollution payloads. The impact is then clear in app.js: the admin guard checks req.user.isAdmin. If Object.prototype.isAdmin is polluted, the decoded JWT payload object inherits that property and the middleware grants admin access.
The remediation in solve/patched-profile.js fixes the bug at the right layer:
- recursively blocks
__proto__,prototype, andconstructor - only iterates own enumerable keys
- only recursively merges plain objects
- sanitizes nested structures before merging
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 patched content. The final solve/solve.py fetches the live routes/profile.js, derives that MD5, sends the patch over Socket.IO, restarts the service, and then verifies the result.
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>:31970 --output loot/flag-candidate.txtWhat the helper does:
GET /api/file?path=routes/profile.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 typesaveforroutes/profile.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/Agriweb --from loot/flag-candidate.txt
python3 scripts/challenge_harness.py complete SecureCoding/Agriweb
python3 scripts/challenge_harness.py validate-state SecureCoding/AgriwebFlag
Raw flag is stored in loot/flag.txt and intentionally not reproduced here.
Lessons
- Prototype pollution is rarely just a data-integrity bug; any authorization path that trusts inherited properties turns it into privilege escalation.
- In editor-backed SecureCoding challenges, save-token semantics matter as much as the patch itself.
- Keep verify artifacts sanitized; the authoritative raw flag belongs in
loot/only.
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: Agriweb
- Category: SecureCoding
- Difficulty: Easy
- Mode: remote
- Remote instance: <TARGET>:31970
- Start time: 2026-06-07T23:12:25Z
- 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/app.js | 4609 | <hash redacted> | Java source, ASCII text | |
files/exposed-source/db/.gitkeep | 0 | <hash redacted> | empty | |
files/exposed-source/directory.json | 1771 | <hash redacted> | JSON data | |
files/exposed-source/exploit/solver.py | 2538 | <hash redacted> | Python script text executable, ASCII text | |
files/exposed-source/package-lock.json | 101219 | <hash redacted> | JSON data | |
files/exposed-source/package.json | 456 | <hash redacted> | JSON data | |
files/exposed-source/routes/auth.js | 2067 | <hash redacted> | Java source, ASCII text | |
files/exposed-source/routes/profile.js | 2631 | <hash redacted> | Java source, ASCII text | |
files/exposed-source/static/css/styles.css | 4767 | <hash redacted> | ASCII text | |
files/exposed-source/static/js/admin.js | 4500 | <hash redacted> | ASCII text | |
files/exposed-source/static/js/auth.js | 2268 | <hash redacted> | Java source, ASCII text | |
files/exposed-source/static/js/dashboard.js | 8988 | <hash redacted> | Unicode text, UTF-8 text | |
files/exposed-source/static/js/farm.js | 2479 | <hash redacted> | Unicode text, UTF-8 text | |
files/exposed-source/static/js/main.js | 257 | <hash redacted> | Java source, ASCII text | |
files/exposed-source/static/js/ui.js | 2802 | <hash redacted> | Java source, ASCII text | |
files/exposed-source/templates/admin.html | 22808 | <hash redacted> | HTML document text, Unicode text, UTF-8 text | |
files/exposed-source/templates/index.html | 27714 | <hash redacted> | HTML document text, Unicode text, UTF-8 text | |
files/exposed-source/templates/login.html | 10790 | <hash redacted> | HTML document text, Unicode text, UTF-8 text | |
files/exposed-source/templates/unauthorized.html | 3824 | <hash redacted> | HTML document text, Unicode text, UTF-8 text | |
files/exposed-source/utils/database.js | 1393 | <hash redacted> | Java source, ASCII text | |
files/exposed-source/utils/jwt.js | 1022 | <hash redacted> | Java source, ASCII text | |
files/exposed-source/utils/random.js | 383 | <hash redacted> | Java source, ASCII text |
Evidence Ledger
| Time | Action | Output/File | Finding | Confidence | Next |
|---|---|---|---|---|---|
| 2026-06-07T23:12:25Z | harness init | challenge-state.json | Workspace initialized with deterministic state file | High | Inventory artifacts |
| 2026-06-07T23:12:37Z | artifact inventory | analysis/artifact-inventory.json | 0 artifact(s) inventoried | High | Build or update hypotheses |
| 2026-06-07T23:12:37Z | hypothesis recorded | hypothesis-board.md | Remote SecureCoding service likely exposes an editable source tree and verification endpoint; mirror source first, then patch only the vulnerable code path. | Medium | GET /, inspect public assets, query /api/directory and /api/verify if editor pattern is present. |
| 2026-06-07T23:12:37Z | checkpoint recorded | analysis/checkpoint-triage-20260607T231237629784Z-451fb15d.md | Checkpoint for TRIAGE | High | Use checkpoint to drive next decision |
| 2026-06-07T23:15:18Z | source audit | analysis/source-audit.md | Source audit recorded | High | Gate before exploit |
| 2026-06-07T23:15:18Z | source audit | analysis/source-audit.md | Source audit recorded | High | Gate before exploit |
| 2026-06-07T23:15:18Z | local memory record | analysis/local-memory-records.md | Prior local notes reviewed as fallback/advisory context | Medium | Validate against current evidence |
| 2026-06-07T23:15:20Z | hypothesis recorded | hypothesis-board.md | Patch profile deep merge prototype pollution by blocking __proto__, prototype, and constructor keys recursively and merging only plain object own keys. | Medium | Save solve/patched-profile.js over routes/profile.js, restart service, call /api/verify, capture returned flag through harness. |
| 2026-06-07T23:15:30Z | artifact inventory | analysis/artifact-inventory.json | 22 artifact(s) inventoried | High | Build or update hypotheses |
| 2026-06-07T23:15:30Z | evaluator | analysis/evaluator-20260607T231530986595Z-e7b11f38.md | Proceed | High | Apply prepared patch, restart, verify, capture flag through harness. |
| 2026-06-07T23:15:41Z | evaluator | analysis/evaluator-20260607T231541522161Z-0a48fccd.md | Proceed | High | Apply prepared patch, restart, verify, capture flag through harness. |
| 2026-06-07T23:17:44Z | 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:18:43Z | completion gate | challenge-state.json | Completion gate passed; state marked COMPLETE | High | Optional sanitized memory summary approval |
Key Findings
- Service fingerprint:
GET /serves the HTB Editor frontend;GET /challenge/serves the AgriWeb application. - Real editor endpoints use
/api/*, including/api/directory,/api/file,/api/restart, and/api/verify. GET /api/directoryexposed the source tree; mirrored copy is stored underfiles/exposed-source/.GET /api/verifyconfirmed the current service is still vulnerable before patching.db/agriweb.dbreturned HTTP 500 through the editor file API; all text source was mirrored successfully.- Root cause:
routes/profile.jsrecursively deep-merges attacker-controlled profile JSON without blocking__proto__,prototype, orconstructor. - Impact: prototype pollution can make normal JWT payload objects inherit
isAdmin, passing theapp.jsadmin middleware check. - The included
exploit/solver.pyconfirms the intended exploit path and payload class. - Prepared remediation artifacts:
- solve/patched-profile.js
- solve/solve.py
- <secret redacted>md
- Remote execution succeeded after patching
routes/profile.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: Agriweb
- 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 an editable source tree and verification endpoint; mirror source first, then patch only the vulnerable code path. | Challenge provides remote host <TARGET>:31970 and SecureCoding category; no archive was provided. | GET /, inspect public assets, query /api/directory and /api/verify if editor pattern is present. | Medium | Active | |
| 1 | Patch profile deep merge prototype pollution by blocking __proto__, prototype, and constructor keys recursively and merging only plain object own keys. | files/exposed-source/routes/profile.js performs unsafe recursive deepMerge; files/exposed-source/exploit/solver.py uses prototype pollution payloads to reach admin. | Save solve/patched-profile.js over routes/profile.js, restart service, call /api/verify, capture returned flag through harness. | 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: SecureCoding
- Challenge: Agriweb
- 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: Agriweb
- Category: SecureCoding
- Difficulty: Easy
- Mode: remote
- Remote instance: <TARGET>:31970
- Start time: 2026-06-07T23:12:25Z
- 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/app.js | 4609 | <hash redacted> | Java source, ASCII text | |
files/exposed-source/db/.gitkeep | 0 | <hash redacted> | empty | |
files/exposed-source/directory.json | 1771 | <hash redacted> | JSON data | |
files/exposed-source/exploit/solver.py | 2538 | <hash redacted> | Python script text executable, ASCII text | |
files/exposed-source/package-lock.json | 101219 | <hash redacted> | JSON data | |
files/exposed-source/package.json | 456 | <hash redacted> | JSON data | |
files/exposed-source/routes/auth.js | 2067 | <hash redacted> | Java source, ASCII text | |
files/exposed-source/routes/profile.js | 2631 | <hash redacted> | Java source, ASCII text | |
files/exposed-source/static/css/styles.css | 4767 | <hash redacted> | ASCII text | |
files/exposed-source/static/js/admin.js | 4500 | <hash redacted> | ASCII text | |
files/exposed-source/static/js/auth.js | 2268 | <hash redacted> | Java source, ASCII text | |
files/exposed-source/static/js/dashboard.js | 8988 | <hash redacted> | Unicode text, UTF-8 text | |
files/exposed-source/static/js/farm.js | 2479 | <hash redacted> | Unicode text, UTF-8 text | |
files/exposed-source/static/js/main.js | 257 | <hash redacted> | Java source, ASCII text | |
files/exposed-source/static/js/ui.js | 2802 | <hash redacted> | Java source, ASCII text | |
files/exposed-source/templates/admin.html | 22808 | <hash redacted> | HTML document text, Unicode text, UTF-8 text | |
files/exposed-source/templates/index.html | 27714 | <hash redacted> | HTML document text, Unicode text, UTF-8 text | |
files/exposed-source/templates/login.html | 10790 | <hash redacted> | HTML document text, Unicode text, UTF-8 text | |
files/exposed-source/templates/unauthorized.html | 3824 | <hash redacted> | HTML document text, Unicode text, UTF-8 text | |
files/exposed-source/utils/database.js | 1393 | <hash redacted> | Java source, ASCII text | |
files/exposed-source/utils/jwt.js | 1022 | <hash redacted> | Java source, ASCII text | |
files/exposed-source/utils/random.js | 383 | <hash redacted> | Java source, ASCII text |
Evidence Ledger
| Time | Action | Output/File | Finding | Confidence | Next |
|---|---|---|---|---|---|
| 2026-06-07T23:12:25Z | harness init | challenge-state.json | Workspace initialized with deterministic state file | High | Inventory artifacts |
| 2026-06-07T23:12:37Z | artifact inventory | analysis/artifact-inventory.json | 0 artifact(s) inventoried | High | Build or update hypotheses |
| 2026-06-07T23:12:37Z | hypothesis recorded | hypothesis-board.md | Remote SecureCoding service likely exposes an editable source tree and verification endpoint; mirror source first, then patch only the vulnerable code path. | Medium | GET /, inspect public assets, query /api/directory and /api/verify if editor pattern is present. |
| 2026-06-07T23:12:37Z | checkpoint recorded | analysis/checkpoint-triage-20260607T231237629784Z-451fb15d.md | Checkpoint for TRIAGE | High | Use checkpoint to drive next decision |
| 2026-06-07T23:15:18Z | source audit | analysis/source-audit.md | Source audit recorded | High | Gate before exploit |
| 2026-06-07T23:15:18Z | source audit | analysis/source-audit.md | Source audit recorded | High | Gate before exploit |
| 2026-06-07T23:15:18Z | local memory record | analysis/local-memory-records.md | Prior local notes reviewed as fallback/advisory context | Medium | Validate against current evidence |
| 2026-06-07T23: <REDACTED>, prototype, and constructor keys recursively and merging only plain object own keys. | Medium | Save solve/patched-profile.js over routes/profile.js, restart service, call /api/verify, capture returned flag through harness. | |||
| 2026-06-07T23:15:30Z | artifact inventory | analysis/artifact-inventory.json | 22 artifact(s) inventoried | High | Build or update hypotheses |
| 2026-06-07T23: <REDACTED>, restart, verify, capture flag through harness. | |||||
| 2026-06-07T23: <REDACTED>, restart, verify, capture flag through harness. | |||||
| 2026-06-07T23: <REDACTED> | |||||
| 2026-06-07T23:18:43Z | completion gate | challenge-state.json | Completion gate passed; state marked COMPLETE | High | Optional sanitized memory summary approval |
Key Findings
- Service fingerprint:
GET /serves the HTB Editor frontend;GET /challenge/serves the AgriWeb application. - Real editor endpoints use
/api/*, including/api/directory,/api/file,/api/restart, and/api/verify. GET /api/directoryexposed the source tree; mirrored copy is stored underfiles/exposed-source/.GET /api/verifyconfirmed the current service is still vulnerable before patching.db/agriweb.dbreturned HTTP 500 through the editor file API; all text source was mirrored successfully.- Root cause:
routes/profile.jsrecursively deep-merges attacker-controlled profile JSON without blocking__proto__,prototype, orconstructor. - Impact: <REDACTED>, passing the
app.jsadmin middleware check. - The included
exploit/solver.pyconfirms the intended exploit path and payload class. - Prepared remediation artifacts:
- solve/patched-profile.js
- solve/solve.py
- <secret redacted>md
- Remote execution succeeded after patching
routes/profile.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 Agriweb, 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.