Polyglot payloads
A polyglot payload is defined as a piece of code that can be executed in multiple contexts in the application. These types of payloads are popular with attackers because they can quickly test an application's input controls for any weaknesses, with minimal noise.
In a complex application, user input can travel through many checkpoints—from the URL through a filter, into a database, and back out to a decoder, before being displayed to the user, as illustrated in the following figure:
Figure 2.29: Typical data flow from user to application
Any one of the steps along the way can alter or block the payload, which may make it more difficult to confirm the existence of a vulnerability in the application. A polyglot payload will attempt to exploit an injection vulnerability by combining multiple methods for executing code in the same stream. This attempts to exploit weaknesses in the application payload filtering, increasing the chance that at least one portion of the code will be missed and will execute successfully. This is made possible by the fact that JavaScript is a very forgiving language. Browsers have always been an easy barrier of entry for developers, and JavaScript is rooted in a similar philosophy.
The OWASP cross-site scripting (XSS) Filter Evasion Cheat Sheet contains examples of polyglot payloads, which can also evade some application filters: https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet.
A good example of a strong polyglot payload can be found on GitHub from researcher Ahmed Elsobky:
jaVasCript:/*-/*'/*\'/*'/*"/**/(/* */oNcliCk=alert() )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=alert()//>\x3e
At first glance, this appears rather messy, but every character has a purpose. This payload was designed to execute JavaScript in a variety of contexts, whether the code is reflected inside an HTML tag or right in the middle of another piece of JavaScript. The browser's HTML and JavaScript parsers are extremely accommodating. They are case-insensitive, error-friendly, and they don't care much about indenting, line endings, or spacing. Escaped or encoded characters are sometimes converted back to their original form and injected into the page. JavaScript in particular does its very best to execute whatever code is passed to it. A good polyglot payload will take advantage of all of this, and seek to evade some filtering as well.
The first thing a sharp eye will notice is that most of the keywords, such as textarea
, javascript
, and onload,
are randomly capitalized:
jaVasCript:/*-/*'/*\'/*'/*"/**/(/* */oNcliCk=alert() )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=alert()//>\x3e
This may seem like a futile attempt to evade application firewall input filters, but you'd be surprised how many are poorly designed. Consider the following regular expression (regex) input filter:
s/onclick=[a-z]+\(.+\)//g
Note
A regex is a piece of text defining a search pattern. Some WAFs may use regex to try and find potentially dangerous strings inside HTTP requests.
This will effectively prevent JavaScript code from being injected via the onclick
event, but with one glaring flaw: it doesn't take into account case-sensitivity. Regular expressions have many modifiers, such as the g
in the preceding example, and by default most engines require the i
modifier to ignore case, or else they will not match and the filter is vulnerable to bypass.
The following figure shows Regex101's visualization of the preceding regex applied to a sample test string. We can see that only two of the four payloads tested matched the expression, while all four would execute JavaScript code:
Figure 2.30: Regex filter visualization
Tip
When assessing an application's regex-based input filter, Regex101 is a great place to test it against several payloads at once. Regex101 is an online tool available for free at https://regex101.com.
Many times, developers work under unrealistic time constraints. When a penetration testing report highlights a particular input sanitization issue, developers are pressured to turn in a security fix that was quickly written, insufficiently tested, and remediates only part of the problem. It is often too time-consuming and expensive to implement a potentially application-breaking framework to handle input filtering, and shortcuts are taken at security's expense.
The Elsobky payload also aims to exploit being passed through an engine that processes hex-encoded values escaped with a backslash. JavaScript and Python, for example, will process two alphanumeric characters preceded by \x
as one byte. This could bypass certain in-line XSS filters that perform primitive string compare checks:
jaVasCript:/*-/*'/*\'/*'/*"/**/(/* */oNcliCk=alert() )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=alert()//>\x3e
It is possible that the payload may be stripped of most of the other keywords, but when the filter reaches \x3c
and \x3e
, it interprets them as benign strings of four characters. The application may parse the string and inadvertently return the one-byte equivalent of the escaped hexadecimal characters <
and >
respectively. The result is an <svg>
HTML element that executes arbitrary JavaScript via the onload
event.
Note
Scalable Vector Graphics (SVG) is an element on a page that can be used to draw complex graphics on the screen without binary data. SVG is used in XSS attacks mainly because it provides an onload
property, which will execute arbitrary JavaScript code when the element is rendered by the browser.
Note
More examples of the power of this particular polyglot are on Elsobky's GitHub page: https://github.com/0xSobky.
A powerful polyglot payload is able to execute some code in a variety of injection scenarios. The Elsobky payload can also be useful when reflected in the server HTTP response:
jaVasCript:/*-/*'/*\'/*'/*"/**/(/* */oNcliCk=alert() )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=alert()//>\x3e
The URL encoded characters %0d
and %0a
represent newline and carriage return. These characters are largely ignored by HTML and JavaScript parsers, but they are significant in the HTTP request or response header.
If the target application fails to filter user input properly, in some cases it may take the arbitrary value and add it as part of the HTTP response. For example, in an attempt to set a "Remember me" cookie, the application reflects the payload unfiltered in the HTTP response headers, which results in XSS in the user's browser:
GET /save.php?remember=username HTTP/1.1 Host: www.cb2.com User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0 Content-Type: application/x-www-form-urlencoded; charset=UTF-8 [...] HTTP/1.1 200 OK Cache-Control: private Content-Type: text/html; charset=utf-8 Server: nginx/1.8.1 Set-Cookie: remember_me=username Connection: close Username saved!
If we pass in the polyglot as the username to remember, the HTTP response headers are altered and the body will contain attacker-controlled data as follows:
GET /save.php?remember=jaVasCript%3A%2F*-%2F*%60%2F*%60%2F*'%2F*%22%2F**%2F(%2F*%20*%2FoNcliCk%3Dalert()%20)%2F%2F%0D%0A%0d%0a%2F%2F%3C%2FstYle%2F%3C%2FtitLe%2F%3C%2FteXtarEa%2F%3C%2FscRipt%2F--!%3E%3CsVg%2F%3CsVg%2FoNloAd%3Dalert()%2F%2F%3E%3E HTTP/1.1
Host: www.cb2.com
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
The server responds with the following:
HTTP/1.1 200 OK Cache-Control: private Content-Type: text/html; charset=utf-8 Server: nginx/1.8.1 Set-Cookie: remember_me=jaVasCript:/*-/*'/*\'/*'/*"/**/(/* */oNcliCk=alert() )// //</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=alert()//>\x3e Connection: close Username saved!
The response is a bit mangled, but we do have code execution. The URL encoded carriage return characters %0D%0A%0d%0a
are interpreted as part of the HTTP response. In the HTTP protocol, two sets of carriage returns and line feeds indicate the end of the header, and anything that follows this will be rendered by the browser as part of the page.
Same payload, different context
There are many other contexts in which this polyglot can successfully execute code.
If the polyglot payload is reflected inside the value
property of the username input, the browser's interpretation of the code clearly shows a broken input field and a malicious <svg>
element. The HTML code before the payload is processed looks like this:
<input type="text" name="username" value="[payload]">
This figure shows how the browser views the HTML code after the payload has been processed:
Figure 2.31: Reflected XSS payload
The polyglot will also execute code if reflected inside an HTML comment, such as <!-- Comment! [payload] -->
.
The payload contains the end of comment indicator -->
, which leaves the rest of the text to be interpreted by the browser as HTML code. Once again, the <svg>
element's onload
property will execute our arbitrary code.
This figure shows how the browser views the HTML code after the payload has been processed:
Figure 2.32: Reflected XSS payload
Our polyglot is also useful if reflected inside some code setting up a regex object, such as var expression = /[payload]/gi
.
We can test this behavior inside the browser console with the preceding sample code:
Figure 2.33: Polyglot visualization
We can see that strategically placed comment indicators, such as /*
, */
, and //
, will cause the browser to ignore the majority of the payload, resulting in valid JavaScript.
It's subtle, but the code execution happens here:
(/* */oNcliCk=alert() )
The multi-line comments are ignored, and JavaScript will execute anything between the parenthesis. In this context, oNcliCk
does not represent a mouse event binder, but instead it is used to store the return of the alert()
function, which results in arbitrary code execution.
Code obfuscation
Not all application firewalls strip input of malicious strings and let the rest go through. Some inline solutions will drop the connection outright, usually in the form of a 403
or 500
HTTP response. In such cases, it may be difficult to determine which part of the payload is considered safe and which triggered the block.
By design, inline firewalls have to be fairly fast and they cannot introduce significant delay when processing incoming data. The result is usually simple logic when attempting to detect SQL injection (SQLi) or XSS attacks. Random capitalization may not fool these filters, but you can safely assume that they do not render on the fly every requested HTML page, let alone execute JavaScript to look for malicious behavior. More often than not, inline application firewalls will look for certain keywords and label the input as potentially malicious. For example, alert()
may trigger the block, while alert
by itself would produce too many false-positives.
To increase the chances of success and lower the noise, we can change the way the alert()
function is called in seemingly unlimited ways — all thanks to JavaScript. We can test this in the browser console by inspecting the native alert()
function. The window
object will hold a reference to it and we can confirm this by calling the function without parentheses. The console will indicate that this is a built-in function with [native code]
displayed as its body. This means that this is not a custom user-defined function and it is defined by the browser core.
In JavaScript, we have multiple ways of accessing properties of an object, including function references such as alert
.
This figure shows how we can access the same function directly or using array notation, with an "alert"
string inside square brackets:
Figure 2.34: Different ways to access the alert() function
To bypass rudimentary filters, which may drop suspicious strings, such as alert
(1
), we can leverage some simple encoding.
Using JavaScript's parseInt
function, we can get the integer representation of any string, using a custom base. In this case, we can get the base 30 representation of the "alert"
string. To convert the resulting integer back to its string equivalent, we can leverage the built-in toString()
method while passing the integer base as the first parameter:
Figure 2.35: The "alert" string encoding and decoding
Now that we know 8680439..toString(30)
is the equivalent of string "alert"
, we can use the window
object and array notation to access the native code for the alert()
function.
This figure shows how we can call the alert()
function using the obfuscated string:
Figure 2.36: Executing alert() with an encoded string
We can follow the same process to obfuscate a call to the console.log()
function. Much like most available native functions, console
is accessible through the window
object as well.
The following figure shows how we can encode the strings console
and log
, and utilize the same array notation to access properties and subproperties until we reach the native code for console.log()
:
Figure 2.37: Encoding the entire console.log command
For the traditional strongly-typed language developer, this convention looks alien. As we've already seen, JavaScript engines are very forgiving and enable a variety of ways to execute code. In the preceding examples, we are decoding the base 30 integer representation of our function and passing it as a key to the window
object.
After some modification, the Elsobky payload could be made a bit more stealthy with obfuscation. It could look something like the following:
jaVasCript:/*-/*'/*\'/*'/*"/**/(/* */oNcliCk=top[8680439..toString(30)]() )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=top[8680439..toString(30)]()//>\x3e
Tip
The top
keyword is a synonym for window and can be used to reference anything you need from the window
object.
With just a minor change, the polyglot payload is still effective and is now more likely to bypass rudimentary inline filters that may attempt to filter or block the discovery attempts.
Brutelogic offers a great list of XSS payloads with many other ways to execute code unconventionally at https
://brutelogic.com.br/blog/cheat-sheet/
.