C:\CTF\GASLIGHT\BISCUIT.EXE _□X

BISCUIT

web 232 solves by sportshead

Challenge

Hello world! italics bold

File given: biscuit.tar.zst.

Understanding the app

It's a little voting site (cake vs biscuit) gated behind login. Auth doesn't use plain session cookies, it uses Biscuit tokens, which encode a small Datalog program of facts and checks, signed with a keypair on the server. On signup or login the server mints one of these for you:

def mint(username: str) -> str:
    builder = BiscuitBuilder(
        f"""
        user("{username}");
        check if user($u), $u.length() > 0;
        """,
    )
    if username == "webmaster":
        builder.add_fact(Fact('role("admin")'))
    return builder.build(root.private_key).to_base64()

Only the hardcoded "webmaster" account gets a role("admin") fact baked into its token. The /flag route checks for exactly that fact before showing anything:

def current_admin() -> str | None:
    return _authorize('allow if user($u), role("admin");')

@app.route("/flag")
def flag():
    if current_user() is None:
        return redirect(url_for("login"))
    if current_admin() is None:
        return render_template("flag.html"), 403
    return render_template("flag.html", flag=FLAG)

The vulnerability

The username gets dropped straight into the Datalog source with an f-string, with no validation and no escaping. They don't validate the input, it's just an empty f-string, so whatever you sign up with becomes literal Datalog syntax inside the token builder. A username like:

sakamotosan"); role("admin

turns the minted program into:

user("sakamotosan"); role("admin");
check if user($u), $u.length() > 0;

which closes the user(...) fact early and appends a role("admin") fact of your own choosing, the exact fact that's normally reserved for the webmaster account. The check clause still passes fine since user($u) is still true, so the token comes out signed and valid with the injected fact intact.

Solution

Sign up with that string as the username, then hit /flag with the resulting cookie. The exact payload used:

user("sakamotosan"); role("admin