$ cat writeup.md…
$ cat writeup.md…
hackthebox
Task: Patch a prototype pollution vulnerability in a Node.js Express web app to get the flag. Solution: Identify unfiltered __proto__/constructor keys in a deepMerge function, upload a patched version that filters dangerous keys, restart the app, and verify the fix via /api/verify endpoint.
"Digital farmlands lie ruined as drones spin out of control and greenhouses overheat; the white-hats must infiltrate the corrupted AgriWeb interface and bring the fields back to life."
Target: http://94.237.120.112:57056
Accessed the target URL and found an "HTB Editor" - a code editor interface with API access to the application source code.
/api/directory - List files/api/file?path=X - Read file contents/api/create-file - Create new files/api/delete - Delete files/api/restart - Restart application/api/verify - Verify if vulnerability is patchedapp.js - Main Express application
routes/auth.js - Authentication routes
routes/profile.js - Profile update routes (VULNERABLE)
utils/jwt.js - JWT token handling
utils/database.js - SQLite database setup
exploit/solver.py - Hint file showing the attack
Found in routes/profile.js:
function deepMerge(target, source) { for (let key in source) { if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { if (!target[key]) target[key] = {}; deepMerge(target[key], source[key]); } else { target[key] = source[key]; } } return target; }
Problem: This function doesn't filter dangerous keys (__proto__, constructor, prototype), allowing prototype pollution.
Impact: The JWT token generation in utils/jwt.js checks user.role === 'admin' to set isAdmin: true. By polluting the Object prototype with isAdmin: true, any user could bypass admin authentication.
The /api/verify endpoint returned: "Application vulnerability is not patched."
This revealed the challenge wasn't about exploiting the vulnerability, but patching it!
curl -X DELETE "http://94.237.120.112:57056/api/delete" \ -H "Content-Type: application/json" \ -d '{"path":"routes/profile.js"}'
The fix adds a check to skip dangerous prototype pollution keys:
function deepMerge(target, source) { for (let key in source) { // PATCH: Filter dangerous prototype pollution keys if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue; if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { if (!target[key]) target[key] = {}; deepMerge(target[key], source[key]); } else { target[key] = source[key]; } } return target; }
curl -X POST "http://94.237.120.112:57056/api/create-file" \ -H "Content-Type: application/json" \ -d '{"path":"routes/profile.js","content":"<patched code>"}'
curl -X POST "http://94.237.120.112:57056/api/restart"
curl "http://94.237.120.112:57056/api/verify" # Response: {"flag": "HTB{pr0totyp3_p0llut10n_t0_4uth_byp4s5}"}
Use this technique when you see:
deepMerge, deepCopy, or extend functions in JavaScriptAlways filter these keys in recursive merge functions:
__proto__ - Direct prototype accessconstructor - Access to constructor.prototypeprototype - Direct prototype property// Safe deepMerge implementation function deepMerge(target, source) { const DANGEROUS_KEYS = ['__proto__', 'constructor', 'prototype']; for (let key in source) { if (DANGEROUS_KEYS.includes(key)) continue; if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { if (!target[key]) target[key] = {}; deepMerge(target[key], source[key]); } else { target[key] = source[key]; } } return target; }
__proto__, constructor, prototype$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar