🕗 3 minutes.
This write-up aims to describe a possible solution to Funnylogin
challenge of DiceCTF, which is a web challenge.
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:
id
;id
;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.
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:
toString
' UNION SELECT 1 as id;/*
Flag: dice{i_l0ve_java5cript!}