| Field | Detail |
|---|---|
| Platform | PortSwigger Web Security Academy |
| Type | Server-Side Prototype Pollution / RCE |
| Difficulty | Practitioner |
| Objective | Pollute Object.prototype via constructor.prototype to inject execArgv into Node.js child process spawning, then trigger RCE via the maintenance jobs endpoint to delete /home/carlos/morale.txt |
Remote Code Execution via Server-Side Prototype Pollution¶
I logged in as wiener:peter — admin privileges are granted by default in this lab.
I confirmed the constructor.prototype pollution source on the address update endpoint — the json spaces gadget produced indented output and teto was reflected:
The admin panel had a "Run Maintenance Jobs" function:
Intercepting its POST showed it triggering db-cleanup and fs-cleanup tasks:
These tasks almost certainly spawn child processes via Node.js's child_process module. When Node spawns a child with child_process.fork(), it accepts an options object that includes execArgv — an array of arguments passed to the Node.js interpreter itself before the script runs. --eval is one of those arguments: it tells Node to execute a JavaScript string before the main script. If execArgv can be inherited from a polluted Object.prototype, the child process executes our code before doing anything else.
The gadget fires when a child process is spawned, not when pollution is injected — injecting into the prototype via the address form primes it; the maintenance jobs endpoint is the trigger. This two-step pattern (poison → trigger) requires identifying which application functionality spawns child processes.
I injected execArgv via the address form:
{
"constructor": {
"prototype": {
"execArgv": [
"--eval=require('child_process').execSync('rm /home/carlos/morale.txt')"
]
}
}
}
The response confirmed execArgv was now inherited from the prototype. Triggering execution by sending the /admin/jobs POST:
The server spawned child processes for each task; each inherited execArgv from the polluted prototype and executed rm /home/carlos/morale.txt before running the maintenance script. File deleted. The source here is constructor.prototype — same as the previous lab — with execArgv as the new gadget. Source and gadget are independent; once the pollution source is confirmed, gadget hunting is separate.
Lab solved