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
14 changes: 9 additions & 5 deletions .scratch/schema-v1-ca-template-detection/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,18 @@ Locksmith2 distinguishes CA-shaped schema v1 certificate templates from end-enti

## Decisions so far

_None recorded yet._
- Add a new `IsCATemplate` property ([bool], defaults `$false`) to `LS2AdcsObject`. Rationale: every synthetic property in the class (`Enabled`, `AuthenticationEKUExist`, `DangerousEditor`, etc.) is declared on the class and set by a `Set-*` function — computed inline in Find functions is not an established pattern, and PS class properties cannot be ad-hoc added later without `Add-Member` hacks.
- Wire enrichment via a new `Set-IsCATemplate` function in `Private/Set/`, inserted into the existing template pipeline in `Initialize-AdcsObjectStore`. Rationale: one-function-per-file rule, matches the `Set-TemplateEnabled` precedent, and keeps Find functions data-only. Implementation: `$_.IsCATemplate = ($_.pKIDefaultKeySpec -eq 2)` for template objects.
- Do NOT change SchemaV1 `Conditions`. The property does not affect which templates match the technique — CA-shaped schema v1 templates are still findings, just with different remediation text. Branching belongs in the Find function/issue text, not the data Conditions.
- Find function: in the SchemaV1 branch of `Find-LS2VulnerableTemplate`, select Issue/Fix/Revert text based on `$template.IsCATemplate`. Since `ESCDefinitions.ps1` holds one IssueTemplate per technique, add a sibling key (e.g. `CAOverride = @{ IssueTemplate = ...; FixTemplate = ...; RevertTemplate = ... }`) inside the SchemaV1 entry rather than a new top-level technique — keeps the technique list clean and the data-driven pattern intact.

## Not yet specified

- Exact wording of the CA-shaped schema v1 template issue/fix/revert text.
- Whether to add a new computed property (`IsCATemplate`) to `LS2AdcsObject` or compute it inline in `Find-LS2VulnerableTemplate`.
- Where to wire the enrichment (existing `Set-*` pipeline vs. a new `Set-IsCATemplate` function).
- Test coverage: unit tests for `Set-IsCATemplate` (if created) and `Find-LS2VulnerableTemplate` SchemaV1 branch for both CA and non-CA schema v1 templates.
- Test coverage: unit tests for `Set-IsCATemplate` and `Find-LS2VulnerableTemplate` SchemaV1 branch for both CA and non-CA schema v1 templates.

### CA-shaped wording decision

Leave the CA template in place. Issue text states that replacing SubCA certificates requires planning and testing rather than simple supersession; Fix script contains no supersession instructions — awareness/documentation only.

## Out of scope

Expand Down
2 changes: 2 additions & 0 deletions Classes/LS2AdcsObject.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
[bool]$AuditingIncomplete
[object[]]$DisableExtensionList
[Nullable[bool]]$SecurityExtensionDisabled
[bool]$IsCATemplate
[object[]]$WebEnrollmentEndpoints
[Nullable[bool]]$HasLinkedGroupOIDPolicy # true when ≥1 CertificatePolicy OID links to a group
[string[]]$LinkedGroupOIDPolicies # group DNs linked via OID application policies
Expand Down Expand Up @@ -166,6 +167,7 @@
$this.SANFlagEnabled = $null
$this.AuditFilter = $null
$this.DisableExtensionList = @()
$this.IsCATemplate = $false

# Initialize CA-specific properties
$this.CAAdministrators = @()
Expand Down
37 changes: 34 additions & 3 deletions Private/Data/ESCDefinitions.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ $script:ESCDefinitions = data {
" - https://posts.specterops.io/adcs-esc13-abuse-technique-fda4272fbd53"
)

