Overview
Windows Server Domain Controller. Domain: tombwatcher.htb. DC: DC01.tombwatcher.htb
(10.10.10.x). A chain of AD ACL/ACE abuses ending in ADCS ESC15 (CVE-2024-49019).
| |
Each step is one ACE/permission in AD that lets you take over the next object. The final stage combines AD Recycle Bin with an ADCS misconfiguration.
Reconnaissance
| |
Ports typical for a domain controller: 53 (DNS), 88 (Kerberos), 135/139/445 (RPC/SMB),
389/636/3268/3269 (LDAP/LDAPS/GC), 464 (kpasswd), 593 (RPC over HTTP), 5985 (WinRM), 80
(default IIS). The LDAPS certificate confirms DC01.tombwatcher.htb and the domain
tombwatcher.htb. IIS on 80 is the default page; the vector is AD.
BloodHound, with assumed-breach credentials henry : H3nry_987TGV!:
| |
The ritual of this box: take over an account, load it into BloodHound, check what it can do next (outbound object control).
Initial Access
henry to alfred (WriteSPN, targeted Kerberoast)
Edge: henry --WriteSPN--> alfred.
Kerberoast works only on accounts with an SPN. For any account with an SPN you can request
a TGS ticket, part of which is encrypted with a key derived from the account’s password;
crack it offline. WriteSPN lets you add an SPN to alfred (which had none): a targeted
Kerberoast, where you create the precondition and then clean it up.
| |
| |
Result: alfred : basketball.
alfred to ansible_dev$ (AddSelf, ReadGMSAPassword)
Edges: alfred --AddSelf--> INFRASTRUCTURE,
INFRASTRUCTURE --ReadGMSAPassword--> ansible_dev$.
A Group Managed Service Account (gMSA) has its password managed automatically by AD. The
password lives in msDS-ManagedPassword, readable only by principals granted the right.
Whoever can read it gets the password blob, from which you derive the NTLM hash.
| |
Result: NTLM hash for ansible_dev$ = 838b2bd83fbe39901be3713e8c79ce37 (used for
Pass-the-Hash).
ansible_dev$ to sam (ForceChangePassword)
Edge: ansible_dev$ --ForceChangePassword--> sam. ForceChangePassword resets a password
without knowing the old one.
| |
Result: sam : rogue.
sam to john (WriteOwner, GenericAll)
Edge: sam --WriteOwner--> john.
WriteOwner lets you change the owner of john to sam. The owner can modify the DACL,
so grant yourself GenericAll (full control), then GenericAll allows a password reset.
| |
john is in Remote Management Users, so WinRM:
| |
User flag: C:\Users\john\Desktop\user.txt.
Privilege Escalation
john GenericAll over OU=ADCS
BloodHound shows john has GenericAll over the OU ADCS. The OU is empty, so it does
nothing alone, but it is a signal to look at ADCS.
Certipy find, anomaly in the WebServer template
| |
The WebServer template’s Enrollment Rights contains an entry the system cannot
resolve to a name, a raw SID:
| |
Certipy also logs: Failed to lookup user with SID ...-1111.
SID-based ACEs stay active even if the account was deleted. If you restore an object with that SID, the permission works again. An unresolvable SID usually means a deleted/orphaned account, so check the AD Recycle Bin.
AD Recycle Bin, finding the right cert_admin
| |
There are three deleted cert_admin accounts with different SIDs (...-1109, ...-1110,
...-1111). The one that matches the template:
| |
| |
LastKnownParent = OU=ADCS, the OU that john controls.
Step 1, Restore-ADObject
| |
On restore from the Recycle Bin:
- AD reads the object metadata, including
LastKnownParent. - Recreates the object in its original container (
OU=ADCS). - The object returns with the same SID (
...-1111) and attributes.
Because SID ...-1111 exists and resolves again, the Enrollment Rights entry on the
WebServer template automatically points at cert_admin again. This is not set manually; it
follows from the SID returning.
john can restore the object because the restore writes it to OU=ADCS, and john has
GenericAll over that OU, so it may create/write objects there.
Step 2, dacledit, giving john control OVER cert_admin
| |
This does NOT restore permissions to cert_admin. It gives permissions to JOHN over cert_admin.
-action write: add a new ACE.-rights FullControl: full control.-inheritance: the ACE is inheritable, so it flows to child objects of the OU.-principal john: john is the beneficiary.-target-dn OU=ADCS...: the target is the OU (not cert_admin itself).
Logic:
johnhasGenericAlloverOU=ADCS, so it can modify that OU’s DACL.- Write an inheritable
FullControlACE forjohnon the OU. cert_adminafter restore is a child of that OU.- Inheritance propagates the ACE down, so
johngetsFullControlovercert_admin.
Then john (now with FullControl over cert_admin) resets its password:
| |
Two steps, not to be confused:
- Restore: cert_admin regains its own enrollment right on the template (because SID -1111 returns). Automatic.
- dacledit: john gains control OVER cert_admin (via inheritance from the OU), to take it over. A separate thing.
From the output: objects with adminCount=1 will not inherit ACEs from parent OU. If
cert_admin were a protected account (adminCount=1), inheritance would not work. Here it
does.
Step 3, verify the vulnerability
| |
| |
ESC15 / CVE-2024-49019 (“EKUwu”)
Vulnerable condition: a Schema Version 1 template plus EnrolleeSuppliesSubject.
- V1 templates predate the EKU / Application Policy split and do not enforce their own Application Policies.
- An unpatched CA honours Application Policies injected by the requester in the CSR.
- So even though the template declares only Server Authentication, the attacker adds any Application Policy (for example Certificate Request Agent / Enrollment Agent) and the CA issues it.
Step A, obtain an Enrollment Agent certificate:
| |
-username cert_admin -p rogue: authenticate as cert_admin (has the WebServer enrollment right; john would not).-template WebServer: the vulnerable V1 template.--application-policies '1.3.6.1.4.1.311.20.2.1': the injected OID = Certificate Request Agent / Enrollment Agent. This is the essence of ESC15.cert_admin.pfxis now an Enrollment Agent cert, allowing requests on behalf of others.
Step B, request a certificate on behalf of Administrator:
| |
-pfx cert_admin.pfx: use the agent cert.-on-behalf-of tombwatcher\administrator: the enrollment agent enrolls on behalf of another user.-template User: a template that allows Client Authentication (needed to log in with the cert).
Step C, authenticate with the cert and extract the hash (PKINIT + UnPAC-the-hash):
| |
Step D, Pass-the-Hash to a shell:
| |
Root flag: C:\Users\Administrator\Desktop\root.txt.
Why was cert_admin needed at all? john does not have the WebServer enrollment right,
only Domain/Enterprise Admins and SID -1111 (cert_admin) do. The whole Recycle Bin chain
exists to recover and take over an account that can issue that certificate.
Detection and Mitigation
- Kerberoast / targeted Kerberoast: monitor Event 4769 (TGS, especially RC4/etype 23),
alert on sudden
servicePrincipalNamechanges (Event 5136). Strong service-account passwords, gMSA. - gMSA: limit
PrincipalsAllowedToRetrieveManagedPasswordto the minimum; auditmsDS-ManagedPasswordreads. - ACL abuse (WriteOwner/GenericAll/ForceChangePassword): regular BloodHound audits; Event 5136 (DACL/owner changes), 4724 (password reset).
- AD Recycle Bin: alert on
Restore-ADObject; regularly review Deleted Objects; remove orphaned SID ACEs from templates and objects. - ADCS ESC15: patch CVE-2024-49019; retire or raise Schema Version 1 templates; disable
EnrolleeSuppliesSubjectwhere unneeded; restrict enrollment rights; require Manager Approval on sensitive templates. - Least privilege on OUs; avoid broadly delegated GenericAll.
Lessons Learned
| Concept | Point |
|---|---|
| WriteSPN | Add an SPN -> targeted Kerberoast -> offline crack |
| gMSA / msDS-ManagedPassword | AD-managed password; readable by authorized principals -> NTLM |
| ForceChangePassword | Reset a password without knowing the old one |
| WriteOwner | Change the owner -> grant yourself GenericAll -> reset |
| Orphaned SID in an ACE | Permissions by SID survive account deletion; restore revives them |
| LastKnownParent | Which OU a Recycle Bin object returns to (here, a controlled OU) |
| Inheritable ACE on an OU | Control over an OU -> FullControl over its children (including a restored account) |
| ESC15 / CVE-2024-49019 | V1 + EnrolleeSuppliesSubject -> inject an Application Policy (Enrollment Agent) -> cert on-behalf-of |
| Enrollment Agent + on-behalf-of | The agent issues a cert on behalf of any user (for example Administrator) |
| certipy auth (PKINIT) | Log in with a cert -> TGT -> UnPAC-the-hash -> NT hash |
Command Reference
| |
OPSEC: dacledit writes a DACL backup (.bak); restore the original DACL after practice
on a real environment. Certificates issued by the CA remain in the CA database (available
for detection/revocation).