CTF Team at the University of British Columbia

[hack.lu 2021] NodeNB

31 Oct 2021 by Angus

tl;dr

sometimes you make the two weird things interact with each other and then you get the flag

Actual problem description

This is the problem entitled NodeNB from Hack.lu CTF 2021.

You are given a web interface to a notebook app written in Node (hence “NodeNB”) that provides basic user register/login functionality and a way to save and display notes for the given user.

The database used for user and note storage is Redis, and it contains the flag as a note. It’s owned by a “system user” that can’t be logged into.

Staring at code

Most of the provided code looks pretty standard, but there’s a few things that stand out:

  1. There’s this weird part in the handler for creating a note that doesn’t do anything useful other than pause for 2-3 seconds:

    if (req.query.random) {
        const ms = Math.floor(2000 + Math.random() * 1000);
        await new Promise(r => setTimeout(r, ms));
        res.flash('info', `Our AI ran ${ms}ms to generate this piece of groundbreaking research.`);
        content = 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.';
    }
    
  2. The app allows you to delete the user you’re logged in as, which performs some session cleanup and deletes the user’s data from the database (but not the user itself):

     async deleteUser(uid) {
         const user = await helpers.getUser(uid);
         await db.set(`user:${user.name}`, -1);
         await db.del(`uid:${uid}`);
         const sessions = await db.smembers(`uid:${uid}:sessions`);
         const notes = await db.smembers(`uid:${uid}:notes`);
         return db.del([
             ...sessions.map((sid) => `sess:${sid}`),
             ...notes.map((nid) => `note:${nid}`),
             `uid:${uid}:sessions`,
             `uid:${uid}:notes`,
         ]);
     }
    
  3. Finally, the function that checks if a user can access a note will allow a user without a password to access any note, because it’s assumed to be the “system user”:

     async hasUserNoteAcess(uid, nid) {
         if (await db.sismember(`uid:${uid}:notes`, nid)) {
             return true;
         }
         if (!await db.hexists(`uid:${uid}`, 'hash')) {
             // system user has no password
             return true;
         }
         return false;
     }
    

Maybe it’ll just work

So if there’s a way to create a note and have it suspiciously pause for a couple seconds, and there’s a way to delete a user, we can do the following:

  1. Register a user and log in.
  2. Create a note with random=x in the query string to trigger the pause.
  3. During the pause, delete the user.
  4. See what happens?

Sure enough, this puts the user in a zombie state where most of their information has been removed from the database. But they still could have a logged-in session that can be used to access the note that was eventually created…and because the user no longer has a password, they can now access the note containing the flag.

wow there’s even a video

The organizers were nice enough to leave the site up at the time of writing this, so here’s a demo: