For the complete documentation index, see llms.txt. This page is also available as Markdown.

Challenge 0326 Write-up

A detailed write-up for Intigriti's March Challenge involving XSS via DOM Clobbering and CSP Bypass.

I managed to solve this month's Intigriti Challenge. It was a fantastic opportunity for me to level-up my XSS skills. I will be elaborating on my approach, the things I learnt and some rabbit holes that I got caught up in. Let's dive in!


The challenge page is a Search Portal where we can enter queries. If some query returns suspicious data, we can report the resulting URL to the Admin.

Since this is an XSS Challenge, the first thing I tried was entering some input with HTML in it to see if it would render.

It did, but playing around with typical XSS payloads like <img> onerror and <script> resulted in the malicious parts of the payload getting filtered out.

Moving on with examining the source code, I noticed that DOMPurify was being used and there were 2 custom JS files - components.js and main.js. There was also a comment hinting at the CSP implementation.

I checked the CSP first.

Allowing unsafe-inline in style-src seemed like something that might be of use.

My burp also highlighted that DOMPurify verison 3.0.6 was in use, which is known to be vulnerable to attacks like mXSS and Prototype Pollution.

Time to check the JavaScript files!

main.js

main.js contained the configuration of DOMPurify. It specified that beyond basic filtering, the attributes id, class and style will also be filtered from the search parameter but their child tags and text content will be retained. This will make it difficult to exploit any kind of injection possible through the style-src CSP we saw earlier.

It also checks if a ComponentManager object exists in the global window object and calls the init() method on it if it does. We shall find out what this is in the next JS file.

component.js

In component.js lies the real juicy stuff. We can find the definition of the ComponentManager class which looks for elements having the data-component attribute set to true. For each such element, it collects the value of the data-config attribute to construct a URL which becomes the source of a script tag which it adds to the DOM. So, this class is dynamically adding a script to the DOM, but since the CSP forbids scripts from sources other than the current domain, we cannot load external scripts with it.

There's more worth noting from this script.

component.js

There's a loginRedirect function which is declared as a part of a global Auth object. It basically redirects the user to a URL defined in another global object - authConfig. In this object, the append key decides if the redirect URL should contain a 'token' and the next key contains the base URL to which the redirection must occur. It is also worth noting that the dataset property which is being used to obtain these keys is a built-in property of HTML tags. It is used to store metadata about that tag. It is declared using data- attributes.

All in all, this function can allow us to receive an authenticated user's token on an endpoint of our choice if we can control the authConfig object.

Also, notice how both the global variables have been declared in conjunction with the OR operator. This introduces a DOM Clobbering vulnerability.

DOM Clobbering is a technique where HTML elements with specific id or name attributes can overwrite global JavaScript variables or DOM properties. This happens because browsers automatically create global references to elements with IDs, which can conflict with expected JavaScript objects.

Since we can inject HTML into the page, we could define an HTML tag with an id or name with the value authConfig. This will create a global variable window.authConfig and then we can add the required properties to it by adding data- attributes to the same tag.

To summarise, we have the following action points:

  1. A dynamic script injector but for the same domain only

  2. A gadget - Auth.loginRedirect function

  3. Clobbering the authConfig object

Initially, I considered the outdated DOMPurify as a possible vector for XSS and spent quite some time playing with mXSS payloads. However, I realised later that it was an unnecessary step because I was able to clobber the authConfig object with a simple <form> tag containing the necessary attributes. This was because the DOMPurify config didn't forbid the name or data- attributes. So, simply using the following it was possible to define the authConfig object with properties we want:

On trying that payload, it can be seen that the Component Manager tries to load a script based on our test inputs. This means that authConfig was clobbered successfully, as shown.

One item checked off the list, two to go. We had to find a way to call the Auth.loginRedirect function. Intigriti released a hint to help with this. It said, "To bypass the inner walls, find the hidden api endpoint that lets you choose the callback."

This means that there must be an API that will take a callback function as an argument. This will satisfy the script-src CSP. I went ahead and fuzzed for API endpoints.

/api/stats exists. On visiting it we can see that it is "padding" its response into the function we provide as a callback:

What we have here is a JSONP mechanism. It has been likely used as a lightweight way to allow the main application to pull data from this API and immediately "pipe" it into the existing logic without complex cross-origin configurations.

JSONP (JSON with Padding) is a legacy technique for bypassing Same-Origin Policy to load cross-origin data by wrapping JSON in a callback function. While it enabled cross-domain data sharing before CORS, it introduces security risks and is largely deprecated.

We can use this for our purpose by providing the Auth.loginRedirect function as the callback. The payload now becomes:

On trying this query, I am successfully redirected:

To solve the challenge, we need to report the URL that results from entering the above query to the admin:

Flag:

Let's summarise the exploit chain:

  1. Inadequate DOMPurify Filtering: The application uses DOMPurify to sanitise user input in the q parameter. Although the configuration explicitly forbids id, class, and style, it permits the name and data- attributes.

  2. DOM Clobbering: The component.js script relies on a global variable window.authConfig to determine redirection logic. Because the name attribute is permitted by the sanitiser, an attacker can inject an HTML element like <form> with name="authConfig". This "clobbers" the global variable, allowing the attacker to control the dataset properties (next and append).

  3. CSP Bypass via Gadget (JSONP): The Content Security Policy (CSP) restricts script-src to 'self'. However, the ComponentManager class dynamically creates script tags based on attributes found in the DOM (data-component="true" and data-config). By combining this with a discovered JSONP endpoint (/api/stats), an attacker can force the application to load a trusted endpoint from its own domain that executes a chosen callback. In this case, the Auth.loginRedirect function.

Last updated