Analyzers/Test-Esc9.ps1

function Test-Esc9 {
    <#
    .SYNOPSIS
        ESC9 - No Security Extension (per-template CT_FLAG_NO_SECURITY_EXTENSION).
    .DESCRIPTION
        Read-only analyzer. Flags certificate templates that set
        CT_FLAG_NO_SECURITY_EXTENSION (0x00080000) in msPKI-Enrollment-Flag AND
        permit client authentication AND allow enrollment by low-privileged
        principals. Such certs omit the szOID_NTDS_CA_SECURITY_EXT (1.3.6.1.4.1.311.25.2)
        SID binding, enabling authentication as another principal when a domain
        controller does not enforce strong certificate binding.

        Exploitability is cross-referenced against $Context.DcMappings: the attack
        requires at least one DC with StrongCertificateBindingEnforcement < 2.
    .PARAMETER Context
        AssessmentContext pscustomobject.
    .PARAMETER ExtraLowPrivSid
        Additional SID strings to treat as low-privileged.
    .OUTPUTS
        Finding[]
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [pscustomobject] $Context,

        [Parameter(Mandatory = $false)]
        [string[]] $ExtraLowPrivSid = @()
    )

    $reference = 'https://research.ifcr.dk/certipy-4-0-esc9-esc10-bloodhound-and-some-fixes-42f9f5f4c142'
    $findings = @()

    $extra = @()
    if ($ExtraLowPrivSid) { $extra += $ExtraLowPrivSid }
    if ($Context.ExtraLowPrivSid) { $extra += $Context.ExtraLowPrivSid }

    if ($null -eq $Context.Templates) {
        return @([pscustomobject]@{
            Id = 'ESC9'; Title = 'ESC9 template data unavailable'; Severity = 'High';
            Status = 'ManualReview'; AffectedObject = 'N/A';
            Evidence = [pscustomobject]@{ Reason = 'Context.Templates was null; no certificate template data collected.' };
            Principals = @(); Exploitability = 'Theoretical'; RiskScore = 0;
            Remediation = 'Re-run collection with certificate template read access.';
            Reference = $reference
        })
    }

    $weakDcs = @()
    $dcDataPresent = $false
    if ($Context.DcMappings) {
        foreach ($dc in $Context.DcMappings) {
            if ($dc.Reachable -eq $false) { continue }
            $dcDataPresent = $true
            if ($null -ne $dc.StrongCertificateBindingEnforcement -and [int]$dc.StrongCertificateBindingEnforcement -lt 2) {
                $weakDcs += $dc.DomainController
            }
        }
    }

    foreach ($tmpl in $Context.Templates) {
        $noSecExt = $false
        if ($null -ne $tmpl.NoSecurityExtension) { $noSecExt = [bool]$tmpl.NoSecurityExtension }
        elseif ($null -ne $tmpl.EnrollmentFlag) { $noSecExt = (([int]$tmpl.EnrollmentFlag) -band 0x00080000) -ne 0 }
        if (-not $noSecExt) { continue }

        if (-not (Test-EscEkuAllowsAuth -EkuList $tmpl.EkuList -ApplicationPolicies $tmpl.ApplicationPolicies)) { continue }

        $lowPrivEnrollers = @()
        foreach ($ace in @($tmpl.EnrollPrincipals)) {
            if ($null -eq $ace) { continue }
            if ($ace.AceType -ne 'Allow') { continue }
            $isEnroll = ($ace.Rights -contains 'Enroll') -or ($ace.Rights -contains 'GenericAll')
            if (-not $isEnroll) { continue }
            $isLow = $false
            if ($ace.IsLowPriv) { $isLow = $true } else { $isLow = Test-EscLowPrivPrincipal -Sid $ace.PrincipalSid -ExtraLowPrivSid $extra }
            if ($isLow) { $lowPrivEnrollers += $ace }
        }
        if ($lowPrivEnrollers.Count -eq 0) { continue }

        $principals = @($lowPrivEnrollers | ForEach-Object { if ($_.PrincipalName) { $_.PrincipalName } else { $_.PrincipalSid } } | Select-Object -Unique)

        if ($weakDcs.Count -gt 0) {
            $exploit = 'High'
            $dcNote = "Weak-binding DC(s) present (StrongCertificateBindingEnforcement < 2): $($weakDcs -join ', ')"
        }
        elseif ($dcDataPresent) {
            $exploit = 'Low'
            $dcNote = 'All reachable DCs enforce StrongCertificateBindingEnforcement >= 2; exploitation blocked unless a weak DC exists.'
        }
        else {
            $exploit = 'Medium'
            $dcNote = 'DC binding enforcement unknown (no reachable DcMappings); verify StrongCertificateBindingEnforcement < 2 on a DC.'
        }

        $findings += [pscustomobject]@{
            Id = 'ESC9'
            Title = "Template '$($tmpl.Name)' omits SID security extension (ESC9)"
            Severity = 'High'
            Status = 'Vulnerable'
            AffectedObject = $tmpl.Name
            Evidence = [pscustomobject]@{
                Template            = $tmpl.Name
                DisplayName         = $tmpl.DisplayName
                EnrollmentFlag      = $tmpl.EnrollmentFlag
                NoSecurityExtension = $true
                EkuList             = @($tmpl.EkuList)
                ApplicationPolicies = @($tmpl.ApplicationPolicies)
                PublishedOnCAs      = @($tmpl.PublishedOnCAs)
                LowPrivEnrollers    = $principals
                DcContext           = $dcNote
                Note                = 'Per-template flag. Contrast ESC16 (per-CA DisableExtensionList disables the same extension CA-wide).'
            }
            Principals = $principals
            Exploitability = $exploit
            RiskScore = 0
            Remediation = 'Remove CT_FLAG_NO_SECURITY_EXTENSION from the template, restrict enrollment to trusted principals, and set StrongCertificateBindingEnforcement=2 on all DCs.'
            Reference = $reference
        }
    }

    if ($findings.Count -eq 0) {
        return @([pscustomobject]@{
            Id = 'ESC9'; Title = 'No ESC9 (No Security Extension) templates found'; Severity = 'Info';
            Status = 'NotVulnerable'; AffectedObject = 'All templates';
            Evidence = [pscustomobject]@{ TemplatesChecked = @($Context.Templates).Count };
            Principals = @(); Exploitability = 'Theoretical'; RiskScore = 0;
            Remediation = 'None required.'; Reference = $reference
        })
    }

    return $findings
}