Public/Deploy-AGEsc5Chain.ps1

function Deploy-AGEsc5Chain {
    <#
    .SYNOPSIS
        Deploys the ESC4-on-unpublished-template + ESC5-on-CA-object chain.

    .DESCRIPTION
        Implements the unpublished chain settled in ADCSGoat issue #30, riding
        the shared scenario pipeline (Invoke-AGTemplateScenario, issue #27).

        The template half is ESC4: "Copy of Workstation" is a verbatim clone of
        the built-in Workstation Authentication template (schema v2) with zero
        attribute overrides and Domain Users Full Control (GenericAll) layered
        on the copied source DACL. It is deliberately NOT published on the
        selected CA — the attacker must first abuse the ESC5 grant to enable it.

        The CA half is ESC5: Authenticated Users are granted Full Control
        (GenericAll) on the selected CA's pKIEnrollmentService directory object
        — the object that holds certificateTemplates — never the CA host or
        service security descriptor. That grant lets a low-privileged principal
        edit certificateTemplates (publish Copy of Workstation, turning the
        ESC4 into an ESC1) and more.

        Redeploy enforces pristine state: if an exercise published Copy of
        Workstation, its cn is stripped from certificateTemplates and the
        removal is logged; the clone itself is replaced under the standard
        collision contract. The Workstation source is never written; the grant
        is reversed at teardown by byte-restoring the CA's pre-change security
        descriptor from the state file.

    .PARAMETER CAName
        The cn of the enterprise CA to target. Optional; autodetected when the
        forest has exactly one enterprise CA.

    .PARAMETER StatePath
        Where the deploy state file lives. Defaults to ADCSGoat.State.xml next
        to the module root.

    .PARAMETER Server
        The domain controller to write to. Defaults to the logon server.

    .PARAMETER Force
        Replaces an ADCSGoat-owned existing clone without prompting. Required
        for non-interactive redeploy.

    .OUTPUTS
        System.Management.Automation.PSCustomObject with CloneCn, Oid, and
        CompanionOidObjectDN.

    .EXAMPLE
        Deploy-AGEsc5Chain

        Clones Workstation Authentication to 'Copy of Workstation' (unpublished,
        Domain Users Full Control) and grants Authenticated Users Full Control
        on the forest's single CA object.

    .EXAMPLE
        Deploy-AGEsc5Chain -CAName 'LabRootCA1' -Force

        Redeploys against the named CA, replacing any owned clone and
        re-enforcing the unpublished pristine state.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '',
        Justification = 'AD writes gated by the Copy-AGTemplate collision prompt / -Force contract per module precedent.')]
    [CmdletBinding()]
    param (
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$CAName,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$StatePath,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Server,

        [Parameter()]
        [switch]$Force
    )

    begin {
        Add-Type -AssemblyName System.DirectoryServices

        if ([string]::IsNullOrEmpty($StatePath)) {
            $StatePath = Join-Path -Path $PSScriptRoot -ChildPath '..\ADCSGoat.State.xml'
        }

        $allPropsGuid = [guid]'00000000-0000-0000-0000-000000000000'

        # Rights on the clone: Domain Users Full Control (GenericAll), layered
        # on the copied DACL. No recipe: zero attribute overrides.
        $accessRules = {
            param($domainSid)
            New-Object System.DirectoryServices.ActiveDirectoryAccessRule $domainSid, ([System.DirectoryServices.ActiveDirectoryRights]::GenericAll), ([System.Security.AccessControl.AccessControlType]::Allow), $allPropsGuid
        }

        $helperParams = @{}
        if ($PSBoundParameters.ContainsKey('Server')) { $helperParams['Server'] = $Server }
    }

    process {
        # 1. The unpublished ESC4 clone. SkipPublish keeps its cn out of the
        # selected CA's certificateTemplates and strips it on redeploy.
        $pipelineParams = @{
            Scenario        = 'ESC5Chain'
            SourceName      = 'Workstation'
            DestinationName = 'Copy of Workstation'
            AccessRules     = $accessRules
            SkipPublish     = $true
            StatePath       = $StatePath
        }
        if ($PSBoundParameters.ContainsKey('CAName')) { $pipelineParams['CAName'] = $CAName }
        if ($helperParams.ContainsKey('Server')) { $pipelineParams['Server'] = $helperParams['Server'] }
        if ($Force.IsPresent) { $pipelineParams['Force'] = $true }

        $cloneResult = Invoke-AGTemplateScenario @pipelineParams

        # 2. The ESC5 grant: Authenticated Users Full Control on the selected
        # CA's pKIEnrollmentService directory object (additive + idempotent).
        if ($PSBoundParameters.ContainsKey('CAName')) {
            $selectedCA = Get-AGEnrollmentService -CAName $CAName @helperParams
        } else {
            $selectedCA = Get-AGEnrollmentService @helperParams
        }
        $authenticatedUsersSid = New-Object System.Security.Principal.SecurityIdentifier('S-1-5-11')
        $caGrant = New-Object System.DirectoryServices.ActiveDirectoryAccessRule $authenticatedUsersSid, ([System.DirectoryServices.ActiveDirectoryRights]::GenericAll), ([System.Security.AccessControl.AccessControlType]::Allow), $allPropsGuid
        Set-AGEnrollmentServiceAce -DistinguishedName $selectedCA.DistinguishedName -AccessRules $caGrant @helperParams

        Write-Output $cloneResult
    }
}