$ cat writeup.md…
$ cat writeup.md…
hackthebox
Task: Exploit a Flask app with multiple SSTI injection points, EXIF metadata processing, and predictable password reset tokens. Solution: Leak admin email via filtered bio SSTI using User ORM object in template context, reset admin password using sha256(email) token, then exploit unfiltered SSTI in EXIF Artist tag using lipsum.__globals__ to read the flag file.
$ cat /etc/rate-limit
Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.
"Welcome to the Guild! But please wait until our Guild Master verify you. Thanks for the wait"
Flask web application with user registration, verification system, and admin panel.
Key files:
views.py - Main routes including verification, profile, admin panelauth.py - Authentication routesmodels.py - User, Verification, Validlinks modelsLocation: /user/<link> route (line 242-243 in views.py)
bio = Verification.query.filter_by(user_id=query1.id).first().bio temp = open("/app/website/templates/newtemplate/shareprofile.html", "r").read() return render_template_string(temp % bio, User=User, Email=email, username=query1.username)
The bio field is inserted via %s into template and rendered with render_template_string. However, there's a filter (checkInput()) blocking many keywords:
payloads = [ "*", "script", "alert", "debug", "%", "include", "html", "if", "for", "config", "img", "src", ".py", "main", "herf", "pre", "class", "subclass", "base", "mro", "__", "[", "]", "def", "return", "self", "os", "popen", "init", "globals", "base", "class", "request", "attr", "args", "eval", "newInstance", "getEngineByName", "getClass", "join" ]
Key observation: The User object is passed to the template context! This allows ORM queries without using blocked keywords.
Location: /verify route (line 137-141 in views.py)
if "Artist" in exif_table.keys(): sec_code = exif_table["Artist"] query.verified = 1 db.session.commit() return render_template_string("Verified! {}".format(sec_code))
The EXIF "Artist" tag from uploaded images is directly passed to render_template_string without any filtering! This is the critical vulnerability.
reset_url = str(hashlib.sha256(email.encode()).hexdigest())
The password reset token is simply sha256(email) - completely predictable if you know the email.
The bio SSTI has filtering, but User object is passed to template. We can query the database:
{{ User.query.filter_by(username="admin").first().email }}
...
$ grep --similar