Arbitrary Parentheses-less XSS
In the past years, an interesting XSS vector was put on a table by some researchers, and that is Parentheses-less XSS.
It’s not a mystery that there are known payloads that will execute arbitrary XSS with limited charsets. One of the simplest payloads out there is
But there was a gap in the research that I attempted to fill and that is
Executing arbitrary parentheses-less XSS against strict Content-Security-Policy’ies (CSP)
As a result of my research, I created an XSS challenge on Twitter that has been solved by 7 people, where 6 of them executed arbitrary XSS. The goal of the challenge was to execute arbitrary XSS using only characters from the limited charset [a-zA-Z$_=.\u007f-\uffff] and with a strict CSP policy, which is:
which will successfully block any execution of an inline code (e.g. location=name, <svg/onload=alert()>) as well as string evaluators (e.g. eval(“alert()”), Function(“alert()”)()).
However, the initial challenge could be solved in easier ways than intended, which only proved that bypassing the CSP can be done in several ways. I released a fixed version which restricted reusing the injected script and simplified the code. The fixed version was only solved by Roman (@shafigullin) and Ben Hayak, where Roman came up with the intended solution and Ben cleverly reused some functions defined by the challenge to achieve a similar outcome.
The solution can be broken into a few vital steps that the player must have noticed:
- Taking control over a method in the callback function
- Discovering Unicode line terminators, U+2028 and U+2029
- Injecting arbitrary HTML into a page
- Crafting a payload for arbitrary XSS execution
- Bypassing the strict CSP
For the tl;dr version, here is the final PoC.
Taking control over a method in the callback function
This step was very simple. In order to inject code into a callback endpoint, you had to escape from the
t= parameter via
Discovering Unicode line terminators
eval('x=123\u2028alert(x)') will pop out an alert.
Injecting arbitrary HTML into a page
To make the challenge not dependant on neither
location.href, at the beginning of the script, I restricted the usage of both. To my surprise, bypassing these two restrictions were proven to be harder than anticipated for most of the players, which resulted in a few cool techniques discovered by participants.
In the XSS Without parentheses repo, a very cool technique can be found for injecting arbitrary HTML into a page, and that is:
document.body.innerHTML=document.body.innerText;// in URL: ?<img/src="x"/onerror=alert(23)>
Because the usage of the location was limited, the intended way was to use
In my solution, I used
after innerHTML -> innerText chain.
Crafting a payload for arbitrary XSS execution & Bypassing the strict CSP
This was probably the toughest step because it required thinking of both CSP bypass and arbitrary XSS at the same time. This is also the step that took me the longest when researching the topic myself.
Because not many players have made progress in the challenge in the first two days, I released a camouflaged hint on Twitter.
It was supposed to be a hint, but Roman quickly proved this technique to be applicable in the challenge even though I tested it wouldn’t work in there. I will elaborate on it the last section of the article.
The crucial problem with the technique was that it would be blocked by CSP when applied directly. Here comes one of the most important steps of the solution — bypassing the CSP.
Reverse-proxy for the rescue
Although any subpage had strict CSP headers set, it was still possible to load a page within the same domain that hasn’t set these headers. This is due to how reverse-proxies work. If reverse-proxy is confused with a request, it will not forward the request to a back-end application and therefore CSP headers will not be set. For example, harderxss.terjanq.me/%2f throws Not Found and /%GG throws Bad Request because it couldn’t url-decode the %GG string. There are also other ways to make the reverse proxy stop the request like overlong URI or overlong request headers such as Referer or Cookie aka Cookie Bomb.
Iframe without CSP
If we used the fact that /%GG will not have CSP set and injected an iframe in the following way
document.body.innerHTML="<iframe name=x src=%GG>"
this looks like it could bypass the CSP, but in reality, it will not. *Actually, it did bypass CSP in Chromium, which I discovered in the process and which was already reported by external researchers.
Why won’t it work?
When iframe is injected into the DOM, it is in a blank state waiting in the event loop for being loaded. For example, the
x.eval('alert(location.href)') payload will pop out an alert with
about:blank URL instead of expected
%GG. Because empty iframes, that are within the same domain and which doesn’t come from the network, inherit CSP from its parent, this call will be blocked by CSP in the challenge.
Why just not wait for iframe to load?
The goal is simple, wait for iframe to load, and then execute the payload. Is it simple though? How can you wait for iframe to load?
In my original solution, I combined onerror + throw technique with prototype overriding technique to achieve the goal.
Let’s see what will happen to the following code:
document.body.innerHTML="<iframe id=x src=data:,1>"
We can see on the screenshot below, that this will throw an invocation error because atob expects to be called on Window object, but it was called on Iframe. I explained how it works in more detail in the last section of the article.
Because one of the rules was to achieve arbitrary XSS on both Chrome and Firefox, let’s see what error will be passed as an argument to a function on both browsers.
We can notice a slight difference between Chrome and Firefox. The error in Chrome will start with Uncaught TypeError: while in Firefox will start with TypeError: Although the messages differ a little, they both share the same error type and that is TypeError. Let’s try to override the prototype of it, so it returns a controlled part of the message.
We can see on the screenshot above that when TypeError.prototype.name is changed to an arbitrary string, the error message will now start with “alert(/1337/)//:” in Firefox. Similarly, “Uncaught alert(/1337/)//:” in Chrome. To make the payload work in both browsers at the same time, I crafter a simple polyglot
TypeError.prototype.name = "-alert(1337);var Uncaught//"
which when evaluated, will trigger an alert in both browsers.
But how to evaluate?
Because the iframe %GG is not protected by CSP we can evaluate strings inside it. To do so, just simply replace
onerror=i.contentWindow.eval and it will be evaluated inside %GG iframe, and will, therefore, pop out an alert!
The final payload looks like the image from the beginning of the section, and the complete payload can be viewed here.
As mentioned, my hint happened to be almost a solution if someone already discovered how to bypass the CSP. This is due to a feature that I missed to test before releasing it to the public :) However, only two players managed to apply the hint into the challenge.
Let’s take a quick look what would happen to my payload if we replaced eval, with setTimeout.
Surprisingly it will be blocked by CSP even though it seems to be executed inside iframe without CSP.
Why did it happen?
Let’s see what happens when we try to assign setTimeout directly to the onload event.
As we can see in the image above, it will throw an invocation error with the message that the object that ‘setTimeout’ was called on, does not implement interface Window. We can learn from the last two screenshots that setTimeout expects to be called on Window object. This is the crucial difference between setTimeout and eval. The latter is bind to a window and does not require to have
this to point to anything.
Given that, I assumed that setTimeout trick mentioned earlier will not work in the challenge. I didn’t realize that there is a feature that actually allows calling setTimeout in the iframe context. The two examples below show how two different onload handlers, can return two different contexts.
When an iframe is in a blank state, waiting in the event loop for load, it can have its onload event defined on a window even before the browser started downloading the contents of the iframe from the server! This is something that I didn’t know and that I learned from Roman, which has solved the challenge in a similar way, by using a slightly different approach :) This obviously only happens if the loaded iframe is within the same origin, otherwise would be an SOP violation.
Here is how the final payload looks like: