Htb Toy Management

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:

The home page of our target app

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:

  1. We have to satisfy this query in order to be logged in:
     SELECT username FROM users WHERE username = '${user}' and password = '${pass}'
    
  2. The injection also has to be syntactically valid for the query used in db.getUser():
     SELECT * FROM users WHERE username = '${user}'
    
  3. We have to authenticate specifically as admin. manager happens to be the first entry in the database so a payload like a' 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):

Logging in as "admin' #"

Annnd the flag.

Getting the flag