Private/Set/Set-LowPrivilegeEnrollee.ps1
|
function Set-LowPrivilegeEnrollee { <# .SYNOPSIS Adds a LowPrivilegeEnrollee property to AD CS certificate template objects. .DESCRIPTION Examines the access control lists (ACLs) of Active Directory Certificate Services certificate template objects to identify enrollment permissions granted to principals that are neither high-privilege administrators nor overly broad dangerous groups. This function identifies "middle ground" enrollees - specific users or groups that have enrollment permissions but aren't part of the standard administrative hierarchy or the dangerous principals that represent broad attack surfaces. The function excludes two categories of principals: 1. Safe/Administrative principals: Domain Admins, Enterprise Admins, SYSTEM, etc. 2. Dangerous principals: Everyone, Authenticated Users, Domain Users, etc. What remains are custom enrollees that may represent specific service accounts, security groups, or users that have been granted enrollment permissions outside the standard model. These should be reviewed to ensure they align with security policies and least privilege principles. The function adds two properties to each template object: 1. LowPrivilegeEnrollee: Array of SIDs for custom enrollees 2. LowPrivilegeEnrolleeNames: Array of human-readable names formatted as "DOMAIN\User (SID)" or "SID (could not resolve)" if the principal cannot be resolved. .PARAMETER AdcsObject One or more DirectoryEntry objects representing AD CS certificate templates. These objects must contain ObjectSecurity.Access information. .PARAMETER Credential PSCredential for authenticating to Active Directory. Required for LDAP queries when converting identity references to directory entries and SIDs. .PARAMETER RootDSE A DirectoryEntry object for the RootDSE. Used to determine the domain context for LDAP queries when resolving principals. .INPUTS System.DirectoryServices.DirectoryEntry[] You can pipe certificate template DirectoryEntry objects to this function. .OUTPUTS System.DirectoryServices.DirectoryEntry[] Returns the input objects with added properties: - LowPrivilegeEnrollee: Array of SIDs - LowPrivilegeEnrolleeNames: Array of human-readable names .EXAMPLE $templates = Get-AdcsObject | Where-Object { $_.objectClass -contains 'pKICertificateTemplate' } $templates | Set-LowPrivilegeEnrollee Processes all certificate templates and adds the LowPrivilegeEnrollee property to each. .EXAMPLE Get-AdcsObject | Set-LowPrivilegeEnrollee | Where-Object { $_.LowPrivilegeEnrollee.Count -gt 0 } Retrieves all AD CS objects, adds LowPrivilegeEnrollee property, and filters to only those with custom enrollees. .EXAMPLE $template = Get-AdcsObject | Where-Object Name -eq 'WebServer' $template | Set-LowPrivilegeEnrollee if ($template.LowPrivilegeEnrollee) { Write-Host "Template has custom enrollees:" $template.LowPrivilegeEnrolleeNames | ForEach-Object { Write-Host " $_" } } Checks a specific template for custom/low-privilege enrollees and displays human-readable names. .NOTES Safe/Administrative principals excluded by default: - Domain Admins (-512), Enterprise Admins (-519), Builtin Administrators (-544) - SYSTEM (-18), Builtin Administrator (-500) - Cert Publishers (-517) - Domain Controllers (-516), Read-Only Domain Controllers (-521) - Enterprise Domain Controllers (-498), Enterprise Read-Only Domain Controllers (-9) - Key Admins (-526), Enterprise Key Admins (-527) - SELF (S-1-5-10) Dangerous principals excluded by default: - NULL SID, Everyone, Anonymous Logon, BUILTIN\Users - Authenticated Users, Domain Users, Domain Computers .LINK https://posts.specterops.io/certified-pre-owned-d95910965cd2 #> [CmdletBinding()] [OutputType([System.DirectoryServices.DirectoryEntry[]])] param ( [Parameter(Mandatory, ValueFromPipeline)] [System.DirectoryServices.DirectoryEntry[]]$AdcsObject ) #requires -Version 5.1 begin { Write-Verbose "Identifying templates that allow low-privilege principals to enroll..." } process { $AdcsObject | Where-Object SchemaClassName -eq pKICertificateTemplate | ForEach-Object { try { $objectName = if ($_.Properties.displayName.Count -gt 0) { $_.Properties.displayName[0] } elseif ($_.Properties.name.Count -gt 0) { $_.Properties.name[0] } else { $_.Properties.distinguishedName[0] } Write-Verbose "Processing template: $objectName" [array]$lowPrivilegeIdentityReference = foreach ($ace in $_.ObjectSecurity.Access) { $aceSid = $ace.IdentityReference | Convert-IdentityReferenceToSid $isLowPrivilegeEnrollee = $aceSid | Test-IsLowPrivilegePrincipal if ($isLowPrivilegeEnrollee) { $isEnrollmentAce = Test-IsEnrollmentAce -Ace $ace if ($isEnrollmentAce) { Write-Verbose "Low-privilege enrollee found: $($ace.IdentityReference)" # Ensure the principal is in the store (triggers cache population) $null = $ace.IdentityReference | Resolve-Principal # Convert to SID and return as the key to PrincipalStore $aceSid.Value } } } $lowPrivilegeIdentityReference = $lowPrivilegeIdentityReference | Sort-Object -Unique if ($lowPrivilegeIdentityReference) { Write-Verbose "Template has $($lowPrivilegeIdentityReference.Count) low privilege enrollee(s): $($lowPrivilegeIdentityReference -join ', ')" # Expand any groups to include their direct members Write-Verbose "Expanding group memberships for low privilege enrollees..." $lowPrivilegeIdentityReference = Expand-GroupMembership -SidList $lowPrivilegeIdentityReference Write-Verbose "After expansion: $($lowPrivilegeIdentityReference.Count) unique principal(s)" } else { Write-Verbose "No low privilege enrollees found in template" } # Build human-readable names array from PrincipalStore [array]$lowPrivilegeEnrolleeNames = $lowPrivilegeIdentityReference | ForEach-Object { if ($script:PrincipalStore -and $script:PrincipalStore.ContainsKey($_)) { $name = $script:PrincipalStore[$_].ntAccountName if ($name) { "$name ($_)" } else { "$_ (could not resolve)" } } else { "$_ (could not resolve)" } } | Sort-Object -Unique # Update the AD CS Object Store with the LowPrivilegeEnrollee property $dn = $_.Properties.distinguishedName[0] if ($script:AdcsObjectStore.ContainsKey($dn)) { $script:AdcsObjectStore[$dn] | Add-Member -NotePropertyName LowPrivilegeEnrollee -NotePropertyValue $lowPrivilegeIdentityReference -Force $script:AdcsObjectStore[$dn] | Add-Member -NotePropertyName LowPrivilegeEnrolleeNames -NotePropertyValue $lowPrivilegeEnrolleeNames -Force Write-Verbose "Updated AD CS Object Store for $dn with LowPrivilegeEnrollee" } # Also add to the pipeline object for backward compatibility $_ | Add-Member -NotePropertyName LowPrivilegeEnrollee -NotePropertyValue $lowPrivilegeIdentityReference -Force $_ | Add-Member -NotePropertyName LowPrivilegeEnrolleeNames -NotePropertyValue $lowPrivilegeEnrolleeNames -Force # Return the modified object $_ } catch { $errorRecord = [System.Management.Automation.ErrorRecord]::new( $_.Exception, 'LowPrivilegeEnrolleeProcessingFailed', [System.Management.Automation.ErrorCategory]::InvalidOperation, $_ ) $PSCmdlet.WriteError($errorRecord) # Still return the object even if processing failed $_ } } } end { Write-Verbose "Done identifying templates that allow low-privilege principals to enroll." } } |