# Fix script template (quick mitigation Manager Approval)
# Fix script template (quick mitigation - Manager Approval)
FixTemplate = @(
"# Quick mitigation: Enable Manager Approval to require approval before certificate issuance"
"`$Object = '`$(DistinguishedName)'"
Expand Down Expand Up @@ -713,7 +713,7 @@ $script:ESCDefinitions = data {
}

ESC15 = @{
# ESC15: Enabled schema v1 template with auth EKU bypasses strong certificate mapping
# ESC15: Enabled schema v1 template with auth EKU - bypasses strong certificate mapping
# (szOID_NTDS_CA_SECURITY_EXT is absent in schema v1 certificates)
Technique = 'ESC15'

Expand Down Expand Up @@ -788,7 +788,7 @@ $script:ESCDefinitions = data {
}

SchemaV1 = @{
# SchemaV1: Enabled schema v1 template without client auth EKU informational hygiene finding
# SchemaV1: Enabled schema v1 template without client auth EKU - informational hygiene finding
Technique = 'SchemaV1'

Conditions = @(
Expand Down Expand Up @@ -819,6 +819,37 @@ $script:ESCDefinitions = data {
"# No automated revert. Template schema version cannot be changed via script."
"# If you superseded this template, re-enable the old template and remove the superseding relationship."
)

# Override for CA-shaped schema v1 templates (pKIDefaultKeySpec -eq 2).
# Supersession has not been observed to work reliably for CA templates in live
# environments, so CA-shaped templates get different remediation guidance.
Overrides = @(
@{
When = @{ Property = 'IsCATemplate'; Value = $true }
IssueTemplate = @(
"The certificate template `$(TemplateName) is a CA template that uses schema version 1.`n`n"
"Schema v1 templates were introduced in Windows 2000 and lack several security features "
"available in later schema versions. Certificates issued from schema v1 templates do not "
"include the CA security extension (szOID_NTDS_CA_SECURITY_EXT), reducing their compatibility "
"with strong certificate mapping requirements.`n`n"
"Because this template issues CA certificates, it cannot be safely superseded like an "
"end-entity template - supersession has not been observed to work reliably for CA templates "
"in live environments. Replacing SubCA certificates requires planning and testing rather "
"than simple supersession. Leave this template in place until a tested replacement plan exists."
)
FixTemplate = @(
"# No automated fix. This is a CA-shaped schema v1 template."
"# Do NOT supersede this template - supersession is unreliable for CA templates."
"# Replacing SubCA certificates requires planning and testing:"
"# 1. Inventory all CAs and subordinate CAs issued from this template."
"# 2. Plan CA certificate renewal/replacement against a schema v2+ CA template."
"# 3. Test the replacement process in a lab before production changes."
)
RevertTemplate = @(
"# No action taken, no revert required."
)
}
)
}
}
}
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 @@ -80,6 +80,7 @@
Set-ManagerApprovalNotRequired |
Set-AuthorizedSignatureNotRequired |
Set-TemplateEnabled |
Set-IsCATemplate |
Set-Owner |
Set-HasNonStandardOwner

Expand Down
49 changes: 49 additions & 0 deletions Private/Set/Set-IsCATemplate.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
function Set-IsCATemplate {
<#
.SYNOPSIS
Adds the IsCATemplate property to AD CS certificate template objects.

.DESCRIPTION
Examines the pKIDefaultKeySpec attribute of certificate template objects to
determine whether a template is CA-shaped (intended to issue CA certificates).

A template is considered CA-shaped when pKIDefaultKeySpec -eq 2 (AT_SIGNATURE),
which is the key specification used for CA signing keys. End-entity templates
use pKIDefaultKeySpec -eq 1 (AT_KEYEXCHANGE) or leave the attribute unset.

This distinction matters for remediation guidance: superseding a schema v1
end-entity template is routine, but supersession has not been observed to
work reliably for CA templates in live environments.

.PARAMETER AdcsObject
One or more LS2AdcsObject objects representing AD CS objects.

.INPUTS
LS2AdcsObject objects.

.OUTPUTS
LS2AdcsObject objects with the IsCATemplate property set.

.EXAMPLE
$templates | Set-IsCATemplate
Sets IsCATemplate on all certificate template objects.

.NOTES
pKIDefaultKeySpec values:
1 = AT_KEYEXCHANGE (end-entity)
2 = AT_SIGNATURE (CA-shaped)
#>
[CmdletBinding()]
param (
[Parameter(Mandatory, ValueFromPipeline)]
[LS2AdcsObject[]]$AdcsObject
)

process {
$AdcsObject | Where-Object SchemaClassName -EQ 'pKICertificateTemplate' | ForEach-Object {
$_.IsCATemplate = ($_.pKIDefaultKeySpec -eq 2)
Write-Verbose "Template '$($_.cn)': IsCATemplate = $($_.IsCATemplate)"
$_
}
}
}
59 changes: 59 additions & 0 deletions Private/Utility/Get-IssueTextOverride.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
function Get-IssueTextOverride {
<#
.SYNOPSIS
Selects issue text overrides for an AD CS object based on technique Overrides.

.DESCRIPTION
Technique definitions in ESCDefinitions.ps1 may carry an Overrides array. Each
override specifies a single When condition (property equality) and alternate
IssueTemplate/FixTemplate/RevertTemplate text.

This function returns the first override whose When condition matches the
object, or the base config when no override matches. Callers use the returned
hashtable's IssueTemplate/FixTemplate/RevertTemplate in place of the base
config's text.

Example override shape:
Overrides = @(
@{
When = @{ Property = 'IsCATemplate'; Value = $true }
IssueTemplate = '...'
FixTemplate = '...'
RevertTemplate = '...'
}
)

.PARAMETER Config
A technique definition hashtable from $script:ESCDefinitions.

.PARAMETER AdcsObject
The LS2AdcsObject being evaluated.

.OUTPUTS
Hashtable — the matching override merged into the issue-text role, or Config unchanged.

.EXAMPLE
$textConfig = Get-IssueTextOverride -Config $config -AdcsObject $template

.NOTES
Overrides only carry text. Placeholder expansion remains the caller's job.
#>
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[hashtable]$Config,

[Parameter(Mandatory)]
[LS2AdcsObject]$AdcsObject
)

foreach ($override in @($Config.Overrides)) {
if ($null -eq $override.When) { continue }
if ($AdcsObject.($override.When.Property) -eq $override.When.Value) {
Write-Verbose "Override matched on $($override.When.Property) = $($override.When.Value)"
return $override
}
}

return $Config
}
10 changes: 7 additions & 3 deletions Public/Find-LS2VulnerableTemplate.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -374,11 +374,15 @@

