Virendra Pawar is a member of the Synack Red Team.
I recently delved into a fascinating finding on one of Synack’s targets, what ended up being a client-side prototype pollution (CSPP). When I researched more about CSPPs, I found valuable resources scattered across the internet. In this blog, I’ll consolidate that information, share my understanding of CSPP vulnerabilities, and provide recommendations for prevention. If you’re short on time, a TL;DR summary is available at the bottom of the page.
What is Prototype Pollution?
Prototype pollution is a type of JavaScript vulnerability that arises when an attacker manipulates the prototype of an object. The term “prototype” in JavaScript refers to an internal property of objects which can be used to define properties that are shared across all instances of that object. In this article, we will only be referring to CSPP cases.
Here’s an example illustrating the concept of a JavaScript prototype without prototype manipulation:
```javascript
function Person(fname, age) {
this.firstName = fname;
this.age = age;
}
// instances
var charlie = new Person('Charlie', 22);
var james = new Person('James', 22);
// manipulating '__proto__'
charlie.__proto__.country = 'Canada';
console.log(charlie.country); // Outputs 'Canada'
console.log(james.country); // Outputs 'Canada'
// manipulating '__proto__.__proto__'
charlie.__proto__.__proto__.planet = 'Earth';
// variable containing integer
var foo = 1337;
console.log(foo.country); // Outputs 'undefined'
console.log(foo.planet); // Outputs 'Earth'
```
Here’s what that code looks like mapped out graphically:
The person
has two properties: firstName
and age
. The instances of person
are charlie
and james
. Here, it is important to note that person
belongs to the object.
Nearly all objects in JavaScript are instances of the global “Object.” You can read more about this distinction from the Mozilla Foundation here.
Now, let’s look at when JavaScript does have prototype manipulation:
This image shows where prototype manipulation comes into action. As seen in the code, charlie
instance was used to add an additional property named country
with value Canada
. This could also be executed using the alternative code mentioned in the image. The key point here is how JavaScript traverses the __proto__
chain while searching for a given property.
Finally, here’s a representation of Object prototype manipulation in action:
This image provides an idea of how manipulation can be done for all instances within the JavaScript runtime that are distant from the manipulation source. The key takeaway from this image is how to inject arbitrary properties in Object
so that all variables and instances contain the injected property. Additionally, it shows how charlie.planet
returns Earth
as depicted in the image.
Now, let’s turn our attention to how to find this vulnerability during testing.
What Are the Risks?
For attackers
By exploiting this vulnerability, an attacker could:
- Modify the application’s behavior: The attacker could manipulate the application’s logic, potentially bypassing security measures or introducing malicious behavior.
- Inject arbitrary properties: They could add new properties that can lead to a variety of attacks, including application crashes, data exposure and arbitrary JavaScript code execution upon finding exploitable gadgets.
For defenders
Understanding this vulnerability means that you can:
- Better secure your applications: By mitigating this vulnerability, you can make your application more robust and resilient against such attacks.
- Raise awareness: You can help raise awareness about this not-so-common but potentially dangerous vulnerability.
What Tools Are Required to Find CSPP?
There are plenty of browser extensions tools available on GitHub that integrate themselves into your testing. These tools attempt to pass the following prototype payloads in GET parameters and hash fragments.
```javascript
__proto__[foo]=bar
__proto__.foo=bar
constructor[prototype][foo]=bar
constructor.prototype.foo=bar
```
Next, the extension waits for the DOM to load and listens if the foo
property was injected into Object.__proto__
with the property bar
or not. This is a straightforward technique, but manually checking for this on all pages with all four types of payloads can be monotonous work. That’s where the best tool to use comes in—DOM Invader—which can be found in Burp Suite’s Chromium browser. Don’t worry, this is available in the community edition as well, so you can use it for free!
By default, “prototype pollution” is turned off, and even I keep it off unless the technology stack heavily relies on the DOM for its frontend. A classic example of prototype pollution involves taking advantage of JavaScript’s dynamic property assignment feature used by a frontend library. If an application indiscriminately merges properties from an incoming object into an existing one, an attacker could potentially manipulate the object’s prototype.
You’ve Found an Injection, But How Can You Exploit It?
Not all injections are exploitable and even those that are may not be so easy to exploit. Once a prototype injection is identified, the next step is to search for prototype pollution gadgets.
Prototype pollution gadgets refer to specific code patterns or combinations of properties and methods that, when manipulated, can lead to unexpected behavior or security vulnerabilities. These gadgets can vary in complexity and may require careful analysis and understanding of the target application’s codebase to identify potential exploit paths.
It’s important to note that the presence of a prototype injection does not automatically guarantee the presence of exploitable gadgets. Extensive testing, analysis and understanding of the underlying code are necessary to determine if a prototype pollution vulnerability can be exploited and the potential impact it may have on the application’s security.
Moving forward, let’s assume that your DOM Invader has detected a prototype pollution vulnerability for you.
To search for potential gadgets, simply click the “Scan for gadgets” button in DOM Invader. This functionality saves you time by automatically testing known keywords for vulnerable gadgets. DOM Invader will list all malicious property names that could potentially lead to a malicious sink.
In this specific case, the transport_ur
l property can be used to achieve DOM-based cross-site scripting (DOM-XSS). The assigned value is reaching the script.sr
sink. To exploit this, an attacker can host malicious JavaScript and use its hyperlink as the value for the vulnerable property. Alternatively, data:text/javascript,alert(1337)
can be employed as the property value to execute the alert(1337)
code.
My Experience
We often stumble upon vulnerabilities that challenge our understanding and push our limits. Over the span of a month, I dove into the depths of this vulnerability, testing it on an actual program. Here’s a timeline of my experience. Success is not just one day of work!
March 15, 2023 – Initial Identification
Late in the evening, I was hunting on a Synack target that relied heavily on DOM manipulation via JavaScript library for frontend. I had begun hunting on this target since the program’s launch, and I had already reported a couple of DOM-XSS vulnerabilities. Once I was certain that there were no more pages left to test, I shifted focus toward understanding the application flow to test the app’s logical function.
During my exploration, the URL shortener generator function piqued my interest. I tested the generated URLs in an incognito session, but nothing unusual caught my eye.
Having exhausted all obvious options, I decided to activate the “PostMessage Interception” and “Prototype Pollution” features from the DOM Invader options. Immediately, DOM Invader flashed a red count indicating a vulnerability. Upon checking the DOM Invader console, I discovered that the application was susceptible to prototype pollution!
To validate this, I visited the URL in a different browser, passing ?__proto__.abc=def
and checked Object.__proto__
property to verify the injection. To my surprise, the vulnerability was real!
At this point, I was feeling confident, thinking that the next steps would be straightforward based on what I had learned in the PortSwigger labs. However, this was not the case. The “Scan for gadgets” option from DOM Invader revealed five to six potential gadgets, but none that could lead to DOM-XSS. Doubting the tool, I tried using the “PPScan” extension, but I found that the tool could only identify prototype pollution vulnerabilities, not valid gadgets.
With no success from these tools, I took on the challenge of learning how to manually exploit this vulnerability by searching for gadgets on my own.
Balancing my full-time position, I could not devote consecutive days to exploit this vulnerability. I decided to take it slow, learning and experimenting at my own pace.
April 2, 2023 – Gadget Scanning
Over the last three weeks, I dedicated my spare time to understanding the nuances of Prototype Pollution’s Gadgets. Here are some of the resources that significantly assisted my learning.
- https://book.hacktricks.xyz/pentesting-web/deserialization/nodejs-proto-prototype-pollution/client-side-prototype-pollution
- https://github.com/BlackFan/client-side-prototype-pollution
- https://infosecwriteups.com/javascript-prototype-pollution-practice-of-finding-and-exploitation-f97284333b2#b7b7
- https://blog.s1r1us.ninja/research/PP
- https://www.youtube.com/watch?v=J3MIOIqvV8w
Equipped with knowledge about gadget scanning and a console for debugging function calls and setting breakpoints, I felt ready to tackle the task at hand. However, this led me into a new series of errors and unexpected behaviors of the application.
1. Interestingly, even though the application relied on JavaScript libraries for its frontend and the source code was the same across all pages, the prototype pollution vulnerability was not identified on a few authenticated pages such as the user profile page or the login pages.
2. Contradicting the first point, some authenticated pages did exhibit the prototype pollution vulnerability. However, using “Scan for gadgets” on these pages resulted in only 3-4 sinks, fewer compared to a previous scan.
3. I observed that the prototype pollution injection interrupted the execution of the frontend loading. Around 10-12 exceptions were logged in the console, each with a stack trace and an error like “Uncaught TypeError: x[n] is not a function.”
Despite my initial enthusiasm ebbing due to these issues, I persevered, intent on understanding what was going on. I retained the knowledge from points 1 and 2, even if the HTML source code was the same across many pages, the pages could behave differently based on their dynamic rendering properties set by the JavaScript library.
With that in mind, I began debugging, using the stack traces to understand the source of the exception mentioned in point 3. I surmised that my malicious input was being executed somewhere, causing these errors.
Hours of digging revealed that the target application’s JavaScript execution contained a loop that traversed all properties of an object, executing their values. Thus, passing ?__proto__.foo=bar
caused the system to expect the value of foo
i.e. bar
to be a function, and it attempted to execute function calls. I considered replacing bar
with alert
as it is a valid function but soon realized that the values that I can pollute are string literals, not references to actual functions.
As I tried to untangle one problem, my challenges were only multiplying, both in complexity and numbers.
April 19, 2023 – Achieving DOM-XSS
Two more weeks passed and I made concerted efforts to resolve and debug the errors with an aim to exploit the vulnerability in a real-world scenario, such as DOM-XSS, to report it to Synack. In the end, I revisited points 1 and 2 from the previous section and started scanning for the gadget vulnerability on each page. Given the dynamic responses of each endpoint, there was a chance of finding a vulnerable gadget.
Since this was a manual process, my first step was to compile a list of all unique and valid endpoints of the application. Next, I launched a gadget scan on all endpoints vulnerable to prototype pollution. Despite scanning all known endpoints, I found nothing of value.
Just when I was about to surrender, it struck me that there was one category I had not yet tested: error pages. After spending a month testing the application, I knew that the only error page displayed was a 404 error page for pages not found. I had not included this in my list of endpoints, nor did I have much hope for it. Nonetheless, I decided to check for the vulnerability on this page and found it vulnerable.
I scanned for gadgets on this page and discovered that it was vulnerable via the cspNonce
gadget.
The DOM Invader console indicated that the values assigned to the cspNonce
gadget were fed into the element.innerHTML
sink. Among security researchers this is known as a danger zone for DOM-XSS vulnerabilities. Although DOM Invader had an exploit button next to the sink detection, I opted to handle it manually, passing ?__proto__.cspNonce=<img src onerror=alert(document.domain)>
. To my surprise, it worked. I had achieved the goal I had been pursuing for a month!
Upon studying the cspNonce
gadget in greater detail, I found that the target application was using a vulnerable version of Adobe Dynamic Tag Management, which was susceptible to prototype pollution.
Another exploitable gadget was not identified by DOM Invader but was identified from a Github repository. However, while there was no proof-of-concept record for cspNonce
on this repository page, DOM Invader was able to detect it.
I wrote a vulnerability report and explained the basics of vulnerability and how I reached this point. Furthermore, I had reported following location as exploitable:
When it came to recommendations for fixes, which is an essential part of bug hunting, I provided multiple workarounds in my report. First, user inputs must be subject to validation and sanitization to prevent malicious code injection. Alternatively, I suggested using Object.freeze()
to prevent modification of critical objects or properties to prevent targeting any specific objects. I also advised the team to use the latest version of JavaScript libraries to prevent known vulnerabilities from being exploited.
Conclusion / TL;DR
Exploring prototype pollution was akin to a quest steeped in challenges yet filled with enlightenment. And indeed, the eventual successful exploitation proved that the journey, regardless of the trials faced, was well worth the effort.
It was a key learning experience that even with identical HTML source codes across all pages, pages could behave differently due to their dynamic rendering properties set by the JavaScript library. This nuance impacted my approach and methodology.
The discovery of the cspNonce
gadget in the overlooked error page, which led to the element.innerHTML
sink reinforced the importance of thorough testing and persistence.
I would like to express my sincere gratitude to all the authors whose articles and shared knowledge aided me in reaching this point. References to their work have been included in this report with respect to their contributions.
Ultimately, this exploration underscored the value of persistence, continuous learning, and a comprehensive approach in successful vulnerability hunting. It was a testament that no effort is ever wasted in this field. Happy hunting!