Intigriti 0422 - XSS Challenge Writeup
Tags: security, hacking, javascript, bugbounty
Published: 2022-04-24 23:16:47

Hi everyone, hope you are having an amazing day! :hugs:

Today, I'm going to be showing how I solved Intigriti's 0422 XSS Challenge. It is really nostalgic, because reminds me of the initial contacts that I had, as a child, with computers.

I hope my thought process does make sense to you guys, but in case of any "unclear points", feel free to DM me! :raising_hand_man:

## :national_park: Getting to Know The Challenge

This is what we find when we access the challenge's page for the first time:

![Windows XP's interface](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uxz3cptojb0cwgqygvca.png)

It's a really good clone of Windows XP's interface, I love it! The form just picks up our configuration and uses everything in order to make an amazing "Windows XP like" window, like the following one:

![Window in which content we have "I love Intigriti"](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/apkiw84h70ytkkbf0r3b.png)

For some reason, it replaced all the space characters with underscores. If we try to add HTML tags, the same happens with the `<` and `>` characters. Curious, very curious! :monocle_face:
 
![Harry potter reference to Olivanders](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rqcac0fa2bw9nn9tb8lr.gif)

After the window is generated, when we look at the challenge URL, there is a huge query string like this one:

`config%5Bwindow-name%5D=I%20love%20Intigriti&config%5Bwindow-content%5D=I%20love%20Intigriti&config%5Bwindow-toolbar%5D%5B0%5D=min&config%5Bwindow-toolbar%5D%5B1%5D=max&config%5Bwindow-toolbar%5D%5B2%5D=close&config%5Bwindow-statusbar%5D=true`

or represented as JSON, for easier understanding:

![config object represented in JSON](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rg7r8t4fl58kw3wv12mk.png)
 

So this config object is being used to build the final window, and while searching for the keyword `config` in the page code, this can be found in the `main()` function:

![Query string object](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9ce55wgrj47vnfbhyb58.png)

Apparently, `qs` is an object that represents everything we passed to the URL query string, and when `qs` has an attribute called `config`, it is merged to another object called `appConfig` with the `merge()` function. Cool, but it seems like really soon we are going to have tons of information to deal with, and when we try to see what's inside `qs` with the browser DevTools console...

![qs is not defined](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y175zgqysnz3up8bcyyf.png)

I would really like to be able to see the variables' values in each part of the code, so I'm gonna change it a little bit!
 
## :see_no_evil: Trying to Help Myself (skippable)

So first of all, as the loaded page is just an HTML file, we may just download it with wget or copy/paste its' source inside of a local .html file.

We could (and will, in the final steps) just use the browser DevTools for debugging, which is actually easier in more general terms, but I like the idea of having things running locally and adapting the code so I don't need to set breakpoints everywhere. If you don't care about this process, you can just skip whatever sections which have `(skippable)` included in their titles.

Once we download the file, and load it into the browser, everything just works as expe...

![a bunch of FILE NOT FOUND errors](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/65il8mimvlr90trzhyto.png)

### :persevere: Solving Boring Setup Issues (skippable)

As we're not in `challenge-0422.intigriti.io` anymore, these files which are being referenced with relative paths won't really be found. So we have to change their relative paths to absolute ones, or maybe use the `unpkg.com` URLs that are in comments like this one down below:

![URL poiting to unpkg.com](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p3xrgnh346fvbk4n9e9v.png)
 
Or my favorite approach, which is adding a `<base>` tag pointing to `challenge-0422.intigriti.io` again! :hugs:

![Replacing base tag](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g9yqo2k1vgfv51v6l2ot.png)
 
Now everything is gonna work perfe...

![CSP errors](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ocnl85psxcgna3156iqj.png)
 
Now it's time for the CSP to bother us :cursing_face:. It happens because although the base tag references intigriti.io, we are not really there, but actually just loading a file locally.

We could also change its' definition in order to make `challenge-0422.intigriti.io` allowed, but since our goal is to just be able to check variables values, the CSP is not that important for now, and we can just delete or comment its' line in the file:

![Commented CSP tag](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lye7kolweoyfw2go3j7h.png)
 
After all this work, everything will be just normally loaded again! :partying_face:

### :monocle_face: Adapting The Code (skippable)

So, the first I would like to see is the `qs` variable, and it's inside the `main()` function. In general terms, variables that are declared inside of functions cannot be used out of them. In case you would like to know more about variables behaviour in JS, Tania Rascia has this [amazing article](https://www.digitalocean.com/community/tutorials/understanding-variables-scope-hoisting-in-javascript).

The `main()` function is declared without expecting any parameters, right above the `qs` declaration:

![main function declaration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bghwp5tslldxvfd0sjpa.png)

And it's called right in the end of the `<script>` tag:

![main function being called](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qd3hzbzc5518v2drywl7.png)
 
So we may just remove both declaration and calling, and then we'll be able to see the value of everything inside `main()`, right? Let's check :partying_face:! And...

![Not yet!](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zq82q3rm5qtg7fr3259j.png)
 
If we look at the beginning of the script, there's something else we forgot about:

![All the code is inside a function expression](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lgqfjrcg3zse21if9gke.png)

All the challenge's code is written inside of a function expression! Although it is not the same thing as the function declaration which we already erased (more about it [here](https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Operators/function)), the same rule about variables are applied here, in a way that after the code is executed, we cannot just check how the variables are in DevTools console.

So we just have to erase/comment the lines where it begins and ends, and after that, when we go back to DevTools console...:partying_face:

![Now it works and we can read the qs variable](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bs9efvm0yxmtcu4xqx05.png)
 
## :ghost: Let The Games Begin! :space_invader:

Now that any debugging checks will be easier, we can really dive into the code!

It's possible to see a bunch of `m()` being called in the entire code, and there's also a file named `mithril.js` being loaded before the challenge script:

![Mithril JS being used](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t6hhq5b1emashpiz6oc4.png)
 
Based on these facts, I might assume that the whole challenge has an issue related to Mithril, which is a JavaScript framework meant for building SPAs. If we quickly make a Google search like `mithril.js cve`, we are going to find something interesting!

![Prototype Pollution in Mithril JS](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/be3z1cpfd9fw8cyycgx5.png)
 
So Mithril may have a prototype pollution issue! When we click on it and see the [content of the page](https://security.snyk.io/vuln/SNYK-JAVA-ORGWEBJARSNPM-2413673), it shows a sample code snippet of a supposed `merge()` function that may be vulnerable to this issue:

![Vulnerable code example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vhxo3u5kwqon975fsjj8.png)

Basically, it is vulnerable because it applies no filtering to whatever is coming as parameters, in a way that the user input may have a prototype definition included! 	:exploding_head:

I have a blog about the importance of filtering user input, which can be found [here](https://dev.to/therealbrenu/api82019-injection-31c3), but remember that this `merge()` function can be found in the code, because we already have seen it being called before, so let's see how it was defined!

![The merge function is defined in a similar way, but a little bit more hardened](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lx6ajttq0vx1g6fwe483.png)

In the case of this month's challenge, the merge function is being defined in a similar way, but it has a blacklist filtering based on this array:
 
![Array of protected keys](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ykgjq686bouolqincxhn.png)

So whenever one of these protected keys are present, they will not be added to the final merged object. `__proto__` is there, probably as a method of avoiding prototype pollution, but `__proto__` is not the only way to get to a data type's prototype. We can also refer to `constructor.protoype`, which keywords are not in the protected keys array :monocle_face:
 
If we try to apply it, using a payload like `config[constructor][prototype][test]=polluted` in the URL, check what happens to `appConfig` after it's being merged with `qs`:

![there is no prototype](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cnfmeyqculq0ascrtud1.png)
 
The console shows no real prototype! :cursing_face:

Why does this happen? Well, if we go back to appConfig's declaration, here's what we're going to find:

![appConfig is created with no prototype](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w380z7rsfhox8j3uq6ln.png)
 
Because `appConfig` was declared with `Object.create(null)`, it has no prototype, and it sort of breaks the prototype chain (see [Object.create()](https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Global_Objects/Object/create)). We cannot mess with the prototype of `appConfig`, but it has an interesting attribute called `window-toolbar` which is an array and was simply defined as `["close"]`.

Some may think that arrays can only have numeric indexes, but in JS that's not the case. We can handle arrays in a way that's very similar to generic objects, since arrays are implemented as objects too, they're just of a more specific type. See this example:

![Array being treated as an object](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oqn8p7xfrvmuey6hx2ef.png)

So what if we try to change the prototype of `appConfig["window-toolbar"]`? With a payload like `config[window-toolbar][constructor][prototype][test]=polluted`, this is what we will find:

![The prototype was polluted!](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ftul2z6d0j0mzr3measv.png)

It did work! :partying_face:

## :checkered_flag: From Proto Pollution to XSS

Okay, we could perform an array prototype pollution. Now what? It's still not an XSS! :clown_face:

If we look for where the content of the page is generated...it's in this piece of code:

![mount function](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mxel0sb6ghb0olbj0tff.png)
 
Regardless of whether `appConfig["customMode"]` is true or not, those beautiful windows will always be mounted inside this element called `devSettings.root`, which is declared in the code section down below:

![root element being declared](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wqd4iqywnc8oanxfc5tk.png)
 
Can we somehow edit `devSettings`? Yes! Oops, No! I don't know, just look:

![dev settings can be merge if being accessed from localhost](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i9jjy1exxx8bwuzfjbg5.png)
 
So `qs.settings` can be merged to `devSettings` if `checkHost()` returns true. Just by the fact that it involves a variable called "dev settings", we might assume that checkHost only returns true in cases of local accesses, but let's check it!

![check host declaration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8o8v6curr49enrru0p1i.png)
 
So yes, it picks up location.host and checks if the access is coming from `localhost` or the port is equal to `8080`. Since I was loading just a file in the browser, location.host will be `undefined`, so I think it's time to go back to `challenge-0422.intigriti.io`. :sob:

Once back in the original challenge page, we can still debug stuff by going to the DevTools `Sources` tab and defining a breakpoint inside of `checkHost()`, just by clicking on the desidered line number, the result would look like this:

![Breakpoint defined before the function returns](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sfystw64ra6dw41pwd7z.png)
 
Now reloading the page and checking the variables inside of the function, this is what we are going to get:

![variable values](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5g23v6dc6q2q9gakkf0i.png)
 
The `temp` array has just one index, since `challenge-0422.intigriti.io` has no explicit port definition, and because of that, the value of `port` becomes 443.

We cannot mess up directly with `location.host`, because it would redirect the page. But hey, we just discovered a way of polluting array prototypes! Let's just use it! :cowboy_hat_face:

`temp` doesn't have a [1] index, so we can define it by ourselves! We just need to pick up the previous test payload and replace the test attribute with 1, and also setting 8080 as its' value instead of "polluted", resulting in:

`config[window-toolbar][constructor][prototype][1]=8080`

By using it and setting the same checkpoint that we've set before, these are the values the we will get now:

![The port is now 8080](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u9h36kcxczt2i7vgpzj5.png)
 
This means that `checkHost()` will return...:drum:

![true](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l67mo26kre5xueyhqlf6.png)
 
Yay! So now we can use `qs.settings` as a way of changing `devSettings`!

Now, before trying to overwrite something inside `devSettings.root`, let's see what it has for us by setting a new breakpoint:

![breakpoint at line 216](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pj5o35dfr7r1zuleb20j.png)
  
Hey, look at this cool attribute that we could just use as a way to get XSS!

![inner HTML attribute](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wortsbikaocwz8mor2ik.png)
 
So we just have to add `settings[root][innerHTML]=<h1>testing...</h1>` to the previous payload and...

![it didn't work](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ny0ce57ub1njdmyrabn2.png)
 
It didn't work! Why!? Let's just set a new a breakpoint right after the merge is done:

![breakpoint at line 221](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/niy9rqjpg6f7yjadr8ya.png)

Look! It's here (sanitized, but here)! Why doesn't it work in the end?

![innerHTML with sanitized h1 tag](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e70taq0uqm6woogmuod0.png)
 
Do you guys remember that Mithril receives `devSettings.root` as an argument for building the page's content? Yeah, in the end, when `m.mount()` is called, it overwrites what we defined for `devSettings.root.innerHTML`, so I could not use it as a direct way of injecting content to the page.

But there's still hope! Look at how `devSettings.root` is added to the page:

![the root is appended to the body](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q5h5hhm018kozdrh9jhg.png)
 
It's appended to the page's body, no overwrites involved. So we can use the `devSettings.root` element as a way to get to the page body. There is more than one way of doing it, but since `body` is an attribute of the page's `document`, we may get there by using the devSettings.root `ownerDocument` attribute, which is a reference to the page's `document`, and then we will get to the `body`.

An example of working payload for that would be to add `settings[root][ownerDocument][body][innerHTML]=<h1>testing...</h1>` to that working prototype pollution, which would result in this:

![h1 tag added to the page, but still sanitzed](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pozsai3u8f22j6xibugy.png)
 
It was added to the page, which is progress in a sense, but it is still being sanitized, because `merge()` calls a `sanitize()` function in some situations. Let's take a look again:

![isPrimitive and sanitize are two important functions](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r9mdk9nsd92fwzir2vh4.png)

Assuming the case of we calling `merge()` for `devSettings`, what will happen is that the code will only add something from `qs.settings` to `devSettings` as the result of `sanitize()`, so there is no way of just "ignoring" it. Let's look at how `sanitize()` works too:

![Function with regex, but only apply it to strings](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5rfwo9yknqgie9m0nje9.png)

It has a pretty strong regex defined there, in order to avoid XSS cases, but it doesn't always apply this replacing method, only for strings.

This approach does make sense, since numbers and boolean values cannot carry an XSS payload inside them. But are these the only possible cases? Let's just check `ìsPrimitive()`:

![function that checks if value is primitive](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eld0q1x22yfv0xzfiwjd.png)
 
Everything seems to be fine here, because it really just checks for primitives, but let's go back to the `merge()` function, and try to look at it with different eyes:

![merge function with adaptations](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ecss84fzebyh7faup9ge.png)

I changed the name of the variables so they relate more to what we are trying to do, and also ignored the `protectedKeys` stuff because it's not a real problem to us anymore.

Pay attention to how `isPrimitive()` is being called. It receives `devSettings[key]` as a parameter, but if there is no attribute with this key being previously declared, then `isPrimitive()` will return true, because `devSettings[key]` will be undefined!

In other words, if our input contains something that still doesn't exist, it will also be considered as a primitive. :monocle_face:

Taking into account that `sanitize()` skips the replacing method when the argument is not a string, we just have to find a different data type/strucutre that can also be printed by the browser in the page. Objects, maybe?

![didn't work](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4tg0ub52saw8eucikjq6.png)

Nah, not what I was expecting :joy:

What about an array? We would just need to add [0] to the previous body's innerHTML payload, resulting in something like `config[window-toolbar][constructor][prototype][1]=8080&settings[root][ownerDocument][body][innerHTML][0]=<h1>testing...</h1>`:

![it did work](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/57x0gltddnsz69q5bt9g.png)

It did work, yay! :partying_face: So no we just have to replace this `<h1>` tag with an XSS payload such as `<style onload=alert(document.domain)>` and it will work, right?

![No XSS happening](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s2q8va505ls56sslo1co.png)
 
Okay, the `<style>` disappeared, but there's no extra prevention method for XSS, so what happened!? :sob:

Let's add a new checkpoint to the line in the end of the main function, in order to see what's going on:

![Checkpoint at line 239](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m7i0q42dz5qystwp189l.png)

What!? Look at what's just happening in `qs`:

![The payload disappeared](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9na49ca561tuc5dheynr.png)
 
The payload disappeared! Is the fact that we have an alert being called a problem? Let's test by changing the payload to just `<style onload>`:

![Style tag appeared again](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/asux1qtsnztujcvvbfia.png)

Okay...and what if we change it to just `<style alert(document.domain)>`?

![Style tag still appears](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/56kazic1r9mdw3aq2rqz.png)

So what's the problem when we try to bring everything together? Well, remember that everything is being parsed by a query string parser, and that the `=` sign is part of the query string specification, so probably Mithril is getting a little bit lost when trying to parse our payload. :monocle_face:

As a way of helping Mithril, we could just URL encode the `=` sign, so our XSS payload becomes `<style onload%3Dalert(document.domain)>`. When we give it a try...

![It worked! XSS Popping up!](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hdndl88qloq1u9olvg27.png)
 
Yay, it did work! XSS Popping up! :partying_face:

So my final payload was `config[window-toolbar][constructor][prototype][1]=8080&settings[root][ownerDocument][body][innerHTML][0]=<style%20onload%3Dalert(document.domain)>`, a little bit too big but that's okay :sunglasses:

### Thank's for taking your time! Hope you got the process and learned something new! :hugs: