How to exploit an API using prototype pollution

How to exploit an API using prototype pollution

Introduction

I read somewhere that NodeJS has now been downloaded over 1.5 BILLION times. It’s one of the most popular runtimes startups use to build their web apps and APIs.

And big businesses use it too. Everyone from LinkedIn and NetFlix to NASA, Uber, and GoDaddy uses it to drive their software.

So what? Why is this important?

As a cross-platform JavaScript runtime environment that may be driving the APIs you are testing, understanding how to exploit it is essential. We’ve seen plenty of examples of exploiting JavaScript on the client side, but can this be done on the server side that NodeJS delivers?

It ends up you can. It’s called server-side prototype pollution (SSPP).

And prototype pollution can be a huge vulnerability you need to know how to detect and exploit.

So let’s dive in and discuss what prototype pollution is, why it matters, and how you can weaponize it to exploit an API.

What is prototype pollution?

Prototype pollution is a type of attack vector in which an attacker can inject data into an object prototype. This attack occurs when the user input isn’t sanitized or filtered properly before being parsed by JavaScript.

When this happens, malicious data is added to the global scope, which can be executable code that will run immediately and access privileged information.

This type of attack is known as “polluting” the prototype chain, which can lead to a whole host of problems, including remote code execution (RCE).

To get a more in-depth understanding, you can check out this article on MDN.

Let’s look at an example of how this works. Consider this code:

obj[a][b] = c;

If an attacker can control a and c, then he can set the value of a to __proto__, and the property b will be defined for all existing objects of the application with the value of c.

Can you think of some ways you can abuse this? I bet you can.

Detecting Server Side Prototype Pollution

So before I show you how to detect SSPP, I need to warn you about something.

When testing client-side prototype pollution, you can easily reverse all your changes and return to a clean environment by simply refreshing the target web page.

That’s more challenging when testing prototype pollution on the server side.

Once you pollute a prototype on the server, this change will persist for the entire lifetime of the Node process, and you don’t have any way of resetting it. This means it can impact production workloads and can negatively impact the API you are testing.

So please be aware and be careful. I will show you both some destructive and non-destructive ways to detect SSPP. Use good judgment when applying these techniques to a target.

Test for polluted property reflection

Look for API endpoints that reflect an object that is created or updated. You can usually find this where objects are created in a POST operation or are updated in a PUT operation.

Consider this example:

POST /api/user HTTP/1.1
Host: vuln-api.com
...
{
     "user":"bob",
     "firstName":"Bob",
     "lastName":"Smith",
     "__proto__":{
          "foo":"bar"
     }
}

If the target is vulnerable to SSPP, then you may see a new property called foo with the value of bar reflected in the response:

HTTP/1.1 200 OK
...
{
     "username":"bob",
     "firstName":"Bob",
     "lastName":"Smith",
     "foo":"bar"
}

If this works, it’s time to start looking for places in the API where this can be abused. Suppose you can start adding properties to objects through SSPP. In that case, you can significantly change the business logic code flow and behavior, leading to privileges escalation or remote code execution.

Test for poisoned server configuration changes

While polluting properties and looking for them in a reflected response works, it is a rather destructive methodology to active objects. And the reality is APIs don’t always reflect objects that are created or updated. This means we don’t always get to SEE that the objects have been modified in this way.

There are other ways to cause prototype pollution that we can more easily monitor and detect. One such approach is to try to poison properties that match configuration options for the server and see if it alters its behavior. If it does, it strongly indicates that you’ve found a server-side prototype pollution vulnerability you can exploit.

There are several techniques you can use for this. Gareth Heyes has published some excellent research on this already. We will look at a few methods that pollute how cache controls work in Express and how to override the spaces in JSON output.

Testing for Cache-Control

So if you read Gareth’s research, you will know that the Express developers were onto his shenanigans and started hardening their code to prevent prototype pollution abuse for crucial server configurations. But they haven’t gotten them all (yet).

One of the best ways to see if you have found an entry point to poison configuration is to see if you can abuse the cache control in Express.

Here is how it works. When you send a request with the “If-None-Match” header, you should expect to receive a 304 (not modified) response.

However, if you found SSPP, you can attempt to pollute the Object prototype on the server by adding cache-control=”no-cache”.

If it works, when you send the original request that returned a 304, you SHOULD now get a 200 back with that data and a corresponding ETag, demonstrating its vulnerable.

Just remember you are affecting the caching for the entire API. If you are doing this on a production server, ensure you have approval and are in constant contact with the Ops group to schedule the testing and notify them when to revert the caching state by restarting the processes.

Testing for JSON spaces override

So Express offers a lot of configurable options in its server framework. One example of this is the fact you can configure how many spaces a JSON object will use to indent any data in the response. You can test this by attempting to pollute the Object prototype by adding “json spaces” and setting it to a larger number than average, like 10 or 15.

If you are testing this with Burp Suite, remember to look at the Raw tab, as the Pretty tab will strip out the extra spaces as it tries to give you easy-to-read JSON output.

This methodology has recently been fixed in Express 4.17.4. But for all those older Express instances out there, you can still abuse this.

If you want to practice this, PortSwigger has a wicked lab for detecting server-side prototype pollution you can mess with.

You can test other ways to detect SSP, like through status codes and character set output. Lots to be learned if you check out the SSPP tutorial on Web Security Academy.

A simple vulnerable API in NodeJS example

So I wanted to give you something to play with to see all this in action. I encourage you to jump into PortSwigger’s Web Security Academy project. But they don’t let you see the code causing these vulnerabilities.

