XSS Filtering Bypass Using the Latest ECMAScript Features
An explanation of XSS filtering bypass techniques and principles utilizing the new features of ECMAScript.
This post was machine-translated from Korean by AI. Some expressions may differ from the original.
Read the original (Korean)Recently, browser JavaScript engines have been updated up to ECMAScript 2019 (ES10), adding many elements and functions. Consequently, new XSS filtering bypass techniques based on the latest ECMAScript standards, rather than legacy JavaScript, are being created. I would like to share these details and briefly explain their principles.
XSS (Cross-Site Scripting) vulnerabilities mainly occur when invalid user input is inserted into the browser's DOM, or when specific JavaScript behaviors allow an attacker to arbitrarily execute desired code or alter the execution flow.
In most cases, the following defense mechanisms are put in place to prevent XSS vulnerabilities.
- Filtering at the Code Level
When parameters are received from a user and need to be rendered in the DOM, the user's parameters are validated. If phrases or characters used for XSS attacks are found, the request is rejected, or the specific phrase/character is deleted.
Representative targets include characters like " (Double Quote), ' (Single Quote), ` (Back Quote), < (Left Angle Bracket), > (Right Angle Bracket), as well as HTML Tags and Attributes such as script, alert, eval, onerror, etc.
Filtering is performed through XSS filtering solutions or self-implemented XSS Filter Functions.
For example, in a Java web server, various filtering behaviors can be implemented, such as replacing strings like <% String param1 = request.getParameter("param1").replaceAll("script", ""); %>, or emptying the parameter if the string is found.
- Filtering at the Web Application Firewall (WAF)
Similar to code-level filtering, user requests are rejected if specific phrases or characters are present.
Since WAF filtering applies globally to the server's pages rather than just at the code level, it is generally characterized by stronger filtering on phrases like HTML Tags or Attributes rather than individual characters (based on experience).
It seems that actions like replace are rarely performed; instead, attacks are prevented via block.
Recently, well-defended sites have almost all HTML Tags and Attributes in their filtering lists. In these cases, XSS requiring direct insertion of HTML Tags, such as DOM XSS like ?param="><script>alert('xss');</script>, is mostly caught by filtering and does not execute. (Additionally, when HTML TAGs must be entered directly like this, they are detected by the client-side browser's XSS Auditor, so the script does not execute except in some special cases. In such instances, weaponizing for actual attacks is unreasonable.)
Therefore, in situations where filtering is heavily applied, one of the scenarios where XSS vulnerabilities occur is when user input is inserted "without restriction" within a <script> tag area. Here, "without restriction" means that while WAF filtering may be in place, code-level filtering is non-existent or insufficient, allowing the creation of a desired code flow. I will explain with an example.
XSS Vuln Code Example
<script>
var username = "<%=request.getParameter("username")%>";
alert(username+"๋ ๋ก๊ทธ์ธ ํ์
จ์ต๋๋ค.");
</script>In the case above, by inserting a " (Double Quote) character into the username parameter, the parameter inserted into the string within the script tag escapes the string literal, making it possible to execute arbitrary scripts.
However, this applies when there is absolutely no filtering. If code-level filtering or WAF filtering exists, one must control the code flow without using those characters or phrases.
In this post, I would like to introduce some useful filtering bypass payloads that can be used in such situations.
When execution of built-in functions like alert is filtered by Regular Expressions
a=alert; a(document.domain);A function in JavaScript is a type of Object. Objects in JavaScript can be assigned to variables.
Therefore, by assigning alert (built-in function) to variable a and calling it, alert(document.domain) can be expressed as shown above.
If the execution of specific functions is blocked via regular expressions (ex. /alert(.+)/), bypass is possible as shown above.
The following are explained in the same way as above.
a=eval; b="aler"; c="t(documen"; d="t.domai"; e="n)"; a(b+c+d+e);a=eval; a(atob("YWxlcnQoZG9jdW1lbnQuZG9tYWluKQ=="));When Quotes cannot be used
eval(8680439..toString(30)+String.fromCharCode(40,49,41))Int..toString(Int) converts the preceding number to a string format, passing the radix (base) as an argument. In other words, this is a command to interpret it as base-30 and replace it with a string.
String.fromCharCode(int, int) replaces the numbers passed as decimal integer arguments into characters, concatenates them, and returns the result.
Therefore, the command above is identical to eval("alert(1)").
eval(/aler/.source+/t(1)/.source)In JavaScript, strings between / represent regular expressions. A regular expression is a type of Object, and among its sub-functions, there exists the .source attribute which can change the type to a string.
Therefore, without Quotes (Single, Double, Back), you can obtain the regular expression characters converted to a string via the .source attribute from the character-based regular expression.
When Parentheses cannot be used
Set.constructor`alert\x28document.domain\x29```Set: JavaScript built-in function. Here Set was used, but any other built-in function works.
constructor: Sets the constructor of the function called Set.
: Back Quotes can pass arguments to a function and execute the function simultaneously. Expressing the above syntax using parentheses and quotes, it is the same as
Set.constructor("alert(document.domain)")().
\x28, \x29: ECMAScript supports Auto Typecasting for Hex Ascii Strings, Unicode Strings, and Octal Strings.
\x28 == ( == \50
Therefore, by setting the behavior of the constructor function of JavaScript's built-in Set function to alert(document.domain) and executing that Set function (``), the constructor function is triggered, allowing the attacker to execute arbitrary scripts.
The following are explained in the same way as above.
Set.constructor`alert\`document.domain\````Set.constructor`alert(document.domain)```setTimeout`alert\x28document.domain\x29`setInterval`alert\x28document.domain\x29`Phrase Filtering + Quote Filtering + Parentheses Filtering
_=URL+0,/aler/.source+/t/.source+_[12]+/documen/.source+/t.domai/.source+/n/.source+_[13]instanceof{[Symbol.hasInstance]:eval}Let's look at it from the beginning.
URL is a built-in function. A function in JavaScript is an Object. When an addition operation occurs between an Object and a String type, the Object.toString() method is executed first to Type Cast the Object itself into a string, and then it is added to the String.
Therefore, it changes to a string as shown in the image above. The reason for performing this action is to obtain parenthesis characters without using parentheses, as parentheses are filtered.
The string was assigned to the variable _. _[12] == (, _[13] == )
As seen above, strings can be obtained and added using regular expressions. Through these operations, the string "alert(document.domain)" was obtained.
In the latest ECMAScript, a data type called Symbol has been newly established in addition to existing data types like String and Integer.
Among the properties of this Symbol data type, there is a hasInstance property. This property allows redefining the behavior when determining if a specific Symbol object is an instance.
Let's express the code above in a slightly easier-to-read way.
"alert(document.domain)" instanceof { [Symbol.hasInstance] : eval }
{ } is an Object. The Symbol.hasInstance property of this Object (the code to run if the object is an instance) is defined as eval.
Then, utilizing the instanceof operator to ask if the string "alert(document.domain)" is an instance of the following object {}, and if this Object is an instance, it calls the defined eval with the string as an argument and returns the result.
Consequently, it becomes eval("alert(document.domain)").
Codes constructed on a similar principle are as follows.
_=URL+0,Array.prototype[Symbol.hasInstance]=eval,/alert/.source+_[12]+1+_[13]instanceof[]_=URL+!0+!1,Array.prototype[Symbol.hasInstance]=eval,_[19]+_[38]+_[40]+_[33]+_[4]+_[12]+1+_[13]instanceof[]Event.prototype[Symbol.toPrimitive]=x=>/javascript:0/.source+location.search,onload=openThe code above contains slightly different content, so I will add comments.
location.search designates the part after ? in the URI.
If the page is redirected via the javascript: scheme, arbitrary JavaScript code can be executed thereafter.
Interpreting the code above:
The return value of Symbol.toPrimitive of the Event built-in function's prototype (constructor) is set to /javascript:0/.source+location.search using function hoisting and an Arrow Function.
As explained above, existing ECMAScript data types were five: String, Integer, Undefined, Null, and Boolean.
From ECMAScript 6, these are called Primitive Types, and a data type called Symbol was newly added.
Symbol is a data type with a slightly different nature from existing Primitive Types. Please refer to the blog below for details.
Symbol ์๋ฃํ ๊ด๋ จ ์ฐธ๊ณ ๋ธ๋ก๊ทธ
[ES6] 8. SymbolBlog posted about front end developmentjaeyeophan.github.io
The attribute method that converts a Symbol type to a primitive type when it matches an existing primitive type is the Symbol.toPrimitive method.
The return value of the Symbol.toPrimitive method is set via an Arrow Function and function hoisting. "javascript:0"+location.search
Through the onload=open syntax, the behavior of document.onload is designated as the open function.
Since onload is an Event attribute, the Event built-in function executes, and Symbol.toPrimitive, which was set in its prototype, gets executed.
As the open built-in function goes through the toPrimitive method, it changes into a syntax like:
open(x => /javascript:0/.source+location.search)
-> x = function(x){return /javascript:0/.source+location.search}; open(x);
-> open(/javascript:0/.source+location.search)
If the attacker sets the URI link like https://ar9ang3.com/test.html?0:alert('xss')//¶m1=foo¶m2=bar and triggers XSS,
The value of location.search becomes ?0:alert('xss')//¶m1=foo¶m2=bar,
and the entire string becomes javascript:0?0:alert('xss')//¶m1=foo¶m2=bar.
The string after 0?0:alert('xss') disappears due to the comment, and since this is a ternary operator expression, it changes to false?false:alert('xss'), eventually executing the JavaScript syntax desired by the attacker.
Besides the syntaxes written above, filtering can be bypassed in countless other ways. If you think of a new bypass technique, I would appreciate it if you shared it.
Thank you for reading.
Reference* xss-cheatsheet_posix
Comments