Private/Find-ESC2Issue.ps1

function Find-ESC2Issue {
    <#
        .SYNOPSIS
        Identifies AD CS certificate templates vulnerable to ESC2 (Any Purpose EKU / no EKU) attacks.
 
        .DESCRIPTION
        ESC2 occurs when a certificate template has the Any Purpose EKU (2.5.29.37.0) or no EKU
        extension at all, AND a low-privileged principal can enroll in it. A certificate issued
        from such a template is valid for any application - including acting as a Certificate
        Request Agent - which enables Enroll On Behalf Of (EOBO) to obtain a client-auth
        certificate for another user.
 
        This function flags templates that:
        1. Have Any Purpose EKU (2.5.29.37.0) or an empty pKIExtendedKeyUsage, AND
        2. Grant Enroll (or GenericAll/FullControl) to a non-safe principal, AND
        3. Do not require manager approval (CT_FLAG_PEND_ALL_REQUESTS not set).
 
        .PARAMETER AdcsObjects
        Array of AD CS objects from Get-AdcsObjects. Filtered to certificate templates.
 
        .PARAMETER SafeUsers
        Regex pattern of SIDs considered safe (default: built-in admin/system principals).
 
        .INPUTS
        System.DirectoryServices.DirectoryEntry[]
 
        .OUTPUTS
        ESCalatorIssue[] with Technique 'ESC2'. Subtypes:
        - Template-AnyPurposeEKU: template has the Any Purpose EKU and is enrollable
        - Template-NoEKU: template has no EKU extension and is enrollable
 
        .EXAMPLE
        $AdcsObjects = Get-AdcsObjects
        $ESC2Issues = Find-ESC2Issue -AdcsObjects $AdcsObjects
 
        .NOTES
        The exploit path is Invoke-EOBOAttack (EOBO via the Any-Purpose cert as enrollment agent).
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.DirectoryServices.DirectoryEntry[]]$AdcsObjects,

        [Parameter()]
        [string]$SafeUsers
    )

    #requires -Version 5.1

    if (-not $SafeUsers) {
        $SafeUsers = '-512$|-519$|-544$|-18$|-517$|-500$|-516$|-521$|-498$|-9$|-526$|-527$|S-1-5-10$'
    }

    $anyPurposeOid = '2.5.29.37.0'
    $enrollGuid = [System.Guid]'0e10c968-78fb-11d2-90d4-00c04f79dc55'
    $autoEnrollGuid = [System.Guid]'a05b8cc2-17bc-4802-a710-e7c15ab866a2'

    $forestName = ''
    try {
        $first = $AdcsObjects | Select-Object -First 1
        if ($first -and $first.Properties['rootDomainNamingContext']) { $forestName = $first.Properties['rootDomainNamingContext'].Value }
    } catch { $forestName = '' }

    $issues = @()

    $templates = $AdcsObjects | Where-Object { $_.SchemaClassName -eq 'pKICertificateTemplate' }
    foreach ($template in $templates) {
        $name = $template.Properties['name'].Value
        $dn = $template.Properties['distinguishedName'].Value

        # ESC2 EKU condition
        $ekuProp = $template.Properties['pKIExtendedKeyUsage']
        $ekus = @()
        if ($ekuProp -and $ekuProp.Count -gt 0) { foreach ($e in $ekuProp) { $ekus += "$e" } }
        $hasAnyPurpose = $ekus -contains $anyPurposeOid
        $hasNoEku = ($ekus.Count -eq 0)
        if (-not ($hasAnyPurpose -or $hasNoEku)) { continue }
        $subtype = if ($hasAnyPurpose) { 'Template-AnyPurposeEKU' } else { 'Template-NoEKU' }

        # Not pending-approval gated
        $enrollFlag = 0
        if ($template.Properties['msPKI-Enrollment-Flag'].Value) { $enrollFlag = [int]$template.Properties['msPKI-Enrollment-Flag'].Value }
        $pendingApproval = ($enrollFlag -band 0x2) -ne 0
        if ($pendingApproval) { continue }

        # Find a non-safe principal with Enroll (or full control) rights
        $security = $template.ObjectSecurity
        foreach ($ace in $security.Access) {
            if ($ace.AccessControlType -ne 'Allow') { continue }
            $sid = $ace.IdentityReference.Value
            # Resolve group/user name to SID if it's not already one
            if ($sid -notmatch '^S-1-') {
                try { $sid = (New-Object System.Security.Principal.NTAccount($sid)).Translate([System.Security.Principal.SecurityIdentifier]).Value } catch { continue }
            }
            if ($sid -match $SafeUsers) { continue }

            $grantsEnroll = ($ace.ActiveDirectoryRights -match 'GenericAll|GenericWrite|WriteDacl|WriteOwner') -or
                            (($ace.ActiveDirectoryRights -match 'ExtendedRight') -and ($ace.ObjectType -eq $enrollGuid -or $ace.ObjectType -eq [System.Guid]::Empty -or $ace.ObjectType -eq $autoEnrollGuid))
            if (-not $grantsEnroll) { continue }

            $issueText = if ($hasAnyPurpose) {
                "$($ace.IdentityReference) can enroll in this template, which has the Any Purpose EKU (2.5.29.37.0). An issued certificate can act as a Certificate Request Agent to Enroll On Behalf Of other users (ESC2)."
            } else {
                "$($ace.IdentityReference) can enroll in this template, which has no EKU restriction. An issued certificate is valid for any application - including acting as a Certificate Request Agent for Enroll On Behalf Of (ESC2)."
            }

            $issues += [ESCalatorIssue]::new(
                $forestName, $name, $dn, $ace.IdentityReference.Value, $sid,
                $ace.ActiveDirectoryRights.ToString(), 'ESC2', $subtype, $issueText, 'High',
                $null, $template
            )
        }
    }

    return $issues
}