Analyzers/Test-Esc5.ps1

function Test-Esc5 {
    <#
    .SYNOPSIS
        ESC5 - Dangerous ACL on a PKI object (CA, NTAuth, OID container, CA computer).
    .DESCRIPTION
        Flags AD PKI objects on which a low-privileged principal holds a write-class
        right (WriteDacl / WriteOwner / WriteProperty / GenericWrite / GenericAll), or
        which are owned by a low-privileged principal. Control of these objects
        (Enrollment Service, CA object, NTAuthCertificates, OID container, CA computer
        account) permits broad AD CS compromise - e.g. publishing a rogue CA cert to
        NTAuth, or taking over the CA host.

        Vulnerable (per object) = any Ace with AceType == Allow AND IsLowPriv == $true
          AND Rights intersects {WriteDacl, WriteOwner, WriteProperty, GenericWrite,
          GenericAll}; OR OwnerSid is a low-priv principal.
    .PARAMETER Context
        AssessmentContext pscustomobject. Reads $Context.PkiAcls.
    .PARAMETER ExtraLowPrivSid
        Extra SID strings to treat as low-privileged.
    .OUTPUTS
        Finding[]
    #>

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

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

    $reference = 'SpecterOps "Certified Pre-Owned" (ESC5) / Certipy'
    $writeRights = @('WriteDacl', 'WriteOwner', 'WriteProperty', 'GenericWrite', 'GenericAll')
    $findings = @()

    $newFinding = {
        param($Title, $Severity, $Status, $Affected, $Evidence, $Principals, $Exploit, $Remediation)
        [pscustomobject]@{
            Id             = 'ESC5'
            Title          = $Title
            Severity       = $Severity
            Status         = $Status
            AffectedObject = $Affected
            Evidence       = $Evidence
            Principals     = @($Principals)
            Exploitability = $Exploit
            RiskScore      = 0
            Remediation    = $Remediation
            Reference      = $reference
        }
    }

    $acls = @()
    if ($null -ne $Context -and $null -ne $Context.PkiAcls) { $acls = @($Context.PkiAcls) }

    if ($acls.Count -eq 0) {
        return @(& $newFinding 'ESC5 - no PKI object ACLs available' 'High' 'ManualReview' 'N/A' `
            ([pscustomobject]@{ Error = 'Context.PkiAcls is empty or unavailable; could not evaluate ESC5.' }) `
            @() 'Theoretical' 'Collect PKI object ACLs and re-run the assessment.')
    }

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

    foreach ($obj in $acls) {
        $badAces = @($obj.Aces | Where-Object {
            $_.AceType -eq 'Allow' -and $_.IsLowPriv -and (@($_.Rights | Where-Object { $writeRights -contains $_ }).Count -gt 0)
        })

        $ownerLowPriv = $false
        if ($obj.OwnerSid) { $ownerLowPriv = Test-EscLowPrivPrincipal -Sid $obj.OwnerSid -ExtraLowPrivSid $extra }

        if ($badAces.Count -eq 0 -and -not $ownerLowPriv) { continue }

        $principalNames = @()
        $principalNames += @($badAces | ForEach-Object { if ($_.PrincipalName) { $_.PrincipalName } else { $_.PrincipalSid } })
        if ($ownerLowPriv) { $principalNames += ('OWNER:{0}' -f $obj.OwnerSid) }
        $principalNames = @($principalNames | Select-Object -Unique)

        $evidence = [pscustomobject]@{
            ObjectType    = $obj.ObjectType
            DN            = $obj.DN
            DangerousAces = @($badAces | ForEach-Object {
                [pscustomobject]@{
                    Principal  = $_.PrincipalName
                    Sid        = $_.PrincipalSid
                    Rights     = @($_.Rights)
                    AccessMask = ('0x{0:X}' -f [int]$_.AccessMask)
                }
            })
            OwnerSid       = $obj.OwnerSid
            OwnerIsLowPriv = $ownerLowPriv
        }

        $findings += & $newFinding `
            ("ESC5 - {0} object '{1}' has a low-priv-writable ACL" -f $obj.ObjectType, $obj.DN) `
            'High' 'Vulnerable' $obj.DN $evidence $principalNames 'High' `
            'Remove write/owner rights from low-privileged principals on this PKI object; restrict the ACL to trusted administrative groups.'
    }

    if ($findings.Count -eq 0) {
        return @(& $newFinding 'ESC5 - no PKI objects with low-priv-writable ACLs' 'High' 'NotVulnerable' 'N/A' `
            ([pscustomobject]@{ ObjectsEvaluated = $acls.Count }) @() 'Theoretical' 'No action required for ESC5.')
    }

    return $findings
}