Pragyan 2026 — Plumbing Çözüm Yazısı
Son hâli tertemiz görünen, çoktan gönderilmiş bir Docker imajı. OCI katmanlarını soymak yapı geçmişini, bir whiteout arkasına gizlenmiş silinmiş bir durum dosyasını ve geliştiricinin üzerine yazdığını sandığı özgün çıktıyı ortaya çıkarıyor. İki sızıntı özel bir blok şifresini tersine çevirip bayrağı geri veriyor.
Pragyan 2026 — Plumbing
Bir konteyner adli bilişim sorusu. Bize çoktan derlenmiş ve gönderilmiş bir Docker imajı veriliyor. Çalışan konteyner temiz görünüyor, ama imajın kendisi bir tarih kitabıdır: her katman kopyalananı saklar, her yapı komutu yazılanı saklar — bir betiğe boruyla aktarılıp unutulması gereken sır dâhil.
- Etkinlik: Pragyan 2026
- Kategori: Adli Bilişim (Forensics)
- Dosya:
app.tar - Bayrak formatı:
p_ctf{...}
1. Tar'ın içinde ne var
$ file app.tar
app.tar: POSIX tar archive
$ tar -xf app.tar -C x && find x -maxdepth 2
x/oci-layout
x/blobs/sha256
x/manifest.json
x/index.jsonBu bir OCI imaj düzeni — docker save / ctr image export komutlarının ürettiği disk üstü format. Düzen basittir:
index.json—app:latestiçin bir manifestoya işaret edermanifest.json— config blob'unu ve katman blob'larını listelerblobs/sha256/…— içeriğe göre adreslenmiş asıl blob'lar
2. Config blob'u yapı geçmişidir
Config blob'u (b3f4caf1…, manifest.json'da Config olarak geçer) imaj yapılandırmasıdır. history dizisi her Dockerfile komutunu created_by olarak kaydeder — tıpkı docker history --no-trunc'un gösterdiği gibi:
$ jq '.history[].created_by' <(cat blobs/sha256/b3f4caf1*)
...
"COPY process.py . # buildkit"
"COPY env /app/.env # buildkit"
"RUN /bin/sh -c echo \"p_ctf{d0ck3r_l34k5_p1p3l1n35}X|O\" | python3 process.py # buildkit"
"RUN /bin/sh -c rm /tmp/state_round7.bin # buildkit"
"RUN /bin/sh -c echo \"uhh it was here ;-;\" > /app/output.bin # buildkit"
"COPY DEV_NOTES.txt . # buildkit"
"COPY process2.py /app/process.py # buildkit"İşte orada. Sır bir RUN satırına yazılmıştı. Bir dosyayı sonraki bir katmanda kaldırmak, onu oluşturan komutu kaldırmaz — geçmiş kalıcıdır ve bayrak orada durmaktadır.
Geçmiş aynı zamanda tüm örtbas hikâyesini anlatır: bir hata ayıklama dosyası state_round7.bin silinmiş, çıktının üzerine bir alay mesajı (uhh it was here ;-;) yazılmış, "temiz" bir process2.py gerçek process.py'nin üzerine kopyalanmış ve DEV_NOTES.txt kanıtları saklama yapılacaklar listesi gibi okunuyor.
3. Silinen dosyaları kurtarma
Docker'da bir dosyayı silmek onu yok etmez; yeni katmana bir whiteout girdisi (sıfır baytlık .wh.<ad> dosyası) ekler, gerçek baytlar ise eski katmanın tar arşivinde kalır. Her katmanı açtığımızda tam olarak bunu görüyoruz:
layer 7 : /app/output.bin /tmp/state_round7.bin <- gerçek veri
layer 8 : /tmp/.wh.state_round7.bin <- whiteout (silme)
layer 9 : /app/output.bin <- üzerine yazılan alay
layer 10: /app/DEV_NOTES.txt
layer 11: /app/process.py <- process2.py (boş)Sonraki katmanların silmeye çalıştığı iki dosya, layer 7'de hayatta kalmış:
$ xxd layers/7/tmp/state_round7.bin
00000000: 0b24 7a67 2f16 302b 273e 376f 282e 6a53 .$zg/.0+'>7o(.jS
$ xxd layers/7/app/output.bin
00000000: 2f62 3335 7758 7631 7438 4f02 3a71 2960 /b35wXv1t8O.:q)`state_round7.bin 16 bayt, output.bin 16 bayt. İkisi de yapı sırasındaki çalıştırmanın artıkları.
4. Özgün betiği ve anahtarı okuma
Gerçek process.py (üzerine yazılmadan önce, layer 5'ten) basit bir blok şifresidir:
KEY = load_key() # /app/.env : AES_KEY=THIS_IS_AES_KEY!
ROUNDS = 10
DEBUG_ROUND = 7
def xor_block(a, b):
return bytes(x ^ y for x, y in zip(a, b))
def permute(block): # 4 bayt sola döndür
return block[4:] + block[:4]
def encrypt_block(block, leak_debug=False):
state = block
for r in range(1, ROUNDS + 1):
state = xor_block(state, KEY)
state = permute(state)
if leak_debug and r == DEBUG_ROUND:
open("/tmp/state_round7.bin", "wb").write(state) # sızıntı
return state
data = sys.stdin.buffer.read().strip()
block1 = data[:16]
block2 = data[16:32]
encrypt_block(block1, leak_debug=True) # 7. tur durumunu sızdırır
open("/app/output.bin", "wb").write(encrypt_block(block2))İmaja kopyalanan .env anahtarı açıkça verir:
AES_KEY=THIS_IS_AES_KEY!Yani: sırrın ilk yarısı 7. turda iç durumunu sızdırır, ikinci yarısı ise şifrelenip output.bin'e yazılır. Her iki yarı da kurtarılabilir.
5. Şifreyi tersine çevirme
Her tur state = rotate_left_by_4( state XOR key )'dir. Tersi, ters sıra ve ters işlemdir: state = rotate_right_by_4( state ) XOR key. Sızan 7. tur durumuyla 1. bloğu elde etmek için 7 turu, gerçek şifreli metinle 2. bloğu elde etmek için 10 turun tamamını tersine çeviririz:
KEY = b"THIS_IS_AES_KEY!"
xor = lambda a, b: bytes(x ^ y for x, y in zip(a, b))
rot = lambda b: b[4:] + b[:4] # ileri permütasyon
ror = lambda b: b[-4:] + b[:-4] # ters permütasyon
def decrypt(state, rounds):
for _ in range(rounds):
state = xor(ror(state), KEY)
return state
state7 = open("7/tmp/state_round7.bin", "rb").read() # 7. turda sızmış
ct2 = open("7/app/output.bin", "rb").read() # özgün şifreli metin
block1 = decrypt(state7, 7) # tur 7 -> 1 tersine
block2 = decrypt(ct2, 10) # tur 10 -> 1 tersine
print(block1, block2)
# b'p_ctf{d0ck3r_l34' b'k5_p1p3l1n35}X|O'- bloğu yeniden şifreleyip sızan 7. tur durumunu ürettiğini (ve 2. bloğu yeniden şifreleyip saklanan şifreli metni verdiğini) doğrulamak, tersine çevirmenin tam olduğunu kanıtlar.
6. Bayrak
Kurtarılan iki yarıyı birleştiriyoruz:
print((block1 + block2).decode())
# p_ctf{d0ck3r_l34k5_p1p3l1n35}X|Op_ctf{d0ck3r_l34k5_p1p3l1n35}Sondaki X|O, ikinci 16 baytlık bloğu doldurmak için eklenmiş dolgudan ibaretti. Bayrak, yapı geçmişine gömülü olanla eşleşiyor — sorunun amaçlanan sızıntısı ile onun iskeleti uyuşuyor.
7. Çıkarımlar
RUNkomutları ölümsüzdür. Bir yapı adımına yazılan sır, kaç sonraki katmanrmuygularsa uygulasın imaj yapılandırmasında sonsuza dek yaşar.- Bir dosyayı silmek yalnızca bir whiteout yazar; baytlar onu oluşturan katmanda kalır.
docker savehepsini gönderir. - İmaja kopyalanan
.envbir sır deposu değildir. - "Son konteyner temiz görünüyor" ifadesi, altındaki katmanlar hakkında hiçbir şey söylemez. Çalışan dosya sistemine değil, her zaman geçmişe ve her katman tar arşivine bakın.
Ne düşünüyorsun?
Tepki bırakarak geri bildirim ver