Public/Find-LS2VulnerableCA.ps1
|
function Find-LS2VulnerableCA { <# .SYNOPSIS Identifies vulnerable AD CS Certification Authorities based on ESC technique definitions. .DESCRIPTION Reads ESC technique definitions from ESCDefinitions.psd1, queries the AdcsObjectStore for matching CAs, and generates issues for configuration problems or dangerous role assignments. ESC6: Detects CAs with EDITF_ATTRIBUTESUBJECTALTNAME2 enabled ESC7a: Detects dangerous CA Administrator role assignments ESC7m: Detects dangerous Certificate Manager role assignments ESC11: Detects CAs that don't require RPC encryption ESC16: Detects CAs with disabled CRL/AIA extensions .PARAMETER Technique ESC technique name to scan for (e.g., 'ESC6', 'ESC7a', 'ESC7m', 'ESC11', 'ESC16') .EXAMPLE Find-LS2VulnerableCA -Technique ESC6 Checks for CAs with EDITF_ATTRIBUTESUBJECTALTNAME2 enabled. .EXAMPLE Find-LS2VulnerableCA -Technique ESC7a Checks for dangerous CA Administrator role assignments. .EXAMPLE Find-LS2VulnerableCA -Technique ESC7m Checks for dangerous Certificate Manager role assignments. .EXAMPLE Find-LS2VulnerableCA -Technique ESC7a -ExpandGroups Checks for CA Administrator roles and expands group assignments into per-member issues. .EXAMPLE Find-LS2VulnerableCA -Technique ESC11 Checks for CAs that don't require RPC encryption. .EXAMPLE Find-LS2VulnerableCA -Technique ESC16 Checks for CAs with disabled security extensions in CRL/AIA. .OUTPUTS LS2Issue LS2Issue objects for each vulnerability found. .NOTES Author: Jake Hildreth (@jakehildreth) Module: Locksmith2 Requires: PowerShell 5.1+ Requires script-scope variables set by Invoke-Locksmith2: - $script:AdcsObjectStore: Cache of AD CS objects - $script:PrincipalStore: Cache of resolved principals Supported techniques: - ESC6: EDITF_ATTRIBUTESUBJECTALTNAME2 flag enabled - ESC7a: Dangerous CA Administrator role assignments - ESC7m: Dangerous Certificate Manager role assignments - ESC11: Missing RPC encryption requirement - ESC16: Disabled CRL/AIA security extensions .LINK Find-LS2VulnerableTemplate .LINK Find-LS2VulnerableObject .LINK Invoke-Locksmith2 #> [CmdletBinding()] param( [Parameter()] [ValidateSet('ESC6', 'ESC7a', 'ESC7m', 'ESC11', 'ESC16')] [string]$Technique, [Parameter()] [string]$Forest, [Parameter()] [PSCredential]$Credential, [Parameter()] [switch]$ExpandGroups, [Parameter()] [switch]$Rescan ) #requires -Version 5.1 # Ensure stores are initialized and populated $initParams = @{} if ($PSBoundParameters.ContainsKey('Forest')) { $initParams['Forest'] = $Forest } if ($PSBoundParameters.ContainsKey('Credential')) { $initParams['Credential'] = $Credential } if ($Rescan) { $initParams['Rescan'] = $true } if (-not (Initialize-LS2Scan @initParams)) { return } # If no technique specified, return all CA issues if (-not $Technique) { Write-Verbose "No technique specified. Returning all CA issues..." $allIssues = Get-FlattenedIssues $caTechniques = @('ESC6', 'ESC7a', 'ESC7m', 'ESC11', 'ESC16') $caIssues = $allIssues | Where-Object { $_.Technique -in $caTechniques } if ($ExpandGroups) { $caIssues | ForEach-Object { Expand-IssueByGroup $_ } } else { $caIssues } return } # Load all ESC definitions $definitionsPath = Join-Path $PSScriptRoot '..\Private\Data\ESCDefinitions.psd1' $allDefinitions = Import-PowerShellDataFile -Path $definitionsPath $config = $allDefinitions[$Technique] Write-Verbose "Scanning for $Technique using definitions from $definitionsPath" # Query AdcsObjectStore for CAs $allCAs = $script:AdcsObjectStore.Values | Where-Object { $_.objectClass -contains 'pKIEnrollmentService' } Write-Verbose "Found $($allCAs.Count) Certification Authority object(s) to check" $issueCount = 0 # ESC7a and ESC7m have a different structure (checks role assignments) if ($Technique -eq 'ESC7a' -or $Technique -eq 'ESC7m') { foreach ($ca in $allCAs) { $caName = if ($ca.cn) { $ca.cn } elseif ($ca.Properties -and $ca.Properties.Contains('cn')) { $ca.Properties['cn'][0] } else { 'Unknown CA' } Write-Verbose " Checking CA: $caName" # Get CAFullName for certutil commands $caFullName = if ($ca.CAFullName) { $ca.CAFullName } else { $null } if (-not $caFullName) { Write-Verbose " CA '$caName' has no CAFullName property - skipping" continue } # Get forest name from DN $forestName = if ($ca.distinguishedName -match 'DC=([^,]+)') { $ca.distinguishedName -replace '^.*?DC=(.*)$', '$1' -replace ',DC=', '.' } else { 'Unknown' } # Check each admin property for problematic principals foreach ($adminProperty in $config.AdminProperties) { $problematicPrincipals = @($ca.$adminProperty) if (-not $problematicPrincipals -or $problematicPrincipals.Count -eq 0) { continue } Write-Verbose " Found $($problematicPrincipals.Count) problematic principal(s) in $adminProperty" # Determine role type $isAdministrator = $adminProperty -like '*CAAdministrator*' $roleType = if ($isAdministrator) { 'Administrators' } else { 'Officers' } # Use the IssueTemplate from config (no longer separate templates) $issueTemplate = $config.IssueTemplate # Create an issue for each problematic principal foreach ($principalSid in $problematicPrincipals) { # Resolve principal name from PrincipalStore $identityReference = if ($script:PrincipalStore -and $script:PrincipalStore.ContainsKey($principalSid)) { $script:PrincipalStore[$principalSid].ntAccountName } else { $principalSid } Write-Verbose " VULNERABLE: $identityReference ($principalSid) has $roleType role" # Join templates if they're arrays $issueTemplateText = if ($issueTemplate -is [array]) { $issueTemplate -join '' } else { $issueTemplate } $fixTemplate = if ($config.FixTemplate -is [array]) { $config.FixTemplate -join "`n" } else { $config.FixTemplate } $revertTemplate = if ($config.RevertTemplate -is [array]) { $config.RevertTemplate -join "`n" } else { $config.RevertTemplate } # Expand template variables $issueText = $issueTemplateText ` -replace '\$\(IdentityReference\)', $identityReference ` -replace '\$\(CAName\)', $caName $fixScript = $fixTemplate ` -replace '\$\(CAFullName\)', $caFullName ` -replace '\$\(IdentityReference\)', $identityReference ` -replace '\$\(RoleType\)', $roleType $revertScript = $revertTemplate ` -replace '\$\(CAFullName\)', $caFullName ` -replace '\$\(IdentityReference\)', $identityReference ` -replace '\$\(RoleType\)', $roleType # Create LS2Issue object $issue = [LS2Issue]@{ Technique = $Technique Forest = $forestName Name = $caName DistinguishedName = $ca.distinguishedName CAFullName = $caFullName IdentityReference = $identityReference IdentityReferenceSID = $principalSid ActiveDirectoryRights = $roleType Issue = $issueText Fix = $fixScript Revert = $revertScript } # Initialize IssueStore structure if needed $dn = $ca.distinguishedName if (-not $script:IssueStore) { $script:IssueStore = @{} } if (-not $script:IssueStore.ContainsKey($dn)) { $script:IssueStore[$dn] = @{} } if (-not $script:IssueStore[$dn].ContainsKey($Technique)) { $script:IssueStore[$dn][$Technique] = @() } # Only add to store if not a duplicate if (-not (Test-IssueExists -Issue $issue -DistinguishedName $dn -Technique $Technique)) { $script:IssueStore[$dn][$Technique] += $issue $issueCount++ } # Always output to pipeline if ($ExpandGroups) { Expand-IssueByGroup -Issue $issue } else { $issue } } } } } # ESC6, ESC11, and ESC16 are configuration-based (no enrollee/principal iteration) else { $vulnerableCAs = @(foreach ($ca in $allCAs) { $matchesAllConditions = $true foreach ($condition in $config.Conditions) { $propertyValue = $ca.($condition.Property) if ($propertyValue -ne $condition.Value) { $matchesAllConditions = $false break } } if ($matchesAllConditions) { $ca } }) Write-Verbose "Found $($vulnerableCAs.Count) CA(s) with $Technique-vulnerable configuration" foreach ($ca in $vulnerableCAs) { $caName = if ($ca.cn) { $ca.cn } elseif ($ca.Properties -and $ca.Properties.Contains('cn')) { $ca.Properties['cn'][0] } else { 'Unknown CA' } Write-Verbose " VULNERABLE CA: $caName" # Get CAFullName for certutil commands $caFullName = if ($ca.CAFullName) { $ca.CAFullName } else { $null } if (-not $caFullName) { Write-Verbose " CA '$caName' has no CAFullName property - skipping issue creation" continue } # Get forest name from DN $forestName = if ($ca.distinguishedName -match 'DC=([^,]+)') { $ca.distinguishedName -replace '^.*?DC=(.*)$', '$1' -replace ',DC=', '.' } else { 'Unknown' } # Join templates if they're arrays $issueTemplate = if ($config.IssueTemplate -is [array]) { $config.IssueTemplate -join '' } else { $config.IssueTemplate } $fixTemplate = if ($config.FixTemplate -is [array]) { $config.FixTemplate -join "`n" } else { $config.FixTemplate } $revertTemplate = if ($config.RevertTemplate -is [array]) { $config.RevertTemplate -join "`n" } else { $config.RevertTemplate } # Expand template variables $issueText = $issueTemplate ` -replace '\$\(CAName\)', $caName ` -replace '\$\(CAFullName\)', $caFullName $fixScript = $fixTemplate ` -replace '\$\(CAFullName\)', $caFullName $revertScript = $revertTemplate ` -replace '\$\(CAFullName\)', $caFullName # Create LS2Issue object $issue = [LS2Issue]@{ Technique = $Technique Forest = $forestName Name = $caName DistinguishedName = $ca.distinguishedName CAFullName = $caFullName Issue = $issueText Fix = $fixScript Revert = $revertScript } # Initialize IssueStore structure if needed $dn = $ca.distinguishedName if (-not $script:IssueStore) { $script:IssueStore = @{} } if (-not $script:IssueStore.ContainsKey($dn)) { $script:IssueStore[$dn] = @{} } if (-not $script:IssueStore[$dn].ContainsKey($Technique)) { $script:IssueStore[$dn][$Technique] = @() } # Only add to store if not a duplicate if (-not (Test-IssueExists -Issue $issue -DistinguishedName $dn -Technique $Technique)) { $script:IssueStore[$dn][$Technique] += $issue $issueCount++ } # Always output to pipeline if ($ExpandGroups) { Expand-IssueByGroup -Issue $issue } else { $issue } } } Write-Verbose "$Technique scan complete. Found $issueCount issue(s)." } |