Toy Management
Toy Management is a challenge in which we are given the source code as well as a hosted copy of a web app that we need to hack in order to find the flag.
Prompt
The evil elves have changed the admin access to Santa’s Toy Management Portal. Can you get the access back and save the Christmas?
First impressions
Navigating to the home page we are presented with a login form:
When we fill out the form, we POST to /api/login
with a JSON object containing our parameters. From the headers, we can see that we’re working with another Express and node.js app.
Of course we could try some things from here but let’s take a look at the source!
Source code review
The build
Starting with the Dockerfile
, I can see that we’re installing MariaDB, a fork of MySQL. Other than that, there isn’t anything really notable other than to say that none of these files point us to the location of the flag :)
The database
As noted above, for this challenge we are working with MariaDB which is a fork of MySQL.
The schema for the app’s database is defined in database.sql
and, most notably, contains the flag as an entry in the toylist
table:
CREATE TABLE `toylist` (
`id` int NOT NULL,
`toy` varchar(256) NOT NULL,
`receiver` varchar(256) NOT NULL,
`location` varchar(256) NOT NULL,
`approved` int NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `toylist` (`id`, `toy`, `receiver`, `location`, `approved`) VALUES
-- 8< snip 8< --
(7, 'HTB{f4k3_fl4g_f0r_t3st1ng}', 'HTBer', 'HTBland', 0);
Notably, the flag’s approved
value is 0
whereas all of the other entries is 1
.
Also in this database we find a users
table with the following two rows:
INSERT INTO `users` (`id`, `username`, `password`) VALUES
(1, 'manager', '69bbdcd1f9feab7842f3a1c152062407'),
(2, 'admin', '592c094d5574fb32fe9d4cce27240588');
I looked up these two hashes; the password for admin
was not readily known while the password for manager
came back as tryharder
:D (there is no salt obviously)
Moving on, this app has created a Database
class which is defined in database.js
. In this class, a connection is established to the database server running on localhost and some methods are defined for interacting with the database: listToys()
, loginuser()
, and getUser()
. Let’s look at each one.
listToys()
listToys()
takes a single argument approved
which defaults to 1
. This query queries for all of the rows in the toylist
table where approved
equals whatever was passed to it. This query uses a prepared statement and thus is not vulnerable to SQL injection.
loginUser()
loginUser()
takes a user
and pass
as an argument and queries for a matching user. If it finds one, will return the result (through Javascript’s async Promise plumbing).
The query used which does not a prepared statement and is vulnerable to SQL injection is:
SELECT username FROM users WHERE username = '${user}' and password = '${pass}'
Provided there isn’t any escaping up the stack, we could log in as whatever user we want by exploiting this vulnerability.
getUser()
getUser()
takes a user
as an argument and queries for their record in the users
table.
Like loginUser()
, this too is vulnerable to SQL injection*, provided that the input isn’t escaped further up the stack.
Routes (routes/index.js
)
Defined in our routes are POST /api/login
, GET /dashboard
, GET /api/toylist
, and GET /logout
. I will talk about GET /api/toylist
then we will discuss authentication as a whole for this app.
GET /api/toylist
GET /api/toylist
looks up the currently logged in user in the database via db.getUser()
(see above).
It then queries the toy list via db.listToys
listing approved toys only unless the user is admin
, in which case it will display unapproved toys only.
Remember that our flag is in the toy list as an unapproved toy!
Let’s talk about auth
Authentication in this app consists of three main components: the POST /api/login
endpoint, the AuthMiddleware
class, and the JWTs holding it all together (okay, fine, there is a GET /logout
endpoint too which is vulnerable to CSRF but let’s move on).
The login endpoint takes the submitted username and password then passes them to db.loginUser()
. The username is completely untouched (no escaping) whereas the password is (md5) hashed.
If db.loginUser()
returns a row, a JWT is issued in the form of {username: username}
.
Finally, the user’s cookie is set containing this JWT and they are successfully authenticated.
The AuthMiddleware
then, just checks (a) the user actually has a cookie and (b) that their cookie is a valid JWT. Looking at the JWTHelper
class, I don’t see any apparent weakness in the verification of of JWTs.
Putting it all together
We know that the flag is stored in the database as an “unapproved toy” and that only “admin” has the privileges to view those via the GET /api/toylist
endpoint.
We also know that the login form is vulnerable to SQL injection.
This means that we should be able to leverage this SQL injection vulnerability to authenticate as admin
. Once authenticated, we should be able to use the cookie issued to us to access the /api/toylist
endpoint and get the flag.
The exploit
In order to successfully exploit this SQL injection vulnerablility we have to meet a few requirements:
- We have to satisfy this query in order to be logged in:
SELECT username FROM users WHERE username = '${user}' and password = '${pass}'
- The injection also has to be syntactically valid for the query used in
db.getUser()
:SELECT * FROM users WHERE username = '${user}'
- We have to authenticate specifically as
admin
.manager
happens to be the first entry in the database so a payload likea' or 'a'='a
will probably return the wrong user.
The simplest exploitation would be to log in as the user admin' #
and any password. This would satisfy both queries and log us in as admin:
SELECT username FROM users WHERE username = 'admin' #' and password = 'md5f00'
SELECT * FROM users WHERE username = 'admin' #'
And here we go, logging in with that payload (password whatever
):
Annnd the flag.