Frost Tower Website Checkup⚓︎
Difficulty:
Direct link: staging website
Terminal hint: The Elf C0de
Objective⚓︎
Request
Investigate Frost Tower's website for security issues. This source code will be useful in your analysis. In Jack Frost's TODO list, what job position does Jack plan to offer Santa? Ribb Bonbowford, in Santa's dining room, may have some pointers for you.
Ingreta Tude
Hey there! I'm Ingreta Tude. I really don't like the direction Jack Frost is leading us.
He seems obsessed with beating Santa and taking over the holiday season. It just doesn't seem right.
Why can't we work together with Santa and the elves instead of trying to beat them?
But, I do have an Objective for you. We're getting ready to launch a new website for Frost Tower, and the big guy has charged me with making sure it's secure.
Hints⚓︎
SQL Injection with Source
When you have the source code, API documentation becomes tremendously valuable.
Solution⚓︎
This challenge is an example of a white-box pentest and solving it requires finding and exploiting 2 vulnerabilities in the source code. First we need to bypass authentication, followed by an SQL injection attack against a vulnerable URL endpoint. Ribb Bonbowford's hint literally tells us which 2 Node.js modules we need to focus on for each step, express-session
and mysql
.
Understanding the application logic
Having access to the source code gives us full insight into the web application's internal logic and understanding the application flow is an important part of solving the challenge. While we could submit the form on the Frost Tower website to determine that the actual site is located at /testsite
, the same information can also be found by analyzing the server.js
source code, which we'll be doing throughout this write-up.
Bypassing authentication⚓︎
When a user successfully logs in using a POST request to /login
, the session.uniqueID
variable is assigned a value, enabling access to backend endpoints like /dashboard
which wrap their logic in an if (session.uniqueID) {}
statement.
There's a flaw in the /postcontact
endpoint logic, however. Using the contact form form to submit the same email address more than once will result in the SQL query on line 141 to return the email that's already in the uniquecontact table and assign the submitted email to session.uniqueID
(line 151), fullfilling the requirement set by endpoints like /dashboard
.
Submitting the same email twice via the contact form and accessing the /dashboard
endpoint confirms the vulnerability.
SQL injection⚓︎
Now that the web application considers us authenticated we can take a closer look at all the server.js
code that's wrapped in if (session.uniqueID) {}
statements. This reveals an additional vulnerability in the /detail
endpoint, which contains logic to parse a comma-separated list of IDs in order to build a database query that will retrieve multiple uniquecontact records at once. On line 207 the code contains a raw()
method which, per the mysql
API documentation, "will skip all escaping functions when used, so be careful when passing in unvalidated input". Honestly, this sounds more like a hint than a warning.
Escaping query values
The mysql
Node.js library provides several methods like mysql.escape()
, connection.escape()
, and pool.escape()
which can be used to escape user-provided data and help prevent SQL injection attacks. In addition, ?
characters can also be used as placeholders for any values that should be escaped.
However, the mysql.raw()
method will leave the data un-touched even when it's used in combination with a ?
placeholder. In other words, the escape()
method used in the tempCont.escape(m.raw(ids[i]));
statement will have no impact on the object returned by the raw()
method.
By using an SQL injection UNION attack we can combine the results from the uniquecontact SELECT
statement on line 198 with whatever we'd like to retrieve from another table, as long as each individual query returns the same number of columns and their data types are compatible. As a request for ID 0 using /detail/0
returns no records, the result of a UNION
statement will only contain the data from the other table. This keeps things nice and clean.
There is one caveat though. Commas will be stripped out as the input string is converted to a list of IDs by the split()
method on line 204, meaning our SQL query can't contain that particular character. Luckily for us, others already solved that problem and SELECT 1,2,3
can be rewritten as SELECT * FROM (SELECT 1)F1 JOIN (SELECT 2)F2 JOIN (SELECT 3)F3
.
While it might be tempting to go for admin access, the goal is to find Jack's TODO list, so craft a query that lists all the tables. In the example below, the table_name
query statement is at the same column index as the full_name
column in the uniquecontact table, which will result in the detail.ejs
template rendering it front and center using <h1><%= encontact.full_name %></h1>
.
0,0 union select * from
((select 1)F1 join (select table_name from information_schema.tables where table_schema='encontact')F2 join
(select 3)F3 join (select 4)F4 join (select 5)F5 join (select 6)F6 join (select 7)F7);--
SELECT * FROM uniquecontact WHERE id=0 OR id=0 union select * from
((select 1)F1 join (select table_name from information_schema.tables where table_schema='encontact')F2 join
(select 3)F3 join (select 4)F4 join (select 5)F5 join (select 6)F6 join (select 7)F7);-- OR id=?
Using the SQLi string above as input for the /detail
endpoint gives us the information we're looking for.
The same technique can now be repeated to retrieve the column information from the todo table (i.e., id
, note
, completed
), followed by the notes themselves. To help automate these final steps we can create a script that takes our desired SQL query as input, inserts it into the SQLi UNION attack string, creates an authenticated session by submitting the contact form twice using the same email, and then performs a GET request against the /detail
endpoint using the crafted SQLi string as input.
query_db.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
|
Answer
clerk
Adding an administrator account
While not necessary for solving the challenge, we can also leverage the same SQL injection vulnerability to add a new administrator account. Start by using the below string to perform an SQLi attack against the /detail
endpoint. This will retrieve all emails from the users table for full administrator accounts.
0,0 union select * from
((select 1)F1 join (select email from users where user_status=1)F2 join
(select 3)F3 join (select 4)F4 join (select 5)F5 join (select 6)F6 join (select 7)F7);--
Next, use one of the email addresses (e.g., root@localhost) to initiate a password reset. Once the reset request has been submitted, use the string below to perform another SQLi attack against the /detail
endpoint to retrieve the password reset token
for the selected account.
0,0 union select * from
((select 1)F1 join (select token from users where email='root@localhost')F2 join
(select 3)F3 join (select 4)F4 join (select 5)F5 join (select 6)F6 join (select 7)F7);--
Making a request to /forgotpass/token/<token>
will allow us to set a new password for the root@localhost account and log in. Our full administrator status now also allows us to delete contact entries, access the user list, and create additional administrator accounts for ourselves and all our friends.