baby breaking grad
4 minutos de lectura
Tenemos esta página web:
Vamos a pinchar en el botón:
Análisis de código fuente
Muy bien, ya que se nos proporciona el código fuente, echémosle un vistazo. Es un proyecto en Node.js usando Express JS. Este es index.js
:
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const routes = require('./routes');
const path = require('path');
app.use(bodyParser.json());
app.set('views','./views');
app.use('/static', express.static(path.resolve('static')));
app.use(routes);
app.all('*', (req, res) => {
return res.status(404).send('404 page not found');
});
app.listen(1337, () => console.log('Listening on port 1337'));
Este archivo es bastante normal. Echemos un vistazo a las rutas (routes/index.js
):
const randomize = require('randomatic');
const path = require('path');
const express = require('express');
const router = express.Router();
const StudentHelper = require('../helpers/StudentHelper');
router.get('/', (req, res) => {
return res.sendFile(path.resolve('views/index.html'));
});
router.post('/api/calculate', (req, res) => {
let student = req.body;
if (student.name === undefined) {
return res.send({
error: 'Specify student name'
})
}
let formula = student.formula || '[0.20 * assignment + 0.25 * exam + 0.25 * paper]';
if (StudentHelper.isDumb(student.name) || !StudentHelper.hasPassed(student, formula)) {
return res.send({
'pass': 'n' + randomize('?', 10, {chars: 'o0'}) + 'pe'
});
}
return res.send({
'pass': 'Passed'
});
});
module.exports = router;
Aquí vemos que /api/calculate
es el único endpoint que tenemos. Podemos pasarle name
y formula
en el cuerpo de la petición, y serán procesados por StudentHelper.isDumb
y StudentHelper.hasPassed
:
const evaluate = require('static-eval');
const parse = require('esprima').parse;
module.exports = {
isDumb(name){
return (name.includes('Baker') || name.includes('Purvis'));
},
hasPassed({ exam, paper, assignment }, formula) {
let ast = parse(formula).body[0].expression;
let weight = evaluate(ast, { exam, paper, assignment });
return parseFloat(weight) >= parseFloat(10.5);
}
};
Las funciones anteriores nos permiten calcular una nota. Para el primero, debemos poner un nombre que no contenga Baker
o Purvis
. Luego, en Student.hasPassed
, el objeto student
debe contener atributos exam
, paper
y assignment
. Después de eso, formula
se analiza y se ejecuta con los atributos anteriores.
Obsérvese que hay un valor predeterminado en formula
:
let formula = student.formula || '[0.20 * assignment + 0.25 * exam + 0.25 * paper]';
Sin embargo, la clave aquí es que podemos definir una nueva fórmula que será procesada por evaluate
(static-eval
). Esto es un problema porque tenemos la posibilidad de ejecutar el código JavaScript.
Comencemos por aprobar la asignatura (nota mayor que 10.5
):
$ curl 134.122.103.40:30580/api/calculate -d '{"formula":"[11]","name":"x","exam":"","paper":"","assignment":""}' -H 'Content-Type: application/json'
{"pass":"Passed"}
Encontrando un exploit que funcione
No hay vulnerabilidades que se apliquen a la versión 2.0.2 de static-eval
en snyk.io:
{
"name": "breaking-grad",
"version": "1.0.0",
"description": "",
"main": "index.js",
"nodeVersion": "v12.18.1",
"scripts": {
"start": "node index.js",
"dev": "nodemon .",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"authors": [
"makelaris",
"makelarisjr"
],
"dependencies": {
"body-parser": "^1.19.0",
"express": "^4.17.1",
"randomatic": "^3.1.1",
"static-eval": "2.0.2"
}
}
Sin embargo, podemos encontrar algunos artículos que muestran cómo saltarse una sandbox restrictiva y obtener ejecución remota de comandos (RCE) en static-eval
mediante Prototype Pollution. Por ejemplo, este artículo de Faraday. No obstante, la clave está en mirar el repositorio de GitHub de static-eval
, ya que podemos encontrar un commit para la versión 2.0.3 que corrigió un problema de seguridad en la versión 2.0.2:
De hecho, la pull request añade un nuevo caso de prueba que parece un exploit:
Usemos Burp Suite (Repeater) para probar:
Ahora, vamos a probar el exploit:
Parece que funciona, ya que no se muestran errores. Por lo tanto, podríamos obtener RCE ya que podemos ejecutar código arbitrario de JavaScript. Por ejemplo, usemos child_process
para ejecutar sleep 5
:
Muy bien, entonces tenemos RCE a ciegas. Sabemos esto porque no vemos la salida de nuestros comandos:
Bueno, no todo el resultado… Aquí tenemos que considerar dos cosas. Primero, el nombre de archivo de la flag se aleatoriza en entrypoint.sh
:
#!/bin/bash
# Secure entrypoint
chmod 600 /entrypoint.sh
# Generate random flag filename
FLAG=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 5 | head -n 1)
mv /app/flag /app/flag$FLAG
exec "$@"
Y esto se puede omitir usando un wildcard /app/flag*
:
Y lo segundo es que se muestran los errores. Por lo tanto, en lugar de imprimir la flag, podemos ejecutar la flag como un comando en una subshell (de manera que se muestre en el error):
Flag
Y ahí la tenemos (nótese que '
es una comilla simple en codificación HTML):
HTB{f33l1ng_4_l1ttl3_blu3_0r_m4yb3_p1nk?...you_n33d_to_b3h4v'eval!!}