Fun challenge to learn prototype pollution
Environment Setup
OS: Parrot Security VM
Tools: BurpSuite, Docker
Unzipping the given file reveals the source code of a website built with node js
.
1 | ┌─[chief@parrot]─[~/Desktop/HTB/breaking_grad] |
Set up docker and run ./build_docker.sh
, and you should find a webserver listening on port 1337.
Source Code Analysis
challenge/routes/index.js
1 | router.get('/', (req, res) => { |
There’s a /debug/
and /api/calculate
endpoint which we can interact with.
If the source code isn’t given we will have to fuzz these endpoints.
The server will “clone” a new js object with our post request to /api/calculate
and name it student.
Let’s follow along and see how this clone is implemented.
challenge/helpers/ObjectHelper.js
1 | module.exports = { |
The clone function simply merges our request body to an empty js object.
The merge function loops through all properties of our request body. If the property is an object or function and it exists in the empty object, it will overwrite the empty object’s original implementation. Otherwise it will implement it for the empty object.
This implementation is actually vulnerable to prototype pollusion, despite its attempts to filter the keyword __proto__
.
Let’s try to break the code locally.
POC
Tidy up the code so it can run on a browser.
poc.js
1 | function isObject(obj) { |
test.html
1 | <script src="poc.js"></script> |
Now we can use firefox to play with this function.
The empty object indeed got all the properties of the new source object, but it also has an inbuilt property __proto__
. This property refers to the prototype of the constructor of the object. For the empty object {}
, its constructor is the Object
function. If we append a property to Object
‘s prototype, all new objects created will have this property inherited.
Even though the word __proto__
is filtered, constructor.prototype
will refer to the same thing.
Before the pollution, object b does not have the property pollutekey:pollutevalue
. After the pollution, not only does object b magically inherit this property, object c that is newly created also inherits it.
Exploitation
The debug function seems like it can execute code, so that can be a good place to gain code execution.
challenge/helpers/DebugHelper.js
1 | const { execSync, fork } = require('child_process'); |
An object literal is passed into the fork function, so that means we get to smuggle our polluted value into the arguments of the fork function.
Reading the docs tells us that the fork function accepts an execPath and execArgv arguments.
1 | execPath <string> Executable used to create the child process. |
By polluting these values, we can gain RCE.
Conclusion
I tried to do this challenge a year ago, when I didn’t know anything about javascript, and obviously I had no idea how to solve it. After spending some time learning basic js it became much clearer how to approach and debug this simple challenge.
Lesson of the day: learn and familiarise yourself with the language you are hacking.
Thanks for reading. byebye :)