XSS Through the Referer Header

2021-10-14

XSS is a vulnerability in which a malicious actor is able to run JavaScript in an unsuspecting clients browser session. Normally, this is done via input fields whose values are reflected back to the user without proper sanitisation.

In this blog, I demonstrate a method I recently used that injected the payload via the Referer [sip] header.

Only do this on websites you own or have permission to do so no. It is illegal in most places to do this without permission.

Initial foothold

I found myself looking at a website whose input fields were all well sanitised, getting ready to write a pretty boring report. However, I spotted a snippet of JavaScript in an analytics script that looked something like this:

<script>
{
...
    "referer": "site.com/page"
...
}
</script>

That sparked my interest so I opened pappy and changed the referer header to " + alert(1),"":".

BRILLIANT! That worked. The referer header was not being sanitised.

Crafting the referer header

So, that is nice, but it would take some serious social engineering to convince someone to intercept a request in burp / pappy / some other proxy tool, change their referer header to our payload and then submit the request. Really we needed a way to control that header ourselves. For anyone that doesn’t know, the referer header holds the address of the page that makes the request. This basically means the previous page if a link was clicked.

There is also a related header called referrer-policy that controls how much information is sent. I want to send as much as possible so I manually set this to unsafe-url for my proof of concept.

<?php
header('Referrer-Policy: unsafe-url');
?>

I am using php here because I know it. I’m not saying php is the best choice. Use what you know and can use quickly for proof of concepts.

My initial thought was to simply use HTTP redirects to first redirect the user to a page whose url contained my payload, and then from there to the vulnerable page.

The code for that was:

<?php
$payload='"+alert(1),"":"';
header('Referrer-Policy: unsafe-url');
if ( $_SERVER['REQUEST_URI'] == "/index.php" ) {
    header('Location: //localhost:8081/index.php/' . urlencode($payload));
}else{
    header('Location: https://vulnerable-site.com/page');
}
?>

That didn’t work. After some searching I realised that the referer header is not changed when an http redirect is followed. It does however if the url is changed with JS.

$payload='"+alert(1),"":"';
header('Referrer-Policy: unsafe-url');
if ( $_SERVER['REQUEST_URI'] == "/index.php" ) {
    $location='//localhost:8081/index.php/' . urlencode($payload);
}else{
    $location='https://vulnerable-site.com/page';
}
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <title>Example</title>
    <script>
        document.location="<?php echo $location; ?>";
    </script>
    </head>
    <body>
        Example
    </body>
</html>

So, after hosting this at localhost:8081, I was able to visit it in my browser. If doing this in the real world, I would host it on a public server somewhere and try and convince a victim to click a link.

In my case, the initial link would be to //localhost:8081/index.php. This would then use JS to redirect the victim to to //localhost:8081/index.php/%22%2Balert%281%29%2C%22%22%3A%22. We would then use JS again to redirct the user to the vulnerable site. With the referrer-policy header set to unsafe-url, the browser will set the referer header to the url including our payload and trigger our payload. In this example, we are doing alert(1). That’s pretty boring and obvious to the user. However, in the real world, we could send another request back to our server with the contents of document.cookie to steal the session, or prompt the user to re-enter their credentials and send that to ourselves. Once you have unrestricted XSS, account compromise is normally possible.

Improvements

Many of you may have noticed ways to improve the payload. For example, I hard coded localhost:8081, the payload and various other information. If you want, feel free to improve it but this is supposed to be a proof of concept, not a well build program. When you’re making POCs, it’s the one time you really don’t need to worry about coding well - it’s about getting something that works in a short time frame.

Solution

This issue came about because http headers were trusted. If the site had validated the http header, or encoded it in some way, this would not have been possible. Always sanitise. Never assume that anything that comes in the request is safe.