Intigriti challenge 0422

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:[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
  • 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 but constructor and prototype
  • 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 =':')
      const hostname = temp[0]
      const port = Number(temp[1]) || 443
      return hostname === 'localhost' || port === 8080

the variable temp has value [''] 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:[window-toolbar][constructor][prototype][1]=8080

See what happened in the browser console:

The black list didn’t work… suggestion: white lists over black lists!

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:[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:[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:[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:
(that I am sharing just for proud! <3)

This entry was posted in Pentesting and tagged , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s