So I’ve written a purposely vulnerable API in NodeJS to walk you through this. I hinted at this when showcasing the prototype pollution for cache control earlier in this article. But I will walk you through an example and demonstrate how you can control code flow in a vulnerable API that ultimately lets you execute a dangerous sink, giving you remote code execution on the API server.

DO NOT RUN THIS ON A MACHINE CONNECTED TO THE INTERNET. YOU’VE BEEN WARNED.

The vulnerable API code

OK, so here is the raw code for our vulnerable API:

'use strict';

const express = require('express');
const bodyParser = require('body-parser')

// App
const app = express();
app.use(bodyParser.json())

const config = {
    //allowEval: false
}

global.users = {
    "admin": {firstName: "The", lastName: "Admin"},
    "bob": {firstName: "Bob", lastName: "Smith"},
}

var findUserByUsername = function (username, callback) {
  if (!users[username])
    return callback(new Error(
      'No user matching '
       + username
      )
    );
  return callback(null, users[username]);
}

function addUser( user ) {	
	Object.assign(users, user);
}

function updateUser(username, prop, value){	
    users[username][prop] = value;
}

app.get("/api/users", (req, res) => {
	return res.status(200).send(users);
});

app.get("/api/users/:username", (req, res) => {
	var username = req.params.username;
	findUserByUsername(username, function(error, user) {
    		if (error) return res.status(404).send('User not found');
    		return res.status(200).send(user);
  	});	
});

app.post("/api/users", (req, res) => {
	var user = JSON.parse(JSON.stringify(req.body));
	addUser(user);
	return res.status(200).send(user);
});

app.put("/api/users/:username", (req, res) => {
	var username = req.params.username;
	var user = JSON.parse(JSON.stringify(req.body));

	for(var attr in user){
		updateUser(username, attr, user[attr]);
	}

	findUserByUsername(username, function(error, user) {
		if (error) return res.status(404).send('User not found');
		return res.status(200).send(user);
  	});	
})

app.post("/api/eval", (req, res) => {
	var body = JSON.parse(JSON.stringify(req.body));
	if (!config.allowEval){
		console.log('allowEval not set!');
		return res.status(403).send();
	}
	console.log("allowEval IS set. RCE on its way...");
	eval(body.code)
	return res.status(200).send();
});


app.listen(3000, '0.0.0.0');
console.log("Vulnerable API running...");

The things I want you to zoom into are the following:

  1. There is a dangerous sink in the POST endpoint for /api/eval (around line 69). It is protected by a simple variable in the config object called allowEval. It’s not set by default, which means this endpoint will never successfully run the dangerous eval() operation. We want to abuse the server and find a way to get here.
  2. The PUT endpoint for /api/users is vulnerable to server-side prototype pollution, following the pattern we discussed earlier where obj[a][b] = c, which we can see in the updateUser() function on line 34.

OK, so with that outlined, let’s go about exploiting this API and get it to run our own malicious code to send us a remote shell from the API server.

Exploring the API

So if we examine the code closely, we can see that when updating a user Express will iterate over each property sent to the endpoint and update the object based on the username. If we wanted to update the “admin” user, we could send a payload that looks something like this:

PUT /api/user/admin HTTP/1.1
Host: vuln-api.com
...
{
     "firstName":"Bob",
     "lastName":"The Builder"
}

So on the first iteration, we would see an update look something like this:

users["admin"]["firstName"] = "Bob";

You can see where this is going. This is a classic prototype pollution construct. We control the username, the property, and the value. So let’s leverage this to change the code flow of the API and get us to our dangerous sink.

Exploiting the API

So let’s first check to see if the /api/eval endpoint is indeed protected from our use:

Sure enough, we get a 403, as expected.

But look closely at the code for the endpoint. It’s simply checking the config object to see if allowEval is present. It’s not even checking if it’s true or false… it’s just checking if it’s there. Knowing we have a potential SSPP vulnerability in the PUT method for /api/users, let’s see if we can abuse that.

Remember when I said this?:

If an attacker can control a and c, then he can set the value of a to __proto__, and the property b will be defined for all existing objects of the application with the value of c.

So, to cause prototype pollution on the server, we need to reconstruct that as:

users["__proto__"]["allowEval"] = true;

And that is pretty easy here. We need to change the endpoint URL path and include a JSON payload with that property. It looks something like this in Burp:

Alright. Everything is staged. We can now execute the dangerous sink and run code on the API server.

Let me first start a netcat listener so I can catch a reverse shell:

netcat -lvp 4242

And now, with the allowEval variable polluted in the config object, we can change the code flow and run our malicious code on the /api/eval endpoint. In this case, I will get NodeJS to load a child process and exec a reverse shell:

Back on your netcat listener, you should have a shell on the API server:

Pwnage via prototype pollution. How sweet it is.

Conclusion

Now that you know what server-side prototype pollution is and how to exploit it, you should be aware of this vulnerability class in the wild. It can be challenging to detect, as it relies on the code flow of a server framework running JavaScript to achieve its malicious goals.

But with enough practice and exploration into different endpoints and payloads, anything is possible.

If you find an API running NodeJS during your recon, it’s worth exploring this. If you aren’t sure how to tell if an API is running NodeJS, check out my article on detecting an API’s programming language.

So the next time you audit or pentest an API, make sure to look for prototype pollution potentials and see if you can manipulate code flow in your favor!

Have fun.

Like what you’re reading? Then join the API Hacker Inner Circle. It’s a FREE weekly newsletter designed for developers, testers, and hackers that keeps you up to date with the community and my work. Hope to see you there!

Dana Epp

Discover more from Dana Epp's Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading