5 methods for Bypassing XSS Detection in WAFs

Posted on 2022-08-09 by Karel Knibbe in Tools of the Trade


Ever since the 1990s, Cross-Site Scripting (XSS) vulnerabilities have plagued the world wide web. It’s been a difficult problem to solve because of the many ways that it can introduce itself in applications. This, and other application level attacks, contributed to the rise of Web Application Firewalls (WAFs). However, like any other solution that does not tackle the problem by its roots, it’s not ideal. Pentesters, red teamers, bug hunters and malicious actors alike have been playing cat and mouse with vendors to find ways around these additional defence mechanisms. In this post, we’ll be discussing a few fundamental techniques that you can use to bypass these firewalls.

Example of a previous bypass

Cloudflare is among the most commonly used WAF products out there, so it’s no surprise that it gets a lot of attention from security researchers. Fortunately for us, @friendly_ (who does their own name justice) was kind enough to blog about one of their discovered bypasses.

Their go-to approach is to use the popular “h1” tag to test the waters. This makes confirming the potential vulnerability quite easy, as it’s much less likely to get flagged by potential filters or firewalls.

After attempting to inject multiple variations of the original payloads with no success, Friendly resorted to using HTML entities with a specific notation. This technique can be applied to all payloads that rely on HTML attributes, but we’ll expand on this later.

The resulting exploit turned out to be:

\"><iframe/src=javascript:alert%26%23x000000028%3b)>

Surprisingly enough, this still works!

https://www.cloudflare.com/?x=\%22%3E%3Ciframe/src=javascript:alert%26%23x000000028%3b)%3E

The above scenario is a great example of how encodings can be used to mess with firewalls and filters.

Fundamental Techniques

Below, we will be explaining some fundamental techniques of bypassing WAFs. Most WAF bypasses are a combination or new iteration of one or more of these fundamental techniques. The techniques listed will likely stand the test of time and should be applicable in the foreseeable future.

Method 1: Multi-Reflection Exploits

It’s not unheard of that a single HTTP parameter has multiple reflections in the response body. We can make use of this situation and circumvent certain defences by changing the written order of our payloads.

Imagine the following PHP code:

Hello, <? $_GET["name"] ?>!
...
It was nice seeing you, <? $_GET["name"] ?>

We could approach this with a noisy payload that’s likely to ring a few alarm bells:

<script>alert()</script>

Or we can split the payload and move the closing tag to the start of our payload:

*/</script><script>alert()/*

As you might have guessed, script tags rarely ever bypass anything, but this is applicable to other HTML elements as well. Anchor tags seem to work nicely because they’re difficult to accurately detect due to their small size.

This is what that could look like:

'href=javascript:alert()>click me<a/y='

Here’s a codepen for that exact payload.

Method 2: Utilising Multiple Parameters

Web applications and WAFs tend to disagree about how HTTP requests should be interpreted. This leaves room for bypasses. One such technique is called HTTP Parameter Pollution.

Certain frameworks handle multiple GET/POST parameters differently. OWASP has even compiled a list of these differences. Take the following raw request:

GET /?foo=1&foo=2 HTTP/1.1
Host: example.com

A smart firewall would scan all occurrences of “foo”, rather than taking either the first or the second and ignoring the rest. However, this isn’t always the case, and can then be bypassed as follows:

GET /?foo=<script>alert()</script>&foo=harmless HTTP/1.1
Host: example.com

Or

GET /?foo=harmless&foo=<script>alert()</script> HTTP/1.1
Host: example.com

If you’re lucky, the targeted web application sometimes concatenates multiple parameters with the same name and separates them with another character (e.g. a comma).

GET /?foo=<script%20&foo=>alert()</script>
Host: example.com

Which would result in:

<script ,>alert()</script>

Make sure that the original payload is split up in enough pieces and you shouldn’t have any issues getting past that annoying blockade.

Method 3: Abusing Many Contexts

There are many places where your XSS probes can end up. Depending on these contexts, there are various encoding and escape sequences available to you.

HTML attributes, for example, have this universal behaviour where they decode HTML entities back to plain text. The purpose of these entities is to allow the use of characters that would otherwise be interpreted as part of the HTML document. This makes a WAF’s job harder and ours easier.

One context you might have come across is the href attribute from the anchor (<a>) tag. In that scenario, arbitrary JavaScript execution is possible by either injecting a new javascript URI or abusing an existing one. In addition to the HTML decoding behaviour inherited by all attributes, this specific attribute also performs URL decoding on any URI schemes. However, since we’re also in a JavaScript context, we can even use unicode/hexadecimal escape sequences. This can allow for interesting payloads that are very tricky for firewalls to detect.

From start to finish, this would look like:

  1. Our starting payload:

    alert()
    
  2. Applying unicode escape sequences:

    \u0061\u006c\u0065\u0072\u0074()
    
  3. Applying URL encoding:

    %5Cu0061%5Cu006C%5Cu0065%5Cu0072%5Cu0074%28%29
    
  4. Using HTML entities:

    &#x25;5Cu0061&#x25;5Cu006C&#x25;5Cu0065&#x25;5Cu0072&#x25;5Cu0074&#x25;28&#x25;29
    

The browser will normalise it like:

Decode HTML entities ➡️ URL decoding ➡️ Normalise Unicode escapes ➡️ plaintext

Tough firewalls that perform recursive decoding will likely not detect this either. Unless of course, they’re paranoid enough to block any characters outside of the standard A-Z range. Try this codepen and see for yourself.

Similar techniques can be used for other attributes (such as iframe’s src/srcdoc) or anything else that consists of multiple contexts.

Method 4: Server Assisted Bypasses

Existing defence mechanisms from the web app can actually weaken the firewall. Any exotic bytes/characters might get stripped from our payload. Meaning we can inject a bunch of garbage into our payloads, have it bypass the firewall, and reflect back to us as if they were never there in the first place.

The catch is that it requires a bit of active fuzzing to see where this happens (if at all). Burp Suite’s Intruder does the job for this nicely.

A somewhat simple, but incomplete setup, can be created by following these steps:

  1. Create a new intruder session.
  2. In the Payloads tab, pick the payload type “Numbers” and set Number range to sequential.
  3. Configure the range to go from 0 to 255 with a step value of 1.
  4. Add a payload processing rule that replaces ^(.{1})$ with 0$1. This is required to form valid URL encodings for the first few fuzzing attempts.
  5. In the Options tab, add a regexp grep match for (?<!x)xx(?!x). This should flag any responses where the byte was stripped without false positives.

After starting the Intruder attack, you should see something like this:

Burp Intruder results

Anything interesting will automatically be flagged via our new regex column between “Length” and “Comment”.

If, for example, the byte %08 (backspace character) was removed from our fuzz. We can probably bypass the firewall by sending something like:

<s%08c%08r%08i%08p%08t>al%08ert%08()<%08/%08s%08c%08r%08i%08p%08t%08>

I’ve personally had success with null bytes (%00) and whitespaces (%08, %09, %20). Though anything could work.

Method 5: Browser Quirks

A more refined approach to finding bypasses is looking for cutting-edge HTML/JavaScript functionality or fuzzing browsers to discover new vectors. Even WAF vendors need time to adapt, so if you find something, you’ll likely have free reign for a while until the firewalls catch up. Something recent that comes to mind is the “navigation.navigate” method.

PortSwigger researchers have written an excellent article on behavioural fuzzing. To sum it up, they discovered that Firefox allowed null bytes in HTML comment openers. The use-case for this vector probably isn’t common enough for your everyday XSS, but it’s interesting nonetheless, and it shows that fuzzing works.

Another thing I’ve played around with in the past is event handlers. You can dynamically fetch them from your browser and feed them to Burp Intruder to find out what’s blocked and what isn’t.

Here are all the window event handlers in Chrome 103:

Listing event handlers from the window object

> Object.keys(window).filter(k => { return k.startsWith("on") })

And here is a list of event handlers inherited by all HTML elements:

Listing all HTML event handlers

> Object.keys(HTMLElement.prototype).filter(k => { return k.startsWith("on") })

You will also need to verify that the event handlers can actually be triggered, even if you do find some that go through. If this does not do the trick, you can check the HTML standard or other non-event handler attributes (e.g. “href”) that are potentially capable of executing JavaScript.

Conclusion

The five methods above are some common fundamental techniques that are used for bypassing WAFs, but there are many, many more! The most interesting WAF bypasses are often constructed by combining these fundamental methods together and getting creative.

Bypasses are much more easily discovered with a deep understanding of browsers, applications, and frameworks. If you’re looking to up your XSS game, and find your own bypasses, don’t be afraid to go down the research rabbit hole to uncover your own techniques.

If you’re in a position where you need to defend a web application, this blog may serve as a warning that implementing a WAF is not enough to thwart exploitation attempts. WAFs have been (unsuccessfully) trying to block XSS payloads for years, and bypasses are still being found, even in the most mature WAFs. For this reason - we always recommend that security testing is performed without the protection of a WAF. The underlying application should be secured first, and the WAF should only be used to facilitate defence-in-depth.

A note to pentesters/bug hunters

This article has used alert() in many examples to keep them simple. In your actual reports you should not simply use alert(), but instead write a PoC showing impact to the specific application you’re exploiting. Happy hacking!


About the author

Karel Knibbe is a part time bug hunter and developer. You can find him on Twitter at @Karel_Origin

Photo by Ussama Azam on Unsplash.

If you need help with your security, get in touch with Volkis.
Follow us on Twitter and LinkedIn