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:

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

Last updated