On my YouTube channel, I’m trying to teach about computer security by showing how to solve a capture the flag challenges. Lately, I’ve published a new video showing how to exploit Prototype Pollution.
I’ve tried and solved a task from this year’s edition of TJCTF named Fruit Store. This article is a companion to that video focused on explaining what is Prototype Pollution and how to use this vulnerability in Node.js to change the application flow and bypass security mechanisms.
You can find the video on my YouTube channel:
👉
Let’s get started…
JavaScript got an unusual way of handling inheritance. Back in the old days unlike other object-oriented languages, it didn’t use classes and related mechanisms to cover it. There was no base class that we could inherit properties from. But any object could have a thing called a prototype. And what’s that?
Creating an object instance in JavaScript, developers can set a “parent” object. An object that can contain methods or fields that, if not overwritten will also be accessible in the “child” object. An example of how it can be done is to use Object.create
method passing any other object that will act as a parent. It will become its prototype.
An object is kind of linked with its prototype and the prototype object can have a prototype of its own making a tree of a linked objects that we can call a prototype chain.
Let’s consider the following example:
The code on the left is responsible for creating a prototype chain (visualised on the right).
A generic MacBook object stating that every MacBook is produced by Apple is the top-level parent, then we have a specific model - MacBook Air and at the end, we create an object representing the unit I’m writing this article on - my Mac. All these are connected as one became a parent of the other.
In the case presented above, when the myMac.manufacturer
field is called JavaScript looks for it in myMac
first, but when it’s not found then it checks its prototype and its prototype… until it’s found.
This mechanism, although being a pretty elegant alternative to more traditional inheritance have one huge flaw that allows Prototype Pollution to be exploited. What’s that?
A prototype can be accessed with the built-in __proto__
field that is part of every object. A thing worth noting is that all the objects created with the curly braces syntax have the same prototype by default.
{}.__proto__ === {}.__proto__ //true
This makes the prototype chain a bit more interesting as most of the objects living in the Node.js application have the same ancestor. What does it mean for us?
If we will be able to access the prototype of a single object created this way and place there a malicious field or method then every single object in the application will have it present as well. Figuring it out the right way can help us to change application execution flow in a very dangerous way…
Let’s consider an example of an app we can exploit and try to use what we know so far to use that vulnerability.
The example app that I’m showing in my YouTube video got an endpoint that allows adding an additional amount of money to our account, but only if the req.session.admin
field is set to true. Normally to become an admin we have to come from the local network which is not possible in that case.
app.post('/api/v1/money', (req, res) => {
if (req.session.admin) {
req.session.money += req.body.money;
res.send('Money added');
} else {
res.status(403).send('Not admin');
}
});
Hopefully, the session object is created in the way making it a part of the global prototype chain, because in this case polluting the base prototype with the admin = true
field would also be reflected here.
How would it work for us? As we are not an admin, the conditional expression in the second line of the code above would normally get us to the else statement saying “Not admin”. But if we pollute the app then reading req.session.admin
would not stop on the session object, but move up the prototype chain until it will find the field admin = true
in the base prototype changing the execution flow and allowing us to add money to our session.
The application is parsing user input sent as JSON. While decoding it, it creates an object using the keys provided in the request. Will this part be vulnerable? What if we send a request like this?
{
"__proto__": {
"admin": true
}
}
Unfortunately, not much…
JSON.parse
method that is used in this case got it figured out and has appropriate mechanisms in place to prevent assigning the admin field to the prototype. In fact, it will just create a __proto__.admin
field in our request object instead of polluting its parent.
So? Are we doomed?
Not yet. We just need to find a place in the app that is vulnerable. Parsing JSON is not that place, but if we only can find a loop going through all the request keys, blindly rewriting them to another object then we are home!
Looking into the rest of the application source code we can find the /api/v1/sell
endpoint with the following code
app.post('/api/v1/sell', (req, res) => {
for (const [key, value] of Object.entries(req.body)) {
if (key === 'grass' && !req.session.admin) {
continue;
}
if (!fruits[key]) {
fruits[key] = JSON.parse(JSON.stringify(fruit));
}
for (const [k, v] of Object.entries(value)) {
if (k === 'quantity') {
fruits[key][k] += v;
} else {
fruits[key][k] = v;
}
}
}
res.send('Sell successful');
});
That’s more like it. This one goes through every field in our request body and if only the key is not “quantity”, then it just rewrites it. The line fruits[key][k] = v
is the one we are looking for. Passing there __proto__.admin
field set to true will poison the global prototype making every object in the app have an admin field set to true. Including our session object.
So the exploit seems to be simple. Just a few steps…
Become an admin using Prototype Pollution at /api/v1/sell
endpoint
Add money to your session using /api/v1/money
as it will now allow doing that
Then use the money to buy the flag (which was the goal for the TJCTF)
If you want to see how I’ve solved this challenge using prototype pollution check out the full video on my YouTube channel:
👉
Thanks for reading. Feel free to comment. Cheers!