diff --git a/.scratch/esc5a-ca-host-self-permission-fp/spec.md b/.scratch/esc5a-ca-host-self-permission-fp/spec.md index 2564e95..3117beb 100644 --- a/.scratch/esc5a-ca-host-self-permission-fp/spec.md +++ b/.scratch/esc5a-ca-host-self-permission-fp/spec.md @@ -18,13 +18,12 @@ ESC5a (Vulnerable PKI Object Access Control) no longer reports findings where th ## Decisions so far -_None recorded yet._ +- Suppress in the `Set-*` enrichment (`Set-DangerousEditor`, `Set-LowPrivilegeEditor`), own-CA only: skip ACEs where the ACE's IdentityReference SID equals the object's own `ComputerPrincipal`. Rationale: one guard per shared function fixes all consumers (Find functions, dashboard, risk scoring) at once; `ComputerPrincipal` (host computer SID) is already populated by `Set-CAComputerPrincipal` earlier in the pipeline, so no new lookup infrastructure is needed. +- Scope is own-CA only. A CA host account with write rights on a *different* CA object remains a finding. +- Non-CA objects (templates, containers) are unaffected — they have no `ComputerPrincipal`, so the guard is a null check. ## Not yet specified -- Where to suppress: filter ACEs in the `Set-*` enrichment that populates `DangerousEditor`/`LowPrivilegeEditor` vs. skip in `Find-LS2VulnerableObject`'s ESC5a evaluation. -- How to resolve the CA host computer account reliably (dNSHostName → computer account lookup vs. existing store correlation). -- Whether suppression applies only when the object is the host's *own* CA, or any CA object. - Test coverage: unit tests with mock ACEs where `IdentityReference` is the CA host account in both NTAccount and SID forms. ## Out of scope diff --git a/Locksmith2.psd1 b/Locksmith2.psd1 index f2688c4..97dd498 100644 --- a/Locksmith2.psd1 +++ b/Locksmith2.psd1 @@ -9,7 +9,7 @@ FormatsToProcess=@('LS2Issue.format.ps1xml') FunctionsToExport=@('*') GUID='e32f7d0d-2b10-4db2-b776-a193958e3d69' - ModuleVersion='2026.8.30507' + ModuleVersion='2026.8.191438' PowerShellVersion='5.1' PrivateData=@{ PSData=@{ diff --git a/Private/Set/Set-DangerousEditor.ps1 b/Private/Set/Set-DangerousEditor.ps1 index 319d82d..64ff097 100644 --- a/Private/Set/Set-DangerousEditor.ps1 +++ b/Private/Set/Set-DangerousEditor.ps1 @@ -108,6 +108,12 @@ if ($isDangerousAce.IsDangerous) { # Now check if the principal holding this ACE is dangerous $aceSid = $ace.IdentityReference | Convert-IdentityReferenceToSid + # Suppress ESC5a false positive: a CA host's own computer account + # legitimately holds rights on its own CA object (#99) + if ($_.ComputerPrincipal -and $aceSid.Value -eq $_.ComputerPrincipal) { + Write-Verbose "Skipping ACE for CA host's own computer account: $($aceSid.Value)" + continue + } $isDangerousPrincipal = $aceSid | Test-IsDangerousPrincipal if ($isDangerousPrincipal) { Write-Verbose "Dangerous template editor found: $($ace.IdentityReference) ($($isDangerousAce.MatchedPermission))" diff --git a/Private/Set/Set-LowPrivilegeEditor.ps1 b/Private/Set/Set-LowPrivilegeEditor.ps1 index 79ef3c9..bdf3550 100644 --- a/Private/Set/Set-LowPrivilegeEditor.ps1 +++ b/Private/Set/Set-LowPrivilegeEditor.ps1 @@ -113,6 +113,12 @@ if ($isDangerousAce.IsDangerous) { # Now check if the principal holding this ACE is low-privilege $aceSid = $ace.IdentityReference | Convert-IdentityReferenceToSid + # Suppress ESC5a false positive: a CA host's own computer account + # legitimately holds rights on its own CA object (#99) + if ($_.ComputerPrincipal -and $aceSid.Value -eq $_.ComputerPrincipal) { + Write-Verbose "Skipping ACE for CA host's own computer account: $($aceSid.Value)" + continue + } $isLowPrivilegePrincipal = $aceSid | Test-IsLowPrivilegePrincipal if ($isLowPrivilegePrincipal) { Write-Verbose "Low-privilege template editor found: $($ace.IdentityReference) ($($isDangerousAce.MatchedPermission))" diff --git a/Tests/Private/Set/Set-Editor.Tests.ps1 b/Tests/Private/Set/Set-Editor.Tests.ps1 index 601b0d4..ccb23e2 100644 --- a/Tests/Private/Set/Set-Editor.Tests.ps1 +++ b/Tests/Private/Set/Set-Editor.Tests.ps1 @@ -138,6 +138,59 @@ Describe 'Set-DangerousEditor' -Tag 'Unit' { Should -Invoke Test-IsDangerousAce -Times 1 -Exactly -ParameterFilter { $ObjectClass -eq 'pKICertificateTemplate' } } } + + Context 'CA host self-permission false positive (#99)' { + It 'should exclude ACEs where the principal is the CA object''s own ComputerPrincipal' { + $hostSid = 'S-1-5-21-1-2-3-1001' + $ace = New-MockAce -IdentityReference $hostSid + $ca = New-MockLS2AdcsObject -Properties @{ + objectClass = @('top', 'pKIEnrollmentService') + SchemaClassName = 'pKIEnrollmentService' + ComputerPrincipal = $hostSid + } + Add-Member -InputObject $ca -MemberType NoteProperty -Name 'ObjectSecurity' -Value (New-MockObjectSecurity -Access @($ace)) -Force + $mockSid = [PSCustomObject]@{ Value = $hostSid } + Mock Test-IsDangerousAce { [PSCustomObject]@{ IsDangerous = $true; MatchedPermission = 'GenericAll' } } + Mock Convert-IdentityReferenceToSid { $mockSid } + Mock Test-IsDangerousPrincipal { $true } + Mock Resolve-Principal { } + $result = $ca | Set-DangerousEditor + $result.DangerousEditor | Should -BeNullOrEmpty + } + + It 'should still flag other dangerous principals on the same CA object' { + $hostSid = 'S-1-5-21-1-2-3-1001' + $otherSid = 'S-1-5-21-1-2-3-999' + $aceHost = New-MockAce -IdentityReference $hostSid + $aceOther = New-MockAce -IdentityReference $otherSid + $ca = New-MockLS2AdcsObject -Properties @{ + objectClass = @('top', 'pKIEnrollmentService') + SchemaClassName = 'pKIEnrollmentService' + ComputerPrincipal = $hostSid + } + Add-Member -InputObject $ca -MemberType NoteProperty -Name 'ObjectSecurity' -Value (New-MockObjectSecurity -Access @($aceHost, $aceOther)) -Force + Mock Test-IsDangerousAce { [PSCustomObject]@{ IsDangerous = $true; MatchedPermission = 'GenericAll' } } + Mock Convert-IdentityReferenceToSid { [PSCustomObject]@{ Value = $IdentityReference.Value } } + Mock Test-IsDangerousPrincipal { $true } + Mock Resolve-Principal { } + $result = $ca | Set-DangerousEditor + $result.DangerousEditor | Should -Contain $otherSid + $result.DangerousEditor | Should -Not -Contain $hostSid + } + + It 'should not filter when ComputerPrincipal is null (non-CA objects)' { + $ace = New-MockAce -IdentityReference 'S-1-5-21-1-2-3-999' + $template = New-MockLS2AdcsObject -Properties @{ SchemaClassName = 'pKICertificateTemplate' } + Add-Member -InputObject $template -MemberType NoteProperty -Name 'ObjectSecurity' -Value (New-MockObjectSecurity -Access @($ace)) -Force + $mockSid = [PSCustomObject]@{ Value = 'S-1-5-21-1-2-3-999' } + Mock Test-IsDangerousAce { [PSCustomObject]@{ IsDangerous = $true; MatchedPermission = 'GenericAll' } } + Mock Convert-IdentityReferenceToSid { $mockSid } + Mock Test-IsDangerousPrincipal { $true } + Mock Resolve-Principal { } + $result = $template | Set-DangerousEditor + $result.DangerousEditor | Should -Contain 'S-1-5-21-1-2-3-999' + } + } } } @@ -215,5 +268,45 @@ Describe 'Set-LowPrivilegeEditor' -Tag 'Unit' { $result.LowPrivilegeEditor | Should -BeNullOrEmpty } } + + Context 'CA host self-permission false positive (#99)' { + It 'should exclude ACEs where the principal is the CA object''s own ComputerPrincipal' { + $hostSid = 'S-1-5-21-1-2-3-1001' + $ace = New-MockAce -IdentityReference $hostSid + $ca = New-MockLS2AdcsObject -Properties @{ + objectClass = @('top', 'pKIEnrollmentService') + SchemaClassName = 'pKIEnrollmentService' + ComputerPrincipal = $hostSid + } + Add-Member -InputObject $ca -MemberType NoteProperty -Name 'ObjectSecurity' -Value (New-MockObjectSecurity -Access @($ace)) -Force + $mockSid = [PSCustomObject]@{ Value = $hostSid } + Mock Test-IsDangerousAce { [PSCustomObject]@{ IsDangerous = $true; MatchedPermission = 'WriteDacl' } } + Mock Convert-IdentityReferenceToSid { $mockSid } + Mock Test-IsLowPrivilegePrincipal { $true } + Mock Resolve-Principal { } + $result = $ca | Set-LowPrivilegeEditor + $result.LowPrivilegeEditor | Should -BeNullOrEmpty + } + + It 'should still flag other low-privilege principals on the same CA object' { + $hostSid = 'S-1-5-21-1-2-3-1001' + $otherSid = 'S-1-5-21-1-2-3-999' + $aceHost = New-MockAce -IdentityReference $hostSid + $aceOther = New-MockAce -IdentityReference $otherSid + $ca = New-MockLS2AdcsObject -Properties @{ + objectClass = @('top', 'pKIEnrollmentService') + SchemaClassName = 'pKIEnrollmentService' + ComputerPrincipal = $hostSid + } + Add-Member -InputObject $ca -MemberType NoteProperty -Name 'ObjectSecurity' -Value (New-MockObjectSecurity -Access @($aceHost, $aceOther)) -Force + Mock Test-IsDangerousAce { [PSCustomObject]@{ IsDangerous = $true; MatchedPermission = 'GenericAll' } } + Mock Convert-IdentityReferenceToSid { [PSCustomObject]@{ Value = $IdentityReference.Value } } + Mock Test-IsLowPrivilegePrincipal { $true } + Mock Resolve-Principal { } + $result = $ca | Set-LowPrivilegeEditor + $result.LowPrivilegeEditor | Should -Contain $otherSid + $result.LowPrivilegeEditor | Should -Not -Contain $hostSid + } + } } }