Intigriti XSS Challenge April 2022
Hello guys I am back again. So let’s start talking rn bc this writeup will be long.
Last updated
Hello guys I am back again. So let’s start talking rn bc this writeup will be long.
Last updated
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.
Hope you enjoy reading this writeup, hope you learn something, take a care and see you in a next one writeup.