Analyzers/Test-Esc4.ps1
|
function Test-Esc4 { <# .SYNOPSIS ESC4 - Certificate template with a dangerous (writable) DACL. .DESCRIPTION Flags certificate templates on which a low-privileged principal holds a write-class right (WriteDacl / WriteOwner / WriteProperty / GenericWrite / GenericAll). Such a principal can reconfigure the template (e.g. add enrollee-supplied SAN, add a client-auth EKU, grant themselves Enroll), turning any template into an ESC1-style escalation. Vulnerable (per template) = any Ace in WritePrincipals with AceType == Allow AND IsLowPriv == $true AND Rights intersects {WriteDacl, WriteOwner, WriteProperty, GenericWrite, GenericAll}; OR the template owner is a low-priv principal (when owner SID is available). .PARAMETER Context AssessmentContext pscustomobject. Reads $Context.Templates. .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" (ESC4) / Certipy' $writeRights = @('WriteDacl', 'WriteOwner', 'WriteProperty', 'GenericWrite', 'GenericAll') $findings = @() $newFinding = { param($Title, $Severity, $Status, $Affected, $Evidence, $Principals, $Exploit, $Remediation) [pscustomobject]@{ Id = 'ESC4' Title = $Title Severity = $Severity Status = $Status AffectedObject = $Affected Evidence = $Evidence Principals = @($Principals) Exploitability = $Exploit RiskScore = 0 Remediation = $Remediation Reference = $reference } } $templates = @() if ($null -ne $Context -and $null -ne $Context.Templates) { $templates = @($Context.Templates) } if ($templates.Count -eq 0) { return @(& $newFinding 'ESC4 - no certificate templates available' 'Critical' 'ManualReview' 'N/A' ` ([pscustomobject]@{ Error = 'Context.Templates is empty or unavailable; could not evaluate ESC4.' }) ` @() 'Theoretical' 'Collect certificate templates and re-run the assessment.') } $extra = @() if ($ExtraLowPrivSid) { $extra += $ExtraLowPrivSid } if ($null -ne $Context.ExtraLowPrivSid) { $extra += $Context.ExtraLowPrivSid } foreach ($tpl in $templates) { $badAces = @($tpl.WritePrincipals | Where-Object { $_.AceType -eq 'Allow' -and $_.IsLowPriv -and (@($_.Rights | Where-Object { $writeRights -contains $_ }).Count -gt 0) }) $ownerSid = $null if ($null -ne $tpl.Raw -and $tpl.Raw.PSObject.Properties['OwnerSid']) { $ownerSid = $tpl.Raw.OwnerSid } $ownerLowPriv = $false if ($ownerSid) { $ownerLowPriv = Test-EscLowPrivPrincipal -Sid $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 $ownerSid) } $principalNames = @($principalNames | Select-Object -Unique) $evidence = [pscustomobject]@{ DangerousAces = @($badAces | ForEach-Object { [pscustomobject]@{ Principal = $_.PrincipalName Sid = $_.PrincipalSid Rights = @($_.Rights) AccessMask = ('0x{0:X}' -f [int]$_.AccessMask) } }) OwnerSid = $ownerSid OwnerIsLowPriv = $ownerLowPriv WriteRightsTested = $writeRights } $findings += & $newFinding ` ("ESC4 - template '{0}' has a low-priv-writable DACL" -f $tpl.Name) ` 'Critical' 'Vulnerable' $tpl.Name $evidence $principalNames 'High' ` 'Remove write/owner rights from low-privileged principals on the template object; restrict the DACL to trusted administrative groups.' } if ($findings.Count -eq 0) { return @(& $newFinding 'ESC4 - no templates with low-priv-writable DACLs' 'Critical' 'NotVulnerable' 'N/A' ` ([pscustomobject]@{ TemplatesEvaluated = $templates.Count }) @() 'Theoretical' 'No action required for ESC4.') } return $findings } |