Skip to content

Commit f2e81c7

Browse files
committed
feat(esc13): add ESC13 (group-linked OID policy) detection
- Add 5 new LS2AdcsObject properties for OID/group-link tracking (CertificatePolicy, CertTemplateOID, OIDToGroupLink, HasLinkedGroupOIDPolicy, LinkedGroupOIDPolicies) - Implement Set-LinkedGroupOIDPolicy to cross-reference msPKI-Enterprise-Oid objects in AdcsObjectStore via OID lookup map - Add ESC13 definition to ESCDefinitions.ps1 with Conditions, IssueTemplate, FixTemplate, and RevertTemplate - Wire ESC13 into Find-LS2VulnerableTemplate (ValidateSet, $(LinkedGroup) expansion), Initialize-AdcsObjectStore pipeline, Initialize-LS2Scan, and Invoke-Locksmith2 - Add 22 new tests (Set-LinkedGroupOIDPolicy + ESC13 scan path); 627 total, 0 failures; build clean
1 parent 5dd06a4 commit f2e81c7

11 files changed

Lines changed: 568 additions & 6 deletions

Classes/LS2AdcsObject.ps1

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ class LS2AdcsObject {
2323
[string[]]$RAApplicationPolicies # msPKI-RA-Application-Policies
2424
[Nullable[int]]$TemplateSchemaVersion # msPKI-Template-Schema-Version
2525
[Nullable[int]]$TemplateMinorRevision # msPKI-Template-Minor-Revision
26+
[string[]]$CertificatePolicy # msPKI-Certificate-Policy (OIDs on templates)
27+
[string]$CertTemplateOID # msPKI-Cert-Template-OID (on msPKI-Enterprise-Oid objects)
28+
[string]$OIDToGroupLink # msDS-OIDToGroupLink (on msPKI-Enterprise-Oid objects)
2629

2730
# CA properties (pKIEnrollmentService)
2831
[string[]]$certificateTemplates
@@ -67,6 +70,8 @@ class LS2AdcsObject {
6770
[object[]]$DisableExtensionList
6871
[Nullable[bool]]$SecurityExtensionDisabled
6972
[object[]]$WebEnrollmentEndpoints
73+
[Nullable[bool]]$HasLinkedGroupOIDPolicy # true when ≥1 CertificatePolicy OID links to a group
74+
[string[]]$LinkedGroupOIDPolicies # group DNs linked via OID application policies
7075

7176
# Schema class name for easy type checking
7277
[string]$SchemaClassName
@@ -114,6 +119,9 @@ class LS2AdcsObject {
114119
$this.RAApplicationPolicies = if ($DirectoryEntry.Properties.Contains('msPKI-RA-Application-Policies')) { @($DirectoryEntry.Properties['msPKI-RA-Application-Policies']) } else { @() }
115120
$this.TemplateSchemaVersion = if ($DirectoryEntry.Properties.Contains('msPKI-Template-Schema-Version')) { [int]$DirectoryEntry.Properties['msPKI-Template-Schema-Version'][0] } else { $null }
116121
$this.TemplateMinorRevision = if ($DirectoryEntry.Properties.Contains('msPKI-Template-Minor-Revision')) { [int]$DirectoryEntry.Properties['msPKI-Template-Minor-Revision'][0] } else { $null }
122+
$this.CertificatePolicy = if ($DirectoryEntry.Properties.Contains('msPKI-Certificate-Policy')) { @($DirectoryEntry.Properties['msPKI-Certificate-Policy']) } else { @() }
123+
$this.CertTemplateOID = if ($DirectoryEntry.Properties.Contains('msPKI-Cert-Template-OID')) { $DirectoryEntry.Properties['msPKI-Cert-Template-OID'][0] } else { $null }
124+
$this.OIDToGroupLink = if ($DirectoryEntry.Properties.Contains('msDS-OIDToGroupLink')) { $DirectoryEntry.Properties['msDS-OIDToGroupLink'][0] } else { $null }
117125

118126
# Security descriptor and ownership
119127
try {
@@ -135,6 +143,8 @@ class LS2AdcsObject {
135143
# Initialize computed properties to defaults
136144
$this.SANAllowed = $null
137145
$this.AuthenticationEKUExist = $null
146+
$this.HasLinkedGroupOIDPolicy = $null
147+
$this.LinkedGroupOIDPolicies = @()
138148
$this.AnyPurposeEKUExist = $null
139149
$this.EnrollmentAgentEKUExist = $null
140150
$this.NoSecurityExtension = $null

Locksmith2.psd1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
Description='An AD CS toolkit for AD Admins, Defensive Security Professionals, and Filthy Red Teamers'
99
FunctionsToExport=@('*')
1010
GUID='e32f7d0d-2b10-4db2-b776-a193958e3d69'
11-
ModuleVersion='2026.5.121323'
11+
ModuleVersion='2026.5.131651'
1212
PowerShellVersion='5.1'
1313
PrivateData=@{
1414
PSData=@{

Private/Data/ESCDefinitions.ps1

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,53 @@ $script:ESCDefinitions = data {
227227
)
228228
}
229229

230+
ESC13 = @{
231+
# ESC13: Vulnerable Certificate Template - Group-Linked
232+
Technique = 'ESC13'
233+
234+
# A template is vulnerable when it can be used for authentication AND at least one of its
235+
# application policy OIDs (msPKI-Certificate-Policy) is linked to a universal group via
236+
# msDS-OIDToGroupLink on an msPKI-Enterprise-Oid AD object.
237+
Conditions = @(
238+
@{ Property = 'AuthenticationEKUExist'; Value = $true }
239+
@{ Property = 'HasLinkedGroupOIDPolicy'; Value = $true }
240+
)
241+
242+
# Properties to check for problematic enrollees
243+
EnrolleeProperties = @(
244+
'DangerousEnrollee'
245+
'LowPrivilegeEnrollee'
246+
)
247+
248+
# Issue description template
249+
IssueTemplate = @(
250+
"`$(IdentityReference) can enroll in the `$(TemplateName) template, which uses a Client Authentication EKU "
251+
"and has an application policy OID linked to the group `$(LinkedGroup) in Active Directory.`n`n"
252+
"If this certificate is used for authentication, the holder will silently gain the rights of the linked "
253+
"group. This group membership is not visible via standard AD enumeration tools.`n`n"
254+
"An attacker can exploit this by enrolling in the template and then using the resulting certificate to "
255+
"authenticate, gaining the privileges of the linked group without appearing in its member list.`n`n"
256+
"More info:`n"
257+
" - https://posts.specterops.io/adcs-esc13-abuse-technique-fda4272fbd53"
258+
)
259+
260+
# Fix script template (quick mitigation — Manager Approval)
261+
FixTemplate = @(
262+
"# Quick mitigation: Enable Manager Approval to require approval before certificate issuance"
263+
"`$Object = '`$(DistinguishedName)'"
264+
"Get-ADObject `$Object | Set-ADObject -Replace @{'msPKI-Enrollment-Flag' = 2}"
265+
"# Long-term fix: remove the OID-to-group link from the msPKI-Enterprise-Oid object"
266+
"# Get-ADObject '<OID object DN>' | Set-ADObject -Clear msDS-OIDToGroupLink"
267+
)
268+
269+
# Revert script template
270+
RevertTemplate = @(
271+
"# Disable Manager Approval"
272+
"`$Object = '`$(DistinguishedName)'"
273+
"Get-ADObject `$Object | Set-ADObject -Replace @{'msPKI-Enrollment-Flag' = 0}"
274+
)
275+
}
276+
230277
ESC6 = @{
231278
Technique = 'ESC6'
232279

Private/Initialize/Initialize-AdcsObjectStore.ps1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ function Initialize-AdcsObjectStore {
6868
$Templates = $Templates |
6969
Set-SANAllowed |
7070
Set-AuthenticationEKUExist |
71+
Set-LinkedGroupOIDPolicy |
7172
Set-AnyPurposeEKUExist |
7273
Set-EnrollmentAgentEKUExist |
7374
Set-RequiresEnrollmentAgentSignature |

Private/Initialize/Initialize-LS2Scan.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ function Initialize-LS2Scan {
153153
try {
154154
# Scan all template techniques
155155
Write-Verbose "Scanning certificate templates..."
156-
$templateTechniques = @('ESC1', 'ESC2', 'ESC3c1', 'ESC3c2', 'ESC9', 'ESC4a', 'ESC4o')
156+
$templateTechniques = @('ESC1', 'ESC2', 'ESC3c1', 'ESC3c2', 'ESC9', 'ESC4a', 'ESC4o', 'ESC13')
157157
foreach ($tech in $templateTechniques) {
158158
Find-LS2VulnerableTemplate -Technique $tech | Out-Null
159159
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
function Set-LinkedGroupOIDPolicy {
2+
<#
3+
.SYNOPSIS
4+
Adds HasLinkedGroupOIDPolicy and LinkedGroupOIDPolicies properties to AD CS certificate
5+
template objects.
6+
7+
.DESCRIPTION
8+
Examines the CertificatePolicy property (msPKI-Certificate-Policy) of each certificate
9+
template against the set of msPKI-Enterprise-Oid objects in the AdcsObjectStore.
10+
11+
An msPKI-Enterprise-Oid object that has msDS-OIDToGroupLink set links its OID value to
12+
a universal group. When a certificate template lists such an OID as an application
13+
policy and supports Client Authentication, any principal who enrolls and uses the issued
14+
certificate gains the rights of the linked group while that group's membership appears
15+
empty — the ESC13 attack path.
16+
17+
This function adds two synthetic properties to each certificate template:
18+
1. HasLinkedGroupOIDPolicy: Boolean indicating whether at least one policy OID on the
19+
template is linked to a group via msDS-OIDToGroupLink.
20+
2. LinkedGroupOIDPolicies: Array of group DNs linked to the template's policy OIDs.
21+
22+
IMPORTANT: This function requires $script:AdcsObjectStore to be fully populated before
23+
it is called. It must run after Get-AdcsObject has completed so that all
24+
msPKI-Enterprise-Oid objects are present in the store.
25+
26+
.PARAMETER AdcsObject
27+
One or more LS2AdcsObject instances representing AD CS certificate templates.
28+
Non-template objects are passed through unmodified.
29+
30+
.INPUTS
31+
LS2AdcsObject[]
32+
33+
.OUTPUTS
34+
LS2AdcsObject[]
35+
Returns the input objects. Templates have HasLinkedGroupOIDPolicy and
36+
LinkedGroupOIDPolicies set. Non-templates are passed through unchanged.
37+
38+
.EXAMPLE
39+
$templates | Set-LinkedGroupOIDPolicy
40+
Processes all certificate templates and adds the linked group OID properties.
41+
42+
.EXAMPLE
43+
Get-AdcsObject | Where-Object { $_.IsCertificateTemplate() } | Set-LinkedGroupOIDPolicy
44+
Retrieves all templates and evaluates which ones have group-linked application policy OIDs.
45+
46+
.NOTES
47+
Author: Jake Hildreth (@jakehildreth)
48+
Module: Locksmith2
49+
Requires: PowerShell 5.1+
50+
51+
Requires script-scope variable set by Initialize-AdcsObjectStore:
52+
- $script:AdcsObjectStore: Cache of all AD CS objects (must include msPKI-Enterprise-Oid objects)
53+
54+
Used by ESC13 detection in Find-LS2VulnerableTemplate.
55+
56+
Reference: https://posts.specterops.io/adcs-esc13-abuse-technique-fda4272fbd53
57+
58+
.LINK
59+
Set-AuthenticationEKUExist
60+
61+
.LINK
62+
Find-LS2VulnerableTemplate
63+
#>
64+
[CmdletBinding()]
65+
param(
66+
[Parameter(Mandatory, ValueFromPipeline)]
67+
[LS2AdcsObject[]]$AdcsObject
68+
)
69+
70+
begin {
71+
Write-Verbose 'Building OID-to-group lookup map from AdcsObjectStore...'
72+
73+
# Build a map of CertTemplateOID -> OIDToGroupLink for all OID objects that have a group link.
74+
# msPKI-Enterprise-Oid objects live in CN=OID,CN=Public Key Services,... and are collected
75+
# by Get-AdcsObject as part of its full subtree search.
76+
$oidGroupMap = @{}
77+
if ($script:AdcsObjectStore) {
78+
$script:AdcsObjectStore.Values | Where-Object { $_.OIDToGroupLink } | ForEach-Object {
79+
if ($_.CertTemplateOID) {
80+
$oidGroupMap[$_.CertTemplateOID] = $_.OIDToGroupLink
81+
Write-Verbose " OID '$($_.CertTemplateOID)' -> group '$($_.OIDToGroupLink)'"
82+
}
83+
}
84+
}
85+
Write-Verbose "OID-to-group map has $($oidGroupMap.Count) entry/entries"
86+
}
87+
88+
process {
89+
foreach ($obj in $AdcsObject) {
90+
if ($obj.SchemaClassName -ne 'pKICertificateTemplate') {
91+
$obj
92+
continue
93+
}
94+
95+
try {
96+
$linkedGroups = [System.Collections.Generic.List[string]]::new()
97+
98+
if ($oidGroupMap.Count -gt 0 -and $obj.CertificatePolicy -and $obj.CertificatePolicy.Count -gt 0) {
99+
foreach ($oid in $obj.CertificatePolicy) {
100+
if ($oidGroupMap.ContainsKey($oid)) {
101+
$linkedGroups.Add($oidGroupMap[$oid])
102+
Write-Verbose " Template '$($obj.Name)': policy OID '$oid' links to group '$($oidGroupMap[$oid])'"
103+
}
104+
}
105+
}
106+
107+
$obj.LinkedGroupOIDPolicies = $linkedGroups.ToArray()
108+
$obj.HasLinkedGroupOIDPolicy = ($linkedGroups.Count -gt 0)
109+
110+
Write-Verbose " Template '$($obj.Name)': HasLinkedGroupOIDPolicy=$($obj.HasLinkedGroupOIDPolicy)"
111+
} catch {
112+
$errorRecord = [System.Management.Automation.ErrorRecord]::new(
113+
$_.Exception,
114+
'SetLinkedGroupOIDPolicyFailed',
115+
[System.Management.Automation.ErrorCategory]::NotSpecified,
116+
$obj.distinguishedName
117+
)
118+
$PSCmdlet.WriteError($errorRecord)
119+
}
120+
121+
$obj
122+
}
123+
}
124+
}

Public/Find-LS2VulnerableTemplate.ps1

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ function Find-LS2VulnerableTemplate {
7272
[CmdletBinding()]
7373
param(
7474
[Parameter()]
75-
[ValidateSet('ESC1', 'ESC2', 'ESC3c1', 'ESC3c2', 'ESC9', 'ESC4a', 'ESC4o')]
75+
[ValidateSet('ESC1', 'ESC2', 'ESC3c1', 'ESC3c2', 'ESC9', 'ESC4a', 'ESC4o', 'ESC13')]
7676
[string]$Technique,
7777

7878
[Parameter()]
@@ -102,7 +102,7 @@ function Find-LS2VulnerableTemplate {
102102
if (-not $Technique) {
103103
Write-Verbose "No technique specified. Returning all template issues..."
104104
$allIssues = Get-FlattenedIssues
105-
$templateTechniques = @('ESC1', 'ESC2', 'ESC3c1', 'ESC3c2', 'ESC9', 'ESC4a', 'ESC4o')
105+
$templateTechniques = @('ESC1', 'ESC2', 'ESC3c1', 'ESC3c2', 'ESC9', 'ESC4a', 'ESC4o', 'ESC13')
106106
$templateIssues = $allIssues | Where-Object { $_.Technique -in $templateTechniques }
107107

108108
if ($ExpandGroups) {
@@ -436,9 +436,16 @@ function Find-LS2VulnerableTemplate {
436436
$identityReferenceName = ($ace.IdentityReference | Convert-IdentityReferenceToNTAccount).Value
437437

438438
# Expand template variables in Issue, Fix, and Revert strings
439+
$linkedGroup = if ($template.LinkedGroupOIDPolicies -and $template.LinkedGroupOIDPolicies.Count -gt 0) {
440+
$template.LinkedGroupOIDPolicies -join ', '
441+
} else {
442+
''
443+
}
444+
439445
$issueText = $issueTemplate `
440446
-replace '\$\(IdentityReference\)', $identityReferenceName `
441-
-replace '\$\(TemplateName\)', $template.Name
447+
-replace '\$\(TemplateName\)', $template.Name `
448+
-replace '\$\(LinkedGroup\)', $linkedGroup
442449

443450
$fixScript = $fixTemplate `
444451
-replace '\$\(DistinguishedName\)', $template.distinguishedName

Public/Invoke-Locksmith2.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ function Invoke-Locksmith2 {
198198
Write-Verbose "`nScan complete. Issue summary:"
199199
$techniques = @(
200200
'ESC1', 'ESC2', 'ESC3c1', 'ESC3c2', 'ESC4a', 'ESC4o',
201-
'ESC5a', 'ESC5o', 'ESC6', 'ESC7a', 'ESC7m', 'ESC8', 'ESC9', 'ESC11', 'ESC16'
201+
'ESC5a', 'ESC5o', 'ESC6', 'ESC7a', 'ESC7m', 'ESC8', 'ESC9', 'ESC11', 'ESC13', 'ESC16'
202202
)
203203

204204
foreach ($technique in $techniques) {

0 commit comments

Comments
 (0)