How to Exploit Prototype Pollution?

Written by lukaszwronski | Published 2022/07/09
Tech Story Tags: security | prototype-pollution | vulnerabilities | vulnerability | hacking | javascript | node | nodejs

TLDRPrototype Pollution is one of the most underrated vulnerabilities that affect a lot of Node.js and JavaScript apps. By crafting the right payload we can poison the prototype chain changing the application flow and bypassing security checks. The most important part is to find a vulnerable piece of code that will rewrite user input to the new object accidentally polluting entire app.via the TL;DR App

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:

👉 https://youtu.be/MzlZIJjqsVE

Let’s get started…

What are prototypes?

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?

Let’s make a mess

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.

Becoming an admin

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!

Final exploit

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…

  1. Become an admin using Prototype Pollution at /api/v1/sell endpoint

  2. Add money to your session using /api/v1/money as it will now allow doing that

  3. 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:

👉 https://youtu.be/MzlZIJjqsVE

Thanks for reading. Feel free to comment. Cheers!


Written by lukaszwronski | CEO, developer, hacker, father of two, bass player, internet troll and meme enthusiast...
Published by HackerNoon on 2022/07/09