This challenge is part of a series of challenges from Hack The Boo 2023 Competition organized by Hack The Box.
Overview
HauntMart is the web category challenge revealed on the first day of the CTF, and involves:-
- Code analysis
- Understanding of Docker containers
- Basic to intermediate web testing abilities
Provided as part of the challenge are:-
- A dedicated Docker instance hosted by Hack The Box
- A ZIP archive containing the source code which also allows for building local instances of the website
Before starting
Note that compromising the underlying infrastructure of the instance was NOT part of the challenge.
Pwning the site
Initial site review
To access further pages on the site, a user needs to authenticate and we are given options to either login or create an account.
Initial checks to see if there are any data leaks through the login form are unsuccessful as the error messages simply state "Invalid username/password!", so enumerating for existing users is not possible.
However attempting to sign up with an existing username does generate an error informing us that the username is taken.
However, that is a dead end as no users exist.
Creating an account and further site review
As seen, the site is a Halloween-themed e-commerce style site, none of the products redirect anywhere however.. there is a second working link in the top navigation bar which redirects to /product.
After submitting a product, we get a very vague message.
So, it's time to take a look at the source code.
Source code review
Dockerfile
Locally, the website would be accessible through http://localhost:1337/
# Expose port the server is reachable on
EXPOSE 1337
main.py
- 2 possible URL prefixes: / and /api
routes.py
- 5 web routes
- 4 api routes
Notice the decorators above each endpoint, we will get to these soon enough.
For now, the most interesting endpoint is /api/addAdmin, which takes in a "username" parameter to change the role of the user in the database to admin.
database.py
In database.py, we find the function "makeUserAdmin()", which updates the role in the database.
util.py
- Contains definitions for the decorators seen in routes.py
The "isFromLocalhost()" function verifies that a request's remote address is "127.0.0.1".
Exploit
So, we know which endpoint to call to "upgrade" our user to an admin role, however the challenge here is that the endpoint can only be called locally.
How do we that? We need to review the source code just a bit more.
On the website, submitting a product sends a POST request to the /api/product route, which takes in the name, price, description and URL of the product in JSON format.
Taking a look at the API route, the program:-
- Checks if the POST request has an authentication token via the @isAuthenticated decorator
- Stores the product data
- Checks if any product data is missing
- And finally, calls the downloadManual() function with the manualUrl from the product information which we inputted
The downloadManual() function:-
- Checks if the URL is safe by passing it to the isSafeUrl() function
- The isSafeUrl() function iterates through the list of blocked hosts ["127.0.0.1", "localhost", "0.0.0.0"], and if the product URL contains any of those strings, it deems it unsafe
- Generates a local filename for the submitted manual from the URL. Eg. http://myproduct.abc/productmanual would be stored as "productmanual"
- Sends a GET request to the provided product URL
So, theoretically, we could exploit this to make the application send a GET request to http://localhost:1337/api/addAdmin?username=<USERNAME>
However, we need to bypass the isSafeUrl() function check.
After a quick Google search for "URL bypass", I found this page from HackTricks
And just like that, all we have to do is:-
- Ensure we have an account on the website
- Navigate to /product and add a product with the following "Manual Url": http://127.000000000000000.1:1337/api/addAdmin?username=USERNAME
Finally, the last thing we need to do is to logout and back in again in order to "update" our user's role on the running application.
And just like that, we get the flag! (Fake flag generated from local instance)