Page cover

Intigriti XSS Challenge April 2022

Hello guys I am back again. So let’s start talking rn bc this writeup will be long.

So this is challenge description. Let’s access to the given URL.

So we got a Windows XP style UI generator. Nostalgia hits. So let’s enter some values and see what happens.

So we submitted input and got this result. But let’s take a look at the query parameters.

Oof there’s a bunch of them. Let’s take a look at the source code.

In the first part of the body tag we can see that the page is made by using Mithril. If we take a look at the official documentation for Mithril we can see that it’s a JavaScript framework for building single page app’s. Now let’s take a look at the script, this part will be interesting.

This is long code tho, so let’s start from the top and see what is interesting.

We can see the usage of the function m, and let’s take a look at the documentation and this is the way to create DOM elements using Mithril. If we check all components we will see that all DOM elements are created using constants. So we are not able to inject any elements. After the components we have the function main.

Hmm interesting. We have the call to parseQueryString which will parse the query string and turn it into an object. If we use the data from the query string we got when sending some values in the initial field, we get the following object from parseQueryString.

{

“config”: {

“window-name”: “Test name”,

“window-content”: “Test content”,

“window-toolbar”: [“min”, “max”, “close”],

“window-statusbar”: true

}

}

After the query string is parsed, a dictionary object named appConfig is created an initialized with some default values. Then a check to see if the config prop exists on the parsed query string object, if it exists a call to the function merge is made with the appConfig and the supplied query string object. Then another dictionary object is created named devSettings, which contains a main element and some other settings. Then a call to checkHost is made and if it returns true a call to the merge function with the devSettings and the settings object from the query string is made.After this there’s a check to see if isTestHostOrPort or isDebug on the devSettings object is true, if one of them are true the appConfig and devSettings objects are printed to the JavaScript konsole. If the customMode on the appConfig is false, the default App component is rendered using the main element from devSettings. Let’s take a look at the merge function.

This function will iterate over each key on an object and assigning each value to the target object. For each key a check is made using the isPrimitive function.

It will check if the type of the value is a primitive type and if it is a primitive type. The result of the call to sanitize using the new value is assigned to the target object. If the current key is a complex type, a recursive call to the merge function is made to make a deep copy of the data structure. Let’s take a look at the sanitize function.

This function replace the characters <, >, %, &, $, space, \ and the word script with underscores if the data type is string, hence the modified output when we submitted the form earlier. Let’s take a look at the checkHost function.

This function takes the location.host variable and checks if the hostname part is localhost or the port part is 8080 and return true if either is a match. Now let’s bypass checkhost. Let’s take a look again at the checkHost function. So, we need to verify the hostname and port, the location.host variable is split by the port-separator, :. But since the location.host variable won’t have any port-separator on the challenge page, we will only get the hostname in the temp array.

This explains that the port constant will be set to 443. Btw It’s not possible to modify the location.host variable since this will redirect us from the challenge page, so we need to find a way to get the port constant to be 8080. Let’s start by looking at the merge function again, since it’s the only place our input is being used at the moment.

We know that this function will assign each value from the parsed query string to the target object, which in this case is appConfig. This means that we should be able to assign our own keys and values to the appConfig object. To verify this we can enter the following query string, ?config[test]=test, set a breakpoint after the merge call using the debugger and finally checking the values of the appConfig object. So let’s set the breakpoint.

Let’s reload the page and use console to examine the object.

We can see in the output we have the key test with the value test. This means that we might be able to use prototype pollution to be able to bypass the checkHost check. But since the key __proto__ is in the protectedKeys array in the merge function, I got idea to do this so, the __proto__ key isn’t the only way to achieve prototype pollution, we could also use the constructor.prototype property to achieve this. And since none of those keys are in the protectedKeys array we can use those keys instead. If we enter the following query string ?config[constructor][prototype][test]=test to set the test property of the object prototype and checking the appConfig object we get the following.

But if we try to verify the prototype pollution it will show us that it failed.\

This is bc the creation of the appConfig object is done by Object.create(null) which will create a constructor-less object. So we have to find another way to abuse this. If we take a look at the appConfig object again we can see that the window-toolbar property is an array. Let’s see if we can pollute the array prototype instead. If we send the following query string ?config[window-toolbar][constructor][prototype][test]=test to set the test property on the Array prototype. We have the property test in the Array prototype. And when we verify that the prototype pollution works we get the following.

We have successfully polluted the Array prototype. Now we need to find way to use this to bypass checkhost. So const temp = location.host.split(':') will result in an array with only one element and the const port = Number(temp[1]) || 443 will try to get the non existent second element of the array, we can try to set the property 1 on the Array prototype to the value 8080. To verify that this should work we can try this out in the console.

To verify that it works, we create another array and try to access the second element.

If we send the query string ?config[window-toolbar][constructor][prototype][1]=8080 we will get the following output in the console.

Let’s take a look at the checkHost check in the main function again.

So the output in the console indicates that we have successfully bypassed the check.

Now let’s inject the payload.

So, we are able to modify both the appConfig and devSettings object. Let’s try to modify the innerHTML property with our payload. To set the values on devSettings the object qs.settings is used, which means that our query string parameter should look like this settings[root][innerHTML]. So let’s try it out with the payload <img src=x onerror=alert(document.domain) /> and see what happens.Using the query string ?config[window-toolbar][constructor][prototype][1]=8080&settings[root][innerHTML]=<img%20src%3Dx%20onerror%3Dalert(document.domain)%20%2F> nothing happens lol. But if we set a breakpoint right after the merge(devSettings, qs.settings) call and taking a look at the innerHTML property we will see _img_src=x_onerror=alert(document.domain)_/_. Now we have left to do is to bypass the sanitize function.

Now bypassing sanitaze function comes. Let’s take a look at the sanitaze function again. So the only data type that will be sanitized is string. But how to bypass it? Let’s take a look at the merge function again. So if the target[key] value is a primitive, sanitize with the source[key] value is called. So this means that if a property on the existing object is a primitive we could assign another type to that property and thus bypassing the sanitization. Array would probably do the trick, since the toString function will basically call array.join(',') and return the result.

But how can we change the type to an array? We can find a section on Deep Data Structures.

“Querystrings that contain bracket notation are correctly parsed into deep data structures”

m.parseQueryString("a[0]=hello&a[1]=world")

// data is {a: ["hello", "world"]}

If we add [] to the end of the settings[root][innerHTML] parameter, we should get an array. Using the query string ?config[window-toolbar][constructor][prototype][1]=8080&settings[root][innerHTML][]=<img%20src%3Dx%20onerror%3Dalert(document.domain)%20%2F> we will get our XSS working.

So we can confirm XSS. This challenge was pretty hard ngl.

README.md

Hope you enjoy reading this writeup, hope you learn something, take a care and see you in a next one writeup.

Last updated