Strong Certificate Mapping Only Helps If the CA Owns the SID. ESC16 Takes It Away
Microsoft spent three years walking the industry toward strong certificate mapping. KB5014754 shipped in May 2022 with the new SID security extension (szOID_NTDS_CA_SECURITY_EXT, OID 1.3.6.1.4.1.311.25.2), a compatibility window, and a promised enforcement date that slipped more than once. The StrongCertificateBindingEnforcement registry override stopped being honored with the September 9, 2025 update — the value may still sit in the registry, but a patched DC ignores it and enforces. So as of now, a domain controller validating a certificate logon wants a strong binding: the certificate carries the account’s objectSid inside that extension, and the KDC checks it against the account it’s about to authenticate. Implicit mapping by UPN or SAN alone — the thing that made ESC1 a one-shot path to Domain Admin — is supposed to be dead.
On a domain controller that actually reached Full Enforcement, the naive version of this is dead — and that matters, because the technique’s write-ups get it backwards in both directions. ESC16 used by itself does not walk around Full Enforcement. Under StrongCertificateBindingEnforcement = 2 the KDC demands a strong mapping; a certificate carrying nothing but a forged UPN or SAN name has none, and the logon is denied with Event ID 39. Strip the SID and present only a weak name, and a fully-enforcing DC rejects the cert outright.
But “ESC16 by itself” is the wrong frame. ESC16 doesn’t authenticate anyone — it removes the CA’s authoritative SID so that whatever mapping value is left in the certificate becomes the one the KDC trusts. On a Compatibility-mode DC that leftover is a weak SAN name, and the DC falls back to it. On a fully-enforcing DC it can be a strong mapping the attacker supplied — the SID-in-SAN URI, below — which the now-missing authoritative extension is no longer there to override. Full Enforcement kills bare ESC16. It does not kill ESC16 chained with a way to supply its own SID.
The reason ESC16 still matters is that “we hit the September deadline” and “every DC is actually at 2” are different sentences. ESC16 is the technique that turns the DCs still sitting in Compatibility mode — the ones a broken app or a missed patch left behind — from “weak fallback, mostly harmless” into a straight path to Domain Admin.
Here’s the shape of it, and the enforcement mode is everything. When the SID extension is present, every mode above Disabled checks it: if it doesn’t match the account being logged into, the auth fails with Event ID 41 — in Compatibility mode and Full Enforcement alike. That is what neutralizes a vanilla ESC1. When a low-priv user enrolls for a certificate naming a Domain Admin in the SAN, the CA doesn’t take the requester’s word for who they are — it stamps the enrollee’s own objectSid into the extension. The DC reads that SID, sees it belongs to svc_lowpriv and not the administrator named in the SAN, and blocks the logon. The extension is doing exactly its job.
The asymmetry is what happens when the extension is absent. Full Enforcement denies — no strong mapping, no logon. But Compatibility mode falls back to the weak SAN/UPN mapping and allows it. So against a Compatibility-mode DC the attacker’s entire problem is getting rid of the SID the CA insists on stamping, and that is what ESC16 delivers. ESC16 is the CA-wide misconfiguration where the certification authority is told to never populate that extension on anything it issues. Not one template. Every certificate, every requester. Set szOID_NTDS_CA_SECURITY_EXT into the CA’s DisableExtensionList and the SID stops getting stamped, forest-wide, from a single registry value on the CA host. Now the ESC1 certificate carries no SID to contradict its forged SAN, the Compatibility-mode DC falls back to that SAN, and the low-priv user authenticates as the admin. ESC16 isn’t redundant with a weak-mapping DC — it’s the piece that makes the weak mapping reachable.
That covers the Compatibility case. The reason ESC16 is more than a compatibility-mode curiosity is the second path, and it survives Full Enforcement. Microsoft’s own strong-mapping scheme accepts a SID carried in the certificate’s SAN, as a URL of the form tag:microsoft.com,2022-09-14:sid:<SID> — added so that CAs which can’t stamp the extension (SCEP, offline, third-party) can still produce strongly-mapped certs. That SAN SID is a strong mapping, not a weak one. Normally it doesn’t help an attacker, because the CA also stamps the authoritative szOID_NTDS_CA_SECURITY_EXT with the enrollee’s real SID and the KDC prefers the extension over the SAN. Take the extension away — that’s ESC16 — and pair it with any primitive that lets the requester choose the SAN (ESC6’s EDITF_ATTRIBUTESUBJECTALTNAME2, or ESC15’s arbitrary-SAN enrollment), and the attacker writes ...:sid:<Domain-Admin-SID> into the SAN themselves. The KDC finds no authoritative extension, sees a well-formed strong SID mapping, and honors it. StrongCertificateBindingEnforcement = 2 doesn’t stop this, because from the DC’s point of view the certificate genuinely is strongly mapped — to the SID the attacker chose.
Think about where that leaves the enforcement story. Flipping every DC to Full Enforcement closes the bare technique — a certificate carrying only a weak name simply fails — but it doesn’t touch the SID-in-SAN chain above, and it does nothing for a DC that quietly slipped back to Compatibility. The trouble is that the halves of this invariant live in different places, owned by different teams, and all of them drift. The DC half (StrongCertificateBindingEnforcement) is the one everyone tracked to the September deadline. The CA half — szOID_NTDS_CA_SECURITY_EXT in HKLM\SYSTEM\CurrentControlSet\Services\CertSvc\Configuration\<CAName>\PolicyModules\CertificateAuthority_MicrosoftDefault.Policy\DisableExtensionList — is the one nobody watches, and it turns into an escalation the instant a single DC slips back to Compatibility. ESC9 was this same “no SID extension” idea scoped to one template with the CT_FLAG_NO_SECURITY_EXTENSION bit. ESC16 is the CA-wide version, set from that one registry value, and it’s nastier precisely because it’s invisible from the template ACLs everyone learned to audit after Certified Pre-Owned in 2021 — and because it lies dormant until it meets a DC that isn’t really at 2.
Where ESC15 fits, because they get conflated
ESC15 — EKUwu, CVE-2024-49019, Justin Bollinger’s find at TrustedSec in late 2024 — is a different animal that people file in the same drawer. It abuses the fact that the built-in version 1 templates (WebServer is the canonical example) can’t be customized, and that Microsoft’s proprietary Application Policies extension takes precedence over the standard X.509 EKU. Per Microsoft’s own documentation, if a certificate carries an Application Policies extension and an EKU extension, the EKU is ignored. So a requester with enrollment rights on a stock V1 template can craft a CSR that injects, say, Client Authentication or Certificate Request Agent as an application policy, and the CA honors it even though the template says Server Authentication. The template looks locked down. The issued cert authenticates a user.
The reason to keep ESC15 and ESC16 straight: ESC15 gives you a cert that can do client auth it shouldn’t. ESC16 makes sure that cert has no authoritative SID extension to override the mapping. Chained, they’re worse than either alone. On a Compatibility-mode DC the EKU trick produces the auth cert and the missing SID drops it into weak SAN mapping. And because ESC15 lets the requester supply an arbitrary SAN, the same chain reaches the strong-mapping path from the previous section on a fully-enforcing DC: inject ...2022-09-14:sid:<target-SID> alongside the client-auth application policy and the SID-less cert the CA hands back is strongly mapped to whoever you named. The EKU alone doesn’t beat Full Enforcement — the EKU plus an attacker-chosen SID-in-SAN does. CVE-2024-49019 is patched — apply the November 12, 2024 fix and the V1 application-policy injection stops working — but patch coverage on internal CA hosts is not something I’d assume on a two-tier PKI where the issuing CA is a pet server nobody reboots without a change ticket.
What the detection actually looks like
Start with the bad news, because it’s the part most teams discover the hard way. The Certificate Services audit events you want — 4886 (request received), 4887 (request approved and certificate issued), 4870 (revoked), 4899/4900 (template or template-security changed), 4891/4892 (CA configuration or property changed) — do not exist on a default CA. Certification Authority object-access auditing is off out of the box. You turn it on in two places, and people forget the second one: the advanced audit policy subcategory (Audit Certification Services under Object Access) and the CA’s own audit filter, certutil -setreg CA\AuditFilter 127 followed by a CertSvc restart. AuditFilter 127 enables all seven categories. Skip it and the Windows audit policy has nothing to write. I have watched more than one “we have full ADCS logging” claim evaporate the moment someone actually greps the Security log on the issuing CA and finds no 4887s at all.
Assume you fixed that. Now the workhorse detection for the SAN-abuse family is 4887 correlation. Event 4887 records the requester and the subject/SAN of the issued certificate. The classic ESC1-style tell is a SAN that names a different, higher-privileged principal than the account that submitted the request — svc_lowpriv enrolls, the certificate’s SAN says administrator@corp.local. In Splunk, if you have the CA’s Security channel forwarded, the sketch is roughly:
index=wineventlog source="WinEventLog:Security" EventCode=4887
| rex field=Attributes "(?i)san:upn=(?<san_upn>[^\s&,]+)"
| where lower(Requester) != lower(san_upn)
The real-world version has more eval glue than that, because the requester field and the subject encoding don’t line up cleanly and the SAN can arrive in the Attributes blob rather than a tidy field. Volume is the other reality check. On a healthy issuing CA in a mid-size shop, 4887 fires every time anything enrolls — machine auto-enrollment, the NDES/SCEP endpoint feeding mobile devices, web server certs, code-signing. Expect a steady baseline of legitimate issuance in the hundreds to low thousands per day depending on auto-enrollment scope. The mismatch condition itself is rare, so the SAN-mismatch rule is usually quiet once it’s built. The first-round tuning problem is not volume, it’s parsing: the certificate subject and SAN come through inconsistently, half your 4887s have an empty SAN because the enrollment used the subject DN, and the requester-to-SAN comparison throws false negatives whenever the account naming convention doesn’t match the UPN suffix. Budget a day for the field extraction, not the logic.
The ESC16 signal is different and, honestly, harder. You’re not looking for a mismatch — you’re looking for an absence. Certificates that authenticate users but carry no SID extension. The 4887 event does not tell you whether the SID extension was populated; that data isn’t in the event. You have to look at the issued certificate itself, or query the CA database with certutil -view, or catch the config change. So the higher-fidelity ESC16 detection is really a configuration-drift alert: watch for writes to the CA’s DisableExtensionList that include 1.3.6.1.4.1.311.25.2, and alert on the CertSvc service restart that has to follow for it to take effect. That’s a registry-monitoring problem (Sysmon Event ID 13 on the CA if you scope the CertSvc\Configuration key, or event 4891/4892 if CA config auditing is on), not a certificate-issuance problem. If your Sysmon config on the CA doesn’t include the CertSvc configuration path, and most stock configs don’t, you have no ESC16 config-drift detection at all. That gap is the whole ballgame.
The drift alert catches the arming. It won’t catch a CA that was already misconfigured before you started watching, and it won’t catch the SID-in-SAN variant at all. For that you have to look at what actually got issued: periodically dump the CA database and inspect the certificates themselves. certutil -view -restrict "NotAfter>=<date>" -out "RequestID,RequesterName,CertificateTemplate,RawCertificate" pulls the raw certs; decode the authentication-capable ones and flag two things — a client-auth cert with no 1.3.6.1.4.1.311.25.2 extension (the ESC16 tell), and any SAN containing tag:microsoft.com,2022-09-14:sid: (the attacker-supplied strong mapping). The second is the only IOC that catches the Full-Enforcement chain, because that logon succeeds as a legitimate-looking strong mapping and never trips a weak-mapping warning.
There’s a second-order detection worth building for the mapping side, and for the Compatibility-mode variant it’s the highest-value one. On the DCs, event 4768 logs the Kerberos TGT request; certificate-based pre-authentication shows PreAuthType 16. Events 39 and 41 are the strong-mapping tell — and they land in the System log on the DCs, source Kdcsvc (Microsoft-Windows-Kerberos-Key-Distribution-Center), not a separate operational channel, so forward the System channel and don’t go hunting for a dedicated one. Enforcement mode decides what they mean: in Full Enforcement they accompany a denial, but in Compatibility mode they fire as warnings while the logon succeeds on a weak mapping — and that second case is precisely the bare-ESC16 success condition. A 39/41 warning paired with a successful PKINIT logon, on a DC you believed was at Full Enforcement, means that DC is actually still falling back and something just authenticated with a SID-less certificate. Watch it — but know its blind spot: the SID-in-SAN chain authenticates as a strong mapping, so it produces no weak-mapping warning at all. For that variant, the certificate-content audit above is your only catch.
False positives, and the environment that changes the answer
The SAN-mismatch rule’s false positives come from legitimate enrollment-on-behalf-of. Enrollment agents — the Certificate Request Agent EKU exists for a reason — request certificates whose subject is somebody else by design. Smartcard enrollment stations do this all day. If you don’t carve out your known enrollment-agent accounts, the ESC1 rule lights up on the exact workflow it’s supposed to distinguish from an attack, and the SOC learns to close it. NDES is the other reliable noise source: the SCEP service account requests on behalf of devices, so requester-versus-subject mismatch is its normal state. Whitelist the NDES service account or you’ll drown.
The environment assumptions that move all of this:
- Standalone versus enterprise CA. ESC15 and the template-driven attacks assume an enterprise CA with AD-integrated templates. A standalone CA doesn’t use templates the same way; the attack surface and the events differ.
- Whether you actually reached full enforcement. This is the pivot the whole technique turns on, so don’t get it backwards: a DC still at
StrongCertificateBindingEnforcement = 1(Compatibility) is not a DC where ESC16 is redundant — it’s the DC where ESC16 works. Compatibility still checks the SID when it’s present, so a plain ESC1 fails on the SID mismatch; ESC16 is what strips the SID so the fallback to weak SAN mapping kicks in. Full Enforcement (2) neutralizes bare ESC16, because there a SID-less cert carrying only a weak name is denied outright — though, as above, it does not neutralize ESC16 chained with a SID-in-SAN injection. So the legacy app that broke at 2 and got rolled back to 1 “temporarily” eighteen months ago didn’t make you safe from ESC16 — it re-armed the exact condition ESC16 needs. Grep that registry value (HKLM\SYSTEM\CurrentControlSet\Services\Kdc\StrongCertificateBindingEnforcement) on every DC before you trust the enforcement narrative. The registry, not the change ticket that claims you’re at 2. - PKI tier and patch latency. On a properly tiered PKI the issuing CA is locked down and monitored. In the far more common two-tier setup where the issuing CA doubles as the thing that hasn’t been patched since the CVE-2024-49019 fix shipped, ESC15 is live and ESC16 is a registry edit away.
Tooling reality: Certipy crossed into the 5.x line and, as of August 2025, enumerates the full ESC1–ESC16 set, which means the recon that finds a vulnerable CA is a single command for anyone who’s already got a foothold and a low-priv account with enrollment rights. Locksmith and TameMyCerts on the defensive side will flag the template and CA misconfigurations; run one of them before an attacker runs Certipy for you.
Control mappings
| Control | Why it’s in scope |
|---|---|
| SC-17 | PKI certificate issuance and management — the CA config that defines whether the SID extension is stamped lives here |
| IA-5(2) | PKI-based authenticator management; strong binding is the control ESC16 defeats |
| AC-6 | Least privilege on template enrollment rights — the precondition for ESC15 and the ESC1 family |
| AU-2, AU-12 | CA object-access auditing that produces 4886/4887 and is off by default |
| CM-6, CM-7 | CA and template configuration baselines; the DisableExtensionList value is a config item that should be baselined and drift-alerted |
| SI-4 | Monitoring for the SAN mismatch, the config drift, and the post-enforcement weak-mapping fallback events |
| RA-5 | Certipy/Locksmith-style assessment of the PKI as part of vulnerability scanning |
The uncomfortable summary: the enforcement deadline everyone treated as the end of ADCS escalation genuinely does raise the bar — but “the deadline passed” is not evidence that your DCs are actually at 2, and “at 2” is not the clean win it sounds like. Full Enforcement kills the bare technique: a SID-less cert with only a weak name is denied. It does not kill a CA that lets a requester choose the SAN, because then the attacker writes the SID in himself as a strong mapping (...2022-09-14:sid:) and the DC honors it. And where a DC quietly slipped back to Compatibility — a broken app, a missed cumulative update — even that much isn’t needed; the CA declining to write the SID becomes a Domain Admin logon on the weak SAN fallback alone. The gap between “supposed to be enforced” and “is enforced” is the entire attack surface, and the audit trail that would show you which side of it you’re on is off until you turn it on, doesn’t record the extension state even when it’s on, and lives on a server that in most shops nobody wants to touch. Turn on CA auditing. Baseline DisableExtensionList. Watch the DC System log for successful weak-mapping fallback, and audit what your CA actually issues for missing SID extensions and sid: SAN URIs. Then go check the actual StrongCertificateBindingEnforcement value on every DC, because I would not bet on the change ticket.
Sources
- ESC16 and the ESC6 SID-in-SAN chain — Certipy Privilege Escalation wiki (ly4k)
- EKUwu: Not just another AD CS ESC (TrustedSec)
- ESC15 (EKUwu)/CVE-2024-49019: Vulnerability in AD CS EKU Certificate Templates (CyCraft)
- AD CS Domain Escalation (HackTricks)
- Breaking ADCS: ESC1 to ESC16 Attack Techniques (xbz0n)
- Certificates security posture assessments — Microsoft Defender for Identity (Microsoft Learn)
- Changes to certificate-based logon to Active Directory with KB5014754 (Uwe Gradenegger)
- Microsoft’s Certificate Strong Mapping Deadline: September 2025 Patch Tuesday and NDES SCEP (tim beer)
- Preventing Privilege Escalation via Active Directory Certificate Services (Cato Networks)
- Detecting ADCS Privilege Escalation (Black Hills Information Security)
- Windows Steal Authentication Certificates – ESC1 Abuse (Splunk Security Content)
- AD CS Template Hardening: ESC1–ESC16 Defense Playbook (Encryption Consulting)