Detecting Server-Side Prototype Pollution
Prototype pollution bugs have been a feature in many CTFs in recent years, and real-world examples in open-source applications have led to impactful exploits such as remote code execution and denial-of-service. The discovery of these bugs has long relied on access to source code, with no safe black-box detection techniques being widely used.
Both myself and PortSwigger’s Gareth Heyes have independently spent time looking into developing safe black-box detection techniques to open up prototype pollution as a more commonly identified bug class. We’ve ended up taking different approaches to the problem, so if you haven’t already read Gareth's post I would highly recommend it.
Prototype pollution is a vulnerability found in JavaScript applications which allows attackers to effectively add accessible properties to all objects, which can often lead to remote code execution or a denial-of-service. Olivier Arteau's NorthSec talk and accompanying paper provide a great introduction to prototype pollution for those not already familiar with it. This post doesn't require a deep understanding of prototype pollution - if you can understand what's going on in the following code block then you understand enough to keep reading:
Detection
Gunship CTF Challenge
The detection technique relies on a common coding pattern found in many applications. This coding pattern can be seen in the "Gunship" CTF challenge from the Hack the Box 2020 University CTF:
The "/api/submit" endpoint uses the "unflatten" function to convert the JSON in the request body to the "artist" object, then accesses the "name" property of this object. The "unflatten" function is imported from an old version of the "flat" library, which is vulnerable to prototype pollution.
The first step of the detection is to find a required parameter. For our purposes, we define this as a parameter which changes the application’s response when it is omitted. We can see a sample request and response using the expected input:
By removing the "name" parameter we get a different response:
This error occurs because the application attempts to access the non-existent "name" property of the "artist" object. If we can now trigger prototype pollution to add the "name" property to every object with a value of "test", we should no longer see the error from the above request.
The way to do this is to cycle through many payloads which are likely to trigger prototype pollution in the application to add the "name" parameter to the prototype. After sending each of these payloads, we can resend the request with the missing "name" parameter to see if we have been successful and the response has changed. In this case, the following payload will trigger the vulnerability:
After we have sent this request, we get the following response from our request with the missing "name" parameter:
This response is different from when we previously omitted the name parameter, and matches the one we saw when we provided a value of "test" for the name parameter. This indicates that we have managed to successfully trigger prototype pollution.
Kibana
This technique is not just limited to simple CTF challenges. For example, it can also be used to detect Michał Bentkowski's CVE-2019-7609 in Kibana. Browsing an instance of Kibana 6.5.4 we can generate the following base request and response:
If we remove the "interval" parameter from this request we can see that we trigger a different response containing an error:
This indicates that the "interval" parameter is required. Now, we send the payload described by Michał Bentkowski to trigger prototype pollution to add the "interval" parameter to the JavaScript prototype with a value of "auto":
Now, we resend our previous request with the missing "interval" parameter and note that we get a different response:
This response matches the one received from our initial request containing the "interval" parameter, suggesting that we have successfully exploited prototype pollution to add this parameter to the prototype.
Limitations
While this technique can work, it is not without its limitations. The most obvious is that it relies on developers converting user input to a JavaScript object. In my experience this is common, but it's by no means universal.
This technique can also be dangerous when used against the wrong application. The following request exploiting Olivier Arteau's bug in GhostCMS to add a meaningless parameter to the prototype initially seems like it may be harmless:
However, after sending this request, all requests to the homepage of the application are met with an error. While causing a complete denial-of-service with a single request certainly shows impact, it should be avoided on live applications.
I have demonstrated this technique against single instances of applications, but if an application is doing any form of load balancing which routes your requests to multiple application servers then detection becomes slightly more complex. You may have polluted the prototype on one of these application servers, but your next request may be sent to a different application server which doesn't have a polluted prototype. This problem can be solved by sending many requests with the missing parameter to ensure that one of them is routed to the same application server as the request containing the payload.
Comparison to Gareth’s Techniques
At this point you may be wondering which techniques to use to detect prototype pollution vulnerabilities. The answer is to almost always use Gareth's techniques. Gareth's techniques don't rely on developers using specific coding patterns, so are going to detect prototype pollution vulnerabilities in a wider range of applications. I would also consider his techniques safer since you will end up attempting to add only a specific set of properties to the prototype.
There may be some edge cases where the technique described in this post comes in useful. I hope that it may also be useful to someone else in further researching prototype pollution. If you have any questions or find anything interesting, please feel free to message me at @_danielthatcher on Twitter, though please bear in mind that I'm checking Twitter quite infrequently these days so please don't be offended if I take a little while to respond.