Hauntmart
Personal Rating: Medium
This challenge was about a SSRF with a filter bypass to create a privileged user at an api endpoint.
At first glance I could see that the flag is copied to /flag in the dockerfile. The file entrypoint.sh is loaded, which sets up a database like so:
DROP DATABASE IF EXISTS hauntmart;
CREATE DATABASE hauntmart;
CREATE TABLE hauntmart.users (
id INTEGER PRIMARY KEY AUTO_INCREMENT,
username varchar(255) NOT NULL UNIQUE,
password varchar(255) NOT NULL,
role varchar(255) NOT NULL DEFAULT 'user'
);
CREATE TABLE hauntmart.products (
id INTEGER PRIMARY KEY AUTO_INCREMENT,
name varchar(255) NOT NULL,
price varchar(255) NOT NULL,
description TEXT NOT NULL
);
CREATE USER 'xclow3n'@'localhost' IDENTIFIED BY 'xclow3n';
GRANT SELECT, INSERT, UPDATE ON hauntmart.users TO 'xclow3n'@'localhost';
GRANT SELECT, INSERT, UPDATE ON hauntmart.products TO 'xclow3n'@'localhost';
FLUSH PRIVILEGES;
The file web_hauntmart/challenge/application/blueprints/routes.py is interesting. It contains route definitions for web and api endpoints. At the web endpoint /home the flag is defined as flag=current_app.config['FLAG'] with the template index.html
For /home and /product it seems like you need to be authenticated
The /register API seems interesting too. You can possibly register a user from an unauthenticated standpoint:

On the live website I could register an account immediately and access the site.
Under Sell Product it seems like you can send data to the server. This site really stands out, it says that you can send a manual url which will be visited by the admins. My first Idea is to host a javascript that triggers another request to me, containing the admin cookie.
The idea of reflected blind XSS did not work as I could not find a working injection. But what we state in the url field is indeed accessed by the server, which is interesting.
This is written in the index.html:
{% if user['role'] == 'admin' %}
{{flag}}
{% endif %}
This code in util.py might also be interesting as it seems to download what we give it in the manual url field under certain circumstances:
def downloadManual(url):
safeUrl = isSafeUrl(url)
if safeUrl:
try:
local_filename = url.split("/")[-1]
r = requests.get(url)
with open(f"/opt/manualFiles/{local_filename}", "wb") as f:
for chunk in r.iter_content(chunk_size=1024):
if chunk:
f.write(chunk)
return True
except:
return False
return False
This might be the way in. I found out before that /api is prepended to the url of api calls. so we should be able to directly access /api/addAdmin. Here is the code from routes.py that shows that:
@api.route('/addAdmin', methods=['GET'])
@isFromLocalhost
def addAdmin():
username = request.args.get('username')
if not username:
return response('Invalid username'), 400
result = makeUserAdmin(username)
if result:
return response('User updated!')
return response('Invalid username'), 400
Trying to access it shows a 403, which is expected because of @isFromLocalhost:

Lets check the isFromLocalhost function; Maybe we can bypass that. It is defined in util.py:
def isFromLocalhost(func):
@wraps(func)
def check_ip(*args, **kwargs):
if request.remote_addr != "127.0.0.1":
return abort(403)
return func(*args, **kwargs)
return check_ip
Well, we have a chance to perform a localhost request. Here:

According to the addAdmin code, the payload http://127.0.0.1:12345/api/addAdmin?username=hacker should work. But no, we receive “Invalid URL”. We see that there is a blacklist for manual url requests defined in util.py:
blocked_host = ["127.0.0.1", "localhost", "0.0.0.0"]
def isSafeUrl(url):
for hosts in blocked_host:
if hosts in url:
return False
return True
There are many ways to bypass this as you can see in this reference: https://book.hacktricks.xyz/pentesting-web/ssrf-server-side-request-forgery/url-format-bypass
Using 0 as the IP did work to bypass the filter and get the flag
Last updated