Prototype Pollution to Command Injection
SecretMerger is a medium difficulty web challenge created for DomeCTF 2021, and in this blog we’ll be discussing how to solve the challenge(the intended way).
We’re given a node project with 3 files:
├── app.js
-> Contains the main application logic
├── package.json
-> Package information and dependencies used
└── utils.js
-> ObjectMerger logic
We can first inspect the app.js
to understand what happens server-side.
1 | // app.js |
Route to GET /
does nothing else other than the welcome message, we’ll have to dig through POST /secret
to find out what’s going on.
It takes two req.body
values, path
& and value
, if they’re defined, they’re passed to the secretMerger()
function which is defined in utils.js
. So we can take a look at how the secretMerger()
processes the values.
1 | // utils.js |
Reading the code, we can see that the function simply merges two objects together(source as an empty object and destination as it’s path and value). Here’s how the merge works:
For each property in source, check if that property is object itself. If it is then go down recursively and try to map child object properties from source to destination. So essentially we merge object hierarchy from source to destination (same as that of $.extent()
in jQuery or _.merge()
in lodash).
In JavaScript, when new objects are created, they carry over the properties and methods of the prototype “object”, which contains basic functionalities such as toString
, constructor
, and hasOwnProperty
. Object-based inheritance gives JavaScript flexibility and efficiency, but it also makes it vulnerable to tampering. Attackers can make application-wide changes to all objects by modifying object, hence creating a prototype pollution vulnerability.
1 | exec('cat info.txt ' + secretSpell, (err, resp)=> { |
Going back to app.js
again, we can see it then attempts to cat info.txt
along with a secretSpell
variable, which is not in scope. Also it uses child_process.exec()
to spawn subprocess, so we could possibly get an arbitrary OS command injection abusing the secretSpell
variable. Here’s how the payload works:
- Create a malicious destination object to pollute
path
. - Overwrite the
secretSpell
variable. - Getting arbitrary command injection.
We can access secretSpell
through __proto__
property of the destination object:
1 | curl --data '{"path":"__proto__.secretSpell", "value":"; ls"}' -H "Content-Type: application/json" --url http://127.0.0.1:3000/secret -X POST |
After code execution, we can find a directory TopSecretData
with the flag in it.
DomeCTF{ea645983443b225edb3046529bf083be}