Intro – What is Prototype Pollution?
Prototype Pollution is a JavaScript vulnerability where it’s possible for an attacker to control unexpected variables in JavaScript, which on the client-side can lead to Cross-Site Scripting, and on the server-side lead to Remote Code Execution.
It is caused by ‘JavaScript Weirdness’, specifically in the declaration and setting of variable names, and is exploitable because of further JavaScript weirdness with weak typing, where it’s possible to have various undeclared variables in code that can be controlled by Prototype Pollution.
If you have control of the Prototype, whatever property you add will end up with all JavaScript objects now having that property!
This becomes exploitable when there are conditionals that check for the existence of certain properties. Let’s say we have some vulnerable code that does the check if user.isAdmin
. If we controlled the prototype and added the property isAdmin
, then all objects will now have the property isAdmin
, which means that if user.isAdmin
is now a weakly-type True, passing the condition and giving us admin access.
This is a simple example, and in practice there’s a bit more complexity, but this is the core of Prototype Pollution – being able to control unexpected variables in JavaScript, and then use them to change the path code takes in unexpected ways.
What even is a Prototype?
Prototypes are the primary inheritance feature of JavaScript. From MDN Docs: “Prototypes are the mechanism by which JavaScript objects inherit features from one another.” But what does that mean in practice?
Let’s start with some JavaScript basics. In JavaScript, objects are created by using the {} syntax. For example:
var my_object = {a:1, b:2}
Even if you’re not familiar with JavaScript, you’re still probably familiar with this style of writing objects – JSON is ‘JavaScript Object Notation’, which is used everywhere.
This object has two properties: a and b. If we want to access the value of a, we can do so by using the dot notation or bracket notation:
console.log(my_object.a) // Output: 1 // functionally equivalent to... console.log(my_object["a"]) // Output: 1
If we want to add a new property to the object, we can do so using the same syntax as above, either with the dot notation or the bracket notation:
my_object["c"] = 3 // or... my_object.c = 3
This is all pretty standard stuff.
But here’s the thing – in JavaScript, practically everything is an object, and inherits from parent object. For example, let’s make a new string called “hello world” in the browser console, and add a ‘.’ to show us some autocomplete options:
We have quite a few interesting things to look at here. For example, we can see that the string has a length property, which is a number. We can also see that it has a charAt method, which is a function. And we can see that it has a split method, which is another function. See how a simple string has properties and methods? This is because the string is an object, and inherits from the Object class, which has all these properties and methods.
You may have noticed that the string has a ‘prototype’ __proto__
property too, the very subject of this article! This is an important property to understand, as it allows us to access the object’s prototype and modify it. We can also see that there are some properties starting with a double underscore (__
) that are not part of the standard JavaScript object. These are special in-built properties and have double underscores in order to avoid any potential conflicts with other properties that a user might create.
Without getting too deep into JavaScript internals, here’s a surprising fact: JavaScript is not a conventional object-oriented language. It’s a prototype-based object-oriented language, which is how this inheritance works. JavaScript doesn’t have objects that other languages like Java might use, with features like inheritance and classes – instead, these JavaScript objects rely on a special property called __proto__
(or prototype
), which points to that object’s ‘prototype’, allowing it to inherit properties and methods. All the ‘class
’ and ‘extends
’ type syntax that you might see in modern JavaScript is just a kind of wrapper around prototype-based inheritance, a ‘syntactic sugar’.
In practice, this creates a chain of inheritance, where the Object
class is often the base class, and the __proto__
property points to the next class in the chain. This is how JavaScript objects inherit properties and methods from other objects, and how your newly made object can have properties and methods that you didn’t define.
Let’s continue with my_object
, from above. It might have a and b as the properties, but it will also always have a __proto__
object, which points to the next object in the chain, granting access to all the properties and methods of the root Object
. If we had inherited from another object, the __proto__
property would point to that object instead, and that object would have its own __proto__
property pointing to the next object in the chain, and so on.
So, if we try to access a property that doesn’t exist on my_object
, JavaScript will next look to see if it is part of the next Object’s __proto__
property. To make things even clearer, let’s create a completely empty object, and then add the x property to the base Object class, and see what happens:
empty_object = {} Object.prototype.x = 'everyone can see me!' console.log(empty_object.x) // Output: everyone can see me!
An important thing to observe here is that this happens dynamically, so if we add a property to the Object
class after we’ve created the empty_object
, the empty_object
will still have access to this new property. This is because the __proto__
property is a reference to the current, living Object
class, which can be modified at any time.
Prototype Pollution
Adding properties directly to the Object
class is generally considered a bad idea, and it’s very rare to see this pattern in-the-wild. A much more common pattern, however, is to control properties of any object by modifying an object’s prototype. This is where things get spicy:
// Let's create a completely empty object. This shouldn't have any properties or methods. blank_object = {} // Next, let's create `my_object` again, with a few properties, as an example of a normal object. my_object = {a:1, b:2} // We define the 'z' property on the `__proto__` object of `my_object`: my_object["__proto__"]["z"] = "wait, what?"
What do you think will happen if we try to access the z
property of blank_object
?
console.log(my_object.z) // Output: wait, what? console.log(blank_object.z) // Output: wait, what? console.log(Object.z) // Output: wait, what?
That’s right – everything now has this property z
!
With the blank_object
, it tried to find the z
property, couldn’t find it, and then looked at the global __proto__
property, which was set by the my_object
object, adding the z
property. We can even reference ‘z’ on the Object class itself!
Clearly, being able to affect which properties and methods are available on an object is a powerful tool, and having control over this can be a very interesting primitive for an attacker. An attacker being able to control a __proto__
property is the core of the ‘prototype pollution’ vulnerability, because it allows them to affect the properties and methods of all other objects in unexpected ways.
So that’s the first primitive of Prototype Pollution, the source – being able to control the __proto__
property of an object, but now we need to identify gadgets in which this can be abused to affect other parts of the code to get ‘code execution’ – which in the browser is Cross-Site Scripting.
Prototype Pollution Sources
The three most common JavaScript patterns that lead to this are merging, cloning and value setting operations. Essentially, any time an object is dynamically built from a user’s input, there’s the potential that this could lead to the prototype being polluted.
Let’s look at some code to see what this looks like in practice.
Merging
Merging objects can be achieved in a variety of ways, but many approaches are unsafe. This is an example of an unsafe merge, simply setting the properties of the source object to the target object:
function merge(target, source) { for (var key in source) { target[key] = source[key]; } return target; } var target = {a: 1}; var source = {a: 3, d: 2}; // Combining the objects target and source var result = merge(target, source); console.log(result); // Output: { a: 3, d: 2 }
It’s not exactly commonplace to have the ability to control a full object, with the exception of when you’re parsing JSON! A common merge pattern is using JSON.parse to turn a JSON string into an object, and then merge the objects:
function merge(target, source) { var output = JSON.parse(target); for (var key in source) { output[key] = source[key]; } return output; }
It’s relatively commonplace to have control over a JSON body of some kind in a request, so if there’s any kind of JSON based input this is a great place to look for unsafe merges.
Cloning
Cloning uses the previously created merge function, but merges with an empty object. The results are the same!
function merge(target, source) { // The same merge function as above for (var key in source) { target[key] = source[key]; } return target; } function clone(obj) { return merge({}, obj); // Creates a copy by merging with an empty object } var jsonInput = JSON.parse('{"a": 1, "b": {"i": "2"}}'); var clonedObject = clone(jsonInput); // Cloning the malicious input console.log(clonedObject); // Output includes the whole object, with all nested components
Value Setting
Value setting is what it says on the tin – setting properties and values directly, modifying the object. If an attacker can control a property and value directly, they can set the property to __proto__
and this could find its way into the global Object’s prototype.
function setValue(object, property, value) { object[property] = value; // Simply sets the property value directly } var test = JSON.parse('{"a": 1, "b": 2}'); setValue(test, "a", 3); // Setting the property "a" of object test to 3 console.log(test); // Output: { a: 3, b: 2 }
Real World Example – jQuery Deparam
A common case for prototype pollution introduction is in key:value pairs being parsed into an object in JavaScript. jQuery-deparam is a library that specifically does this – looking at the code we can see that jQuery-deparam lets us assign arbitrary key:value pairs through the parameters in a URL, creating a prototype pollution vulnerability. This vulnerability is assigned as CVE-2021-20087 which, at the time of writing, is unfixed – presumably because that’s the point of the library:
if ( Object.prototype.toString.call( obj[key] ) === '[object Array]' ) { // val is already an array, so push on the next value. obj[key].push( val ); } else if ( {}.hasOwnProperty.call(obj, key) ) { // val isn't an array, but since a second value has been specified, // convert val into an array. obj[key] = [ obj[key], val ]; } else { obj[key] = val; }
Finding (Client Side) Prototype Pollution Sources Dynamically
If you don’t want to spend hours reading code, there are some dynamic approaches that can make finding prototype pollution sources easier. We’ll just focus on client-side prototype pollution here, because we can dynamically interact with the application’s JavaScript in the browser – for server-side prototype pollution, there has been some excellent work in recent years from PortSwigger.
The simplest approach, of course, is to use tooling that already exists, such as DOM Invader and PPScan, and hope that they will alert when a source is identified. Alternatively, you could identify known prototype-pollution vulnerabilities in a third party JavaScript libraries used by the application, and identify a gadget you can chain to exploit the issue.
Identifying prototype pollution sources dynamically, however, is much easier than reading code. The methodology is simply:
1 – Using a common XSS source, such as the URL parameters or hash, set a __proto__
payload, like the following:
https://example.com/?__proto__[polluted]=Polluted // or https://example.com/#__proto__[polluted]=Polluted // or https://example.com/?__proto__.polluted=Polluted
2 – Using your Browser’s Console, run the following code to see if the polluted
property is now available on all objects:
Object.prototype.polluted // Output should be "Polluted" if the prototype is polluted.
3 – Repeat with different sources!
One important thing to note here is that the []
and .
notations are not valid JavaScript here, but are simply defined by the developer. Usually the JavaScript .
and []
notations are mirrored by developers, as it’s a common syntax for hierarchical data, but when parsing a URL or any kind of user-supplied source these notations are not actually provided by the JavaScript langauge.
This means that when searching for Prototype Pollution, the pattern you should look for in any source is ‘nesting’ – as in, anything that has a complex enough structure to warrant some kind of recursive parsing. As a kind of ridiculous example to help make my point, any of these could be a source of Prototype Pollution if the developer is parsing the URL in this way:
https://example.com?firstParam=__prototype__&secondParam=polluted&thirdParam=Polluted https://example.com?param->__proto__->polluted=Polluted
Another excellent place to look is in any kind of JSON – encoded or otherwise. A JWT could be parsed client-side for example, without any kind of validation, and this could be a great place to try to find Prototype Pollution – here’s an example of what an exploit could look like:
{ "alg": "HS256", "typ": "JWT", "kid": "123", "__proto__": { "polluted": "Polluted" } }
Prototype Pollution Gadgets
Prototype Pollution gadgets are the other half of the vulnerability – the ‘Sink’. These aren’t vulnerabilities themselves and are a common artefact of the way JavaScript is written – but they can be used to exploit any pollution issues identified – being able to affect the global __proto__
property is not very useful unless you can use it to affect other parts of the code!
As a basic example, this is a very common pattern in JavaScript:
var myObject = myObject || {}
This code checks to see if the item on the left of the ||
exists, and if not, it will create it as a blank object {}
. It’s used to set default values for variables, or to ensure that a variable is defined before using it.
This pattern is everywhere in JavaScript, and it’s very useful if you want to take control of a variable that will get used later in the code, because if you use your prototype pollution source to set the leftmost variable to exist, you can now control the value of that variable!
Let’s look at a very slightly different but more realistic example:
newObject = already.initialized || example.init()
If already.initialized
is undefined, then example.init()
will be called. However, if already.initialized
is true
(or generally anything that isn’t False or undefined, because of JavaScript’s weak-typing), then already.initialized
will be the value of the variable.
This means that if we have prototype pollution, we can create the initialized
property on the global prototype, and set it to whatever we want – and now we can control the value of newObject
!
Exploitation Using Pollution Gadgets
Armed with the understanding of how this works, let’s look into completing our exploit with a gadget. The simplest way of finding gadgets is to look at pre-existing gadgets in third-party libraries used by an application – for this there is a great repository of prototype pollution gadgets, which can be found here: https://github.com/BlackFan/client-side-prototype-pollution.
But instead of just using pre-existing gadgets without understanding, let’s explore how we might find our own gadgets in the wild.
DOM XSS Sinks
Take this benign Hello World example with Vue.js:
<!DOCTYPE html> <html> <head> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> </head> <body> <div id="app"> <p>{{ message }}</p> </div> <script> var app = new Vue({ el: '#app', data: { message: 'Hello World!' } }) </script>
It may seem harmless, but with Prototype Pollution we can control undeclared configuration variables – if we look at the Vue (2.0) documentation, we can see that the options can contain the dangerous variable ‘template’ – and there’s no template variable declared in our above snippet!
This means that Vue.js will first look for a template variable in the Options, and if it can’t find this template variable then JavaScript will helpfully traverse up the prototype chain to find it… If we can add a template variable to the global Object with Prototype Pollution, then we can control the dangerous template variable and add any HTML without sanitization, getting XSS!
Client-side XSS Filters
Client-side XSS filters like DOMPurify are a useful target for Prototype Pollution. There are usually configurations which add ‘allowed elements’, ‘allowed attributes’, and so on. If this isn’t explicitly set in the configuration, it’ll end up using the global variables… but this is an example of what it might look like in the client-side filter. See where we might be able to use Prototype Pollution?:
globalConfig.ALLOWED_TAGS = ['a', 'img', 'b', 'i', 'u', 'em', 'strong', 'br'] ALLOWED_TAGS = userConfig.ALLOWED_TAGS || globalConfig.ALLOWED_TAGS
Which to make it a little clearer, is the same as:
globalConfig.ALLOWED_TAGS = ['a', 'img', 'b', 'i', 'u', 'em', 'strong', 'br'] if (userConfig.ALLOWED_TAGS) { ALLOWED_TAGS = userConfig.ALLOWED_TAGS } else { ALLOWED_TAGS = globalConfig.ALLOWED_TAGS }
The userConfig.ALLOWED_TAGS
is controllable with Prototype Pollution, and if we add the ALLOWED_TAGS
property to the global prototype with an array of tags we want to use, we can add our malicious tags to the filter!
JavaScript APIs
It turns out that even in-built JavaScript APIs have prototype pollution gadgets! We won’t go into detail here, but Gareth Heyes’s article on these gadgets is an excellent resource – https://portswigger.net/research/widespread-prototype-pollution-gadgets
The article shows a Prototype Pollution gadget in the fetch
API, where it is possible to control the credentials
property of the fetch
object, allowing for bypasses of the credentials
: ‘same-origin
‘ protection. Useful for credential exfil!
Dynamically Identifying Gadgets
Dynamic identification of gadgets is tricky – you’ll generally need to first do some kind of code analysis to identify what is possible in the context of the application. With client-side prototype pollution, the primary exploit path we’re looking for is XSS, so we should look for any DOM-XSS Sinks, or client-side XSS filters in use.
Let’s look into DOM XSS Sinks. In your browser console, go to the debug tab, and ctrl+shift+f to search across all included JavaScript files for common DOM XSS sinks, like eval
, .innerHTML
, document.write
, document.writeln
, document.writeIn
, document.open
, etc.
Here we’re looking for appendChild
, and can see that it’s used in a lot of scripts, including Google’s recaptcha.
If we open and minifiy the script, and look at the first appendChild
case being used, we can see that there’s a ||
operator used… A potential gadget?
We can add breakpoints by clicking on the line numbers, so we can see the names of various variables and classes dynamically – then refresh the page to see what’s happening. We can use the mouse to hover over any variables to see their values.
It seems like S.C
is being added to appendChild
, and contains the IFRAME
element. S
is being set to an object, and S.C
is being set to the function k[E[0]]
. k
looks like an array of functions, which I’m assuming is just a collection of near-identical functions that take different parameters, given that JavaScript doesn’t support overloading.
Given that the parameters we’re sending to k are relevant to building some HTML content and appending it to the DOM, I’d make the educated guess that S.C
is being set to a function that creates an IFRAME
element with various attributes.
We can actually view the function k we’ll be using by using the browser console whilst we’re at our breakpoint, and entering k[E[0]]
. We can then click the highlighted arrow to view the function being called, and add breakpoints here:
k
returns an iFrame DOM element, and S.C
is being set to this element. We can see that the S.C
iFrame object contains all the possible DOM attributes – maybe there’s a way of us controlling a DOM attribute, allowing us to get XSS?
We can look at ‘simulating’ Prototype Pollution with the following snippet in our console:
Object.prototype.polluted = 'Polluted'
We then refresh, and review the breakpoint to see what we might control. Important point – make sure to pause the script execution at the start of the script, and run the pollution simulator before any of the script executes.
If we view the S.C
object after simulating pollution, we can see that there’s a new parameter in the iFrame’s URL – Polluted
!
< iframe title="reCAPTCHA" width="256" height="60" role="presentation" name="a-qstnad1luaxf" frameborder="0" scrolling="no" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-top-navigation allow-modals allow-popups-to-escape-sandbox allow-storage-access-by-user-activation" src="https://www.google.com/recaptcha/api2/a[TRUNCATED]&polluted=polluted&cb=3f9nclcmi23n”> < / iframe >
Still, this isn’t quite an XSS sink, it’s just in the URL. But knowing that k
is performing some conditional functionality and potentially adding it to the DOM, we could try and add a common iFrame XSS vector srcdoc
to the S.C
object, and see if it gets added to the DOM:
Object.prototype.srcdoc = '<script>alert(1)</script>'
This leads to an error that suggests that the given data type isn’t quite right – what if we made it into an array instead?
Object.prototype.srcdoc = ['<script>alert(1)</script>']
The srcdoc attribute has now been added to the iFrame with our payload – which is we can see in the S.C
object.
If we let the script run, without breakpoints, we’re presented with a familiar alert box – we’ve successfully exploited the Prototype Pollution gadget in Google’s reCAPTCHA.
A Simpler Approach – Dynamic Instrumentation
If we don’t want to spend hours reading and debugging JavaScript, there are some other options that help with some dynamic analysis – notably, the script ‘pollute.js’ from Securitum takes a JavaScript file and identifies potential gadgets from the provided code.
There’s more detail on how to use this script for dynamic gadget discovery in this blog post by s1r1us and the original research from Securitum, which we won’t cover here.
Putting the Pollution and Gadget together
Now that we have a working gadget, let’s combine it with the jQuery Deparam prototype pollution we explored earlier. The example we’ll look at is a web application that adds the jQuery Deparam library and uses Google reCAPTCHA:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Form with reCAPTCHA</title> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script> <script src="https://www.google.com/recaptcha/api.js" async defer></script> <!-- Include jQuery Deparam --> <script src="https://cdn.jsdelivr.net/npm/jquery-deparam@1.0.0/jquery-deparam.js"></script> </head> <body> <h2>Sign Up</h2> <form id="signupForm" action="/submit-form" method="POST"> <div> <label for="email">Email:</label> <input type="email" id="email" name="email" required> </div> <div class="g-recaptcha" data-sitekey="REDACTED-SITE-KEY"></div> <br/> <input type="submit" value="Submit"> </form> <script> // Example usage of jQuery Deparam $(document).ready(function(){ var queryParams = $.deparam(window.location.search.substring(1)); console.log(queryParams); }); </script> </body> </html>
If we use a prototype pollution payload in the URL, jQuery Deparam will add it to the global object – and if we use the reCAPTCHA gadget, we can get XSS! The final payload looks something like this: https://example.com/?__proto__[srcdoc]=< script >alert(document.domain)< /script >
Conclusion
Prototype Pollution is a fascinating vulnerability, and one that is often overlooked in the world of web security due to its complexity. It’s a JavaScript-Only attack, but with JavaScript being the most common language on the web, and increasing in server-side popularity, it’s an important bug to understand.
Hopefully you can now go about testing and understanding Prototype Pollution, and use the techniques in this article to find this bug in the wild.
References
- https://portswigger.net/research/widespread-prototype-pollution-gadgets
- https://blog.ulisesgascon.com/prototype-pollution
- https://github.com/BlackFan/client-side-prototype-pollution
- https://www.securitum.com/prototype-pollution-and-bypassing-client-side-html-sanitizers.html
- https://mizu.re/post/intigriti-january-2024-xss-challenge
- https://arxiv.org/abs/2311.03919
- https://blog.s1r1us.ninja/research/PP
- https://blog.huli.tw/2021/09/29/en/prototype-pollution
The post Prototype Pollution: A Deep-Dive appeared first on NetSPI.