$forestName = Get-ForestNameFromDN -DistinguishedName $template.distinguishedName

$issueText = ($config.IssueTemplate -join '') `
# Resolve any technique Overrides (e.g., CA-shaped schema v1 templates get
# alternate text — supersession is unreliable for CA templates)
$issueConfig = Get-IssueTextOverride -Config $config -AdcsObject $template

$issueText = ($issueConfig.IssueTemplate -join '') `
-replace '\$\(TemplateName\)', $templateName

$fixScript = ($config.FixTemplate -join "`n")
$revertScript = ($config.RevertTemplate -join "`n")
$fixScript = ($issueConfig.FixTemplate -join "`n")
$revertScript = ($issueConfig.RevertTemplate -join "`n")

$issue = [LS2Issue]@{
Technique = $Technique
Expand Down
74 changes: 74 additions & 0 deletions Tests/Private/Set/Set-IsCATemplate.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
BeforeDiscovery {
$ModuleRoot = Split-Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) -Parent
$ls2Manifest = if ($env:LS2_MODULE_ROOT) { Join-Path $env:LS2_MODULE_ROOT 'Locksmith2.psd1' } else { Join-Path $ModuleRoot 'Locksmith2.psd1' }
Import-Module $ls2Manifest -Force -ErrorAction Stop
}
BeforeAll {
$ModuleRoot = Split-Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) -Parent
$ls2Manifest = if ($env:LS2_MODULE_ROOT) { Join-Path $env:LS2_MODULE_ROOT 'Locksmith2.psd1' } else { Join-Path $ModuleRoot 'Locksmith2.psd1' }
Import-Module $ls2Manifest -Force -ErrorAction Stop
Import-Module (Join-Path $ModuleRoot 'Tests\Shared\TestHelpers.psm1') -Force -ErrorAction Stop
}

Describe 'Set-IsCATemplate' -Tag 'Unit' {
InModuleScope 'Locksmith2' {
BeforeEach {
$script:IssueStore = @{}; $script:PrincipalStore = @{}; $script:AdcsObjectStore = @{}
$script:DomainStore = @{}; $script:SafePrincipals = @(); $script:DangerousPrincipals = @()
$script:StandardOwners = @(); $script:DangerousAces = $null; $script:InitializingStores = $false
$script:RootDSE = $null; $script:Server = $null; $script:Forest = $null; $script:Credential = $null
}

Context 'Non-template objects' {
It 'should not set IsCATemplate on non-template objects' {
$ca = New-MockLS2AdcsObject -Properties @{
SchemaClassName = 'pKIEnrollmentService'
cn = 'MyCA'
}

$result = $ca | Set-IsCATemplate

$result.IsCATemplate | Should -BeFalse
}
}

Context 'CA-shaped templates' {
It 'should set IsCATemplate=$true when pKIDefaultKeySpec is 2 (AT_SIGNATURE)' {
$template = New-MockLS2AdcsObject -Properties @{
SchemaClassName = 'pKICertificateTemplate'
cn = 'SubCA'
pKIDefaultKeySpec = 2
}

$result = $template | Set-IsCATemplate

$result.IsCATemplate | Should -BeTrue
}
}

Context 'End-entity templates' {
It 'should set IsCATemplate=$false when pKIDefaultKeySpec is 1 (AT_KEYEXCHANGE)' {
$template = New-MockLS2AdcsObject -Properties @{
SchemaClassName = 'pKICertificateTemplate'
cn = 'WebServer'
pKIDefaultKeySpec = 1
}

$result = $template | Set-IsCATemplate

$result.IsCATemplate | Should -BeFalse
}

It 'should set IsCATemplate=$false when pKIDefaultKeySpec is null' {
$template = New-MockLS2AdcsObject -Properties @{
SchemaClassName = 'pKICertificateTemplate'
cn = 'WebServer'
}

$result = $template | Set-IsCATemplate

$result.IsCATemplate | Should -BeFalse
}
}
}
}
Loading
Loading