diff --git a/Classes/LS2AdcsObject.ps1 b/Classes/LS2AdcsObject.ps1 index c46527e..c690ad5 100644 --- a/Classes/LS2AdcsObject.ps1 +++ b/Classes/LS2AdcsObject.ps1 @@ -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 @@ -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 @@ -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 { @@ -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 diff --git a/Locksmith2.psd1 b/Locksmith2.psd1 index 1a92f28..0c7a8c6 100644 --- a/Locksmith2.psd1 +++ b/Locksmith2.psd1 @@ -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=@{ diff --git a/Private/Data/ESCDefinitions.ps1 b/Private/Data/ESCDefinitions.ps1 index 842cd26..138ac88 100644 --- a/Private/Data/ESCDefinitions.ps1 +++ b/Private/Data/ESCDefinitions.ps1 @@ -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 '' | 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' diff --git a/Private/Initialize/Initialize-AdcsObjectStore.ps1 b/Private/Initialize/Initialize-AdcsObjectStore.ps1 index 74f0896..7697bf8 100644 --- a/Private/Initialize/Initialize-AdcsObjectStore.ps1 +++ b/Private/Initialize/Initialize-AdcsObjectStore.ps1 @@ -68,6 +68,7 @@ function Initialize-AdcsObjectStore { $Templates = $Templates | Set-SANAllowed | Set-AuthenticationEKUExist | + Set-LinkedGroupOIDPolicy | Set-AnyPurposeEKUExist | Set-EnrollmentAgentEKUExist | Set-RequiresEnrollmentAgentSignature | diff --git a/Private/Initialize/Initialize-LS2Scan.ps1 b/Private/Initialize/Initialize-LS2Scan.ps1 index 4a123c8..e403816 100644 --- a/Private/Initialize/Initialize-LS2Scan.ps1 +++ b/Private/Initialize/Initialize-LS2Scan.ps1 @@ -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 } diff --git a/Private/Set/Set-LinkedGroupOIDPolicy.ps1 b/Private/Set/Set-LinkedGroupOIDPolicy.ps1 new file mode 100644 index 0000000..b17e3dc --- /dev/null +++ b/Private/Set/Set-LinkedGroupOIDPolicy.ps1 @@ -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 + } + } +} diff --git a/Public/Find-LS2VulnerableTemplate.ps1 b/Public/Find-LS2VulnerableTemplate.ps1 index fb43003..369939d 100644 --- a/Public/Find-LS2VulnerableTemplate.ps1 +++ b/Public/Find-LS2VulnerableTemplate.ps1 @@ -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()] @@ -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) { @@ -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 diff --git a/Public/Invoke-Locksmith2.ps1 b/Public/Invoke-Locksmith2.ps1 index f81f984..34cd8b1 100644 --- a/Public/Invoke-Locksmith2.ps1 +++ b/Public/Invoke-Locksmith2.ps1 @@ -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) { diff --git a/README.MD b/README.MD index 0814dc1..74ec4d5 100644 --- a/README.MD +++ b/README.MD @@ -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. @@ -112,7 +113,7 @@ Scans certificate templates for specific ESC vulnerabilities. ```powershell Find-LS2VulnerableTemplate -Technique -# Supported: ESC1, ESC2, ESC3c1, ESC3c2, ESC4a, ESC4o, ESC9 +# Supported: ESC1, ESC2, ESC3c1, ESC3c2, ESC4a, ESC4o, ESC9, ESC13 ``` Returns: LS2Issue objects for programmatic use diff --git a/Tests/Private/Set/Set-LinkedGroupOIDPolicy.Tests.ps1 b/Tests/Private/Set/Set-LinkedGroupOIDPolicy.Tests.ps1 new file mode 100644 index 0000000..64da369 --- /dev/null +++ b/Tests/Private/Set/Set-LinkedGroupOIDPolicy.Tests.ps1 @@ -0,0 +1,244 @@ +#requires -Version 5.1 +BeforeDiscovery { + $ModuleRoot = Split-Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) -Parent + Import-Module (Join-Path $ModuleRoot 'Locksmith2.psd1') -Force -ErrorAction Stop +} +BeforeAll { + $ModuleRoot = Split-Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) -Parent + Import-Module (Join-Path $ModuleRoot 'Locksmith2.psd1') -Force -ErrorAction Stop + Import-Module (Join-Path $ModuleRoot 'Tests\Shared\TestHelpers.psm1') -Force -ErrorAction Stop +} + +InModuleScope 'Locksmith2' { + Describe 'Set-LinkedGroupOIDPolicy' -Tag 'Unit' { + 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 'No OID objects in store' { + It 'should set HasLinkedGroupOIDPolicy to $false when store has no OID objects' { + $template = New-MockLS2AdcsObject -Properties @{ + SchemaClassName = 'pKICertificateTemplate' + CertificatePolicy = @('1.3.6.1.4.1.311.21.8.1234567.7654321.1111111.2222222.3333333.1.4444444.5555555') + } + + $result = $template | Set-LinkedGroupOIDPolicy + + $result.HasLinkedGroupOIDPolicy | Should -BeFalse + } + + It 'should set LinkedGroupOIDPolicies to empty when store has no OID objects' { + $template = New-MockLS2AdcsObject -Properties @{ + SchemaClassName = 'pKICertificateTemplate' + CertificatePolicy = @('1.3.6.1.4.1.311.21.8.1234567.7654321.1111111.2222222.3333333.1.4444444.5555555') + } + + $result = $template | Set-LinkedGroupOIDPolicy + + $result.LinkedGroupOIDPolicies | Should -BeNullOrEmpty + } + } + + Context 'OID object in store without group link' { + BeforeEach { + $oidObject = New-MockLS2AdcsObject -Properties @{ + SchemaClassName = 'msPKI-Enterprise-Oid' + CertTemplateOID = '1.3.6.1.4.1.311.21.8.1234567.7654321.1111111.2222222.3333333.1.4444444.5555555' + OIDToGroupLink = $null + } + $script:AdcsObjectStore['OID-1'] = $oidObject + } + + It 'should set HasLinkedGroupOIDPolicy to $false when matching OID has no group link' { + $template = New-MockLS2AdcsObject -Properties @{ + SchemaClassName = 'pKICertificateTemplate' + CertificatePolicy = @('1.3.6.1.4.1.311.21.8.1234567.7654321.1111111.2222222.3333333.1.4444444.5555555') + } + + $result = $template | Set-LinkedGroupOIDPolicy + + $result.HasLinkedGroupOIDPolicy | Should -BeFalse + } + + It 'should set LinkedGroupOIDPolicies to empty when matching OID has no group link' { + $template = New-MockLS2AdcsObject -Properties @{ + SchemaClassName = 'pKICertificateTemplate' + CertificatePolicy = @('1.3.6.1.4.1.311.21.8.1234567.7654321.1111111.2222222.3333333.1.4444444.5555555') + } + + $result = $template | Set-LinkedGroupOIDPolicy + + $result.LinkedGroupOIDPolicies | Should -BeNullOrEmpty + } + } + + Context 'OID object in store with group link and matching template policy' { + BeforeEach { + $oidObject = New-MockLS2AdcsObject -Properties @{ + SchemaClassName = 'msPKI-Enterprise-Oid' + CertTemplateOID = '1.3.6.1.4.1.311.21.8.1234567.7654321.1111111.2222222.3333333.1.4444444.5555555' + OIDToGroupLink = 'CN=PrivilegedGroup,CN=Users,DC=contoso,DC=com' + } + $script:AdcsObjectStore['OID-1'] = $oidObject + } + + It 'should set HasLinkedGroupOIDPolicy to $true when template policy OID matches a group-linked OID' { + $template = New-MockLS2AdcsObject -Properties @{ + SchemaClassName = 'pKICertificateTemplate' + CertificatePolicy = @('1.3.6.1.4.1.311.21.8.1234567.7654321.1111111.2222222.3333333.1.4444444.5555555') + } + + $result = $template | Set-LinkedGroupOIDPolicy + + $result.HasLinkedGroupOIDPolicy | Should -BeTrue + } + + It 'should populate LinkedGroupOIDPolicies with the linked group DN' { + $template = New-MockLS2AdcsObject -Properties @{ + SchemaClassName = 'pKICertificateTemplate' + CertificatePolicy = @('1.3.6.1.4.1.311.21.8.1234567.7654321.1111111.2222222.3333333.1.4444444.5555555') + } + + $result = $template | Set-LinkedGroupOIDPolicy + + $result.LinkedGroupOIDPolicies | Should -Contain 'CN=PrivilegedGroup,CN=Users,DC=contoso,DC=com' + } + + It 'should pass the template object through the pipeline' { + $template = New-MockLS2AdcsObject -Properties @{ + SchemaClassName = 'pKICertificateTemplate' + CertificatePolicy = @('1.3.6.1.4.1.311.21.8.1234567.7654321.1111111.2222222.3333333.1.4444444.5555555') + } + + $result = $template | Set-LinkedGroupOIDPolicy + + $result | Should -Not -BeNullOrEmpty + $result.GetType().Name | Should -Be 'LS2AdcsObject' + } + } + + Context 'Multiple policy OIDs — only one linked' { + BeforeEach { + $oidObject = New-MockLS2AdcsObject -Properties @{ + SchemaClassName = 'msPKI-Enterprise-Oid' + CertTemplateOID = '1.3.6.1.4.1.311.21.8.LINKED.OID' + OIDToGroupLink = 'CN=LinkedGroup,CN=Users,DC=contoso,DC=com' + } + $script:AdcsObjectStore['OID-linked'] = $oidObject + } + + It 'should only include the group DN for the OID that has a group link' { + $template = New-MockLS2AdcsObject -Properties @{ + SchemaClassName = 'pKICertificateTemplate' + CertificatePolicy = @( + '1.3.6.1.4.1.311.21.8.UNLINKED.OID' + '1.3.6.1.4.1.311.21.8.LINKED.OID' + ) + } + + $result = $template | Set-LinkedGroupOIDPolicy + + $result.LinkedGroupOIDPolicies.Count | Should -Be 1 + $result.LinkedGroupOIDPolicies | Should -Contain 'CN=LinkedGroup,CN=Users,DC=contoso,DC=com' + } + + It 'should set HasLinkedGroupOIDPolicy to $true when at least one policy OID is linked' { + $template = New-MockLS2AdcsObject -Properties @{ + SchemaClassName = 'pKICertificateTemplate' + CertificatePolicy = @( + '1.3.6.1.4.1.311.21.8.UNLINKED.OID' + '1.3.6.1.4.1.311.21.8.LINKED.OID' + ) + } + + $result = $template | Set-LinkedGroupOIDPolicy + + $result.HasLinkedGroupOIDPolicy | Should -BeTrue + } + } + + Context 'Multiple policy OIDs — multiple linked to different groups' { + BeforeEach { + $oidObject1 = New-MockLS2AdcsObject -Properties @{ + SchemaClassName = 'msPKI-Enterprise-Oid' + CertTemplateOID = '1.3.6.1.4.1.311.21.8.LINKED.OID.1' + OIDToGroupLink = 'CN=GroupA,CN=Users,DC=contoso,DC=com' + } + $oidObject2 = New-MockLS2AdcsObject -Properties @{ + SchemaClassName = 'msPKI-Enterprise-Oid' + CertTemplateOID = '1.3.6.1.4.1.311.21.8.LINKED.OID.2' + OIDToGroupLink = 'CN=GroupB,CN=Users,DC=contoso,DC=com' + } + $script:AdcsObjectStore['OID-1'] = $oidObject1 + $script:AdcsObjectStore['OID-2'] = $oidObject2 + } + + It 'should include all linked group DNs when multiple policy OIDs link to groups' { + $template = New-MockLS2AdcsObject -Properties @{ + SchemaClassName = 'pKICertificateTemplate' + CertificatePolicy = @( + '1.3.6.1.4.1.311.21.8.LINKED.OID.1' + '1.3.6.1.4.1.311.21.8.LINKED.OID.2' + ) + } + + $result = $template | Set-LinkedGroupOIDPolicy + + $result.LinkedGroupOIDPolicies.Count | Should -Be 2 + $result.LinkedGroupOIDPolicies | Should -Contain 'CN=GroupA,CN=Users,DC=contoso,DC=com' + $result.LinkedGroupOIDPolicies | Should -Contain 'CN=GroupB,CN=Users,DC=contoso,DC=com' + } + } + + Context 'Non-template objects piped in' { + It 'should not set HasLinkedGroupOIDPolicy on a CA object' { + $ca = New-MockLS2AdcsObject -Properties @{ + objectClass = @('top', 'pKIEnrollmentService') + SchemaClassName = 'pKIEnrollmentService' + cn = 'MyCA' + name = 'MyCA' + } + + $result = $ca | Set-LinkedGroupOIDPolicy + + $result.HasLinkedGroupOIDPolicy | Should -BeNullOrEmpty + } + + It 'should pass non-template objects through the pipeline' { + $ca = New-MockLS2AdcsObject -Properties @{ + objectClass = @('top', 'pKIEnrollmentService') + SchemaClassName = 'pKIEnrollmentService' + cn = 'MyCA' + name = 'MyCA' + } + + $result = $ca | Set-LinkedGroupOIDPolicy + + $result | Should -Not -BeNullOrEmpty + } + } + + Context 'Template with no CertificatePolicy' { + It 'should set HasLinkedGroupOIDPolicy to $false when template has no policy OIDs' { + $oidObject = New-MockLS2AdcsObject -Properties @{ + SchemaClassName = 'msPKI-Enterprise-Oid' + CertTemplateOID = '1.3.6.1.4.1.311.21.8.SOME.OID' + OIDToGroupLink = 'CN=SomeGroup,CN=Users,DC=contoso,DC=com' + } + $script:AdcsObjectStore['OID-1'] = $oidObject + + $template = New-MockLS2AdcsObject -Properties @{ + SchemaClassName = 'pKICertificateTemplate' + CertificatePolicy = @() + } + + $result = $template | Set-LinkedGroupOIDPolicy + + $result.HasLinkedGroupOIDPolicy | Should -BeFalse + } + } + } +} diff --git a/Tests/Public/Find-LS2VulnerableTemplate.Tests.ps1 b/Tests/Public/Find-LS2VulnerableTemplate.Tests.ps1 index 361e0d1..81f9fc3 100644 --- a/Tests/Public/Find-LS2VulnerableTemplate.Tests.ps1 +++ b/Tests/Public/Find-LS2VulnerableTemplate.Tests.ps1 @@ -166,5 +166,129 @@ InModuleScope 'Locksmith2' { $result.Count | Should -Be 0 } } + + Context 'Path C — ESC13 technique-specific scan' { + BeforeAll { + function script:New-ESC13VulnerableTemplate { + $t = New-MockLS2AdcsObject -Properties @{ + objectClass = @('top', 'pKICertificateTemplate') + SchemaClassName = 'pKICertificateTemplate' + AuthenticationEKUExist = $true + HasLinkedGroupOIDPolicy = $true + LinkedGroupOIDPolicies = @('CN=PrivilegedGroup,CN=Users,DC=contoso,DC=com') + DangerousEnrollee = @('S-1-1-0') + distinguishedName = 'CN=ESC13Template,CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,DC=contoso,DC=com' + Name = 'ESC13Template' + Enabled = $true + EnabledOn = @('CONTOSO-CA\CA01') + } + $security = New-Object System.DirectoryServices.ActiveDirectorySecurity + $sid = [System.Security.Principal.SecurityIdentifier]::new('S-1-1-0') + $rights = [System.DirectoryServices.ActiveDirectoryRights]::ExtendedRight + $atype = [System.Security.AccessControl.AccessControlType]::Allow + $rule = [System.DirectoryServices.ActiveDirectoryAccessRule]::new($sid, $rights, $atype) + $security.AddAccessRule($rule) + $t.ObjectSecurity = $security + return $t + } + } + + BeforeEach { + Mock 'Convert-IdentityReferenceToSid' { + [System.Security.Principal.SecurityIdentifier]::new('S-1-1-0') + } + Mock 'Convert-IdentityReferenceToNTAccount' { + [System.Security.Principal.NTAccount]::new('Everyone') + } + Mock 'Test-IssueExists' { $false } + } + + It 'should return an LS2Issue when template has AuthenticationEKU, HasLinkedGroupOIDPolicy, and DangerousEnrollee' { + $vulnTemplate = New-ESC13VulnerableTemplate + $script:AdcsObjectStore = @{ $vulnTemplate.distinguishedName = $vulnTemplate } + + $result = @(Find-LS2VulnerableTemplate -Technique 'ESC13') + + $result.Count | Should -Be 1 + $result[0].GetType().Name | Should -Be 'LS2Issue' + } + + It 'should return an issue with Technique ESC13' { + $vulnTemplate = New-ESC13VulnerableTemplate + $script:AdcsObjectStore = @{ $vulnTemplate.distinguishedName = $vulnTemplate } + + $result = @(Find-LS2VulnerableTemplate -Technique 'ESC13') + + $result[0].Technique | Should -Be 'ESC13' + } + + It 'should include the linked group DN in the issue text' { + $vulnTemplate = New-ESC13VulnerableTemplate + $script:AdcsObjectStore = @{ $vulnTemplate.distinguishedName = $vulnTemplate } + + $result = @(Find-LS2VulnerableTemplate -Technique 'ESC13') + + $result[0].Issue | Should -Match 'CN=PrivilegedGroup,CN=Users,DC=contoso,DC=com' + } + + It 'should not return an issue when AuthenticationEKUExist is false' { + $safeTemplate = New-MockLS2AdcsObject -Properties @{ + SchemaClassName = 'pKICertificateTemplate' + AuthenticationEKUExist = $false + HasLinkedGroupOIDPolicy = $true + DangerousEnrollee = @('S-1-1-0') + distinguishedName = 'CN=SafeTemplate,CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,DC=contoso,DC=com' + Name = 'SafeTemplate' + } + $script:AdcsObjectStore = @{ $safeTemplate.distinguishedName = $safeTemplate } + + $result = @(Find-LS2VulnerableTemplate -Technique 'ESC13') + + $result.Count | Should -Be 0 + } + + It 'should not return an issue when HasLinkedGroupOIDPolicy is false' { + $safeTemplate = New-MockLS2AdcsObject -Properties @{ + SchemaClassName = 'pKICertificateTemplate' + AuthenticationEKUExist = $true + HasLinkedGroupOIDPolicy = $false + DangerousEnrollee = @('S-1-1-0') + distinguishedName = 'CN=SafeTemplate,CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,DC=contoso,DC=com' + Name = 'SafeTemplate' + } + $script:AdcsObjectStore = @{ $safeTemplate.distinguishedName = $safeTemplate } + + $result = @(Find-LS2VulnerableTemplate -Technique 'ESC13') + + $result.Count | Should -Be 0 + } + + It 'should not return an issue when DangerousEnrollee is empty' { + $safeTemplate = New-MockLS2AdcsObject -Properties @{ + SchemaClassName = 'pKICertificateTemplate' + AuthenticationEKUExist = $true + HasLinkedGroupOIDPolicy = $true + LinkedGroupOIDPolicies = @('CN=PrivilegedGroup,CN=Users,DC=contoso,DC=com') + DangerousEnrollee = @() + LowPrivilegeEnrollee = @() + distinguishedName = 'CN=SafeTemplate,CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,DC=contoso,DC=com' + Name = 'SafeTemplate' + } + $script:AdcsObjectStore = @{ $safeTemplate.distinguishedName = $safeTemplate } + + $result = @(Find-LS2VulnerableTemplate -Technique 'ESC13') + + $result.Count | Should -Be 0 + } + + It 'should add the issue to script:IssueStore when not a duplicate' { + $vulnTemplate = New-ESC13VulnerableTemplate + $script:AdcsObjectStore = @{ $vulnTemplate.distinguishedName = $vulnTemplate } + + Find-LS2VulnerableTemplate -Technique 'ESC13' | Out-Null + + $script:IssueStore.Count | Should -BeGreaterThan 0 + } + } } } diff --git a/Tests/Shared/TestHelpers.psm1 b/Tests/Shared/TestHelpers.psm1 index f3110cf..338aa42 100644 --- a/Tests/Shared/TestHelpers.psm1 +++ b/Tests/Shared/TestHelpers.psm1 @@ -66,6 +66,9 @@ function New-MockLS2AdcsObject { $obj.RAApplicationPolicies = @() $obj.TemplateSchemaVersion = $null $obj.TemplateMinorRevision = $null + $obj.CertificatePolicy = @() + $obj.CertTemplateOID = $null + $obj.OIDToGroupLink = $null $obj.certificateTemplates = @() $obj.dNSHostName = $null $obj.CAFullName = $null @@ -105,6 +108,8 @@ function New-MockLS2AdcsObject { $obj.AuditFilter = $null $obj.DisableExtensionList = @() $obj.SecurityExtensionDisabled = $null + $obj.HasLinkedGroupOIDPolicy = $null + $obj.LinkedGroupOIDPolicies = @() foreach ($key in $Properties.Keys) { $obj.$key = $Properties[$key]