Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Classes/LS2AdcsObject.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ class LS2AdcsObject {
[string[]]$RAApplicationPolicies # msPKI-RA-Application-Policies
[Nullable[int]]$TemplateSchemaVersion # msPKI-Template-Schema-Version
[Nullable[int]]$TemplateMinorRevision # msPKI-Template-Minor-Revision
[string[]]$CertificatePolicy # msPKI-Certificate-Policy (OIDs on templates)
[string]$CertTemplateOID # msPKI-Cert-Template-OID (on msPKI-Enterprise-Oid objects)
[string]$OIDToGroupLink # msDS-OIDToGroupLink (on msPKI-Enterprise-Oid objects)

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

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

# Security descriptor and ownership
try {
Expand All @@ -135,6 +143,8 @@ class LS2AdcsObject {
# Initialize computed properties to defaults
$this.SANAllowed = $null
$this.AuthenticationEKUExist = $null
$this.HasLinkedGroupOIDPolicy = $null
$this.LinkedGroupOIDPolicies = @()
$this.AnyPurposeEKUExist = $null
$this.EnrollmentAgentEKUExist = $null
$this.NoSecurityExtension = $null
Expand Down
2 changes: 1 addition & 1 deletion Locksmith2.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
Description='An AD CS toolkit for AD Admins, Defensive Security Professionals, and Filthy Red Teamers'
FunctionsToExport=@('*')
GUID='e32f7d0d-2b10-4db2-b776-a193958e3d69'
ModuleVersion='2026.5.121323'
ModuleVersion='2026.5.131651'
PowerShellVersion='5.1'
PrivateData=@{
PSData=@{
Expand Down
47 changes: 47 additions & 0 deletions Private/Data/ESCDefinitions.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,53 @@ $script:ESCDefinitions = data {
)
}

ESC13 = @{
# ESC13: Vulnerable Certificate Template - Group-Linked
Technique = 'ESC13'

# A template is vulnerable when it can be used for authentication AND at least one of its
# application policy OIDs (msPKI-Certificate-Policy) is linked to a universal group via
# msDS-OIDToGroupLink on an msPKI-Enterprise-Oid AD object.
Conditions = @(
@{ Property = 'AuthenticationEKUExist'; Value = $true }
@{ Property = 'HasLinkedGroupOIDPolicy'; Value = $true }
)

# Properties to check for problematic enrollees
EnrolleeProperties = @(
'DangerousEnrollee'
'LowPrivilegeEnrollee'
)

# Issue description template
IssueTemplate = @(
"`$(IdentityReference) can enroll in the `$(TemplateName) template, which uses a Client Authentication EKU "
"and has an application policy OID linked to the group `$(LinkedGroup) in Active Directory.`n`n"
"If this certificate is used for authentication, the holder will silently gain the rights of the linked "
"group. This group membership is not visible via standard AD enumeration tools.`n`n"
"An attacker can exploit this by enrolling in the template and then using the resulting certificate to "
"authenticate, gaining the privileges of the linked group without appearing in its member list.`n`n"
"More info:`n"
" - https://posts.specterops.io/adcs-esc13-abuse-technique-fda4272fbd53"
)

# Fix script template (quick mitigation — Manager Approval)
FixTemplate = @(
"# Quick mitigation: Enable Manager Approval to require approval before certificate issuance"
"`$Object = '`$(DistinguishedName)'"
"Get-ADObject `$Object | Set-ADObject -Replace @{'msPKI-Enrollment-Flag' = 2}"
"# Long-term fix: remove the OID-to-group link from the msPKI-Enterprise-Oid object"
"# Get-ADObject '<OID object DN>' | Set-ADObject -Clear msDS-OIDToGroupLink"
)

# Revert script template
RevertTemplate = @(
"# Disable Manager Approval"
"`$Object = '`$(DistinguishedName)'"
"Get-ADObject `$Object | Set-ADObject -Replace @{'msPKI-Enrollment-Flag' = 0}"
)
}

ESC6 = @{
Technique = 'ESC6'

Expand Down
1 change: 1 addition & 0 deletions Private/Initialize/Initialize-AdcsObjectStore.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ function Initialize-AdcsObjectStore {
$Templates = $Templates |
Set-SANAllowed |
Set-AuthenticationEKUExist |
Set-LinkedGroupOIDPolicy |
Set-AnyPurposeEKUExist |
Set-EnrollmentAgentEKUExist |
Set-RequiresEnrollmentAgentSignature |
Expand Down
2 changes: 1 addition & 1 deletion Private/Initialize/Initialize-LS2Scan.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ function Initialize-LS2Scan {
try {
# Scan all template techniques
Write-Verbose "Scanning certificate templates..."
$templateTechniques = @('ESC1', 'ESC2', 'ESC3c1', 'ESC3c2', 'ESC9', 'ESC4a', 'ESC4o')
$templateTechniques = @('ESC1', 'ESC2', 'ESC3c1', 'ESC3c2', 'ESC9', 'ESC4a', 'ESC4o', 'ESC13')
foreach ($tech in $templateTechniques) {
Find-LS2VulnerableTemplate -Technique $tech | Out-Null
}
Expand Down
124 changes: 124 additions & 0 deletions Private/Set/Set-LinkedGroupOIDPolicy.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
function Set-LinkedGroupOIDPolicy {
<#
.SYNOPSIS
Adds HasLinkedGroupOIDPolicy and LinkedGroupOIDPolicies properties to AD CS certificate
template objects.

.DESCRIPTION
Examines the CertificatePolicy property (msPKI-Certificate-Policy) of each certificate
template against the set of msPKI-Enterprise-Oid objects in the AdcsObjectStore.

An msPKI-Enterprise-Oid object that has msDS-OIDToGroupLink set links its OID value to
a universal group. When a certificate template lists such an OID as an application
policy and supports Client Authentication, any principal who enrolls and uses the issued
certificate gains the rights of the linked group while that group's membership appears
empty — the ESC13 attack path.

This function adds two synthetic properties to each certificate template:
1. HasLinkedGroupOIDPolicy: Boolean indicating whether at least one policy OID on the
template is linked to a group via msDS-OIDToGroupLink.
2. LinkedGroupOIDPolicies: Array of group DNs linked to the template's policy OIDs.

IMPORTANT: This function requires $script:AdcsObjectStore to be fully populated before
it is called. It must run after Get-AdcsObject has completed so that all
msPKI-Enterprise-Oid objects are present in the store.

.PARAMETER AdcsObject
One or more LS2AdcsObject instances representing AD CS certificate templates.
Non-template objects are passed through unmodified.

.INPUTS
LS2AdcsObject[]

.OUTPUTS
LS2AdcsObject[]
Returns the input objects. Templates have HasLinkedGroupOIDPolicy and
LinkedGroupOIDPolicies set. Non-templates are passed through unchanged.

.EXAMPLE
$templates | Set-LinkedGroupOIDPolicy
Processes all certificate templates and adds the linked group OID properties.

.EXAMPLE
Get-AdcsObject | Where-Object { $_.IsCertificateTemplate() } | Set-LinkedGroupOIDPolicy
Retrieves all templates and evaluates which ones have group-linked application policy OIDs.

.NOTES
Author: Jake Hildreth (@jakehildreth)
Module: Locksmith2
Requires: PowerShell 5.1+

Requires script-scope variable set by Initialize-AdcsObjectStore:
- $script:AdcsObjectStore: Cache of all AD CS objects (must include msPKI-Enterprise-Oid objects)

Used by ESC13 detection in Find-LS2VulnerableTemplate.

Reference: https://posts.specterops.io/adcs-esc13-abuse-technique-fda4272fbd53

.LINK
Set-AuthenticationEKUExist

.LINK
Find-LS2VulnerableTemplate
#>
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[LS2AdcsObject[]]$AdcsObject
)

begin {
Write-Verbose 'Building OID-to-group lookup map from AdcsObjectStore...'

# Build a map of CertTemplateOID -> OIDToGroupLink for all OID objects that have a group link.
# msPKI-Enterprise-Oid objects live in CN=OID,CN=Public Key Services,... and are collected
# by Get-AdcsObject as part of its full subtree search.
$oidGroupMap = @{}
if ($script:AdcsObjectStore) {
$script:AdcsObjectStore.Values | Where-Object { $_.OIDToGroupLink } | ForEach-Object {
if ($_.CertTemplateOID) {
$oidGroupMap[$_.CertTemplateOID] = $_.OIDToGroupLink
Write-Verbose " OID '$($_.CertTemplateOID)' -> group '$($_.OIDToGroupLink)'"
}
}
}
Write-Verbose "OID-to-group map has $($oidGroupMap.Count) entry/entries"
}

process {
foreach ($obj in $AdcsObject) {
if ($obj.SchemaClassName -ne 'pKICertificateTemplate') {
$obj
continue
}

try {
$linkedGroups = [System.Collections.Generic.List[string]]::new()

if ($oidGroupMap.Count -gt 0 -and $obj.CertificatePolicy -and $obj.CertificatePolicy.Count -gt 0) {
foreach ($oid in $obj.CertificatePolicy) {
if ($oidGroupMap.ContainsKey($oid)) {
$linkedGroups.Add($oidGroupMap[$oid])
Write-Verbose " Template '$($obj.Name)': policy OID '$oid' links to group '$($oidGroupMap[$oid])'"
}
}
}

$obj.LinkedGroupOIDPolicies = $linkedGroups.ToArray()
$obj.HasLinkedGroupOIDPolicy = ($linkedGroups.Count -gt 0)

Write-Verbose " Template '$($obj.Name)': HasLinkedGroupOIDPolicy=$($obj.HasLinkedGroupOIDPolicy)"
} catch {
$errorRecord = [System.Management.Automation.ErrorRecord]::new(
$_.Exception,
'SetLinkedGroupOIDPolicyFailed',
[System.Management.Automation.ErrorCategory]::NotSpecified,
$obj.distinguishedName
)
$PSCmdlet.WriteError($errorRecord)
}

$obj
}
}
}
13 changes: 10 additions & 3 deletions Public/Find-LS2VulnerableTemplate.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ function Find-LS2VulnerableTemplate {
[CmdletBinding()]
param(
[Parameter()]
[ValidateSet('ESC1', 'ESC2', 'ESC3c1', 'ESC3c2', 'ESC9', 'ESC4a', 'ESC4o')]
[ValidateSet('ESC1', 'ESC2', 'ESC3c1', 'ESC3c2', 'ESC9', 'ESC4a', 'ESC4o', 'ESC13')]
[string]$Technique,

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

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

# Expand template variables in Issue, Fix, and Revert strings
$linkedGroup = if ($template.LinkedGroupOIDPolicies -and $template.LinkedGroupOIDPolicies.Count -gt 0) {
$template.LinkedGroupOIDPolicies -join ', '
} else {
''
}

$issueText = $issueTemplate `
-replace '\$\(IdentityReference\)', $identityReferenceName `
-replace '\$\(TemplateName\)', $template.Name
-replace '\$\(TemplateName\)', $template.Name `
-replace '\$\(LinkedGroup\)', $linkedGroup

$fixScript = $fixTemplate `
-replace '\$\(DistinguishedName\)', $template.distinguishedName
Expand Down
2 changes: 1 addition & 1 deletion Public/Invoke-Locksmith2.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ function Invoke-Locksmith2 {
Write-Verbose "`nScan complete. Issue summary:"
$techniques = @(
'ESC1', 'ESC2', 'ESC3c1', 'ESC3c2', 'ESC4a', 'ESC4o',
'ESC5a', 'ESC5o', 'ESC6', 'ESC7a', 'ESC7m', 'ESC8', 'ESC9', 'ESC11', 'ESC16'
'ESC5a', 'ESC5o', 'ESC6', 'ESC7a', 'ESC7m', 'ESC8', 'ESC9', 'ESC11', 'ESC13', 'ESC16'
)

foreach ($technique in $techniques) {
Expand Down
3 changes: 2 additions & 1 deletion README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ $stores.IssueStore['ESC1']
| **ESC8** | Vulnerable Web Enrollment Endpoints | CAs | — |
| **ESC9** | Weak Certificate Mappings | Templates | — |
| **ESC11** | Missing RPC Encryption | CAs | — |
| **ESC13** | Group-Linked OID Application Policy | Templates | — |
| **ESC16** | Disabled CRL/AIA Security Extensions | CAs | — |

For detailed information on ESC techniques, see [Certified Pre-Owned](https://posts.specterops.io/certified-pre-owned-d95910965cd2) by SpecterOps.
Expand Down Expand Up @@ -112,7 +113,7 @@ Scans certificate templates for specific ESC vulnerabilities.

```powershell
Find-LS2VulnerableTemplate -Technique <String>
# Supported: ESC1, ESC2, ESC3c1, ESC3c2, ESC4a, ESC4o, ESC9
# Supported: ESC1, ESC2, ESC3c1, ESC3c2, ESC4a, ESC4o, ESC9, ESC13
```

Returns: LS2Issue objects for programmatic use
Expand Down
Loading
Loading