Private/AD/Checks/Invoke-ADCertificateServicesChecks.ps1
|
# PSGuerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0 # https://github.com/jimrtyler/PSGuerrilla | https://creativecommons.org/licenses/by/4.0/ # AI/LLM use: see AI-USAGE.md for required attribution function Invoke-ADCertificateServicesChecks { [CmdletBinding()] param( [Parameter(Mandatory)] [hashtable]$AuditData ) $checkDefs = Get-AuditCategoryDefinitions -Category 'ADCertificateServicesChecks' $findings = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($check in $checkDefs.checks) { $funcName = "Test-Recon$($check.id -replace '-', '')" if (Get-Command $funcName -ErrorAction SilentlyContinue) { try { $finding = & $funcName -AuditData $AuditData -CheckDefinition $check if ($finding) { $findings.Add($finding) } } catch { $findings.Add((New-AuditFinding -CheckDefinition $check -Status 'ERROR' ` -CurrentValue "Check failed: $_")) } } else { $findings.Add((New-AuditFinding -CheckDefinition $check -Status 'SKIP' ` -CurrentValue 'Check not yet implemented')) } } return @($findings) } # ============================================================================ # Helper: Well-known low-privileged SIDs that indicate dangerous enrollment # ============================================================================ function Test-IsLowPrivilegedSID { param([string]$SID) # Well-known SIDs that represent low-privileged / broad groups $lowPrivSIDs = @( 'S-1-1-0' # Everyone 'S-1-5-7' # Anonymous 'S-1-5-11' # Authenticated Users ) if ($SID -in $lowPrivSIDs) { return $true } # Domain Users (RID 513) and Domain Computers (RID 515) if ($SID -match '-513$' -or $SID -match '-515$') { return $true } return $false } # ============================================================================ # Helper: Get low-privileged enrollment ACEs from a template # ============================================================================ function Get-LowPrivEnrollmentACEs { param([array]$EnrollmentPermissions) if (-not $EnrollmentPermissions -or $EnrollmentPermissions.Count -eq 0) { return @() } $dangerousACEs = @($EnrollmentPermissions | Where-Object { ($_.Right -eq 'Enroll' -or $_.Right -eq 'AutoEnroll' -or $_.Right -eq 'FullControl') -and (Test-IsLowPrivilegedSID -SID $_.SID) }) return $dangerousACEs } # ============================================================================ # Helper: Get dangerous write ACEs on an object (for ESC4/ESC5) # ============================================================================ function Get-DangerousWriteACEs { param([array]$Permissions) if (-not $Permissions -or $Permissions.Count -eq 0) { return @() } $writeRights = @('WriteDacl', 'WriteOwner', 'FullControl', 'WriteAllProperties') $dangerousACEs = @($Permissions | Where-Object { ($_.Right -in $writeRights) -and (Test-IsLowPrivilegedSID -SID $_.SID) }) return $dangerousACEs } # ============================================================================ # Helper: Check if a template has authentication-capable EKU # ============================================================================ function Test-HasAuthenticationEKU { param([array]$EKUOIDs) $authOIDs = @( '1.3.6.1.5.5.7.3.2' # Client Authentication '1.3.6.1.4.1.311.20.2.2' # Smart Card Logon '1.3.6.1.5.2.3.4' # PKINIT Client Authentication '2.5.29.37.0' # Any Purpose ) # No EKU means the cert can be used for anything (SubCA equivalent) if (-not $EKUOIDs -or $EKUOIDs.Count -eq 0) { return $true } foreach ($oid in $EKUOIDs) { if ($oid -in $authOIDs) { return $true } } return $false } # ── ADCS-001: CA Server Inventory ────────────────────────────────────────── function Test-ReconADCS001 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $adcs = $AuditData.CertificateServices if (-not $adcs) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'ADCS data not available - no Certificate Services found' } $cas = @($adcs.CertificateAuthorities) if ($cas.Count -eq 0 -or ($cas.Count -eq 1 -and $null -eq $cas[0])) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'No Certificate Authorities found in the environment' } $caSummary = @($cas | ForEach-Object { $templateCount = 0 if ($_.CertificateTemplates) { $templateCount = @($_.CertificateTemplates).Count } @{ Name = $_.Name DNSHostName = $_.DNSHostName DN = $_.DN Flags = $_.Flags PublishedTemplates = $templateCount } }) $totalTemplatesPublished = ($caSummary | Measure-Object -Property PublishedTemplates -Sum).Sum $currentValue = "$($cas.Count) Certificate Authority(ies) found publishing $totalTemplatesPublished template(s) total" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue $currentValue ` -Details @{ TotalCAs = $cas.Count TotalPublishedTemplates = $totalTemplatesPublished CASummary = $caSummary } } # ── ADCS-002: ESC1 - Enrollee Supplies SAN ───────────────────────────────── function Test-ReconADCS002 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $adcs = $AuditData.CertificateServices if (-not $adcs) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'ADCS data not available - no Certificate Services found' } $templates = @($adcs.CertificateTemplates) if ($templates.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'No certificate templates found' } $vulnerableTemplates = [System.Collections.Generic.List[hashtable]]::new() foreach ($tmpl in $templates) { # Skip unpublished templates if (-not $tmpl.IsPublished) { continue } # ESC1 requires: enrollee supplies subject (SAN flag) if (-not $tmpl.EnrolleeSuppliesSubject) { continue } # ESC1 requires: authentication EKU (Client Auth, Smart Card Logon, Any Purpose, or empty) $ekuOIDs = @($tmpl.ExtendedKeyUsageOIDs) if (-not (Test-HasAuthenticationEKU -EKUOIDs $ekuOIDs)) { continue } # ESC1 requires: manager approval not required if ($tmpl.RASignaturesRequired -gt 0) { continue } # ESC1 requires: low-privileged users can enroll $lowPrivACEs = Get-LowPrivEnrollmentACEs -EnrollmentPermissions $tmpl.EnrollmentPermissions if ($lowPrivACEs.Count -eq 0) { continue } $principals = @($lowPrivACEs | ForEach-Object { $_.Identity } | Sort-Object -Unique) $vulnerableTemplates.Add(@{ TemplateName = $tmpl.Name DisplayName = $tmpl.DisplayName EnrollablePrincipals = $principals EKUs = @($tmpl.ExtendedKeyUsage | ForEach-Object { $_.Name }) SchemaVersion = $tmpl.SchemaVersion }) } if ($vulnerableTemplates.Count -gt 0) { $templateNames = @($vulnerableTemplates | ForEach-Object { $_.TemplateName }) $currentValue = "$($vulnerableTemplates.Count) template(s) vulnerable to ESC1: $($templateNames -join ', '). " + 'These templates allow enrollees to specify a SAN, have authentication EKUs, and permit low-privileged enrollment' return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' ` -CurrentValue $currentValue ` -Details @{ VulnerableTemplateCount = $vulnerableTemplates.Count VulnerableTemplates = @($vulnerableTemplates) } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No published templates vulnerable to ESC1 (enrollee-supplied SAN with authentication EKU and low-privileged enrollment)' ` -Details @{ TemplatesChecked = $templates.Count } } # ── ADCS-003: ESC2 - Any Purpose / No EKU ───────────────────────────────── function Test-ReconADCS003 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $adcs = $AuditData.CertificateServices if (-not $adcs) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'ADCS data not available - no Certificate Services found' } $templates = @($adcs.CertificateTemplates) if ($templates.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'No certificate templates found' } $vulnerableTemplates = [System.Collections.Generic.List[hashtable]]::new() foreach ($tmpl in $templates) { if (-not $tmpl.IsPublished) { continue } $ekuOIDs = @($tmpl.ExtendedKeyUsageOIDs) $hasAnyPurpose = '2.5.29.37.0' -in $ekuOIDs $hasNoEKU = $ekuOIDs.Count -eq 0 if (-not $hasAnyPurpose -and -not $hasNoEKU) { continue } # Must allow low-privileged enrollment $lowPrivACEs = Get-LowPrivEnrollmentACEs -EnrollmentPermissions $tmpl.EnrollmentPermissions if ($lowPrivACEs.Count -eq 0) { continue } # Skip if manager approval required if ($tmpl.RASignaturesRequired -gt 0) { continue } $principals = @($lowPrivACEs | ForEach-Object { $_.Identity } | Sort-Object -Unique) $reason = if ($hasAnyPurpose) { 'Any Purpose EKU (2.5.29.37.0)' } else { 'No EKU defined (SubCA equivalent)' } $vulnerableTemplates.Add(@{ TemplateName = $tmpl.Name DisplayName = $tmpl.DisplayName Reason = $reason EnrollablePrincipals = $principals SchemaVersion = $tmpl.SchemaVersion }) } if ($vulnerableTemplates.Count -gt 0) { $templateNames = @($vulnerableTemplates | ForEach-Object { $_.TemplateName }) $currentValue = "$($vulnerableTemplates.Count) template(s) vulnerable to ESC2: $($templateNames -join ', '). " + 'These templates have Any Purpose EKU or no EKU and permit low-privileged enrollment' return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' ` -CurrentValue $currentValue ` -Details @{ VulnerableTemplateCount = $vulnerableTemplates.Count VulnerableTemplates = @($vulnerableTemplates) } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No published templates vulnerable to ESC2 (Any Purpose EKU or no EKU with low-privileged enrollment)' ` -Details @{ TemplatesChecked = $templates.Count } } # ── ADCS-004: ESC3 Condition 1 - Certificate Request Agent ──────────────── function Test-ReconADCS004 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $adcs = $AuditData.CertificateServices if (-not $adcs) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'ADCS data not available - no Certificate Services found' } $templates = @($adcs.CertificateTemplates) if ($templates.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'No certificate templates found' } # Certificate Request Agent EKU OID $requestAgentOID = '1.3.6.1.4.1.311.20.2.1' $vulnerableTemplates = [System.Collections.Generic.List[hashtable]]::new() foreach ($tmpl in $templates) { if (-not $tmpl.IsPublished) { continue } $ekuOIDs = @($tmpl.ExtendedKeyUsageOIDs) if ($requestAgentOID -notin $ekuOIDs) { continue } # Must allow low-privileged enrollment $lowPrivACEs = Get-LowPrivEnrollmentACEs -EnrollmentPermissions $tmpl.EnrollmentPermissions if ($lowPrivACEs.Count -eq 0) { continue } $principals = @($lowPrivACEs | ForEach-Object { $_.Identity } | Sort-Object -Unique) $vulnerableTemplates.Add(@{ TemplateName = $tmpl.Name DisplayName = $tmpl.DisplayName EnrollablePrincipals = $principals SchemaVersion = $tmpl.SchemaVersion }) } if ($vulnerableTemplates.Count -gt 0) { $templateNames = @($vulnerableTemplates | ForEach-Object { $_.TemplateName }) $currentValue = "$($vulnerableTemplates.Count) template(s) vulnerable to ESC3 Condition 1: $($templateNames -join ', '). " + 'These templates have the Certificate Request Agent EKU and allow low-privileged enrollment' return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' ` -CurrentValue $currentValue ` -Details @{ VulnerableTemplateCount = $vulnerableTemplates.Count VulnerableTemplates = @($vulnerableTemplates) } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No published templates with Certificate Request Agent EKU enrollable by low-privileged users' ` -Details @{ TemplatesChecked = $templates.Count } } # ── ADCS-005: ESC3 Condition 2 - Enrollment Agent Co-signing ────────────── function Test-ReconADCS005 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $adcs = $AuditData.CertificateServices if (-not $adcs) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'ADCS data not available - no Certificate Services found' } $templates = @($adcs.CertificateTemplates) if ($templates.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'No certificate templates found' } # Certificate Request Agent application policy OID $requestAgentOID = '1.3.6.1.4.1.311.20.2.1' # Find templates that require an enrollment agent signature and have authentication EKU $vulnerableTemplates = [System.Collections.Generic.List[hashtable]]::new() foreach ($tmpl in $templates) { if (-not $tmpl.IsPublished) { continue } # Must require at least one RA signature (enrollment agent co-signing) if ($tmpl.RASignaturesRequired -lt 1) { continue } # Check if the application policy requires Certificate Request Agent $appPolicies = @($tmpl.ApplicationPolicies) $requiresRequestAgent = $requestAgentOID -in $appPolicies if (-not $requiresRequestAgent) { continue } # Must have authentication EKU $ekuOIDs = @($tmpl.ExtendedKeyUsageOIDs) if (-not (Test-HasAuthenticationEKU -EKUOIDs $ekuOIDs)) { continue } $ekuNames = @($tmpl.ExtendedKeyUsage | ForEach-Object { $_.Name }) $vulnerableTemplates.Add(@{ TemplateName = $tmpl.Name DisplayName = $tmpl.DisplayName RASignaturesRequired = $tmpl.RASignaturesRequired EKUs = $ekuNames SchemaVersion = $tmpl.SchemaVersion }) } if ($vulnerableTemplates.Count -gt 0) { $templateNames = @($vulnerableTemplates | ForEach-Object { $_.TemplateName }) $currentValue = "$($vulnerableTemplates.Count) template(s) vulnerable to ESC3 Condition 2: $($templateNames -join ', '). " + 'These templates require enrollment agent co-signing and have authentication EKUs, enabling on-behalf-of enrollment for any user' return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' ` -CurrentValue $currentValue ` -Details @{ VulnerableTemplateCount = $vulnerableTemplates.Count VulnerableTemplates = @($vulnerableTemplates) } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No published templates with enrollment agent co-signing and authentication EKU found' ` -Details @{ TemplatesChecked = $templates.Count } } # ── ADCS-006: ESC4 - Vulnerable Certificate Template ACLs ───────────────── function Test-ReconADCS006 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $adcs = $AuditData.CertificateServices if (-not $adcs) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'ADCS data not available - no Certificate Services found' } $templates = @($adcs.CertificateTemplates) if ($templates.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'No certificate templates found' } $vulnerableTemplates = [System.Collections.Generic.List[hashtable]]::new() foreach ($tmpl in $templates) { # Check all templates, not just published ones, since write access could publish them $dangerousACEs = Get-DangerousWriteACEs -Permissions $tmpl.EnrollmentPermissions if ($dangerousACEs.Count -eq 0) { continue } $principals = @($dangerousACEs | ForEach-Object { "$($_.Identity) ($($_.Right))" } | Sort-Object -Unique) $vulnerableTemplates.Add(@{ TemplateName = $tmpl.Name DisplayName = $tmpl.DisplayName IsPublished = $tmpl.IsPublished DangerousACEs = @($dangerousACEs | ForEach-Object { @{ Identity = $_.Identity; SID = $_.SID; Right = $_.Right } }) PrincipalSummary = $principals }) } if ($vulnerableTemplates.Count -gt 0) { $templateNames = @($vulnerableTemplates | ForEach-Object { $_.TemplateName }) $currentValue = "$($vulnerableTemplates.Count) template(s) vulnerable to ESC4 (write ACL abuse): $($templateNames -join ', '). " + 'Low-privileged principals have write permissions that could modify these templates to create exploitable conditions' return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' ` -CurrentValue $currentValue ` -Details @{ VulnerableTemplateCount = $vulnerableTemplates.Count VulnerableTemplates = @($vulnerableTemplates) } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No certificate templates with dangerous write ACLs for low-privileged principals' ` -Details @{ TemplatesChecked = $templates.Count } } # ── ADCS-007: ESC4 - Vulnerable Certificate Template Ownership ──────────── function Test-ReconADCS007 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $adcs = $AuditData.CertificateServices if (-not $adcs) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'ADCS data not available - no Certificate Services found' } $templates = @($adcs.CertificateTemplates) if ($templates.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'No certificate templates found' } # Check for templates where the Owner permission is held by a low-privileged principal. # The data collector reports WriteOwner in EnrollmentPermissions when low-priv users hold it. # We also check for explicit Owner entries if available. $vulnerableTemplates = [System.Collections.Generic.List[hashtable]]::new() foreach ($tmpl in $templates) { $permissions = @($tmpl.EnrollmentPermissions) if ($permissions.Count -eq 0) { continue } # Look for WriteOwner or FullControl ACEs from low-privileged principals # A principal with WriteOwner can take ownership and then modify the DACL $ownerACEs = @($permissions | Where-Object { ($_.Right -eq 'WriteOwner' -or $_.Right -eq 'FullControl') -and (Test-IsLowPrivilegedSID -SID $_.SID) }) if ($ownerACEs.Count -eq 0) { continue } $principals = @($ownerACEs | ForEach-Object { "$($_.Identity) ($($_.Right))" } | Sort-Object -Unique) $vulnerableTemplates.Add(@{ TemplateName = $tmpl.Name DisplayName = $tmpl.DisplayName IsPublished = $tmpl.IsPublished OwnershipACEs = @($ownerACEs | ForEach-Object { @{ Identity = $_.Identity; SID = $_.SID; Right = $_.Right } }) PrincipalSummary = $principals }) } if ($vulnerableTemplates.Count -gt 0) { $templateNames = @($vulnerableTemplates | ForEach-Object { $_.TemplateName }) $currentValue = "$($vulnerableTemplates.Count) template(s) with ownership vulnerability (ESC4): $($templateNames -join ', '). " + 'Low-privileged principals can take ownership or already have ownership-level control over these templates' return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' ` -CurrentValue $currentValue ` -Details @{ VulnerableTemplateCount = $vulnerableTemplates.Count VulnerableTemplates = @($vulnerableTemplates) } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No certificate templates with ownership vulnerabilities for low-privileged principals' ` -Details @{ TemplatesChecked = $templates.Count } } # ── ADCS-008: ESC5 - Vulnerable PKI Object ACLs ────────────────────────── function Test-ReconADCS008 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $adcs = $AuditData.CertificateServices if (-not $adcs) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'ADCS data not available - no Certificate Services found' } $pkiObjects = @($adcs.PKIObjects) if ($pkiObjects.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'No PKI container objects found for ACL analysis' } $vulnerableObjects = [System.Collections.Generic.List[hashtable]]::new() foreach ($pkiObj in $pkiObjects) { $dangerousACEs = Get-DangerousWriteACEs -Permissions $pkiObj.Permissions if ($dangerousACEs.Count -eq 0) { continue } $principals = @($dangerousACEs | ForEach-Object { "$($_.Identity) ($($_.Right))" } | Sort-Object -Unique) $vulnerableObjects.Add(@{ ObjectName = $pkiObj.Name DN = $pkiObj.DN ObjectClass = $pkiObj.ObjectClass DangerousACEs = @($dangerousACEs | ForEach-Object { @{ Identity = $_.Identity; SID = $_.SID; Right = $_.Right } }) PrincipalSummary = $principals }) } if ($vulnerableObjects.Count -gt 0) { $objectNames = @($vulnerableObjects | ForEach-Object { "$($_.ObjectName) ($($_.ObjectClass))" }) $currentValue = "$($vulnerableObjects.Count) PKI object(s) vulnerable to ESC5: $($objectNames -join ', '). " + 'Low-privileged principals have write permissions on PKI container objects that could enable rogue CA addition or enrollment manipulation' return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' ` -CurrentValue $currentValue ` -Details @{ VulnerableObjectCount = $vulnerableObjects.Count VulnerableObjects = @($vulnerableObjects) } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue "All $($pkiObjects.Count) PKI container object(s) have appropriate ACLs - no dangerous write permissions for low-privileged principals" ` -Details @{ PKIObjectsChecked = $pkiObjects.Count } } # ── ADCS-009: ESC6 - EDITF_ATTRIBUTESUBJECTALTNAME2 Flag ───────────────── function Test-ReconADCS009 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $adcs = $AuditData.CertificateServices if (-not $adcs) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'ADCS data not available - no Certificate Services found' } $cas = @($adcs.CertificateAuthorities) if ($cas.Count -eq 0 -or ($cas.Count -eq 1 -and $null -eq $cas[0])) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'No Certificate Authorities found' } # EDITF_ATTRIBUTESUBJECTALTNAME2 = 0x00040000 (262144) $editfSanFlag = 0x00040000 $vulnerableCAs = [System.Collections.Generic.List[hashtable]]::new() $checkedCAs = [System.Collections.Generic.List[hashtable]]::new() foreach ($ca in $cas) { $flags = [int]$ca.Flags $hasSanFlag = ($flags -band $editfSanFlag) -ne 0 $caEntry = @{ CAName = $ca.Name DNSHostName = $ca.DNSHostName Flags = $flags HasSANFlag = $hasSanFlag } $checkedCAs.Add($caEntry) if ($hasSanFlag) { $vulnerableCAs.Add($caEntry) } } if ($vulnerableCAs.Count -gt 0) { $caNames = @($vulnerableCAs | ForEach-Object { $_.CAName }) $currentValue = "$($vulnerableCAs.Count) CA(s) have EDITF_ATTRIBUTESUBJECTALTNAME2 enabled: $($caNames -join ', '). " + 'Any certificate request can include a user-defined SAN regardless of template configuration, making all templates vulnerable to ESC1-style attacks' return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' ` -CurrentValue $currentValue ` -Details @{ VulnerableCACount = $vulnerableCAs.Count VulnerableCAs = @($vulnerableCAs) AllCAs = @($checkedCAs) } } # If flags are all zero, the flag data may not have been collected via LDAP $allZeroFlags = ($checkedCAs | Where-Object { $_.Flags -eq 0 }).Count -eq $checkedCAs.Count if ($allZeroFlags -and $checkedCAs.Count -gt 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue "CA flags could not be fully verified via LDAP for $($cas.Count) CA(s). Run 'certutil -getreg policy\EditFlags' on each CA server to check for EDITF_ATTRIBUTESUBJECTALTNAME2" ` -Details @{ AllCAs = @($checkedCAs) } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue "EDITF_ATTRIBUTESUBJECTALTNAME2 flag is not set on any of the $($cas.Count) CA(s)" ` -Details @{ AllCAs = @($checkedCAs) } } # ── ADCS-010: ESC7 - Vulnerable CA ACLs ────────────────────────────────── function Test-ReconADCS010 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $adcs = $AuditData.CertificateServices if (-not $adcs) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'ADCS data not available - no Certificate Services found' } $cas = @($adcs.CertificateAuthorities) if ($cas.Count -eq 0 -or ($cas.Count -eq 1 -and $null -eq $cas[0])) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'No Certificate Authorities found' } # Check PKI objects for CA enrollment service objects with dangerous ACLs # ManageCA and ManageCertificates are exposed as WriteDacl/WriteOwner/FullControl # on the CA's enrollment service AD object $pkiObjects = @($adcs.PKIObjects) $vulnerableCAs = [System.Collections.Generic.List[hashtable]]::new() # Check the enrollment service objects in PKIObjects foreach ($pkiObj in $pkiObjects) { # Match enrollment service objects to known CAs $matchingCA = $cas | Where-Object { $_.Name -eq $pkiObj.Name } if (-not $matchingCA) { continue } $dangerousACEs = @() if ($pkiObj.Permissions) { $dangerousACEs = @($pkiObj.Permissions | Where-Object { ($_.Right -eq 'WriteDacl' -or $_.Right -eq 'WriteOwner' -or $_.Right -eq 'FullControl' -or $_.Right -eq 'WriteAllProperties') -and (Test-IsLowPrivilegedSID -SID $_.SID) }) } if ($dangerousACEs.Count -eq 0) { continue } $principals = @($dangerousACEs | ForEach-Object { "$($_.Identity) ($($_.Right))" } | Sort-Object -Unique) $vulnerableCAs.Add(@{ CAName = $pkiObj.Name DN = $pkiObj.DN DangerousACEs = @($dangerousACEs | ForEach-Object { @{ Identity = $_.Identity; SID = $_.SID; Right = $_.Right } }) PrincipalSummary = $principals }) } if ($vulnerableCAs.Count -gt 0) { $caNames = @($vulnerableCAs | ForEach-Object { $_.CAName }) $currentValue = "$($vulnerableCAs.Count) CA(s) vulnerable to ESC7: $($caNames -join ', '). " + 'Low-privileged principals have ManageCA or ManageCertificates-equivalent permissions that could enable CA configuration changes or pending request approval' return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' ` -CurrentValue $currentValue ` -Details @{ VulnerableCACount = $vulnerableCAs.Count VulnerableCAs = @($vulnerableCAs) } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue "No CA enrollment service objects have dangerous ACLs for low-privileged principals. Verify CA permissions manually with 'certutil -getacl' for full ManageCA/ManageCertificates audit" ` -Details @{ CAsChecked = $cas.Count PKIObjectsChecked = $pkiObjects.Count } } # ── ADCS-011: ESC8 - NTLM Relay to HTTP Endpoints ──────────────────────── function Test-ReconADCS011 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $adcs = $AuditData.CertificateServices if (-not $adcs) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'ADCS data not available - no Certificate Services found' } $cas = @($adcs.CertificateAuthorities) if ($cas.Count -eq 0 -or ($cas.Count -eq 1 -and $null -eq $cas[0])) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'No Certificate Authorities found' } # HTTP enrollment endpoints cannot be fully detected via LDAP alone. # We report the CA hostnames and advise manual verification of IIS bindings. $caHostnames = @($cas | ForEach-Object { $_.DNSHostName } | Where-Object { $_ } | Sort-Object -Unique) if ($caHostnames.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue 'CA DNS hostnames not available. Manually verify that no HTTP-based enrollment endpoints (certsrv, CES) are configured on CA servers' ` -Details @{ CAsChecked = $cas.Count } } # Report as WARN since we cannot confirm HTTP enrollment status via LDAP $currentValue = "$($caHostnames.Count) CA server(s) identified: $($caHostnames -join ', '). " + 'Verify that no HTTP-based enrollment endpoints (certsrv/CES) are configured without HTTPS and Extended Protection for Authentication (EPA). ' + 'Check IIS bindings on each CA server. HTTP enrollment enables NTLM relay attacks (ESC8)' return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue $currentValue ` -Details @{ CAHostnames = $caHostnames Note = 'HTTP enrollment endpoint detection requires direct IIS inspection on each CA server. LDAP-based detection is limited.' } } # ── ADCS-012: ESC9 - StrongCertificateBindingEnforcement ───────────────── function Test-ReconADCS012 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $adcs = $AuditData.CertificateServices if (-not $adcs) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'ADCS data not available - no Certificate Services found' } # StrongCertificateBindingEnforcement is a registry value on DCs, not in AD LDAP. # Check GPO data if available. $gpoData = $AuditData.GroupPolicies $bindingValue = $null if ($gpoData -and $gpoData.ContainsKey('SYSVOLContent')) { $sysvolContent = $gpoData.SYSVOLContent foreach ($gpoId in $sysvolContent.Keys) { $gpoContent = $sysvolContent[$gpoId] if ($gpoContent -is [hashtable] -and $gpoContent.ContainsKey('RegistryPolicies')) { foreach ($regPolicy in $gpoContent.RegistryPolicies) { if ($regPolicy.ValueName -eq 'StrongCertificateBindingEnforcement' -or $regPolicy.ValueName -eq 'strongcertificatebindingenforcement') { $bindingValue = [int]$regPolicy.Value } } } } } if ($null -ne $bindingValue) { # 0 = Disabled, 1 = Compatibility mode, 2 = Full enforcement $status = if ($bindingValue -eq 2) { 'PASS' } elseif ($bindingValue -eq 1) { 'WARN' } else { 'FAIL' } $valueLabel = switch ($bindingValue) { 0 { 'Disabled (no enforcement)' } 1 { 'Compatibility mode (partial enforcement)' } 2 { 'Full enforcement mode' } default { "Unknown ($bindingValue)" } } $currentValue = "StrongCertificateBindingEnforcement: $bindingValue ($valueLabel)" if ($bindingValue -lt 2) { $currentValue += '. ESC9 and ESC10 attacks may be possible without full enforcement' } return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status ` -CurrentValue $currentValue ` -Details @{ StrongCertificateBindingEnforcement = $bindingValue Description = $valueLabel } } # Check for templates with CT_FLAG_NO_SECURITY_EXTENSION (0x80000) in enrollment flag $noSecExtFlag = 0x80000 $templatesWithNoSecExt = [System.Collections.Generic.List[string]]::new() $templates = @($adcs.CertificateTemplates) foreach ($tmpl in $templates) { if (-not $tmpl.IsPublished) { continue } $enrollFlag = [int]$tmpl.EnrollmentFlag if (($enrollFlag -band $noSecExtFlag) -ne 0) { $templatesWithNoSecExt.Add($tmpl.Name) } } $details = @{ Note = 'StrongCertificateBindingEnforcement registry value not found in GPO data. Check HKLM\SYSTEM\CurrentControlSet\Services\Kdc\StrongCertificateBindingEnforcement on all DCs. Value should be 2.' } if ($templatesWithNoSecExt.Count -gt 0) { $details['TemplatesWithNoSecurityExtension'] = @($templatesWithNoSecExt) return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue "StrongCertificateBindingEnforcement could not be verified from GPO data. Additionally, $($templatesWithNoSecExt.Count) published template(s) have CT_FLAG_NO_SECURITY_EXTENSION set: $($templatesWithNoSecExt -join ', '). Verify enforcement is set to 2 on all DCs" ` -Details $details } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue 'StrongCertificateBindingEnforcement could not be verified from GPO data. Check registry value on all DCs to ensure it is set to 2 (full enforcement) to mitigate ESC9 attacks' ` -Details $details } # ── ADCS-013: ESC11 - RPC Relay Without Encryption ─────────────────────── function Test-ReconADCS013 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $adcs = $AuditData.CertificateServices if (-not $adcs) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'ADCS data not available - no Certificate Services found' } $cas = @($adcs.CertificateAuthorities) if ($cas.Count -eq 0 -or ($cas.Count -eq 1 -and $null -eq $cas[0])) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'No Certificate Authorities found' } # IF_ENFORCEENCRYPTICERTREQUEST cannot be directly verified via LDAP. # We can only advise manual verification on each CA server. $caHostnames = @($cas | ForEach-Object { $_.DNSHostName } | Where-Object { $_ } | Sort-Object -Unique) $caNames = @($cas | ForEach-Object { $_.Name }) $currentValue = "$($cas.Count) CA(s) identified: $($caNames -join ', '). " + "Verify IF_ENFORCEENCRYPTICERTREQUEST flag is enabled on each CA using 'certutil -getreg CA\InterfaceFlags'. " + 'Without this flag, the RPC enrollment interface is vulnerable to NTLM relay attacks (ESC11)' return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue $currentValue ` -Details @{ CANames = $caNames CAHostnames = $caHostnames Note = 'IF_ENFORCEENCRYPTICERTREQUEST flag status cannot be determined via LDAP. Manual verification required on each CA server.' } } # ── ADCS-014: ESC13 - Issuance Policy OID Group Link ──────────────────── function Test-ReconADCS014 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $adcs = $AuditData.CertificateServices if (-not $adcs) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'ADCS data not available - no Certificate Services found' } $oidObjects = @($adcs.OIDObjects) if ($oidObjects.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'No OID objects found in PKI configuration' } # Find OIDs with group links (msDS-OIDToGroupLink populated) $linkedOIDs = [System.Collections.Generic.List[hashtable]]::new() foreach ($oidObj in $oidObjects) { if (-not $oidObj.HasGroupLink) { continue } # Determine which templates use this issuance policy OID $linkedTemplates = @() $templates = @($adcs.CertificateTemplates) foreach ($tmpl in $templates) { if (-not $tmpl.IsPublished) { continue } $appPolicies = @($tmpl.ApplicationPolicies) if ($oidObj.OID -in $appPolicies) { $linkedTemplates += $tmpl.Name } } $linkedOIDs.Add(@{ OIDName = $oidObj.Name DisplayName = $oidObj.DisplayName OID = $oidObj.OID GroupLink = $oidObj.GroupLink DN = $oidObj.DN LinkedTemplates = $linkedTemplates }) } if ($linkedOIDs.Count -gt 0) { $oidNames = @($linkedOIDs | ForEach-Object { "$($_.OIDName) -> $($_.GroupLink)" }) $currentValue = "$($linkedOIDs.Count) issuance policy OID(s) linked to security groups (ESC13): $($oidNames -join '; '). " + 'Enrollees in templates using these issuance policies effectively gain group membership via the linked group' return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' ` -CurrentValue $currentValue ` -Details @{ LinkedOIDCount = $linkedOIDs.Count LinkedOIDs = @($linkedOIDs) } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue "No issuance policy OIDs linked to security groups among $($oidObjects.Count) OID object(s)" ` -Details @{ OIDObjectsChecked = $oidObjects.Count } } # ── ADCS-015: ESC15 - Application Policies in Schema v1 Templates ──────── function Test-ReconADCS015 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $adcs = $AuditData.CertificateServices if (-not $adcs) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'ADCS data not available - no Certificate Services found' } $templates = @($adcs.CertificateTemplates) if ($templates.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'No certificate templates found' } # Schema v1 templates do not enforce Application Policies from the template definition # This allows enrollees to add arbitrary EKUs to the certificate request $vulnerableTemplates = [System.Collections.Generic.List[hashtable]]::new() foreach ($tmpl in $templates) { if (-not $tmpl.IsPublished) { continue } # Only Schema v1 templates are affected if ([int]$tmpl.SchemaVersion -ne 1) { continue } # Check if low-privileged users can enroll $lowPrivACEs = Get-LowPrivEnrollmentACEs -EnrollmentPermissions $tmpl.EnrollmentPermissions if ($lowPrivACEs.Count -eq 0) { continue } $principals = @($lowPrivACEs | ForEach-Object { $_.Identity } | Sort-Object -Unique) $vulnerableTemplates.Add(@{ TemplateName = $tmpl.Name DisplayName = $tmpl.DisplayName SchemaVersion = $tmpl.SchemaVersion EKUs = @($tmpl.ExtendedKeyUsage | ForEach-Object { $_.Name }) EnrollablePrincipals = $principals }) } if ($vulnerableTemplates.Count -gt 0) { $templateNames = @($vulnerableTemplates | ForEach-Object { $_.TemplateName }) $currentValue = "$($vulnerableTemplates.Count) Schema v1 template(s) vulnerable to ESC15: $($templateNames -join ', '). " + 'Schema v1 templates do not enforce Application Policies, allowing enrollees to add authentication EKUs to certificate requests' return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' ` -CurrentValue $currentValue ` -Details @{ VulnerableTemplateCount = $vulnerableTemplates.Count VulnerableTemplates = @($vulnerableTemplates) } } # Report count of v1 templates even if none are low-priv enrollable $v1Count = @($templates | Where-Object { [int]$_.SchemaVersion -eq 1 -and $_.IsPublished }).Count return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue "No published Schema v1 templates with low-privileged enrollment found ($v1Count published v1 template(s) total)" ` -Details @{ TemplatesChecked = $templates.Count SchemaV1Published = $v1Count } } # ── ADCS-016: ESC16 - UPN SAN Misconfiguration ────────────────────────── function Test-ReconADCS016 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $adcs = $AuditData.CertificateServices if (-not $adcs) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'ADCS data not available - no Certificate Services found' } $templates = @($adcs.CertificateTemplates) if ($templates.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'No certificate templates found' } # CT_FLAG_SUBJECT_ALT_REQUIRE_UPN = 0x02000000 $upnSanFlag = 0x02000000 $vulnerableTemplates = [System.Collections.Generic.List[hashtable]]::new() foreach ($tmpl in $templates) { if (-not $tmpl.IsPublished) { continue } $certNameFlag = [int]$tmpl.CertificateNameFlag # Check for enrollee supplies subject or UPN SAN requirement $hasEnrolleeSuppliesSubject = $tmpl.EnrolleeSuppliesSubject $hasUPNSan = ($certNameFlag -band $upnSanFlag) -ne 0 if (-not $hasEnrolleeSuppliesSubject -and -not $hasUPNSan) { continue } # Must have authentication EKU $ekuOIDs = @($tmpl.ExtendedKeyUsageOIDs) if (-not (Test-HasAuthenticationEKU -EKUOIDs $ekuOIDs)) { continue } # Must allow low-privileged enrollment $lowPrivACEs = Get-LowPrivEnrollmentACEs -EnrollmentPermissions $tmpl.EnrollmentPermissions if ($lowPrivACEs.Count -eq 0) { continue } $principals = @($lowPrivACEs | ForEach-Object { $_.Identity } | Sort-Object -Unique) $flags = @() if ($hasEnrolleeSuppliesSubject) { $flags += 'ENROLLEE_SUPPLIES_SUBJECT' } if ($hasUPNSan) { $flags += 'SUBJECT_ALT_REQUIRE_UPN' } $vulnerableTemplates.Add(@{ TemplateName = $tmpl.Name DisplayName = $tmpl.DisplayName NameFlags = $flags EKUs = @($tmpl.ExtendedKeyUsage | ForEach-Object { $_.Name }) EnrollablePrincipals = $principals SchemaVersion = $tmpl.SchemaVersion }) } if ($vulnerableTemplates.Count -gt 0) { $templateNames = @($vulnerableTemplates | ForEach-Object { $_.TemplateName }) $currentValue = "$($vulnerableTemplates.Count) template(s) potentially vulnerable to ESC16: $($templateNames -join ', '). " + 'These templates allow UPN specification in the SAN with authentication EKUs. ' + 'If StrongCertificateBindingEnforcement is not set to 2, UPN-based certificate mapping enables impersonation' return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' ` -CurrentValue $currentValue ` -Details @{ VulnerableTemplateCount = $vulnerableTemplates.Count VulnerableTemplates = @($vulnerableTemplates) } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No published templates with UPN SAN misconfiguration and low-privileged enrollment found' ` -Details @{ TemplatesChecked = $templates.Count } } # ── ADCS-017: EKEUwu - Extended Key Usage Abuse ───────────────────────── function Test-ReconADCS017 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $adcs = $AuditData.CertificateServices if (-not $adcs) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'ADCS data not available - no Certificate Services found' } $templates = @($adcs.CertificateTemplates) if ($templates.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'No certificate templates found' } # EKEUwu targets templates where the EKU is not strictly enforced. # This includes: Schema v1 templates (EKU not enforced), templates with # Any Purpose EKU, templates with no EKU, and templates where application # policies can be overridden by the enrollee. $vulnerableTemplates = [System.Collections.Generic.List[hashtable]]::new() foreach ($tmpl in $templates) { if (-not $tmpl.IsPublished) { continue } $ekuOIDs = @($tmpl.ExtendedKeyUsageOIDs) $schemaVersion = [int]$tmpl.SchemaVersion $isVulnerable = $false $reasons = [System.Collections.Generic.List[string]]::new() # Schema v1: EKU not enforced from template if ($schemaVersion -eq 1) { $isVulnerable = $true $reasons.Add('Schema v1 template - EKU not enforced by template') } # Any Purpose EKU if ('2.5.29.37.0' -in $ekuOIDs) { $isVulnerable = $true $reasons.Add('Any Purpose EKU allows certificate to be used for any purpose') } # No EKU (SubCA equivalent) if ($ekuOIDs.Count -eq 0) { $isVulnerable = $true $reasons.Add('No EKU defined - certificate can be used for any purpose (SubCA equivalent)') } if (-not $isVulnerable) { continue } # Must allow low-privileged enrollment $lowPrivACEs = Get-LowPrivEnrollmentACEs -EnrollmentPermissions $tmpl.EnrollmentPermissions if ($lowPrivACEs.Count -eq 0) { continue } # Skip if manager approval required if ($tmpl.RASignaturesRequired -gt 0) { continue } $principals = @($lowPrivACEs | ForEach-Object { $_.Identity } | Sort-Object -Unique) $vulnerableTemplates.Add(@{ TemplateName = $tmpl.Name DisplayName = $tmpl.DisplayName SchemaVersion = $schemaVersion Reasons = @($reasons) EKUs = @($tmpl.ExtendedKeyUsage | ForEach-Object { $_.Name }) EnrollablePrincipals = $principals }) } if ($vulnerableTemplates.Count -gt 0) { $templateNames = @($vulnerableTemplates | ForEach-Object { $_.TemplateName }) $currentValue = "$($vulnerableTemplates.Count) template(s) vulnerable to EKU abuse: $($templateNames -join ', '). " + 'These templates have configurations where the EKU can be influenced by the enrollee, allowing addition of authentication capabilities' return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' ` -CurrentValue $currentValue ` -Details @{ VulnerableTemplateCount = $vulnerableTemplates.Count VulnerableTemplates = @($vulnerableTemplates) } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No published templates with exploitable EKU abuse patterns and low-privileged enrollment found' ` -Details @{ TemplatesChecked = $templates.Count } } # ── ADCS-018: CA Auditing Configuration ────────────────────────────────── function Test-ReconADCS018 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $adcs = $AuditData.CertificateServices if (-not $adcs) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'ADCS data not available - no Certificate Services found' } $cas = @($adcs.CertificateAuthorities) if ($cas.Count -eq 0 -or ($cas.Count -eq 1 -and $null -eq $cas[0])) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'No Certificate Authorities found' } # CA audit flags are typically stored in the registry and not directly exposed # in the AD enrollment service object via LDAP. The Flags attribute on the CA # object may contain some audit information but is not comprehensive. # Check if audit flag data is available via GPO or CA flags. # Audit flag bits (from certutil -getreg CA\AuditFilter): # 0x01 = Start/Stop # 0x02 = Backup/Restore # 0x04 = Certificate Issued # 0x08 = Certificate Denied/Failed # 0x10 = Certificate Revoked # 0x20 = CA Security Changed # 0x40 = Store/Retrieve Archived Key # 0x80 = CA Configuration Changed # All enabled = 0xFF (255) $fullAuditMask = 0x7F # 127 = all seven standard categories $caNames = @($cas | ForEach-Object { $_.Name }) $caHostnames = @($cas | ForEach-Object { $_.DNSHostName } | Where-Object { $_ } | Sort-Object -Unique) # Since CA audit flags cannot be reliably determined via LDAP alone, # report as WARN with manual verification guidance $currentValue = "$($cas.Count) CA(s) identified: $($caNames -join ', '). " + "CA audit configuration cannot be verified via LDAP. Run 'certutil -getreg CA\AuditFilter' on each CA server. " + 'All audit categories should be enabled (AuditFilter = 0x7F or 127) for comprehensive logging of certificate issuance, revocation, and CA configuration changes' return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue $currentValue ` -Details @{ CANames = $caNames CAHostnames = $caHostnames Note = 'CA AuditFilter registry value (HKLM\SYSTEM\CurrentControlSet\Services\CertSvc\Configuration\<CAName>\AuditFilter) must be checked directly on each CA server. Expected value: 0x7F (127) for all audit categories enabled.' ExpectedAuditFilter = $fullAuditMask } } # ── ADCS-019: Certificate Template Enumeration ────────────────────────── function Test-ReconADCS019 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $adcs = $AuditData.CertificateServices if (-not $adcs) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'ADCS data not available - no Certificate Services found' } $templates = @($adcs.CertificateTemplates) if ($templates.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'No certificate templates found in the environment' } $publishedCount = @($templates | Where-Object { $_.IsPublished }).Count $unpublishedCount = $templates.Count - $publishedCount # Schema version breakdown $v1Count = @($templates | Where-Object { [int]$_.SchemaVersion -eq 1 }).Count $v2Count = @($templates | Where-Object { [int]$_.SchemaVersion -eq 2 }).Count $v3Count = @($templates | Where-Object { [int]$_.SchemaVersion -eq 3 }).Count $v4Count = @($templates | Where-Object { [int]$_.SchemaVersion -ge 4 }).Count # Templates with authentication EKUs $authTemplateCount = @($templates | Where-Object { $_.IsPublished -and $_.AllowsAuthentication }).Count # Templates with enrollee-supplies-subject $sanTemplateCount = @($templates | Where-Object { $_.IsPublished -and $_.EnrolleeSuppliesSubject }).Count # Templates with low-privileged enrollment $lowPrivTemplateCount = 0 foreach ($tmpl in $templates) { if (-not $tmpl.IsPublished) { continue } $lowPrivACEs = Get-LowPrivEnrollmentACEs -EnrollmentPermissions $tmpl.EnrollmentPermissions if ($lowPrivACEs.Count -gt 0) { $lowPrivTemplateCount++ } } $templateSummary = @($templates | Where-Object { $_.IsPublished } | ForEach-Object { @{ Name = $_.Name DisplayName = $_.DisplayName SchemaVersion = $_.SchemaVersion EKUs = @($_.ExtendedKeyUsage | ForEach-Object { $_.Name }) AllowsAuth = $_.AllowsAuthentication SuppliesSAN = $_.EnrolleeSuppliesSubject RASignatures = $_.RASignaturesRequired } }) $currentValue = "$($templates.Count) certificate template(s) found: $publishedCount published, $unpublishedCount unpublished. " + "Schema versions: v1=$v1Count, v2=$v2Count, v3=$v3Count, v4+=$v4Count. " + "$authTemplateCount published template(s) allow authentication, $sanTemplateCount allow enrollee-supplied SAN, " + "$lowPrivTemplateCount enrollable by low-privileged users" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue $currentValue ` -Details @{ TotalTemplates = $templates.Count PublishedTemplates = $publishedCount UnpublishedTemplates = $unpublishedCount SchemaV1Count = $v1Count SchemaV2Count = $v2Count SchemaV3Count = $v3Count SchemaV4PlusCount = $v4Count AuthenticationTemplates = $authTemplateCount SANTemplates = $sanTemplateCount LowPrivEnrollable = $lowPrivTemplateCount PublishedTemplateSummary = $templateSummary } } |