Reading time: 3 minutes

Introduction

This write-up aims to describe a possible solution to Funnylogin challenge of DiceCTF, which is a web challenge.

Reconnaissance

The application is just a login page. The challenge goal is to get the flag once a valid user is logged in with admin permission.

The users are randomly created as follows:

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(", ")}`);

The admin user is randomly defined at runtime, such as:

const isAdmin = {};
const newAdmin = users[Math.floor(Math.random() * users.length)];
isAdmin[newAdmin.user] = true;

The login SQL is defined as:

const { user, pass } = req.body;
const query = `SELECT id FROM users WHERE username = '${user}' AND password = '${pass}';`;

The query is vulnerable to SQL injection due to the concatenation of user and pass constants from the request body.

The flag can be obtained by respecting the following restrictions:

const FLAG = process.env.FLAG || "dice{test_flag}";

const id = db.prepare(query).get()?.id;
if (!id) {
  return res.redirect("/?message=Incorrect username or password");
}

if (users[id] && isadmin[user]) {
  return res.redirect("/?flag=" + encodeURIComponent(FLAG));
}

In summary, the restrictions are:

  1. The user credential provided should return an id;
  2. It should exist a user based on the id;
  3. The user should have admin permission.

As the application is vulnerable to SQL injection, it is possible to list all users of the application, but the admin is defined at runtime. Therefore, a possible solution is brute forcing all users based on the id to figure out who is the admin to get the flag. The brute force attempts are up to 100000 users.

Payload to use in the user parameter to return the id 1:

' OR id = '1

As brute force is not allowed, it requires another exploit.

Exploit

The exploit is based on the bypass of all restrictions listed in the recon section.

The restriction 1 can be bypassed by providing a SQL query that returns any id in the database. A simple way is to use the following query:

SELECT 1 as id;

Using the SQL injection, it is possible to use UNION statement to include this select.

Payload to use with the id equal to 1:

' UNION SELECT 1 as id;/*

Note the /* is used to comment the rest of the query. The final SQL query using pass post HTTP parameter is:

SELECT id FROM users WHERE username = '' AND password = '' UNION SELECT 1 as id;/*';

To bypass the restriction 2, the id should be from 1 to 100000.

Finally, restriction 3 can be bypassed using JavaScript prototype inheritance [1].

The admin verification uses the code isadmin[user]. For example, if the user EffectRenan is not the admin, isadmin['EffectRenan'] will return undefined.

As JavaScript is based on prototypes, if the content of the user is not a valid property in the object and there is a valid property in the prototype, it returns the content from the prototype. The prototype of an object has the following default properties:

> obj = {}
> obj.
obj.__proto__             obj.constructor           obj.hasOwnProperty
obj.isPrototypeOf         obj.propertyIsEnumerable  obj.toLocaleString
obj.toString              obj.valueOf

Thus, if the user is toString, the check isadmin[user] will return true, bypassing the restriction 3.

Note that restrictions 1 and 2 are bypassed using pass parameter, as the information comes from the SQL query. Restriction 3 is bypassed using the user parameter.

To get the flag, make a post HTTP request to https://funnylogin.mc.ax/api/login with the following parameters:

Flag: dice{i_l0ve_java5cript!}

Proof of Concept

References

  1. JavaScript prototype inheritance.
  2. DiceCTF