From 7f0f801b37af65ecf404122e9ca45c19bcca2032 Mon Sep 17 00:00:00 2001 From: Jake Hildreth Date: Sat, 17 May 2025 06:59:57 -0400 Subject: [PATCH 01/11] ihavenoideawhatimdoingdog.tiff --- Build/Build-Module.ps1 | 2 +- Locksmith.psd1 | 2 +- Private/Find-ESC7.ps1 | 86 ++++++++++++++++++++++++++++ Private/Set-AdditionalCAProperty.ps1 | 32 +++++++++++ Public/Invoke-Locksmith.ps1 | 1 + 5 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 Private/Find-ESC7.ps1 diff --git a/Build/Build-Module.ps1 b/Build/Build-Module.ps1 index 527ef38..723e583 100644 --- a/Build/Build-Module.ps1 +++ b/Build/Build-Module.ps1 @@ -129,7 +129,7 @@ Build-Module -ModuleName 'Locksmith' { # The scans to run. Defaults to 'All'. [Parameter()] - [ValidateSet('Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC8', 'ESC11', 'ESC13', 'ESC15', 'EKUwu', 'All', 'PromptMe')] + [ValidateSet('Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC7', 'ESC8', 'ESC11', 'ESC13', 'ESC15', 'EKUwu', 'All', 'PromptMe')] [array]$Scans = 'All' ) } diff --git a/Locksmith.psd1 b/Locksmith.psd1 index d6c714d..f93043b 100644 --- a/Locksmith.psd1 +++ b/Locksmith.psd1 @@ -8,7 +8,7 @@ FunctionsToExport = 'Invoke-Locksmith' GUID = 'b1325b42-8dc4-4f17-aa1f-dcb5984ca14a' HelpInfoURI = 'https://raw.githubusercontent.com/jakehildreth/Locksmith/main/en-US/' - ModuleVersion = '2025.4.20' + ModuleVersion = '2025.5.17' PowerShellVersion = '5.1' PrivateData = @{ PSData = @{ diff --git a/Private/Find-ESC7.ps1 b/Private/Find-ESC7.ps1 new file mode 100644 index 0000000..67e11b3 --- /dev/null +++ b/Private/Find-ESC7.ps1 @@ -0,0 +1,86 @@ +function Find-ESC6 { + <# + .SYNOPSIS + This script finds Active Directory Certificate Services (AD CS) Certificate Authorities (CA) that have the ESC7 vulnerability. + + .DESCRIPTION + The script takes an array of AD CS objects as input and filters them based on objects that have the objectClass + 'pKIEnrollmentService' and the + + .PARAMETER ADCSObjects + Specifies the array of AD CS objects to be processed. This parameter is mandatory. + + .OUTPUTS + The script outputs an array of custom objects representing the matching AD CS objects and their associated information. + + .EXAMPLE + $ADCSObjects = Get-ADCSObjects + $Results = $ADCSObjects | Find-ESC6 + $Results + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [Microsoft.ActiveDirectory.Management.ADEntity[]]$ADCSObjects, + [Parameter(Mandatory)] + [string]$UnsafeUsers, + [switch]$SkipRisk + ) + process { + $ADCSObjects | Where-Object { + ($_.objectClass -eq 'pKIEnrollmentService') -and + ($_.CAAdministrator) + } | ForEach-Object { + [string]$CAFullName = "$($_.dNSHostName)\$($_.Name)" + $Issue = [pscustomobject]@{ + Forest = $_.CanonicalName.split('/')[0] + Name = $_.Name + DistinguishedName = $_.DistinguishedName + Issue = $_.CAAdministrator + Fix = 'N/A' + Revert = 'N/A' + Technique = 'ESC7' + } + if ($_.CAAdministrator -eq 'Yes') { + $Issue.Issue = @" +The dangerous EDITF_ATTRIBUTESUBJECTALTNAME2 flag is enabled on $($_.CAFullname). +All templates enabled on this CA will accept a Subject Alternative Name (SAN) +during enrollment even if the template is not specifically configured to allow a SAN. + +As of May 2022, Microsoft has neutered this situation by requiring all SANs to +be strongly mapped to certificates. + +However, if strong mapping has been explicitly disabled on Domain Controllers, +this configuration remains vulnerable to privilege escalation attacks. + +More info: + - https://posts.specterops.io/certified-pre-owned-d95910965cd2 + - https://support.microsoft.com/en-us/topic/kb5014754-certificate-based-authentication-changes-on-windows-domain-controllers-ad2c23b0-15d8-4340-a468-4d4f3b188f16 + +"@ + $Issue.Fix = @" +# Disable the flag +certutil -config '$CAFullname' -setreg policy\EditFlags -EDITF_ATTRIBUTESUBJECTALTNAME2 + +# Restart the Certificate Authority service +Invoke-Command -ComputerName '$($_.dNSHostName)' -ScriptBlock { + Get-Service -Name certsvc | Restart-Service -Force +} +"@ + $Issue.Revert = @" +# Enable the flag +certutil -config '$CAFullname' -setreg policy\EditFlags +EDITF_ATTRIBUTESUBJECTALTNAME2 + +# Restart the Certificate Authority service +Invoke-Command -ComputerName '$($_.dNSHostName)' -ScriptBlock { + Get-Service -Name certsvc | Restart-Service -Force +} +"@ + } + if ($SkipRisk -eq $false) { + Set-RiskRating -ADCSObjects $ADCSObjects -Issue $Issue -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers + } + $Issue + } + } +} diff --git a/Private/Set-AdditionalCAProperty.ps1 b/Private/Set-AdditionalCAProperty.ps1 index ca66874..acde65a 100644 --- a/Private/Set-AdditionalCAProperty.ps1 +++ b/Private/Set-AdditionalCAProperty.ps1 @@ -25,6 +25,8 @@ Date: July 15, 2022 #> + # TODO REfactor to move the creation of each property into its own function + [CmdletBinding(SupportsShouldProcess)] param ( [parameter( @@ -156,10 +158,22 @@ } catch { $InterfaceFlag = 'Failure' } + try { + if ($Credential) { + $CertutilSecurity = Invoke-Command -ComputerName $CAHostFQDN -Credential $Credential -ScriptBlock { certutil -config $using:CAFullName -getreg CA\Security } + } else { + $CertutilSecurity = certutil -config $CAFullName -getreg CA\Security + } + } catch { + $CAAdministrator = 'Failure' + $CertificateManager = 'Failure' + } } else { $AuditFilter = 'CA Unavailable' $SANFlag = 'CA Unavailable' $InterfaceFlag = 'CA Unavailable' + $CAAdministrator = 'CA Unavailable' + $CertificateManager = 'CA Unavailable' } if ($CertutilAudit) { try { @@ -190,6 +204,22 @@ $InterfaceFlag = 'No' } } + if ($CertutilSecurity) { + [string]$CAAdministrator = $CertutilSecurity | ForEach-Object { + if ($_ -match '^.*Allow.*CA Administrator.*?\s+([^\s\\]+\\.+)$') { + [PSCustomObject]@{ + CAAdministrator = $matches[1] + } + } + } + [string]$CertificateManager = $CertutilSecurity | ForEach-Object { + if ($_ -match '^.*Allow.*Certificate Manager.*?\s+([^\s\\]+\\.+)$') { + [PSCustomObject]@{ + CertificateManager = $matches[1] + } + } + } + } Add-Member -InputObject $_ -MemberType NoteProperty -Name AuditFilter -Value $AuditFilter -Force Add-Member -InputObject $_ -MemberType NoteProperty -Name CAEnrollmentEndpoint -Value $CAEnrollmentEndpoint -Force Add-Member -InputObject $_ -MemberType NoteProperty -Name CAFullName -Value $CAFullName -Force @@ -197,6 +227,8 @@ Add-Member -InputObject $_ -MemberType NoteProperty -Name CAHostDistinguishedName -Value $CAHostDistinguishedName -Force Add-Member -InputObject $_ -MemberType NoteProperty -Name SANFlag -Value $SANFlag -Force Add-Member -InputObject $_ -MemberType NoteProperty -Name InterfaceFlag -Value $InterfaceFlag -Force + Add-Member -InputObject $_ -MemberType NoteProperty -Name CAAdministrator -Value $CAAdministrator -Force + Add-Member -InputObject $_ -MemberType NoteProperty -Name CertificateManager -Value $CertificateManager -Force } } } diff --git a/Public/Invoke-Locksmith.ps1 b/Public/Invoke-Locksmith.ps1 index 4dd14fb..762cf0b 100644 --- a/Public/Invoke-Locksmith.ps1 +++ b/Public/Invoke-Locksmith.ps1 @@ -93,6 +93,7 @@ function Invoke-Locksmith { 'ESC4', 'ESC5', 'ESC6', + 'ESC7', 'ESC8', 'ESC11', 'ESC13', From 999f571db30faea3b7588c1ed7db3fe01ccf57b4 Mon Sep 17 00:00:00 2001 From: Jake Hildreth Date: Sat, 17 May 2025 10:37:10 -0400 Subject: [PATCH 02/11] ESC7 Detections are working. Needs better Issue, Fix, and Revert attributes, but it's working! --- Invoke-Locksmith.ps1 | 167 +++++++++++++++++++++++++-- Private/Find-ESC7.ps1 | 112 ++++++++++-------- Private/Format-Result.ps1 | 7 +- Private/Invoke-Scans.ps1 | 11 +- Private/Set-AdditionalCAProperty.ps1 | 16 +-- Private/Set-RiskRating.ps1 | 4 +- Public/Invoke-Locksmith.ps1 | 3 + 7 files changed, 248 insertions(+), 72 deletions(-) diff --git a/Invoke-Locksmith.ps1 b/Invoke-Locksmith.ps1 index 375745d..a505a3d 100644 --- a/Invoke-Locksmith.ps1 +++ b/Invoke-Locksmith.ps1 @@ -7,7 +7,7 @@ param ( # The scans to run. Defaults to 'All'. [Parameter()] - [ValidateSet('Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC8', 'ESC11', 'ESC13', 'ESC15', 'EKUwu', 'All', 'PromptMe')] + [ValidateSet('Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC7', 'ESC8', 'ESC11', 'ESC13', 'ESC15', 'EKUwu', 'All', 'PromptMe')] [array]$Scans = 'All' ) function Convert-IdentityReferenceToSid { @@ -1456,6 +1456,113 @@ Invoke-Command -ComputerName '$($_.dNSHostName)' -ScriptBlock { } } +function Find-ESC7 { + <# + .SYNOPSIS + This script finds Active Directory Certificate Services (AD CS) Certificate Authorities (CA) that have the ESC7 vulnerability. + + .DESCRIPTION + The script takes an array of AD CS objects as input and filters them based on objects that have the objectClass + 'pKIEnrollmentService'. If the CA objects have non-standard/unsafe principals as administrators or managers, an issue is created. + + .PARAMETER ADCSObjects + Specifies the array of AD CS objects to be processed. This parameter is mandatory. + + .PARAMETER UnsafeUsers + Principals that should never be granted control of a CA. + + .PARAMETER SafeUsers + Principals that are generally recognized as safe to control a CA. + + .PARAMETER SkipRisk + Switch used when processing second-order risks. + + .OUTPUTS + The script outputs an array of custom objects representing the matching AD CS objects and their associated information. + + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [Microsoft.ActiveDirectory.Management.ADEntity[]]$ADCSObjects, + [Parameter(Mandatory)] + [string]$UnsafeUsers, + [Parameter(Mandatory)] + [string]$SafeUsers, + [switch]$SkipRisk + ) + process { + $ADCSObjects | Where-Object { + ($_.objectClass -eq 'pKIEnrollmentService') -and + ( ($_.CAAdministrator) -or ($_.CertificateManager) ) + } | ForEach-Object { + $UnsafeCAAdministrators = Write-Output $_.CAAdministrator -PipelineVariable admin | ForEach-Object { + $SID = Convert-IdentityReferenceToSid -Object $admin + if ($SID -notmatch $SafeUsers) { + $admin + } + } + $UnsafeCertificateManagers = Write-Output $_.CertificateManager -PipelineVariable manager | ForEach-Object { + $SID = Convert-IdentityReferenceToSid -Object $manager + if ($SID -notmatch $SafeUsers) { + $manager + } + } + if ($UnsafeCAAdministrators -or $UnsafeCertificateManagers) { + $Issue = [pscustomobject]@{ + Forest = $_.CanonicalName.split('/')[0] + Name = $_.Name + DistinguishedName = $_.DistinguishedName + CAAdministrator = $_.CAAdministrator + CertificateManager = $_.CertificateManager + Issue = $null + Fix = $null + Revert = $null + Technique = 'ESC7' + } + if ($UnsafeCAAdministrators) { + $Issue.Issue = $Issue.Issue + @" +Unexpected prinicipals ($($UnsafeCAAdministrators -join ', ')) are granted "CA Administrator" rights on this Certification Authority. + +"@ + $Issue.Fix = $Issue.Fix + @" +Revoke CA Administrator rights from $($UnsafeCAAdministrators -join ', ') + +"@ + $Issue.Revert = $Issue.Revert + @" +Reinstate CA Administrator rights for $($UnsafeCAAdministrators -join ', ') + +"@ + } + if ($UnsafeCertificateManagers) { + $Issue.Issue = $Issue.Issue + @" +Unexpected prinicipals ($($UnsafeCertificateManagers -join ', ')) are granted "Certificate Manager" rights on this Certification Authority. + +"@ + $Issue.Fix = $Issue.Fix + @" +Revoke CA Administrator rights from $($UnsafeCertificateManagers -join ', ') + +"@ + $Issue.Revert = $Issue.Revert + @" +Reinstate CA Administrator rights for $($UnsafeCertificateManagers -join ', ') + +"@ + } + if ($SkipRisk -eq $false) { + Set-RiskRating -ADCSObjects $ADCSObjects -Issue $Issue -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers + } + $Issue.Issue = $Issue.Issue + @" + +More info: + - https://posts.specterops.io/certified-pre-owned-d95910965cd2 + +"@ + $Issue + } + } + } +} + function Find-ESC8 { <# .SYNOPSIS @@ -1693,7 +1800,7 @@ function Format-Result { Formats the issue result in list format. .NOTES - Author: Spencer Alessi + Authors: Spencer Alessi & Jake Hildreth #> [CmdletBinding()] param( @@ -1710,6 +1817,7 @@ function Format-Result { ESC4 = 'ESC4 - Vulnerable Access Control - Certificate Template' ESC5 = 'ESC5 - Vulnerable Access Control - PKI Object' ESC6 = 'ESC6 - EDITF_ATTRIBUTESUBJECTALTNAME2 Flag Enabled' + ESC7 = 'ESC7 - Non-standard PKI Admins' ESC8 = 'ESC8 - HTTP/S Enrollment Enabled' ESC11 = 'ESC11 - IF_ENFORCEENCRYPTICERTREQUEST Flag Disabled' ESC13 = 'ESC13 - Vulnerable Certificate Template - Group-Linked' @@ -1737,7 +1845,7 @@ function Format-Result { if ($Mode -eq 0) { # TODO Refactor this switch ($UniqueIssue) { - { $_ -in @('DETECT', 'ESC6', 'ESC8', 'ESC11') } { + { $_ -in @('DETECT', 'ESC6', 'ESC7', 'ESC8', 'ESC11') } { $Issue | Format-Table Technique, @{l = 'CA Name'; e = { $_.Name } }, @{l = 'Risk'; e = { $_.RiskName } }, Issue -Wrap | Write-HostColorized -PatternColorMap $RiskTable -CaseSensitive @@ -1756,7 +1864,7 @@ function Format-Result { } elseif ($Mode -eq 1) { switch ($UniqueIssue) { - { $_ -in @('DETECT', 'ESC6', 'ESC8', 'ESC11') } { + { $_ -in @('DETECT', 'ESC6', 'ESC7', 'ESC8', 'ESC11') } { $Issue | Format-List Technique, @{l = 'CA Name'; e = { $_.Name } }, @{l = 'Risk'; e = { $_.RiskName } }, DistinguishedName, Issue, Fix, @{l = 'Risk Score'; e = { $_.RiskValue } }, @{l = 'Risk Score Detail'; e = { $_.RiskScoring -join "`n" } } | Write-HostColorized -PatternColorMap $RiskTable -CaseSensitive @@ -2448,7 +2556,7 @@ function Invoke-Scans { [string]$SafeUsers, [Parameter(Mandatory)] [string]$SafeOwners, - [ValidateSet('Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC8', 'ESC11', 'ESC13', 'ESC15', 'EKUwu', 'All', 'PromptMe')] + [ValidateSet('Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC7', 'ESC8', 'ESC11', 'ESC13', 'ESC15', 'EKUwu', 'All', 'PromptMe')] [array]$Scans = 'All', [Parameter(Mandatory)] [string]$UnsafeUsers, @@ -2503,6 +2611,10 @@ function Invoke-Scans { Write-Host 'Identifying Issuing CAs with EDITF_ATTRIBUTESUBJECTALTNAME2 enabled (ESC6)...' [array]$ESC6 = Find-ESC6 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers } + ESC7 { + Write-Host 'Identifying Issuing CAs with ESC7...' + [array]$ESC7 = Find-ESC7 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers -SafeUsers $SafeUsers + } ESC8 { Write-Host 'Identifying HTTP-based certificate enrollment interfaces (ESC8)...' [array]$ESC8 = Find-ESC8 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers @@ -2539,6 +2651,8 @@ function Invoke-Scans { [array]$ESC5 = Find-ESC5 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -DangerousRights $DangerousRights -SafeOwners $SafeOwners -SafeObjectTypes $SafeObjectTypes -UnsafeUsers $UnsafeUsers Write-Host 'Identifying Certificate Authorities with EDITF_ATTRIBUTESUBJECTALTNAME2 enabled (ESC6)...' [array]$ESC6 = Find-ESC6 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers + Write-Host 'Identifying Certificate Authorities with ESC7...' + [array]$ESC7 = Find-ESC7 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers -SafeUsers $SafeUsers Write-Host 'Identifying HTTP-based certificate enrollment interfaces (ESC8)...' [array]$ESC8 = Find-ESC8 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers Write-Host 'Identifying Certificate Authorities with IF_ENFORCEENCRYPTICERTREQUEST disabled (ESC11)...' @@ -2551,7 +2665,7 @@ function Invoke-Scans { } } - [array]$AllIssues = $AuditingIssues + $ESC1 + $ESC2 + $ESC3 + $ESC4 + $ESC5 + $ESC6 + $ESC8 + $ESC11 + $ESC13 + $ESC15 + [array]$AllIssues = $AuditingIssues + $ESC1 + $ESC2 + $ESC3 + $ESC4 + $ESC5 + $ESC6 + $ESC7 + $ESC8 + $ESC11 + $ESC13 + $ESC15 # If these are all empty = no issues found, exit if ($AllIssues.Count -lt 1) { @@ -2569,6 +2683,7 @@ function Invoke-Scans { ESC4 = $ESC4 ESC5 = $ESC5 ESC6 = $ESC6 + ESC7 = $ESC7 ESC8 = $ESC8 ESC11 = $ESC11 ESC13 = $ESC13 @@ -2799,6 +2914,8 @@ function Set-AdditionalCAProperty { Date: July 15, 2022 #> + # TODO REfactor to move the creation of each property into its own function + [CmdletBinding(SupportsShouldProcess)] param ( [parameter( @@ -2946,11 +3063,25 @@ function Set-AdditionalCAProperty { catch { $InterfaceFlag = 'Failure' } + try { + if ($Credential) { + $CertutilSecurity = Invoke-Command -ComputerName $CAHostFQDN -Credential $Credential -ScriptBlock { certutil -config $using:CAFullName -getreg CA\Security } + } + else { + $CertutilSecurity = certutil -config $CAFullName -getreg CA\Security + } + } + catch { + $CAAdministrator = 'Failure' + $CertificateManager = 'Failure' + } } else { $AuditFilter = 'CA Unavailable' $SANFlag = 'CA Unavailable' $InterfaceFlag = 'CA Unavailable' + $CAAdministrator = 'CA Unavailable' + $CertificateManager = 'CA Unavailable' } if ($CertutilAudit) { try { @@ -2985,6 +3116,18 @@ function Set-AdditionalCAProperty { $InterfaceFlag = 'No' } } + if ($CertutilSecurity) { + [string[]]$CAAdministrator = $CertutilSecurity | ForEach-Object { + if ($_ -match '^.*Allow.*CA Administrator.*.*\t(.*)$') { + $matches[1].ToString() + } + } + [string[]]$CertificateManager = $CertutilSecurity | ForEach-Object { + if ($_ -match '^.*Allow.*Certificate Manager.*\t(.*)$') { + $matches[1].ToString() + } + } + } Add-Member -InputObject $_ -MemberType NoteProperty -Name AuditFilter -Value $AuditFilter -Force Add-Member -InputObject $_ -MemberType NoteProperty -Name CAEnrollmentEndpoint -Value $CAEnrollmentEndpoint -Force Add-Member -InputObject $_ -MemberType NoteProperty -Name CAFullName -Value $CAFullName -Force @@ -2992,6 +3135,8 @@ function Set-AdditionalCAProperty { Add-Member -InputObject $_ -MemberType NoteProperty -Name CAHostDistinguishedName -Value $CAHostDistinguishedName -Force Add-Member -InputObject $_ -MemberType NoteProperty -Name SANFlag -Value $SANFlag -Force Add-Member -InputObject $_ -MemberType NoteProperty -Name InterfaceFlag -Value $InterfaceFlag -Force + Add-Member -InputObject $_ -MemberType NoteProperty -Name CAAdministrator -Value $CAAdministrator -Force + Add-Member -InputObject $_ -MemberType NoteProperty -Name CertificateManager -Value $CertificateManager -Force } } } @@ -3099,7 +3244,7 @@ function Set-RiskRating { $RiskScoring = @() # CA issues don't rely on a principal and have a base risk of Medium. - if ($Issue.Technique -in @('DETECT', 'ESC6', 'ESC8', 'ESC11')) { + if ($Issue.Technique -in @('DETECT', 'ESC6', 'ESC7', 'ESC8', 'ESC11')) { $RiskValue += 3 $RiskScoring += 'Base Score: 3' @@ -3113,7 +3258,7 @@ function Set-RiskRating { } # Template and object issues rely on a principal and have complex scoring. - if ($Issue.Technique -notin @('DETECT', 'ESC6', 'ESC8', 'ESC11')) { + if ($Issue.Technique -notin @('DETECT', 'ESC6', 'ESC7', 'ESC8', 'ESC11')) { $RiskScoring += 'Base Score: 0' # Templates are more dangerous when enabled, but objects cannot be enabled/disabled. @@ -4381,6 +4526,7 @@ function Invoke-Locksmith { 'ESC4', 'ESC5', 'ESC6', + 'ESC7', 'ESC8', 'ESC11', 'ESC13', @@ -4401,7 +4547,7 @@ function Invoke-Locksmith { [System.Management.Automation.PSCredential]$Credential ) - $Version = '2025.4.20' + $Version = '2025.5.17' $LogoPart1 = @' _ _____ _______ _ _ _______ _______ _____ _______ _ _ | | | | |____/ |______ | | | | | |_____| @@ -4572,6 +4718,7 @@ function Invoke-Locksmith { $ESC4 = $Results['ESC4'] $ESC5 = $Results['ESC5'] $ESC6 = $Results['ESC6'] + $ESC7 = $Results['ESC7'] $ESC8 = $Results['ESC8'] $ESC11 = $Results['ESC11'] $ESC13 = $Results['ESC13'] @@ -4594,6 +4741,7 @@ function Invoke-Locksmith { Format-Result -Issue $ESC4 -Mode 0 Format-Result -Issue $ESC5 -Mode 0 Format-Result -Issue $ESC6 -Mode 0 + Format-Result -Issue $ESC7 -Mode 0 Format-Result -Issue $ESC8 -Mode 0 Format-Result -Issue $ESC11 -Mode 0 Format-Result -Issue $ESC13 -Mode 0 @@ -4623,6 +4771,7 @@ Invoke-Locksmith -Mode 1 Format-Result -Issue $ESC4 -Mode 1 Format-Result -Issue $ESC5 -Mode 1 Format-Result -Issue $ESC6 -Mode 1 + Format-Result -Issue $ESC7 -Mode 1 Format-Result -Issue $ESC8 -Mode 1 Format-Result -Issue $ESC11 -Mode 1 Format-Result -Issue $ESC13 -Mode 1 diff --git a/Private/Find-ESC7.ps1 b/Private/Find-ESC7.ps1 index 67e11b3..7a8265e 100644 --- a/Private/Find-ESC7.ps1 +++ b/Private/Find-ESC7.ps1 @@ -1,22 +1,27 @@ -function Find-ESC6 { +function Find-ESC7 { <# .SYNOPSIS This script finds Active Directory Certificate Services (AD CS) Certificate Authorities (CA) that have the ESC7 vulnerability. .DESCRIPTION The script takes an array of AD CS objects as input and filters them based on objects that have the objectClass - 'pKIEnrollmentService' and the + 'pKIEnrollmentService'. If the CA objects have non-standard/unsafe principals as administrators or managers, an issue is created. .PARAMETER ADCSObjects Specifies the array of AD CS objects to be processed. This parameter is mandatory. + .PARAMETER UnsafeUsers + Principals that should never be granted control of a CA. + + .PARAMETER SafeUsers + Principals that are generally recognized as safe to control a CA. + + .PARAMETER SkipRisk + Switch used when processing second-order risks. + .OUTPUTS The script outputs an array of custom objects representing the matching AD CS objects and their associated information. - .EXAMPLE - $ADCSObjects = Get-ADCSObjects - $Results = $ADCSObjects | Find-ESC6 - $Results #> [CmdletBinding()] param( @@ -24,63 +29,78 @@ [Microsoft.ActiveDirectory.Management.ADEntity[]]$ADCSObjects, [Parameter(Mandatory)] [string]$UnsafeUsers, + [Parameter(Mandatory)] + [string]$SafeUsers, [switch]$SkipRisk ) process { $ADCSObjects | Where-Object { ($_.objectClass -eq 'pKIEnrollmentService') -and - ($_.CAAdministrator) + ( ($_.CAAdministrator) -or ($_.CertificateManager) ) } | ForEach-Object { - [string]$CAFullName = "$($_.dNSHostName)\$($_.Name)" - $Issue = [pscustomobject]@{ - Forest = $_.CanonicalName.split('/')[0] - Name = $_.Name - DistinguishedName = $_.DistinguishedName - Issue = $_.CAAdministrator - Fix = 'N/A' - Revert = 'N/A' - Technique = 'ESC7' + $UnsafeCAAdministrators = Write-Output $_.CAAdministrator -PipelineVariable admin | ForEach-Object { + $SID = Convert-IdentityReferenceToSid -Object $admin + if ($SID -notmatch $SafeUsers) { + $admin + } + } + $UnsafeCertificateManagers = Write-Output $_.CertificateManager -PipelineVariable manager | ForEach-Object { + $SID = Convert-IdentityReferenceToSid -Object $manager + if ($SID -notmatch $SafeUsers) { + $manager + } } - if ($_.CAAdministrator -eq 'Yes') { - $Issue.Issue = @" -The dangerous EDITF_ATTRIBUTESUBJECTALTNAME2 flag is enabled on $($_.CAFullname). -All templates enabled on this CA will accept a Subject Alternative Name (SAN) -during enrollment even if the template is not specifically configured to allow a SAN. + if ($UnsafeCAAdministrators -or $UnsafeCertificateManagers) { + $Issue = [pscustomobject]@{ + Forest = $_.CanonicalName.split('/')[0] + Name = $_.Name + DistinguishedName = $_.DistinguishedName + CAAdministrator = $_.CAAdministrator + CertificateManager = $_.CertificateManager + Issue = $null + Fix = $null + Revert = $null + Technique = 'ESC7' + } + if ($UnsafeCAAdministrators) { + $Issue.Issue = $Issue.Issue + @" +Unexpected prinicipals ($($UnsafeCAAdministrators -join ', ')) are granted "CA Administrator" rights on this Certification Authority. -As of May 2022, Microsoft has neutered this situation by requiring all SANs to -be strongly mapped to certificates. +"@ + $Issue.Fix = $Issue.Fix + @" +Revoke CA Administrator rights from $($UnsafeCAAdministrators -join ', ') -However, if strong mapping has been explicitly disabled on Domain Controllers, -this configuration remains vulnerable to privilege escalation attacks. +"@ + $Issue.Revert = $Issue.Revert + @" +Reinstate CA Administrator rights for $($UnsafeCAAdministrators -join ', ') -More info: - - https://posts.specterops.io/certified-pre-owned-d95910965cd2 - - https://support.microsoft.com/en-us/topic/kb5014754-certificate-based-authentication-changes-on-windows-domain-controllers-ad2c23b0-15d8-4340-a468-4d4f3b188f16 +"@ + } + if ($UnsafeCertificateManagers) { + $Issue.Issue = $Issue.Issue + @" +Unexpected prinicipals ($($UnsafeCertificateManagers -join ', ')) are granted "Certificate Manager" rights on this Certification Authority. "@ - $Issue.Fix = @" -# Disable the flag -certutil -config '$CAFullname' -setreg policy\EditFlags -EDITF_ATTRIBUTESUBJECTALTNAME2 + $Issue.Fix = $Issue.Fix + @" +Revoke CA Administrator rights from $($UnsafeCertificateManagers -join ', ') -# Restart the Certificate Authority service -Invoke-Command -ComputerName '$($_.dNSHostName)' -ScriptBlock { - Get-Service -Name certsvc | Restart-Service -Force -} "@ - $Issue.Revert = @" -# Enable the flag -certutil -config '$CAFullname' -setreg policy\EditFlags +EDITF_ATTRIBUTESUBJECTALTNAME2 + $Issue.Revert = $Issue.Revert + @" +Reinstate CA Administrator rights for $($UnsafeCertificateManagers -join ', ') -# Restart the Certificate Authority service -Invoke-Command -ComputerName '$($_.dNSHostName)' -ScriptBlock { - Get-Service -Name certsvc | Restart-Service -Force -} "@ + } + if ($SkipRisk -eq $false) { + Set-RiskRating -ADCSObjects $ADCSObjects -Issue $Issue -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers + } + $Issue.Issue = $Issue.Issue + @" + +More info: + - https://posts.specterops.io/certified-pre-owned-d95910965cd2 + +"@ + $Issue } - if ($SkipRisk -eq $false) { - Set-RiskRating -ADCSObjects $ADCSObjects -Issue $Issue -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers - } - $Issue } } } diff --git a/Private/Format-Result.ps1 b/Private/Format-Result.ps1 index b4b6d8b..3e253c1 100644 --- a/Private/Format-Result.ps1 +++ b/Private/Format-Result.ps1 @@ -21,7 +21,7 @@ function Format-Result { Formats the issue result in list format. .NOTES - Author: Spencer Alessi + Authors: Spencer Alessi & Jake Hildreth #> [CmdletBinding()] param( @@ -38,6 +38,7 @@ function Format-Result { ESC4 = 'ESC4 - Vulnerable Access Control - Certificate Template' ESC5 = 'ESC5 - Vulnerable Access Control - PKI Object' ESC6 = 'ESC6 - EDITF_ATTRIBUTESUBJECTALTNAME2 Flag Enabled' + ESC7 = 'ESC7 - Non-standard PKI Admins' ESC8 = 'ESC8 - HTTP/S Enrollment Enabled' ESC11 = 'ESC11 - IF_ENFORCEENCRYPTICERTREQUEST Flag Disabled' ESC13 = 'ESC13 - Vulnerable Certificate Template - Group-Linked' @@ -65,7 +66,7 @@ function Format-Result { if ($Mode -eq 0) { # TODO Refactor this switch ($UniqueIssue) { - { $_ -in @('DETECT', 'ESC6', 'ESC8', 'ESC11') } { + { $_ -in @('DETECT', 'ESC6', 'ESC7', 'ESC8', 'ESC11') } { $Issue | Format-Table Technique, @{l = 'CA Name'; e = { $_.Name } }, @{l = 'Risk'; e = { $_.RiskName } }, Issue -Wrap | Write-HostColorized -PatternColorMap $RiskTable -CaseSensitive @@ -83,7 +84,7 @@ function Format-Result { } } elseif ($Mode -eq 1) { switch ($UniqueIssue) { - { $_ -in @('DETECT', 'ESC6', 'ESC8', 'ESC11') } { + { $_ -in @('DETECT', 'ESC6', 'ESC7', 'ESC8', 'ESC11') } { $Issue | Format-List Technique, @{l = 'CA Name'; e = { $_.Name } }, @{l = 'Risk'; e = { $_.RiskName } }, DistinguishedName, Issue, Fix, @{l = 'Risk Score'; e = { $_.RiskValue } }, @{l = 'Risk Score Detail'; e = { $_.RiskScoring -join "`n" } } | Write-HostColorized -PatternColorMap $RiskTable -CaseSensitive diff --git a/Private/Invoke-Scans.ps1 b/Private/Invoke-Scans.ps1 index 4dfcfd8..ce24a64 100644 --- a/Private/Invoke-Scans.ps1 +++ b/Private/Invoke-Scans.ps1 @@ -48,7 +48,7 @@ function Invoke-Scans { [string]$SafeUsers, [Parameter(Mandatory)] [string]$SafeOwners, - [ValidateSet('Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC8', 'ESC11', 'ESC13', 'ESC15', 'EKUwu', 'All', 'PromptMe')] + [ValidateSet('Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC7', 'ESC8', 'ESC11', 'ESC13', 'ESC15', 'EKUwu', 'All', 'PromptMe')] [array]$Scans = 'All', [Parameter(Mandatory)] [string]$UnsafeUsers, @@ -101,6 +101,10 @@ function Invoke-Scans { Write-Host 'Identifying Issuing CAs with EDITF_ATTRIBUTESUBJECTALTNAME2 enabled (ESC6)...' [array]$ESC6 = Find-ESC6 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers } + ESC7 { + Write-Host 'Identifying Issuing CAs with ESC7...' + [array]$ESC7 = Find-ESC7 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers -SafeUsers $SafeUsers + } ESC8 { Write-Host 'Identifying HTTP-based certificate enrollment interfaces (ESC8)...' [array]$ESC8 = Find-ESC8 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers @@ -137,6 +141,8 @@ function Invoke-Scans { [array]$ESC5 = Find-ESC5 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -DangerousRights $DangerousRights -SafeOwners $SafeOwners -SafeObjectTypes $SafeObjectTypes -UnsafeUsers $UnsafeUsers Write-Host 'Identifying Certificate Authorities with EDITF_ATTRIBUTESUBJECTALTNAME2 enabled (ESC6)...' [array]$ESC6 = Find-ESC6 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers + Write-Host 'Identifying Certificate Authorities with ESC7...' + [array]$ESC7 = Find-ESC7 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers -SafeUsers $SafeUsers Write-Host 'Identifying HTTP-based certificate enrollment interfaces (ESC8)...' [array]$ESC8 = Find-ESC8 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers Write-Host 'Identifying Certificate Authorities with IF_ENFORCEENCRYPTICERTREQUEST disabled (ESC11)...' @@ -149,7 +155,7 @@ function Invoke-Scans { } } - [array]$AllIssues = $AuditingIssues + $ESC1 + $ESC2 + $ESC3 + $ESC4 + $ESC5 + $ESC6 + $ESC8 + $ESC11 + $ESC13 + $ESC15 + [array]$AllIssues = $AuditingIssues + $ESC1 + $ESC2 + $ESC3 + $ESC4 + $ESC5 + $ESC6 + $ESC7 + $ESC8 + $ESC11 + $ESC13 + $ESC15 # If these are all empty = no issues found, exit if ($AllIssues.Count -lt 1) { @@ -167,6 +173,7 @@ function Invoke-Scans { ESC4 = $ESC4 ESC5 = $ESC5 ESC6 = $ESC6 + ESC7 = $ESC7 ESC8 = $ESC8 ESC11 = $ESC11 ESC13 = $ESC13 diff --git a/Private/Set-AdditionalCAProperty.ps1 b/Private/Set-AdditionalCAProperty.ps1 index acde65a..f80f9b3 100644 --- a/Private/Set-AdditionalCAProperty.ps1 +++ b/Private/Set-AdditionalCAProperty.ps1 @@ -205,18 +205,14 @@ } } if ($CertutilSecurity) { - [string]$CAAdministrator = $CertutilSecurity | ForEach-Object { - if ($_ -match '^.*Allow.*CA Administrator.*?\s+([^\s\\]+\\.+)$') { - [PSCustomObject]@{ - CAAdministrator = $matches[1] - } + [string[]]$CAAdministrator = $CertutilSecurity | ForEach-Object { + if ($_ -match '^.*Allow.*CA Administrator.*.*\t(.*)$') { + $matches[1].ToString() } } - [string]$CertificateManager = $CertutilSecurity | ForEach-Object { - if ($_ -match '^.*Allow.*Certificate Manager.*?\s+([^\s\\]+\\.+)$') { - [PSCustomObject]@{ - CertificateManager = $matches[1] - } + [string[]]$CertificateManager = $CertutilSecurity | ForEach-Object { + if ($_ -match '^.*Allow.*Certificate Manager.*\t(.*)$') { + $matches[1].ToString() } } } diff --git a/Private/Set-RiskRating.ps1 b/Private/Set-RiskRating.ps1 index d226dbc..27256e1 100644 --- a/Private/Set-RiskRating.ps1 +++ b/Private/Set-RiskRating.ps1 @@ -55,7 +55,7 @@ function Set-RiskRating { $RiskScoring = @() # CA issues don't rely on a principal and have a base risk of Medium. - if ($Issue.Technique -in @('DETECT', 'ESC6', 'ESC8', 'ESC11')) { + if ($Issue.Technique -in @('DETECT', 'ESC6', 'ESC7', 'ESC8', 'ESC11')) { $RiskValue += 3 $RiskScoring += 'Base Score: 3' @@ -69,7 +69,7 @@ function Set-RiskRating { } # Template and object issues rely on a principal and have complex scoring. - if ($Issue.Technique -notin @('DETECT', 'ESC6', 'ESC8', 'ESC11')) { + if ($Issue.Technique -notin @('DETECT', 'ESC6', 'ESC7', 'ESC8', 'ESC11')) { $RiskScoring += 'Base Score: 0' # Templates are more dangerous when enabled, but objects cannot be enabled/disabled. diff --git a/Public/Invoke-Locksmith.ps1 b/Public/Invoke-Locksmith.ps1 index 762cf0b..06449a7 100644 --- a/Public/Invoke-Locksmith.ps1 +++ b/Public/Invoke-Locksmith.ps1 @@ -282,6 +282,7 @@ function Invoke-Locksmith { $ESC4 = $Results['ESC4'] $ESC5 = $Results['ESC5'] $ESC6 = $Results['ESC6'] + $ESC7 = $Results['ESC7'] $ESC8 = $Results['ESC8'] $ESC11 = $Results['ESC11'] $ESC13 = $Results['ESC13'] @@ -304,6 +305,7 @@ function Invoke-Locksmith { Format-Result -Issue $ESC4 -Mode 0 Format-Result -Issue $ESC5 -Mode 0 Format-Result -Issue $ESC6 -Mode 0 + Format-Result -Issue $ESC7 -Mode 0 Format-Result -Issue $ESC8 -Mode 0 Format-Result -Issue $ESC11 -Mode 0 Format-Result -Issue $ESC13 -Mode 0 @@ -333,6 +335,7 @@ Invoke-Locksmith -Mode 1 Format-Result -Issue $ESC4 -Mode 1 Format-Result -Issue $ESC5 -Mode 1 Format-Result -Issue $ESC6 -Mode 1 + Format-Result -Issue $ESC7 -Mode 1 Format-Result -Issue $ESC8 -Mode 1 Format-Result -Issue $ESC11 -Mode 1 Format-Result -Issue $ESC13 -Mode 1 From ef2234775284c84a645a96e4893cfb69c7fcf407 Mon Sep 17 00:00:00 2001 From: Jake Hildreth Date: Sat, 17 May 2025 12:33:34 -0400 Subject: [PATCH 03/11] ESC16 detections are working. Needs better description, fix, revert, and risk calculation, but otherwise good! --- Build/Build-Module.ps1 | 2 +- Invoke-Locksmith.ps1 | 139 ++++++++++++++++++++++++--- Private/Find-ESC16.ps1 | 75 +++++++++++++++ Private/Find-ESC7.ps1 | 2 +- Private/Format-Result.ps1 | 5 +- Private/Invoke-Scans.ps1 | 20 ++-- Private/New-Dictionary.ps1 | 2 +- Private/Set-AdditionalCAProperty.ps1 | 19 ++++ Private/Set-RiskRating.ps1 | 6 +- Public/Invoke-Locksmith.ps1 | 4 + README.md | 2 +- 11 files changed, 245 insertions(+), 31 deletions(-) create mode 100644 Private/Find-ESC16.ps1 diff --git a/Build/Build-Module.ps1 b/Build/Build-Module.ps1 index 723e583..1ad9df9 100644 --- a/Build/Build-Module.ps1 +++ b/Build/Build-Module.ps1 @@ -129,7 +129,7 @@ Build-Module -ModuleName 'Locksmith' { # The scans to run. Defaults to 'All'. [Parameter()] - [ValidateSet('Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC7', 'ESC8', 'ESC11', 'ESC13', 'ESC15', 'EKUwu', 'All', 'PromptMe')] + [ValidateSet('Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC7', 'ESC8', 'ESC11', 'ESC13', 'ESC15', 'EKUwu', 'ESC16', 'All', 'PromptMe')] [array]$Scans = 'All' ) } diff --git a/Invoke-Locksmith.ps1 b/Invoke-Locksmith.ps1 index a505a3d..d317945 100644 --- a/Invoke-Locksmith.ps1 +++ b/Invoke-Locksmith.ps1 @@ -7,7 +7,7 @@ param ( # The scans to run. Defaults to 'All'. [Parameter()] - [ValidateSet('Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC7', 'ESC8', 'ESC11', 'ESC13', 'ESC15', 'EKUwu', 'All', 'PromptMe')] + [ValidateSet('Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC7', 'ESC8', 'ESC11', 'ESC13', 'ESC15', 'EKUwu', 'ESC16', 'All', 'PromptMe')] [array]$Scans = 'All' ) function Convert-IdentityReferenceToSid { @@ -602,6 +602,82 @@ Invoke-WebRequest -Uri https://gist.githubusercontent.com/jakehildreth/13c7d615a } } +function Find-ESC16 { + <# + .SYNOPSIS + This script finds Active Directory Certificate Services (AD CS) Certification Authorities (CA) that have the ESC16 vulnerability. + + .DESCRIPTION + The script takes an array of ADCS objects as input and filters them based on objects that have the objectClass + 'pKIEnrollmentService' and the szOID_NTDS_CA_SECURITY_EXT disabled. For each matching object, it creates a custom object with + properties representing various information about the object, such as Forest, Name, DistinguishedName, Technique, + Issue, Fix, and Revert. + + .PARAMETER ADCSObjects + Specifies the array of AD CS objects to be processed. This parameter is mandatory. + + .OUTPUTS + The script outputs an array of custom objects representing the matching ADCS objects and their associated information. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [Microsoft.ActiveDirectory.Management.ADEntity[]]$ADCSObjects, + [Parameter(Mandatory)] + [string]$UnsafeUsers, + [switch]$SkipRisk + ) + process { + $ADCSObjects | Where-Object { + ($_.objectClass -eq 'pKIEnrollmentService') -and + ($_.DisableExtensionList -ne 'No') + } | ForEach-Object { + $Issue = [pscustomobject]@{ + Forest = $_.CanonicalName.split('/')[0] + Name = $_.Name + DistinguishedName = $_.DistinguishedName + Issue = $_.DisableExtensionList + Fix = 'N/A' + Revert = 'N/A' + Technique = 'ESC16' + } + if ($_.DisableExtensionList -eq 'Yes') { + $Issue.Issue = @" +The Certification Authority (CA) $($_.CAFullName) has the szOID_NTDS_CA_SECURITY_EXT security extension disabled. When +this extension is disabled, every certificate issued by this CA will be unable to to reliably map a certificate to a +user or computer account's SID for authentication. + +More info: + - https://github.com/ly4k/Certipy/wiki/06-%E2%80%90-Privilege-Escalation#esc16-security-extension-disabled-on-ca-globally + +"@ + $Issue.Fix = @" +# Enable the flag +# TODO + +# Restart the Certificate Authority service +Invoke-Command -ComputerName '$($_.dNSHostName)' -ScriptBlock { + Get-Service -Name certsvc | Restart-Service -Force +} +"@ + $Issue.Revert = @" +# Disable the flag +TODO + +# Restart the Certificate Authority service +Invoke-Command -ComputerName '$($_.dNSHostName)' -ScriptBlock { + Get-Service -Name certsvc | Restart-Service -Force +} +"@ + } + if ($SkipRisk -eq $false) { + Set-RiskRating -ADCSObjects $ADCSObjects -Issue $Issue -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers + } + $Issue + } + } +} + function Find-ESC2 { <# .SYNOPSIS @@ -1494,7 +1570,7 @@ function Find-ESC7 { process { $ADCSObjects | Where-Object { ($_.objectClass -eq 'pKIEnrollmentService') -and - ( ($_.CAAdministrator) -or ($_.CertificateManager) ) + ( ($_.CAAdministrator -notmatch 'Failure|CA Unavailable') -or ($_.CertificateManager) ) } | ForEach-Object { $UnsafeCAAdministrators = Write-Output $_.CAAdministrator -PipelineVariable admin | ForEach-Object { $SID = Convert-IdentityReferenceToSid -Object $admin @@ -1822,6 +1898,7 @@ function Format-Result { ESC11 = 'ESC11 - IF_ENFORCEENCRYPTICERTREQUEST Flag Disabled' ESC13 = 'ESC13 - Vulnerable Certificate Template - Group-Linked' 'ESC15/EKUwu' = 'ESC15 - Vulnerable Certificate Template - Schema V1' + ESC16 = 'ESC16 - szOID_NTDS_CA_SECURITY_EXT Extension Disabled' } $RiskTable = @{ @@ -1845,7 +1922,7 @@ function Format-Result { if ($Mode -eq 0) { # TODO Refactor this switch ($UniqueIssue) { - { $_ -in @('DETECT', 'ESC6', 'ESC7', 'ESC8', 'ESC11') } { + { $_ -in @('DETECT', 'ESC6', 'ESC7', 'ESC8', 'ESC11', 'ESC16') } { $Issue | Format-Table Technique, @{l = 'CA Name'; e = { $_.Name } }, @{l = 'Risk'; e = { $_.RiskName } }, Issue -Wrap | Write-HostColorized -PatternColorMap $RiskTable -CaseSensitive @@ -1864,7 +1941,7 @@ function Format-Result { } elseif ($Mode -eq 1) { switch ($UniqueIssue) { - { $_ -in @('DETECT', 'ESC6', 'ESC7', 'ESC8', 'ESC11') } { + { $_ -in @('DETECT', 'ESC6', 'ESC7', 'ESC8', 'ESC11', 'ESC16') } { $Issue | Format-List Technique, @{l = 'CA Name'; e = { $_.Name } }, @{l = 'Risk'; e = { $_.RiskName } }, DistinguishedName, Issue, Fix, @{l = 'Risk Score'; e = { $_.RiskValue } }, @{l = 'Risk Score Detail'; e = { $_.RiskScoring -join "`n" } } | Write-HostColorized -PatternColorMap $RiskTable -CaseSensitive @@ -2514,11 +2591,11 @@ function Invoke-Scans { .PARAMETER Scans Specifies the type of scans to perform. Multiple scan options can be provided as an array. The default value is 'All'. The available scan options are: 'Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC8', 'ESC11', - 'ESC13', 'ESC15, 'EKUwu', 'All', 'PromptMe'. + 'ESC13', 'ESC15, 'EKUwu', 'ESC16', 'All', 'PromptMe'. .NOTES - The script requires the following functions to be defined: Find-AuditingIssue, Find-ESC1, Find-ESC2, Find-ESC3C1, - Find-ESC3C2, Find-ESC4, Find-ESC5, Find-ESC6, Find-ESC8, Find-ESC11, Find-ESC13, Find-ESC15 + Find-ESC3C2, Find-ESC4, Find-ESC5, Find-ESC6, Find-ESC8, Find-ESC11, Find-ESC13, Find-ESC15, Find-ESC16 - The script uses Out-GridView or Out-ConsoleGridView for interactive selection when the 'PromptMe' scan option is chosen. - The script returns a hash table containing the results of the scans. @@ -2556,7 +2633,7 @@ function Invoke-Scans { [string]$SafeUsers, [Parameter(Mandatory)] [string]$SafeOwners, - [ValidateSet('Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC7', 'ESC8', 'ESC11', 'ESC13', 'ESC15', 'EKUwu', 'All', 'PromptMe')] + [ValidateSet('Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC7', 'ESC8', 'ESC11', 'ESC13', 'ESC15', 'EKUwu', 'ESC16', 'All', 'PromptMe')] [array]$Scans = 'All', [Parameter(Mandatory)] [string]$UnsafeUsers, @@ -2612,7 +2689,7 @@ function Invoke-Scans { [array]$ESC6 = Find-ESC6 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers } ESC7 { - Write-Host 'Identifying Issuing CAs with ESC7...' + Write-Host 'Identifying Issuing CAs with Non-Standard Admins (ESC7)...' [array]$ESC7 = Find-ESC7 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers -SafeUsers $SafeUsers } ESC8 { @@ -2635,6 +2712,10 @@ function Invoke-Scans { Write-Host 'Identifying AD CS templates with dangerous ESC15/EKUwu configurations...' [array]$ESC15 = Find-ESC15 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers } + ESC16 { + Write-Host 'Identifying Issuing CAs with szOID_NTDS_CA_SECURITY_EXT disabled (ESC16)...' + [array]$ESC16 = Find-ESC16 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers + } All { Write-Host 'Identifying auditing issues...' [array]$AuditingIssues = Find-AuditingIssue -ADCSObjects $ADCSObjects @@ -2651,7 +2732,7 @@ function Invoke-Scans { [array]$ESC5 = Find-ESC5 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -DangerousRights $DangerousRights -SafeOwners $SafeOwners -SafeObjectTypes $SafeObjectTypes -UnsafeUsers $UnsafeUsers Write-Host 'Identifying Certificate Authorities with EDITF_ATTRIBUTESUBJECTALTNAME2 enabled (ESC6)...' [array]$ESC6 = Find-ESC6 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers - Write-Host 'Identifying Certificate Authorities with ESC7...' + Write-Host 'Identifying Certificate Authorities with Non-Standard Admins (ESC7)...' [array]$ESC7 = Find-ESC7 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers -SafeUsers $SafeUsers Write-Host 'Identifying HTTP-based certificate enrollment interfaces (ESC8)...' [array]$ESC8 = Find-ESC8 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers @@ -2661,11 +2742,12 @@ function Invoke-Scans { [array]$ESC13 = Find-ESC13 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -ClientAuthEKUs $ClientAuthEkus -UnsafeUsers $UnsafeUsers Write-Host 'Identifying AD CS templates with dangerous ESC15 configurations...' [array]$ESC15 = Find-ESC15 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers - Write-Host + Write-Host 'Identifying Certificate Authorities with szOID_NTDS_CA_SECURITY_EXT disabled (ESC16)...' + [array]$ESC6 = Find-ESC16 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers } } - [array]$AllIssues = $AuditingIssues + $ESC1 + $ESC2 + $ESC3 + $ESC4 + $ESC5 + $ESC6 + $ESC7 + $ESC8 + $ESC11 + $ESC13 + $ESC15 + [array]$AllIssues = $AuditingIssues + $ESC1 + $ESC2 + $ESC3 + $ESC4 + $ESC5 + $ESC6 + $ESC7 + $ESC8 + $ESC11 + $ESC13 + $ESC15 + $ESC16 # If these are all empty = no issues found, exit if ($AllIssues.Count -lt 1) { @@ -2688,6 +2770,7 @@ function Invoke-Scans { ESC11 = $ESC11 ESC13 = $ESC13 ESC15 = $ESC15 + ESC16 = $ESC16 } } @@ -2787,7 +2870,7 @@ function New-Dictionary { Category = 'Escalation Path' Subcategory = 'Vulnerable Certificate Authority Access Control' Summary = '' - FindIt = { Write-Output 'We have not created Find-ESC7 yet.' } + FindIt = { Find-ESC7 } FixIt = { Write-Output 'Add code to fix the vulnerable configuration.' } ReferenceUrls = 'https://posts.specterops.io/certified-pre-owned-d95910965cd2#:~:text=Vulnerable%20Certificate%20Authority%20Access%20Control%20%E2%80%94%20ESC7' }, @@ -3075,6 +3158,17 @@ function Set-AdditionalCAProperty { $CAAdministrator = 'Failure' $CertificateManager = 'Failure' } + try { + if ($Credential) { + $CertutilDisableExtensionList = Invoke-Command -ComputerName $CAHostFQDN -Credential $Credential -ScriptBlock { certutil -config $using:CAFullName -getreg policy\DisableExtensionList } + } + else { + $CertutilDisableExtensionList = certutil -config $CAFullName -getreg policy\DisableExtensionList + } + } + catch { + $CertutilDisableExtensionList = 'Failure' + } } else { $AuditFilter = 'CA Unavailable' @@ -3082,6 +3176,7 @@ function Set-AdditionalCAProperty { $InterfaceFlag = 'CA Unavailable' $CAAdministrator = 'CA Unavailable' $CertificateManager = 'CA Unavailable' + $DisableExtensionList = 'CA Unavailable' } if ($CertutilAudit) { try { @@ -3128,6 +3223,15 @@ function Set-AdditionalCAProperty { } } } + if ($CertutilDisableExtensionList) { + [string]$DisableExtensionList = $CertutilDisableExtensionList | Select-String '1\.3\.6\.1\.4\.1\.311\.25\.2' + if ($DisableExtensionList) { + $DisableExtensionList = 'Yes' + } + else { + $DisableExtensionList = 'No' + } + } Add-Member -InputObject $_ -MemberType NoteProperty -Name AuditFilter -Value $AuditFilter -Force Add-Member -InputObject $_ -MemberType NoteProperty -Name CAEnrollmentEndpoint -Value $CAEnrollmentEndpoint -Force Add-Member -InputObject $_ -MemberType NoteProperty -Name CAFullName -Value $CAFullName -Force @@ -3137,6 +3241,7 @@ function Set-AdditionalCAProperty { Add-Member -InputObject $_ -MemberType NoteProperty -Name InterfaceFlag -Value $InterfaceFlag -Force Add-Member -InputObject $_ -MemberType NoteProperty -Name CAAdministrator -Value $CAAdministrator -Force Add-Member -InputObject $_ -MemberType NoteProperty -Name CertificateManager -Value $CertificateManager -Force + Add-Member -InputObject $_ -MemberType NoteProperty -Name DisableExtensionList -Value $DisableExtensionList -Force } } } @@ -3244,7 +3349,7 @@ function Set-RiskRating { $RiskScoring = @() # CA issues don't rely on a principal and have a base risk of Medium. - if ($Issue.Technique -in @('DETECT', 'ESC6', 'ESC7', 'ESC8', 'ESC11')) { + if ($Issue.Technique -in @('DETECT', 'ESC6', 'ESC7', 'ESC8', 'ESC11', 'ESC16')) { $RiskValue += 3 $RiskScoring += 'Base Score: 3' @@ -3253,12 +3358,12 @@ function Set-RiskRating { $RiskScoring += 'HTTP Enrollment: +2' } - # TODO Check NtAuthCertificates for CA thumbnail. If found, +2, else -1 + # TODO Check NtAuthCertificates for CA thumbprint. If found, +2, else -1 # TODO Check if NTLMv1 is allowed. } # Template and object issues rely on a principal and have complex scoring. - if ($Issue.Technique -notin @('DETECT', 'ESC6', 'ESC7', 'ESC8', 'ESC11')) { + if ($Issue.Technique -notin @('DETECT', 'ESC6', 'ESC7', 'ESC8', 'ESC11', 'ESC16')) { $RiskScoring += 'Base Score: 0' # Templates are more dangerous when enabled, but objects cannot be enabled/disabled. @@ -4532,6 +4637,7 @@ function Invoke-Locksmith { 'ESC13', 'ESC15', 'EKUwu', + 'ESC16', 'All', 'PromptMe' )] @@ -4723,6 +4829,7 @@ function Invoke-Locksmith { $ESC11 = $Results['ESC11'] $ESC13 = $Results['ESC13'] $ESC15 = $Results['ESC15'] + $ESC16 = $Results['ESC16'] # If these are all empty = no issues found, exit if ($null -eq $Results) { @@ -4746,6 +4853,7 @@ function Invoke-Locksmith { Format-Result -Issue $ESC11 -Mode 0 Format-Result -Issue $ESC13 -Mode 0 Format-Result -Issue $ESC15 -Mode 0 + Format-Result -Issue $ESC16 -Mode 0 Write-Host @" [!] You ran Locksmith in Mode 0 which only provides an high-level overview of issues identified in the environment. For more details including: @@ -4776,6 +4884,7 @@ Invoke-Locksmith -Mode 1 Format-Result -Issue $ESC11 -Mode 1 Format-Result -Issue $ESC13 -Mode 1 Format-Result -Issue $ESC15 -Mode 1 + Format-Result -Issue $ESC16 -Mode 1 } 2 { $Output = Join-Path -Path $OutputPath -ChildPath "$FilePrefix ADCSIssues.CSV" diff --git a/Private/Find-ESC16.ps1 b/Private/Find-ESC16.ps1 new file mode 100644 index 0000000..0f02ecd --- /dev/null +++ b/Private/Find-ESC16.ps1 @@ -0,0 +1,75 @@ +function Find-ESC16 { + <# + .SYNOPSIS + This script finds Active Directory Certificate Services (AD CS) Certification Authorities (CA) that have the ESC16 vulnerability. + + .DESCRIPTION + The script takes an array of ADCS objects as input and filters them based on objects that have the objectClass + 'pKIEnrollmentService' and the szOID_NTDS_CA_SECURITY_EXT disabled. For each matching object, it creates a custom object with + properties representing various information about the object, such as Forest, Name, DistinguishedName, Technique, + Issue, Fix, and Revert. + + .PARAMETER ADCSObjects + Specifies the array of AD CS objects to be processed. This parameter is mandatory. + + .OUTPUTS + The script outputs an array of custom objects representing the matching ADCS objects and their associated information. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [Microsoft.ActiveDirectory.Management.ADEntity[]]$ADCSObjects, + [Parameter(Mandatory)] + [string]$UnsafeUsers, + [switch]$SkipRisk + ) + process { + $ADCSObjects | Where-Object { + ($_.objectClass -eq 'pKIEnrollmentService') -and + ($_.DisableExtensionList -ne 'No') + } | ForEach-Object { + $Issue = [pscustomobject]@{ + Forest = $_.CanonicalName.split('/')[0] + Name = $_.Name + DistinguishedName = $_.DistinguishedName + Issue = $_.DisableExtensionList + Fix = 'N/A' + Revert = 'N/A' + Technique = 'ESC16' + } + if ($_.DisableExtensionList -eq 'Yes') { + $Issue.Issue = @" +The Certification Authority (CA) $($_.CAFullName) has the szOID_NTDS_CA_SECURITY_EXT security extension disabled. When +this extension is disabled, every certificate issued by this CA will be unable to to reliably map a certificate to a +user or computer account's SID for authentication. + +More info: + - https://github.com/ly4k/Certipy/wiki/06-%E2%80%90-Privilege-Escalation#esc16-security-extension-disabled-on-ca-globally + +"@ + $Issue.Fix = @" +# Enable the flag +# TODO + +# Restart the Certificate Authority service +Invoke-Command -ComputerName '$($_.dNSHostName)' -ScriptBlock { + Get-Service -Name certsvc | Restart-Service -Force +} +"@ + $Issue.Revert = @" +# Disable the flag +TODO + +# Restart the Certificate Authority service +Invoke-Command -ComputerName '$($_.dNSHostName)' -ScriptBlock { + Get-Service -Name certsvc | Restart-Service -Force +} +"@ + } + if ($SkipRisk -eq $false) { + Set-RiskRating -ADCSObjects $ADCSObjects -Issue $Issue -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers + } + $Issue + } + } +} diff --git a/Private/Find-ESC7.ps1 b/Private/Find-ESC7.ps1 index 7a8265e..9d6ac55 100644 --- a/Private/Find-ESC7.ps1 +++ b/Private/Find-ESC7.ps1 @@ -36,7 +36,7 @@ process { $ADCSObjects | Where-Object { ($_.objectClass -eq 'pKIEnrollmentService') -and - ( ($_.CAAdministrator) -or ($_.CertificateManager) ) + ( ($_.CAAdministrator -notmatch 'Failure|CA Unavailable') -or ($_.CertificateManager) ) } | ForEach-Object { $UnsafeCAAdministrators = Write-Output $_.CAAdministrator -PipelineVariable admin | ForEach-Object { $SID = Convert-IdentityReferenceToSid -Object $admin diff --git a/Private/Format-Result.ps1 b/Private/Format-Result.ps1 index 3e253c1..4b2565a 100644 --- a/Private/Format-Result.ps1 +++ b/Private/Format-Result.ps1 @@ -43,6 +43,7 @@ function Format-Result { ESC11 = 'ESC11 - IF_ENFORCEENCRYPTICERTREQUEST Flag Disabled' ESC13 = 'ESC13 - Vulnerable Certificate Template - Group-Linked' 'ESC15/EKUwu' = 'ESC15 - Vulnerable Certificate Template - Schema V1' + ESC16 = 'ESC16 - szOID_NTDS_CA_SECURITY_EXT Extension Disabled' } $RiskTable = @{ @@ -66,7 +67,7 @@ function Format-Result { if ($Mode -eq 0) { # TODO Refactor this switch ($UniqueIssue) { - { $_ -in @('DETECT', 'ESC6', 'ESC7', 'ESC8', 'ESC11') } { + { $_ -in @('DETECT', 'ESC6', 'ESC7', 'ESC8', 'ESC11', 'ESC16') } { $Issue | Format-Table Technique, @{l = 'CA Name'; e = { $_.Name } }, @{l = 'Risk'; e = { $_.RiskName } }, Issue -Wrap | Write-HostColorized -PatternColorMap $RiskTable -CaseSensitive @@ -84,7 +85,7 @@ function Format-Result { } } elseif ($Mode -eq 1) { switch ($UniqueIssue) { - { $_ -in @('DETECT', 'ESC6', 'ESC7', 'ESC8', 'ESC11') } { + { $_ -in @('DETECT', 'ESC6', 'ESC7', 'ESC8', 'ESC11', 'ESC16') } { $Issue | Format-List Technique, @{l = 'CA Name'; e = { $_.Name } }, @{l = 'Risk'; e = { $_.RiskName } }, DistinguishedName, Issue, Fix, @{l = 'Risk Score'; e = { $_.RiskValue } }, @{l = 'Risk Score Detail'; e = { $_.RiskScoring -join "`n" } } | Write-HostColorized -PatternColorMap $RiskTable -CaseSensitive diff --git a/Private/Invoke-Scans.ps1 b/Private/Invoke-Scans.ps1 index ce24a64..58a0fc2 100644 --- a/Private/Invoke-Scans.ps1 +++ b/Private/Invoke-Scans.ps1 @@ -6,11 +6,11 @@ function Invoke-Scans { .PARAMETER Scans Specifies the type of scans to perform. Multiple scan options can be provided as an array. The default value is 'All'. The available scan options are: 'Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC8', 'ESC11', - 'ESC13', 'ESC15, 'EKUwu', 'All', 'PromptMe'. + 'ESC13', 'ESC15, 'EKUwu', 'ESC16', 'All', 'PromptMe'. .NOTES - The script requires the following functions to be defined: Find-AuditingIssue, Find-ESC1, Find-ESC2, Find-ESC3C1, - Find-ESC3C2, Find-ESC4, Find-ESC5, Find-ESC6, Find-ESC8, Find-ESC11, Find-ESC13, Find-ESC15 + Find-ESC3C2, Find-ESC4, Find-ESC5, Find-ESC6, Find-ESC8, Find-ESC11, Find-ESC13, Find-ESC15, Find-ESC16 - The script uses Out-GridView or Out-ConsoleGridView for interactive selection when the 'PromptMe' scan option is chosen. - The script returns a hash table containing the results of the scans. @@ -48,7 +48,7 @@ function Invoke-Scans { [string]$SafeUsers, [Parameter(Mandatory)] [string]$SafeOwners, - [ValidateSet('Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC7', 'ESC8', 'ESC11', 'ESC13', 'ESC15', 'EKUwu', 'All', 'PromptMe')] + [ValidateSet('Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC7', 'ESC8', 'ESC11', 'ESC13', 'ESC15', 'EKUwu', 'ESC16', 'All', 'PromptMe')] [array]$Scans = 'All', [Parameter(Mandatory)] [string]$UnsafeUsers, @@ -102,7 +102,7 @@ function Invoke-Scans { [array]$ESC6 = Find-ESC6 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers } ESC7 { - Write-Host 'Identifying Issuing CAs with ESC7...' + Write-Host 'Identifying Issuing CAs with Non-Standard Admins (ESC7)...' [array]$ESC7 = Find-ESC7 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers -SafeUsers $SafeUsers } ESC8 { @@ -125,6 +125,10 @@ function Invoke-Scans { Write-Host 'Identifying AD CS templates with dangerous ESC15/EKUwu configurations...' [array]$ESC15 = Find-ESC15 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers } + ESC16 { + Write-Host 'Identifying Issuing CAs with szOID_NTDS_CA_SECURITY_EXT disabled (ESC16)...' + [array]$ESC16 = Find-ESC16 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers + } All { Write-Host 'Identifying auditing issues...' [array]$AuditingIssues = Find-AuditingIssue -ADCSObjects $ADCSObjects @@ -141,7 +145,7 @@ function Invoke-Scans { [array]$ESC5 = Find-ESC5 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -DangerousRights $DangerousRights -SafeOwners $SafeOwners -SafeObjectTypes $SafeObjectTypes -UnsafeUsers $UnsafeUsers Write-Host 'Identifying Certificate Authorities with EDITF_ATTRIBUTESUBJECTALTNAME2 enabled (ESC6)...' [array]$ESC6 = Find-ESC6 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers - Write-Host 'Identifying Certificate Authorities with ESC7...' + Write-Host 'Identifying Certificate Authorities with Non-Standard Admins (ESC7)...' [array]$ESC7 = Find-ESC7 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers -SafeUsers $SafeUsers Write-Host 'Identifying HTTP-based certificate enrollment interfaces (ESC8)...' [array]$ESC8 = Find-ESC8 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers @@ -151,11 +155,12 @@ function Invoke-Scans { [array]$ESC13 = Find-ESC13 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -ClientAuthEKUs $ClientAuthEkus -UnsafeUsers $UnsafeUsers Write-Host 'Identifying AD CS templates with dangerous ESC15 configurations...' [array]$ESC15 = Find-ESC15 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers - Write-Host + Write-Host 'Identifying Certificate Authorities with szOID_NTDS_CA_SECURITY_EXT disabled (ESC16)...' + [array]$ESC6 = Find-ESC16 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers } } - [array]$AllIssues = $AuditingIssues + $ESC1 + $ESC2 + $ESC3 + $ESC4 + $ESC5 + $ESC6 + $ESC7 + $ESC8 + $ESC11 + $ESC13 + $ESC15 + [array]$AllIssues = $AuditingIssues + $ESC1 + $ESC2 + $ESC3 + $ESC4 + $ESC5 + $ESC6 + $ESC7 + $ESC8 + $ESC11 + $ESC13 + $ESC15 + $ESC16 # If these are all empty = no issues found, exit if ($AllIssues.Count -lt 1) { @@ -178,5 +183,6 @@ function Invoke-Scans { ESC11 = $ESC11 ESC13 = $ESC13 ESC15 = $ESC15 + ESC16 = $ESC16 } } diff --git a/Private/New-Dictionary.ps1 b/Private/New-Dictionary.ps1 index 4a8bd9c..6c2ca39 100644 --- a/Private/New-Dictionary.ps1 +++ b/Private/New-Dictionary.ps1 @@ -94,7 +94,7 @@ function New-Dictionary { Category = 'Escalation Path' Subcategory = 'Vulnerable Certificate Authority Access Control' Summary = '' - FindIt = { Write-Output 'We have not created Find-ESC7 yet.' } + FindIt = { Find-ESC7 } FixIt = { Write-Output 'Add code to fix the vulnerable configuration.' } ReferenceUrls = 'https://posts.specterops.io/certified-pre-owned-d95910965cd2#:~:text=Vulnerable%20Certificate%20Authority%20Access%20Control%20%E2%80%94%20ESC7' }, diff --git a/Private/Set-AdditionalCAProperty.ps1 b/Private/Set-AdditionalCAProperty.ps1 index f80f9b3..16975cc 100644 --- a/Private/Set-AdditionalCAProperty.ps1 +++ b/Private/Set-AdditionalCAProperty.ps1 @@ -168,12 +168,22 @@ $CAAdministrator = 'Failure' $CertificateManager = 'Failure' } + try { + if ($Credential) { + $CertutilDisableExtensionList = Invoke-Command -ComputerName $CAHostFQDN -Credential $Credential -ScriptBlock { certutil -config $using:CAFullName -getreg policy\DisableExtensionList } + } else { + $CertutilDisableExtensionList = certutil -config $CAFullName -getreg policy\DisableExtensionList + } + } catch { + $CertutilDisableExtensionList = 'Failure' + } } else { $AuditFilter = 'CA Unavailable' $SANFlag = 'CA Unavailable' $InterfaceFlag = 'CA Unavailable' $CAAdministrator = 'CA Unavailable' $CertificateManager = 'CA Unavailable' + $DisableExtensionList = 'CA Unavailable' } if ($CertutilAudit) { try { @@ -216,6 +226,14 @@ } } } + if ($CertutilDisableExtensionList) { + [string]$DisableExtensionList = $CertutilDisableExtensionList | Select-String '1\.3\.6\.1\.4\.1\.311\.25\.2' + if ($DisableExtensionList) { + $DisableExtensionList = 'Yes' + } else { + $DisableExtensionList = 'No' + } + } Add-Member -InputObject $_ -MemberType NoteProperty -Name AuditFilter -Value $AuditFilter -Force Add-Member -InputObject $_ -MemberType NoteProperty -Name CAEnrollmentEndpoint -Value $CAEnrollmentEndpoint -Force Add-Member -InputObject $_ -MemberType NoteProperty -Name CAFullName -Value $CAFullName -Force @@ -225,6 +243,7 @@ Add-Member -InputObject $_ -MemberType NoteProperty -Name InterfaceFlag -Value $InterfaceFlag -Force Add-Member -InputObject $_ -MemberType NoteProperty -Name CAAdministrator -Value $CAAdministrator -Force Add-Member -InputObject $_ -MemberType NoteProperty -Name CertificateManager -Value $CertificateManager -Force + Add-Member -InputObject $_ -MemberType NoteProperty -Name DisableExtensionList -Value $DisableExtensionList -Force } } } diff --git a/Private/Set-RiskRating.ps1 b/Private/Set-RiskRating.ps1 index 27256e1..e83ff78 100644 --- a/Private/Set-RiskRating.ps1 +++ b/Private/Set-RiskRating.ps1 @@ -55,7 +55,7 @@ function Set-RiskRating { $RiskScoring = @() # CA issues don't rely on a principal and have a base risk of Medium. - if ($Issue.Technique -in @('DETECT', 'ESC6', 'ESC7', 'ESC8', 'ESC11')) { + if ($Issue.Technique -in @('DETECT', 'ESC6', 'ESC7', 'ESC8', 'ESC11', 'ESC16')) { $RiskValue += 3 $RiskScoring += 'Base Score: 3' @@ -64,12 +64,12 @@ function Set-RiskRating { $RiskScoring += 'HTTP Enrollment: +2' } - # TODO Check NtAuthCertificates for CA thumbnail. If found, +2, else -1 + # TODO Check NtAuthCertificates for CA thumbprint. If found, +2, else -1 # TODO Check if NTLMv1 is allowed. } # Template and object issues rely on a principal and have complex scoring. - if ($Issue.Technique -notin @('DETECT', 'ESC6', 'ESC7', 'ESC8', 'ESC11')) { + if ($Issue.Technique -notin @('DETECT', 'ESC6', 'ESC7', 'ESC8', 'ESC11', 'ESC16')) { $RiskScoring += 'Base Score: 0' # Templates are more dangerous when enabled, but objects cannot be enabled/disabled. diff --git a/Public/Invoke-Locksmith.ps1 b/Public/Invoke-Locksmith.ps1 index 06449a7..5ba780e 100644 --- a/Public/Invoke-Locksmith.ps1 +++ b/Public/Invoke-Locksmith.ps1 @@ -99,6 +99,7 @@ function Invoke-Locksmith { 'ESC13', 'ESC15', 'EKUwu', + 'ESC16', 'All', 'PromptMe' )] @@ -287,6 +288,7 @@ function Invoke-Locksmith { $ESC11 = $Results['ESC11'] $ESC13 = $Results['ESC13'] $ESC15 = $Results['ESC15'] + $ESC16 = $Results['ESC16'] # If these are all empty = no issues found, exit if ($null -eq $Results) { @@ -310,6 +312,7 @@ function Invoke-Locksmith { Format-Result -Issue $ESC11 -Mode 0 Format-Result -Issue $ESC13 -Mode 0 Format-Result -Issue $ESC15 -Mode 0 + Format-Result -Issue $ESC16 -Mode 0 Write-Host @" [!] You ran Locksmith in Mode 0 which only provides an high-level overview of issues identified in the environment. For more details including: @@ -340,6 +343,7 @@ Invoke-Locksmith -Mode 1 Format-Result -Issue $ESC11 -Mode 1 Format-Result -Issue $ESC13 -Mode 1 Format-Result -Issue $ESC15 -Mode 1 + Format-Result -Issue $ESC16 -Mode 1 } 2 { $Output = Join-Path -Path $OutputPath -ChildPath "$FilePrefix ADCSIssues.CSV" diff --git a/README.md b/README.md index f9af5ae..2a53ab7 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,7 @@ Example Output for Mode 4: ### Scans: Select Which Scans to Invoke -Use the `-Scans` parameter to choose which vulnerabilities to scan for. Acceptable values include `All`, `Auditing`, `ESC1`, `ESC2`, `ESC3`, `ESC4`, `ESC5`, `ESC6`, `ESC8`, `ESC11`, `ESC13`, `ESC15`, `EKEUwu`, or `PromptMe`. The `PromptMe` option presents an interactive list allowing you to select one or more scans. +Use the `-Scans` parameter to choose which vulnerabilities to scan for. Acceptable values include `All`, `Auditing`, `ESC1`, `ESC2`, `ESC3`, `ESC4`, `ESC5`, `ESC6`, `ESC7`, `ESC8`, `ESC11`, `ESC13`, `ESC15`, `EKEUwu`, `ESC16` or `PromptMe`. The `PromptMe` option presents an interactive list allowing you to select one or more scans. ``` powershell # Run all scans From f0696b5e19b94f2730c0a5ddfafd509f2fed451b Mon Sep 17 00:00:00 2001 From: Jake Hildreth Date: Sun, 18 May 2025 15:15:51 -0400 Subject: [PATCH 04/11] Fixed typos. Improved readability. --- Invoke-Locksmith.ps1 | 12 +++++++----- Locksmith.psd1 | 2 +- Private/Find-ESC7.ps1 | 10 ++++++---- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/Invoke-Locksmith.ps1 b/Invoke-Locksmith.ps1 index a505a3d..be1d87c 100644 --- a/Invoke-Locksmith.ps1 +++ b/Invoke-Locksmith.ps1 @@ -1522,7 +1522,8 @@ function Find-ESC7 { } if ($UnsafeCAAdministrators) { $Issue.Issue = $Issue.Issue + @" -Unexpected prinicipals ($($UnsafeCAAdministrators -join ', ')) are granted "CA Administrator" rights on this Certification Authority. +Unexpected principals are granted "CA Administrator" rights on this Certification Authority. +Unsafe CA Administrators: $($UnsafeCAAdministrators -join ', '). "@ $Issue.Fix = $Issue.Fix + @" @@ -1536,15 +1537,16 @@ Reinstate CA Administrator rights for $($UnsafeCAAdministrators -join ', ') } if ($UnsafeCertificateManagers) { $Issue.Issue = $Issue.Issue + @" -Unexpected prinicipals ($($UnsafeCertificateManagers -join ', ')) are granted "Certificate Manager" rights on this Certification Authority. +Unexpected principals are granted "Certificate Manager" rights on this Certification Authority. +Unexpected Principals: $($UnsafeCertificateManagers -join ', ') "@ $Issue.Fix = $Issue.Fix + @" -Revoke CA Administrator rights from $($UnsafeCertificateManagers -join ', ') +Revoke Certificate Manager rights from $($UnsafeCertificateManagers -join ', ') "@ $Issue.Revert = $Issue.Revert + @" -Reinstate CA Administrator rights for $($UnsafeCertificateManagers -join ', ') +Reinstate Certificate Manager rights for $($UnsafeCertificateManagers -join ', ') "@ } @@ -4547,7 +4549,7 @@ function Invoke-Locksmith { [System.Management.Automation.PSCredential]$Credential ) - $Version = '2025.5.17' + $Version = '2025.5.18' $LogoPart1 = @' _ _____ _______ _ _ _______ _______ _____ _______ _ _ | | | | |____/ |______ | | | | | |_____| diff --git a/Locksmith.psd1 b/Locksmith.psd1 index f93043b..1514545 100644 --- a/Locksmith.psd1 +++ b/Locksmith.psd1 @@ -8,7 +8,7 @@ FunctionsToExport = 'Invoke-Locksmith' GUID = 'b1325b42-8dc4-4f17-aa1f-dcb5984ca14a' HelpInfoURI = 'https://raw.githubusercontent.com/jakehildreth/Locksmith/main/en-US/' - ModuleVersion = '2025.5.17' + ModuleVersion = '2025.5.18' PowerShellVersion = '5.1' PrivateData = @{ PSData = @{ diff --git a/Private/Find-ESC7.ps1 b/Private/Find-ESC7.ps1 index 7a8265e..85ee6b3 100644 --- a/Private/Find-ESC7.ps1 +++ b/Private/Find-ESC7.ps1 @@ -64,7 +64,8 @@ } if ($UnsafeCAAdministrators) { $Issue.Issue = $Issue.Issue + @" -Unexpected prinicipals ($($UnsafeCAAdministrators -join ', ')) are granted "CA Administrator" rights on this Certification Authority. +Unexpected principals are granted "CA Administrator" rights on this Certification Authority. +Unsafe CA Administrators: $($UnsafeCAAdministrators -join ', '). "@ $Issue.Fix = $Issue.Fix + @" @@ -78,15 +79,16 @@ Reinstate CA Administrator rights for $($UnsafeCAAdministrators -join ', ') } if ($UnsafeCertificateManagers) { $Issue.Issue = $Issue.Issue + @" -Unexpected prinicipals ($($UnsafeCertificateManagers -join ', ')) are granted "Certificate Manager" rights on this Certification Authority. +Unexpected principals are granted "Certificate Manager" rights on this Certification Authority. +Unexpected Principals: $($UnsafeCertificateManagers -join ', ') "@ $Issue.Fix = $Issue.Fix + @" -Revoke CA Administrator rights from $($UnsafeCertificateManagers -join ', ') +Revoke Certificate Manager rights from $($UnsafeCertificateManagers -join ', ') "@ $Issue.Revert = $Issue.Revert + @" -Reinstate CA Administrator rights for $($UnsafeCertificateManagers -join ', ') +Reinstate Certificate Manager rights for $($UnsafeCertificateManagers -join ', ') "@ } From a7d240cd48c3f143580d1759f9aad93573b7e571 Mon Sep 17 00:00:00 2001 From: Jake Hildreth Date: Sun, 18 May 2025 18:10:04 -0400 Subject: [PATCH 05/11] Typo fixed! --- Invoke-Locksmith.ps1 | 2 +- Private/Invoke-Scans.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Invoke-Locksmith.ps1 b/Invoke-Locksmith.ps1 index 5d801cc..bd30fda 100644 --- a/Invoke-Locksmith.ps1 +++ b/Invoke-Locksmith.ps1 @@ -2745,7 +2745,7 @@ function Invoke-Scans { Write-Host 'Identifying AD CS templates with dangerous ESC15 configurations...' [array]$ESC15 = Find-ESC15 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers Write-Host 'Identifying Certificate Authorities with szOID_NTDS_CA_SECURITY_EXT disabled (ESC16)...' - [array]$ESC6 = Find-ESC16 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers + [array]$ESC16 = Find-ESC16 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers } } diff --git a/Private/Invoke-Scans.ps1 b/Private/Invoke-Scans.ps1 index 58a0fc2..55d37e7 100644 --- a/Private/Invoke-Scans.ps1 +++ b/Private/Invoke-Scans.ps1 @@ -156,7 +156,7 @@ function Invoke-Scans { Write-Host 'Identifying AD CS templates with dangerous ESC15 configurations...' [array]$ESC15 = Find-ESC15 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers Write-Host 'Identifying Certificate Authorities with szOID_NTDS_CA_SECURITY_EXT disabled (ESC16)...' - [array]$ESC6 = Find-ESC16 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers + [array]$ESC16 = Find-ESC16 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers } } From 2252abde69c6828308fac66bafd3e38a01bfe98a Mon Sep 17 00:00:00 2001 From: Jonathan Colon Date: Mon, 19 May 2025 15:30:25 -0400 Subject: [PATCH 06/11] Enhance error handling in Get-CAHostObject and Find-ESC7 scripts by checking for CAHostDistinguishedName before retrieving AD objects. --- Private/Find-ESC7.ps1 | 12 +++++------- Private/Get-CAHostObject.ps1 | 4 ++-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/Private/Find-ESC7.ps1 b/Private/Find-ESC7.ps1 index 85ee6b3..a40fb7e 100644 --- a/Private/Find-ESC7.ps1 +++ b/Private/Find-ESC7.ps1 @@ -35,7 +35,7 @@ ) process { $ADCSObjects | Where-Object { - ($_.objectClass -eq 'pKIEnrollmentService') -and + ($_.objectClass -eq 'pKIEnrollmentService') -and $_.CAHostDistinguishedName -and ( ($_.CAAdministrator) -or ($_.CertificateManager) ) } | ForEach-Object { $UnsafeCAAdministrators = Write-Output $_.CAAdministrator -PipelineVariable admin | ForEach-Object { @@ -64,8 +64,7 @@ } if ($UnsafeCAAdministrators) { $Issue.Issue = $Issue.Issue + @" -Unexpected principals are granted "CA Administrator" rights on this Certification Authority. -Unsafe CA Administrators: $($UnsafeCAAdministrators -join ', '). +Unexpected prinicipals ($($UnsafeCAAdministrators -join ', ')) are granted "CA Administrator" rights on this Certification Authority. "@ $Issue.Fix = $Issue.Fix + @" @@ -79,16 +78,15 @@ Reinstate CA Administrator rights for $($UnsafeCAAdministrators -join ', ') } if ($UnsafeCertificateManagers) { $Issue.Issue = $Issue.Issue + @" -Unexpected principals are granted "Certificate Manager" rights on this Certification Authority. -Unexpected Principals: $($UnsafeCertificateManagers -join ', ') +Unexpected prinicipals ($($UnsafeCertificateManagers -join ', ')) are granted "Certificate Manager" rights on this Certification Authority. "@ $Issue.Fix = $Issue.Fix + @" -Revoke Certificate Manager rights from $($UnsafeCertificateManagers -join ', ') +Revoke CA Administrator rights from $($UnsafeCertificateManagers -join ', ') "@ $Issue.Revert = $Issue.Revert + @" -Reinstate Certificate Manager rights for $($UnsafeCertificateManagers -join ', ') +Reinstate CA Administrator rights for $($UnsafeCertificateManagers -join ', ') "@ } diff --git a/Private/Get-CAHostObject.ps1 b/Private/Get-CAHostObject.ps1 index 43026d8..b435074 100644 --- a/Private/Get-CAHostObject.ps1 +++ b/Private/Get-CAHostObject.ps1 @@ -40,11 +40,11 @@ process { if ($Credential) { $ADCSObjects | Where-Object objectClass -Match 'pKIEnrollmentService' | ForEach-Object { - Get-ADObject $_.CAHostDistinguishedName -Properties * -Server $ForestGC -Credential $Credential + if ($_.CAHostDistinguishedName) { Get-ADObject $_.CAHostDistinguishedName -Properties * -Server $ForestGC -Credential $Credential } else { Write-Warning "Get-CAHostObject: Unable to get information from $($_.DisplayName)" } } } else { $ADCSObjects | Where-Object objectClass -Match 'pKIEnrollmentService' | ForEach-Object { - Get-ADObject $_.CAHostDistinguishedName -Properties * -Server $ForestGC + if ($_.CAHostDistinguishedName) { Get-ADObject -Identity $_.CAHostDistinguishedName -Properties * -Server $ForestGC } else { Write-Warning "Get-CAHostObject: Unable to get information from $($_.DisplayName)" } } } } From c47b95a34de3f380aa9dccba426b743721f94c40 Mon Sep 17 00:00:00 2001 From: Jake Hildreth Date: Sun, 18 May 2025 23:57:03 -0400 Subject: [PATCH 07/11] Fresh build after merges. --- Invoke-Locksmith.ps1 | 20 +++++++++++++++----- Private/Find-ESC7.ps1 | 10 ++++++---- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/Invoke-Locksmith.ps1 b/Invoke-Locksmith.ps1 index bd30fda..ad41146 100644 --- a/Invoke-Locksmith.ps1 +++ b/Invoke-Locksmith.ps1 @@ -1569,8 +1569,8 @@ function Find-ESC7 { ) process { $ADCSObjects | Where-Object { - ($_.objectClass -eq 'pKIEnrollmentService') -and - ( ($_.CAAdministrator -notmatch 'Failure|CA Unavailable') -or ($_.CertificateManager) ) + ($_.objectClass -eq 'pKIEnrollmentService') -and $_.CAHostDistinguishedName -and + ( ($_.CAAdministrator) -or ($_.CertificateManager) ) } | ForEach-Object { $UnsafeCAAdministrators = Write-Output $_.CAAdministrator -PipelineVariable admin | ForEach-Object { $SID = Convert-IdentityReferenceToSid -Object $admin @@ -1613,7 +1613,7 @@ Reinstate CA Administrator rights for $($UnsafeCAAdministrators -join ', ') } if ($UnsafeCertificateManagers) { $Issue.Issue = $Issue.Issue + @" -Unexpected principals are granted "Certificate Manager" rights on this Certification Authority. +expected principals are granted "Certificate Manager" rights on this Certification Authority. Unexpected Principals: $($UnsafeCertificateManagers -join ', ') "@ @@ -2044,12 +2044,22 @@ function Get-CAHostObject { process { if ($Credential) { $ADCSObjects | Where-Object objectClass -Match 'pKIEnrollmentService' | ForEach-Object { - Get-ADObject $_.CAHostDistinguishedName -Properties * -Server $ForestGC -Credential $Credential + if ($_.CAHostDistinguishedName) { + Get-ADObject $_.CAHostDistinguishedName -Properties * -Server $ForestGC -Credential $Credential + } + else { + Write-Warning "Get-CAHostObject: Unable to get information from $($_.DisplayName)" + } } } else { $ADCSObjects | Where-Object objectClass -Match 'pKIEnrollmentService' | ForEach-Object { - Get-ADObject $_.CAHostDistinguishedName -Properties * -Server $ForestGC + if ($_.CAHostDistinguishedName) { + Get-ADObject -Identity $_.CAHostDistinguishedName -Properties * -Server $ForestGC + } + else { + Write-Warning "Get-CAHostObject: Unable to get information from $($_.DisplayName)" + } } } } diff --git a/Private/Find-ESC7.ps1 b/Private/Find-ESC7.ps1 index a40fb7e..dee7cf2 100644 --- a/Private/Find-ESC7.ps1 +++ b/Private/Find-ESC7.ps1 @@ -64,7 +64,8 @@ } if ($UnsafeCAAdministrators) { $Issue.Issue = $Issue.Issue + @" -Unexpected prinicipals ($($UnsafeCAAdministrators -join ', ')) are granted "CA Administrator" rights on this Certification Authority. +Unexpected principals are granted "CA Administrator" rights on this Certification Authority. +Unsafe CA Administrators: $($UnsafeCAAdministrators -join ', '). "@ $Issue.Fix = $Issue.Fix + @" @@ -78,15 +79,16 @@ Reinstate CA Administrator rights for $($UnsafeCAAdministrators -join ', ') } if ($UnsafeCertificateManagers) { $Issue.Issue = $Issue.Issue + @" -Unexpected prinicipals ($($UnsafeCertificateManagers -join ', ')) are granted "Certificate Manager" rights on this Certification Authority. +expected principals are granted "Certificate Manager" rights on this Certification Authority. +Unexpected Principals: $($UnsafeCertificateManagers -join ', ') "@ $Issue.Fix = $Issue.Fix + @" -Revoke CA Administrator rights from $($UnsafeCertificateManagers -join ', ') +Revoke Certificate Manager rights from $($UnsafeCertificateManagers -join ', ') "@ $Issue.Revert = $Issue.Revert + @" -Reinstate CA Administrator rights for $($UnsafeCertificateManagers -join ', ') +Reinstate Certificate Manager rights for $($UnsafeCertificateManagers -join ', ') "@ } From 0db9c3bd845a3bee169e00abdb4d480ca438ec9f Mon Sep 17 00:00:00 2001 From: Jake Hildreth Date: Sat, 24 May 2025 20:10:33 -0400 Subject: [PATCH 08/11] ESC9 detections + Improved ESC6/9/16 risk ratings. --- Build/Build-Module.ps1 | 2 +- Invoke-Locksmith.ps1 | 365 ++++++++++++++++++++--------- Locksmith.psd1 | 2 +- Private/Find-ESC6.ps1 | 1 + Private/Find-ESC9.ps1 | 183 +++++++-------- Private/Format-Result.ps1 | 7 +- Private/Invoke-Scans.ps1 | 17 +- Private/Set-RiskRating.ps1 | 36 ++- Private/Update-ESC9Remediation.ps1 | 110 +++++++++ Public/Invoke-Locksmith.ps1 | 4 + 10 files changed, 509 insertions(+), 218 deletions(-) create mode 100644 Private/Update-ESC9Remediation.ps1 diff --git a/Build/Build-Module.ps1 b/Build/Build-Module.ps1 index 1ad9df9..6ea0a70 100644 --- a/Build/Build-Module.ps1 +++ b/Build/Build-Module.ps1 @@ -129,7 +129,7 @@ Build-Module -ModuleName 'Locksmith' { # The scans to run. Defaults to 'All'. [Parameter()] - [ValidateSet('Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC7', 'ESC8', 'ESC11', 'ESC13', 'ESC15', 'EKUwu', 'ESC16', 'All', 'PromptMe')] + [ValidateSet('Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC7', 'ESC8', 'ESC9', 'ESC11', 'ESC13', 'ESC15', 'EKUwu', 'ESC16', 'All', 'PromptMe')] [array]$Scans = 'All' ) } diff --git a/Invoke-Locksmith.ps1 b/Invoke-Locksmith.ps1 index ad41146..73c1048 100644 --- a/Invoke-Locksmith.ps1 +++ b/Invoke-Locksmith.ps1 @@ -7,7 +7,7 @@ param ( # The scans to run. Defaults to 'All'. [Parameter()] - [ValidateSet('Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC7', 'ESC8', 'ESC11', 'ESC13', 'ESC15', 'EKUwu', 'ESC16', 'All', 'PromptMe')] + [ValidateSet('Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC7', 'ESC8', 'ESC9', 'ESC11', 'ESC13', 'ESC15', 'EKUwu', 'ESC16', 'All', 'PromptMe')] [array]$Scans = 'All' ) function Convert-IdentityReferenceToSid { @@ -1482,6 +1482,7 @@ function Find-ESC6 { $Issue = [pscustomobject]@{ Forest = $_.CanonicalName.split('/')[0] Name = $_.Name + CAFullname = $CAFullName DistinguishedName = $_.DistinguishedName Issue = $_.SANFlag Fix = 'N/A' @@ -1738,119 +1739,115 @@ Disable NTLM authentication (if possible.) } } -<# - This is a working POC. I need to test both checks and possibly blend pieces of them. - Then I need to fold this function into the Locksmith workflow. -#> - function Find-ESC9 { <# .SYNOPSIS - Checks for ESC9 (No Security Extension) Vulnerability + This function finds Active Directory Certificate Services (AD CS) objects that have the ESC9 vulnerability. .DESCRIPTION - This function checks for certificate templates that contain the flag CT_FLAG_NO_SECURITY_EXTENSION (0x80000), - which will likely make them vulnerable to ESC9. Another factor to check for ESC9 is the registry values on AD - domain controllers that can help harden certificate based authentication for Kerberos and SChannel. + The script takes an array of ADCS objects as input and filters them based on the specified conditions. + For each matching object, it creates a custom object with properties representing various information about + the object, such as Forest, Name, DistinguishedName, IdentityReference, ActiveDirectoryRights, Issue, Fix, Revert, and Technique. - .NOTES - An ESC9 condition exists when: - - - the new msPKI-Enrollment-Flag value on a certificate contains the flag CT_FLAG_NO_SECURITY_EXTENSION (0x80000) - - AND an insecure registry value is set on domain controllers: - - - the StrongCertificateBindingEnforcement registry value for Kerberos is not set to 2 (the default is 1) on domain controllers - at HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Kdc - - OR the CertificateMappingMethods registry value for SCHANNEL contains the UPN flag on domain controllers at - HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\SecurityProviders\Schannel - - When the CT_FLAG_NO_SECURITY_EXTENSION (0x80000) flag is set on a certificate template, the new szOID_NTDS_CA_SECURITY_EXT - security extension will not be embedded in issued certificates. This security extension was added by Microsoft's - patch KB5014754 ("Certificate-based authentication changes on Windows domain controllers") on May 10, 2022. - - The patch applies to all servers that run Active Directory Certificate Services and Windows domain controllers that - service certificate-based authentication. - https://support.microsoft.com/en-us/topic/kb5014754-certificate-based-authentication-changes-on-windows-domain-controllers-ad2c23b0-15d8-4340-a468-4d4f3b188f16 - - Based on research from - https://research.ifcr.dk/certipy-4-0-esc9-esc10-bloodhound-gui-new-authentication-and-request-methods-and-more-7237d88061f7, - https://support.microsoft.com/en-us/topic/kb5014754-certificate-based-authentication-changes-on-windows-domain-controllers-ad2c23b0-15d8-4340-a468-4d4f3b188f16, - and on a very long conversation with Bing Chat. - - Additional notes from Cortana -- Bing when I pressed her to tell me whether both conditions were required for ESC9 or only one of them: - A certificate template can still be vulnerable to ESC9 even if the msPKI-Enrollment-Flag does not include - CT_FLAG_NO_SECURITY_EXTENSION. This is because the vulnerability primarily arises from the ability of a - requester to specify the subjectAltName in a Certificate Signing Request (CSR). If a requester can specify - the subjectAltName in a CSR, they can request a certificate as anyone, including a domain admin user. - Therefore, if a certificate template allows requesters to specify a subjectAltName and - StrongCertificateBindingEnforcement is not set to 2, it could potentially be vulnerable to ESC9. However, - the presence of CT_FLAG_NO_SECURITY_EXTENSION in msPKI-Enrollment-Flag is a clear indicator of a template - being vulnerable to ESC9. -#> + .PARAMETER ADCSObjects + Specifies the array of ADCS objects to be processed. This parameter is mandatory. + + .PARAMETER SafeUsers + Specifies the list of SIDs of safe users who are allowed to have specific rights on the objects. This parameter is mandatory. + .PARAMETER UnsafeUsers + Specifies the list of SIDs of safe users who should never have specific rights on the objects. This parameter is mandatory. + + .PARAMETER ClientAuthEKUs + A list of EKUs that can be used for client authentication. + + .OUTPUTS + The script outputs an array of custom objects representing the matching ADCS objects and their associated information. + + .EXAMPLE + $Targets = Get-Target + $ADCSObjects = Get-ADCSObject -Targets $Targets + $SafeUsers = '-512$|-519$|-544$|-18$|-517$|-500$|-516$|-521$|-498$|-9$|-526$|-527$|S-1-5-10' + $ClientAuthEKUs = '1\.3\.6\.1\.5\.5\.7\.3\.2|1\.3\.6\.1\.5\.2\.3\.4|1\.3\.6\.1\.4\.1\.311\.20\.2\.2|2\.5\.29\.37\.0' + $Results = Find-ESC1 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -ClientAuthEKUs $ClientAuthEKUs + $Results + #> [CmdletBinding()] param( [Parameter(Mandatory)] [Microsoft.ActiveDirectory.Management.ADEntity[]]$ADCSObjects, [Parameter(Mandatory)] + [string]$SafeUsers, + [Parameter(Mandatory)] + $ClientAuthEKUs, + [int]$Mode = 0, + [Parameter(Mandatory)] [string]$UnsafeUsers, [switch]$SkipRisk ) - - # Import the required module - Import-Module ActiveDirectory - - # Get the configuration naming context - $configNC = (Get-ADRootDSE).configurationNamingContext - - # Define the path to the Certificate Templates container - $path = "CN=Certificate Templates,CN=Public Key Services,CN=Services,$configNC" - - # Get all certificate templates - $templates = Get-ADObject -Filter * -SearchBase $path -Properties msPKI-Enrollment-Flag, msPKI-Certificate-Name-Flag - - foreach ($template in $templates) { - # Check if msPKI-Enrollment-Flag contains the CT_FLAG_NO_SECURITY_EXTENSION (0x80000) flag - if ($template.'msPKI-Enrollment-Flag' -band 0x80000) { - # Check if msPKI-Certificate-Name-Flag contains the CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT_ALT_NAME (0x2) flag - if ($template.'msPKI-Certificate-Name-Flag' -band 0x2) { - # Output the template name - Write-Output "Template Name: $($template.Name), Vulnerable to ESC9" + $ADCSObjects | Where-Object { + ($_.objectClass -eq 'pKICertificateTemplate') -and + ($_.pkiExtendedKeyUsage -match $ClientAuthEKUs) -and + ($_.'msPKI-Enrollment-Flag' -band 0x80000) + } | ForEach-Object { + foreach ($entry in $_.nTSecurityDescriptor.Access) { + $Principal = New-Object System.Security.Principal.NTAccount($entry.IdentityReference) + if ($Principal -match '^(S-1|O:)') { + $SID = $Principal } - } - } + else { + $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value + } + if ( ($SID -notmatch $SafeUsers) -and ( ($entry.ActiveDirectoryRights -match 'ExtendedRight') -or ($entry.ActiveDirectoryRights -match 'GenericAll') ) ) { + $Issue = [pscustomobject]@{ + Forest = $_.CanonicalName.split('/')[0] + Name = $_.Name + DistinguishedName = $_.DistinguishedName + IdentityReference = $entry.IdentityReference + IdentityReferenceSID = $SID + ActiveDirectoryRights = $entry.ActiveDirectoryRights + Enabled = $_.Enabled + EnabledOn = $_.EnabledOn + Issue = @" +This Client Authentication template has the szOID_NTDS_CA_SECURITY_EXT security +extension disabled. Certificates issued from this template will not enforce +strong certificate binding. Depending on the current Certificate Binding +Enforcement level ESC6 status, it may be possible to request and receive +certificates that rely on weak (aka attacker-controllable) binding methods. + +An attacker can abuse this weakness by: +1. Getting access to a user or computer account. +2. Modifying the user's userPrincipalName attribute (or the computer's dNSHostName attribute) to match a higher-privileged account. +3. Requesting a client authentication certificate from a template that w/ szOID_NTDS_CA_SECURITY_EXT disabled. +4. Using the client authentication certificiate to authenticate as the higher-privileged account. +5. Profiting. - # AND / OR / ALSO +More info: + - ESC9 description: https://github.com/ly4k/Certipy/wiki/06-%E2%80%90-Privilege-Escalation#esc9-no-security-extension-on-certificate-template + - Strong Mapping/Enforcement Mode: https://support.microsoft.com/en-us/topic/kb5014754-certificate-based-authentication-changes-on-windows-domain-controllers-ad2c23b0-15d8-4340-a468-4d4f3b188f16 - Import-Module ActiveDirectory +"@ + Fix = @" +# Enable Manager Approval +`$Object = '$($_.DistinguishedName)' +Get-ADObject `$Object | Set-ADObject -Replace @{'msPKI-Enrollment-Flag' = 2} +"@ + Revert = @" +# Disable Manager Approval +`$Object = '$($_.DistinguishedName)' +Get-ADObject `$Object | Set-ADObject -Replace @{'msPKI-Enrollment-Flag' = 0} +"@ + Technique = 'ESC9' + } - $templates = Get-ADObject -Filter { ObjectClass -eq 'pKICertificateTemplate' } -Properties * - foreach ($template in $templates) { - $name = $template.Name - - $subjectNameFlag = $template.'msPKI-Cert-Template-OID' - $subjectType = $template.'msPKI-Certificate-Application-Policy' - $enrollmentFlag = $template.'msPKI-Enrollment-Flag' - $certificateNameFlag = $template.'msPKI-Certificate-Name-Flag' - - # Check if the template is vulnerable to ESC9 - if ($subjectNameFlag -eq 'Supply in the request' -and - ($subjectType -eq 'User' -or $subjectType -eq 'Computer') -and - # 0x200 means a certificate needs to include a template name certificate extension - # 0x220 instructs the client to perform auto-enrollment for the specified template - ($enrollmentFlag -eq 0x200 -or $enrollmentFlag -eq 0x220) -and - # 0x2 instructs the client to supply subject information in the certificate request (CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT). - # This means that any user who is allowed to enroll in a certificate with this setting can request a certificate as any - # user in the network, including a privileged user. - # 0x3 instructs the client to supply both the subject and subject alternate name information in the certificate request - ($certificateNameFlag -eq 0x2 -or $certificateNameFlag -eq 0x3)) { - - # Print the template name and the vulnerability - Write-Output "$name is vulnerable to ESC9" - } - else { - # Print the template name and the status - Write-Output "$name is not vulnerable to ESC9" + if ( $Mode -in @(1, 3, 4) ) { + Update-ESC9Remediation -Issue $Issue + } + if ($SkipRisk -eq $false) { + Set-RiskRating -ADCSObjects $ADCSObjects -Issue $Issue -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers + } + $Issue + } } } } @@ -1897,10 +1894,11 @@ function Format-Result { ESC6 = 'ESC6 - EDITF_ATTRIBUTESUBJECTALTNAME2 Flag Enabled' ESC7 = 'ESC7 - Non-standard PKI Admins' ESC8 = 'ESC8 - HTTP/S Enrollment Enabled' + ESC9 = 'ESC9 - szOID_NTDS_CA_SECURITY_EXT Extension Disabled on Template' ESC11 = 'ESC11 - IF_ENFORCEENCRYPTICERTREQUEST Flag Disabled' ESC13 = 'ESC13 - Vulnerable Certificate Template - Group-Linked' 'ESC15/EKUwu' = 'ESC15 - Vulnerable Certificate Template - Schema V1' - ESC16 = 'ESC16 - szOID_NTDS_CA_SECURITY_EXT Extension Disabled' + ESC16 = 'ESC16 - szOID_NTDS_CA_SECURITY_EXT Extension Disabled on CA' } $RiskTable = @{ @@ -1929,7 +1927,7 @@ function Format-Result { Format-Table Technique, @{l = 'CA Name'; e = { $_.Name } }, @{l = 'Risk'; e = { $_.RiskName } }, Issue -Wrap | Write-HostColorized -PatternColorMap $RiskTable -CaseSensitive } - { $_ -in @('ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC13', 'ESC15/EKUwu') } { + { $_ -in @('ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC9', 'ESC13', 'ESC15/EKUwu') } { $Issue | Format-Table Technique, @{l = 'Template Name'; e = { $_.Name } }, @{l = 'Risk'; e = { $_.RiskName } }, Enabled, Issue -Wrap | Write-HostColorized -PatternColorMap $RiskTable -CaseSensitive @@ -1948,7 +1946,7 @@ function Format-Result { Format-List Technique, @{l = 'CA Name'; e = { $_.Name } }, @{l = 'Risk'; e = { $_.RiskName } }, DistinguishedName, Issue, Fix, @{l = 'Risk Score'; e = { $_.RiskValue } }, @{l = 'Risk Score Detail'; e = { $_.RiskScoring -join "`n" } } | Write-HostColorized -PatternColorMap $RiskTable -CaseSensitive } - { $_ -in @('ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC13', 'ESC15/EKUwu') } { + { $_ -in @('ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC9', 'ESC13', 'ESC15/EKUwu') } { $Issue | Format-List Technique, @{l = 'Template Name'; e = { $_.Name } }, @{l = 'Risk'; e = { $_.RiskName } }, DistinguishedName, Enabled, EnabledOn, Issue, Fix, @{l = 'Risk Score'; e = { $_.RiskValue } }, @{l = 'Risk Score Detail'; e = { $_.RiskScoring -join "`n" } } | Write-HostColorized -PatternColorMap $RiskTable -CaseSensitive @@ -2602,12 +2600,12 @@ function Invoke-Scans { .PARAMETER Scans Specifies the type of scans to perform. Multiple scan options can be provided as an array. The default value is 'All'. - The available scan options are: 'Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC8', 'ESC11', + The available scan options are: 'Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC8', 'ESC9', 'ESC11', 'ESC13', 'ESC15, 'EKUwu', 'ESC16', 'All', 'PromptMe'. .NOTES - The script requires the following functions to be defined: Find-AuditingIssue, Find-ESC1, Find-ESC2, Find-ESC3C1, - Find-ESC3C2, Find-ESC4, Find-ESC5, Find-ESC6, Find-ESC8, Find-ESC11, Find-ESC13, Find-ESC15, Find-ESC16 + Find-ESC3C2, Find-ESC4, Find-ESC5, Find-ESC6, Find-ESC8, Find-ESC9, Find-ESC11, Find-ESC13, Find-ESC15, Find-ESC16 - The script uses Out-GridView or Out-ConsoleGridView for interactive selection when the 'PromptMe' scan option is chosen. - The script returns a hash table containing the results of the scans. @@ -2645,7 +2643,7 @@ function Invoke-Scans { [string]$SafeUsers, [Parameter(Mandatory)] [string]$SafeOwners, - [ValidateSet('Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC7', 'ESC8', 'ESC11', 'ESC13', 'ESC15', 'EKUwu', 'ESC16', 'All', 'PromptMe')] + [ValidateSet('Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC7', 'ESC8', 'ESC9', 'ESC11', 'ESC13', 'ESC15', 'EKUwu', 'ESC16', 'All', 'PromptMe')] [array]$Scans = 'All', [Parameter(Mandatory)] [string]$UnsafeUsers, @@ -2708,6 +2706,10 @@ function Invoke-Scans { Write-Host 'Identifying HTTP-based certificate enrollment interfaces (ESC8)...' [array]$ESC8 = Find-ESC8 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers } + ESC9 { + Write-Host 'Identifying AD CS templates with szOID_NTDS_CA_SECURITY_EXT disabled (ESC9)...' + [array]$ESC9 = Find-ESC9 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -ClientAuthEKUs $ClientAuthEkus -Mode $Mode -UnsafeUsers $UnsafeUsers + } ESC11 { Write-Host 'Identifying Issuing CAs with IF_ENFORCEENCRYPTICERTREQUEST disabled (ESC11)...' [array]$ESC11 = Find-ESC11 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers @@ -2748,6 +2750,8 @@ function Invoke-Scans { [array]$ESC7 = Find-ESC7 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers -SafeUsers $SafeUsers Write-Host 'Identifying HTTP-based certificate enrollment interfaces (ESC8)...' [array]$ESC8 = Find-ESC8 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers + Write-Host 'Identifying AD CS templates with szOID_NTDS_CA_SECURITY_EXT disabled (ESC9)...' + [array]$ESC9 = Find-ESC9 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -ClientAuthEKUs $ClientAuthEkus -Mode $Mode -UnsafeUsers $UnsafeUsers Write-Host 'Identifying Certificate Authorities with IF_ENFORCEENCRYPTICERTREQUEST disabled (ESC11)...' [array]$ESC11 = Find-ESC11 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers Write-Host 'Identifying AD CS templates with dangerous ESC13 configurations...' @@ -2759,11 +2763,11 @@ function Invoke-Scans { } } - [array]$AllIssues = $AuditingIssues + $ESC1 + $ESC2 + $ESC3 + $ESC4 + $ESC5 + $ESC6 + $ESC7 + $ESC8 + $ESC11 + $ESC13 + $ESC15 + $ESC16 + [array]$AllIssues = $AuditingIssues + $ESC1 + $ESC2 + $ESC3 + $ESC4 + $ESC5 + $ESC6 + $ESC7 + $ESC8 + $ESC9 + $ESC11 + $ESC13 + $ESC15 + $ESC16 # If these are all empty = no issues found, exit if ($AllIssues.Count -lt 1) { - Write-Host "`n$(Get-Date) : No ADCS issues were found." -ForegroundColor Green + Write-Host "`n$(Get-Date) : No ADCS issues were found. :)" -ForegroundColor Green break } @@ -2779,6 +2783,7 @@ function Invoke-Scans { ESC6 = $ESC6 ESC7 = $ESC7 ESC8 = $ESC8 + ESC9 = $ESC9 ESC11 = $ESC11 ESC13 = $ESC13 ESC15 = $ESC15 @@ -3370,6 +3375,20 @@ function Set-RiskRating { $RiskScoring += 'HTTP Enrollment: +2' } + # Modifiers that rely on the existence of other ESCs + if ($Issue.Technique -eq 'ESC6') { + [array]$ESC9 = Find-ESC9 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -ClientAuthEKUs $ClientAuthEkus -UnsafeUsers $UnsafeUsers + [array]$ESC16 = Find-ESC16 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers + $ESC9and16 = $ESC9 + $ESC16 + if ($ESC9and16) { + $RiskValue += 2 + $RiskScoring += "Additional risky configurations exist which make this issue more severe: +2" + foreach ($otherIssue in $ESC9and16) { + $RiskScoring += " - $($otherIssue.Technique) exists on $($otherIssue.Name)" + } + } # end if ($ESC9and16) + } + # TODO Check NtAuthCertificates for CA thumbprint. If found, +2, else -1 # TODO Check if NTLMv1 is allowed. } @@ -3397,7 +3416,7 @@ function Set-RiskRating { # ESC1 and ESC4 templates are more dangerous than other templates because they can result in immediate compromise. if ($Issue.Technique -in @('ESC1', 'ESC4')) { $RiskValue += 1 - $RiskScoring += 'ESC1/4: +1' + $RiskScoring += "$($Issue.Technique) +1" } if ($Issue.IdentityReferenceSID -match $UnsafeUsers) { @@ -3411,8 +3430,8 @@ function Set-RiskRating { $RiskScoring += 'Group: +1' } - # Safe users and managed service accounts are inherently safer than other principals - except in ESC3 Condition 2! - if ($Issue.Technique -eq 'ESC3' -and $Issue.Condition -eq 2) { + # Safe users and managed service accounts are inherently safer than other principals - except in ESC3 Condition 2 and ESC9! + if (($Issue.Technique -eq 'ESC9') -or ($Issue.Technique -eq 'ESC3' -and $Issue.Condition -eq 2)) { if ($Issue.IdentityReferenceSID -match $SafeUsers) { # Safe Users are admins. Authenticating as an admin is bad. $RiskValue += 2 @@ -3616,8 +3635,8 @@ function Set-RiskRating { $RiskValue += $OtherTemplateRisk } - # Disabled ESC1, ESC2, ESC3, ESC4, and ESC15 templates are more dangerous if there's an ESC5 on one or more CA objects - if ($Issue.Technique -match 'ESC1|ESC2|ESC3|ESC4' -and $Issue.Enabled -eq $false ) { + # Disabled ESC1, ESC2, ESC3, ESC4, ESC9, and ESC15 templates are more dangerous if there's an ESC5 on one or more CA objects + if ($Issue.Technique -match 'ESC1|ESC2|ESC3|ESC4|ESC9' -and $Issue.Enabled -eq $false ) { $ESC5 = Find-ESC5 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers -DangerousRights $DangerousRights -SafeOwners '-519$' -SafeObjectTypes $SafeObjectTypes -SkipRisk | Where-Object { $_.objectClass -eq 'pKIEnrollmentService' } $ESC5Names = @(($ESC5 | Select-Object -Property Name -Unique).Name) @@ -3690,6 +3709,18 @@ function Set-RiskRating { } } + # ESC9/ESC16 are much more dangerous when ESC6 exists + if ($Issue.Technique -match 'ESC9|ESC16') { + $ESC6 = Find-ESC6 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers -SkipRisk + if ($ESC6) { + $RiskValue += 2 + $RiskScoring += "One or more CAs have an ESC6: +2" + foreach ($ca in $ESC6.CAFullName) { + $RiskScoring += " - ESC6 exists on $ca" + } + } # end if ($ESC6) + } + # Convert Value to Name $RiskName = switch ($RiskValue) { { $_ -le 1 } { @@ -4282,6 +4313,118 @@ Set-Acl -Path `$Path -AclObject `$ACL } # end elseif ($Issue.Issue -match 'GenericAll') } +function Update-ESC9Remediation { + <# + .SYNOPSIS + This function asks the user a set of questions to provide the most appropriate remediation for ESC9 issues. + + .DESCRIPTION + This function takes a single ESC9 issue as input then asks a series of questions to determine the correct + remediation. + + Questions: + 1. Does the identified principal need to enroll in this template? [Yes/No/Unsure] + 2. Is this certificate widely used and/or frequently requested? [Yes/No/Unsure] + + Depending on answers to these questions, the Issue and Fix attributes on the Issue object are updated. + + TODO: More questions: + Should the identified principal be able to request certs that include a SAN or SANs? + + .PARAMETER Issue + A pscustomobject that includes all pertinent information about the ESC1 issue. + + .OUTPUTS + This function updates ESC9 remediations customized to the user's needs. + + .EXAMPLE + $Targets = Get-Target + $ADCSObjects = Get-ADCSObject -Targets $Targets + $SafeUsers = '-512$|-519$|-544$|-18$|-517$|-500$|-516$|-521$|-498$|-9$|-526$|-527$|S-1-5-10' + $ESC9Issues = Find-ESC9 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers + foreach ($issue in $ESC9Issues) { Update-ESC9Remediation -Issue $Issue } + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [object]$Issue + ) + + $Header = "`n[!] ESC9 Issue detected in $($Issue.Name)" + Write-Host $Header -ForegroundColor Yellow + Write-Host $('-' * $Header.Length) -ForegroundColor Yellow + Write-Host @" +The $($Issue.Name) template has the szOID_NTDS_CA_SECURITY_EXT security extension +disabled. Certificates issued from this template will not enforce strong +certificate binding. Manager approval is not required for a certificate to be issued. +To provide the most appropriate remediation for this issue, Locksmith will now +ask you a few questions. +"@ + + $Enroll = '' + do { + $Enroll = Read-Host "`nDoes $($Issue.IdentityReference) need to Enroll in the $($Issue.Name) template? [y/n/unsure]" + } while ( ($Enroll -ne 'y') -and ($Enroll -ne 'n') -and ($Enroll -ne 'unsure')) + + if ($Enroll -eq 'y') { + $Frequent = '' + do { + $Frequent = Read-Host "`nIs the $($Issue.Name) certificate frequently requested? [y/n/unsure]" + } while ( ($Frequent -ne 'y') -and ($Frequent -ne 'n') -and ($Frequent -ne 'unsure')) + + if ($Frequent -ne 'n') { + $Issue.Fix = @" +# Locksmith cannot currently determine the best remediation course. +# Remediation Options: +# 1. If $($Issue.IdentityReference) is a group, remove its Enroll/AutoEnroll rights and grant those rights +# to a smaller group or a single user/service account. + +# 2. Enable Manager Approval +`$Object = '$($_.DistinguishedName)' +Get-ADObject `$Object | Set-ADObject -Replace @{'msPKI-Enrollment-Flag' = 2} + +# 3. Enable the szOID_NTDS_CA_SECURITY_EXT security extension. +TODO +"@ + + $Issue.Revert = @" +# 1. Replace Enroll/AutoEnroll rights from the smaller group/single user/service account and grant those rights +# back to $($Issue.IdentityReference). + +# 2. Disable Manager Approval +`$Object = '$($_.DistinguishedName)' +Get-ADObject `$Object | Set-ADObject -Replace @{'msPKI-Enrollment-Flag' = 0} + +# 3. Disable the szOID_NTDS_CA_SECURITY_EXT security extension. +TODO +"@ + } + } + elseif ($Enroll -eq 'n') { + $Issue.Fix = @" +<# + 1. Open the Certification Templates Console: certtmpl.msc + 2. Double-click the $($Issue.Name) template to open its Properties page. + 3. Select the Security tab. + 4. Select the entry for $($Issue.IdentityReference). + 5. Uncheck the "Enroll" and/or "Autoenroll" boxes. + 6. Click OK. +#> +"@ + + $Issue.Revert = @" +<# + 1. Open the Certification Templates Console: certtmpl.msc + 2. Double-click the $($Issue.Name) template to open its Properties page. + 3. Select the Security tab. + 4. Select the entry for $($Issue.IdentityReference). + 5. Check the "Enroll" and/or "Autoenroll" boxes depending on your specific needs. + 6. Click OK. +#> +"@ + } # end if ($Enroll -eq 'y')/elseif ($Enroll -eq 'n') +} + <# Prerequisites: PowerShell version 2 or above. License: MIT @@ -4645,6 +4788,7 @@ function Invoke-Locksmith { 'ESC6', 'ESC7', 'ESC8', + 'ESC9', 'ESC11', 'ESC13', 'ESC15', @@ -4665,7 +4809,7 @@ function Invoke-Locksmith { [System.Management.Automation.PSCredential]$Credential ) - $Version = '2025.5.18' + $Version = '2025.5.24' $LogoPart1 = @' _ _____ _______ _ _ _______ _______ _____ _______ _ _ | | | | |____/ |______ | | | | | |_____| @@ -4838,6 +4982,7 @@ function Invoke-Locksmith { $ESC6 = $Results['ESC6'] $ESC7 = $Results['ESC7'] $ESC8 = $Results['ESC8'] + $ESC9 = $Results['ESC9'] $ESC11 = $Results['ESC11'] $ESC13 = $Results['ESC13'] $ESC15 = $Results['ESC15'] @@ -4862,6 +5007,7 @@ function Invoke-Locksmith { Format-Result -Issue $ESC6 -Mode 0 Format-Result -Issue $ESC7 -Mode 0 Format-Result -Issue $ESC8 -Mode 0 + Format-Result -Issue $ESC9 -Mode 0 Format-Result -Issue $ESC11 -Mode 0 Format-Result -Issue $ESC13 -Mode 0 Format-Result -Issue $ESC15 -Mode 0 @@ -4893,6 +5039,7 @@ Invoke-Locksmith -Mode 1 Format-Result -Issue $ESC6 -Mode 1 Format-Result -Issue $ESC7 -Mode 1 Format-Result -Issue $ESC8 -Mode 1 + Format-Result -Issue $ESC9 -Mode 1 Format-Result -Issue $ESC11 -Mode 1 Format-Result -Issue $ESC13 -Mode 1 Format-Result -Issue $ESC15 -Mode 1 diff --git a/Locksmith.psd1 b/Locksmith.psd1 index 1514545..bdf4476 100644 --- a/Locksmith.psd1 +++ b/Locksmith.psd1 @@ -8,7 +8,7 @@ FunctionsToExport = 'Invoke-Locksmith' GUID = 'b1325b42-8dc4-4f17-aa1f-dcb5984ca14a' HelpInfoURI = 'https://raw.githubusercontent.com/jakehildreth/Locksmith/main/en-US/' - ModuleVersion = '2025.5.18' + ModuleVersion = '2025.5.24' PowerShellVersion = '5.1' PrivateData = @{ PSData = @{ diff --git a/Private/Find-ESC6.ps1 b/Private/Find-ESC6.ps1 index a0ed1b7..906a6e6 100644 --- a/Private/Find-ESC6.ps1 +++ b/Private/Find-ESC6.ps1 @@ -37,6 +37,7 @@ $Issue = [pscustomobject]@{ Forest = $_.CanonicalName.split('/')[0] Name = $_.Name + CAFullname = $CAFullName DistinguishedName = $_.DistinguishedName Issue = $_.SANFlag Fix = 'N/A' diff --git a/Private/Find-ESC9.ps1 b/Private/Find-ESC9.ps1 index 354e85e..2d3c2b1 100644 --- a/Private/Find-ESC9.ps1 +++ b/Private/Find-ESC9.ps1 @@ -1,116 +1,111 @@ -<# - This is a working POC. I need to test both checks and possibly blend pieces of them. - Then I need to fold this function into the Locksmith workflow. -#> - -function Find-ESC9 { +function Find-ESC9 { <# .SYNOPSIS - Checks for ESC9 (No Security Extension) Vulnerability + This function finds Active Directory Certificate Services (AD CS) objects that have the ESC9 vulnerability. .DESCRIPTION - This function checks for certificate templates that contain the flag CT_FLAG_NO_SECURITY_EXTENSION (0x80000), - which will likely make them vulnerable to ESC9. Another factor to check for ESC9 is the registry values on AD - domain controllers that can help harden certificate based authentication for Kerberos and SChannel. - - .NOTES - An ESC9 condition exists when: - - - the new msPKI-Enrollment-Flag value on a certificate contains the flag CT_FLAG_NO_SECURITY_EXTENSION (0x80000) - - AND an insecure registry value is set on domain controllers: + The script takes an array of ADCS objects as input and filters them based on the specified conditions. + For each matching object, it creates a custom object with properties representing various information about + the object, such as Forest, Name, DistinguishedName, IdentityReference, ActiveDirectoryRights, Issue, Fix, Revert, and Technique. - - the StrongCertificateBindingEnforcement registry value for Kerberos is not set to 2 (the default is 1) on domain controllers - at HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Kdc - - OR the CertificateMappingMethods registry value for SCHANNEL contains the UPN flag on domain controllers at - HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\SecurityProviders\Schannel + .PARAMETER ADCSObjects + Specifies the array of ADCS objects to be processed. This parameter is mandatory. - When the CT_FLAG_NO_SECURITY_EXTENSION (0x80000) flag is set on a certificate template, the new szOID_NTDS_CA_SECURITY_EXT - security extension will not be embedded in issued certificates. This security extension was added by Microsoft's - patch KB5014754 ("Certificate-based authentication changes on Windows domain controllers") on May 10, 2022. + .PARAMETER SafeUsers + Specifies the list of SIDs of safe users who are allowed to have specific rights on the objects. This parameter is mandatory. - The patch applies to all servers that run Active Directory Certificate Services and Windows domain controllers that - service certificate-based authentication. - https://support.microsoft.com/en-us/topic/kb5014754-certificate-based-authentication-changes-on-windows-domain-controllers-ad2c23b0-15d8-4340-a468-4d4f3b188f16 + .PARAMETER UnsafeUsers + Specifies the list of SIDs of safe users who should never have specific rights on the objects. This parameter is mandatory. - Based on research from - https://research.ifcr.dk/certipy-4-0-esc9-esc10-bloodhound-gui-new-authentication-and-request-methods-and-more-7237d88061f7, - https://support.microsoft.com/en-us/topic/kb5014754-certificate-based-authentication-changes-on-windows-domain-controllers-ad2c23b0-15d8-4340-a468-4d4f3b188f16, - and on a very long conversation with Bing Chat. + .PARAMETER ClientAuthEKUs + A list of EKUs that can be used for client authentication. - Additional notes from Cortana -- Bing when I pressed her to tell me whether both conditions were required for ESC9 or only one of them: - A certificate template can still be vulnerable to ESC9 even if the msPKI-Enrollment-Flag does not include - CT_FLAG_NO_SECURITY_EXTENSION. This is because the vulnerability primarily arises from the ability of a - requester to specify the subjectAltName in a Certificate Signing Request (CSR). If a requester can specify - the subjectAltName in a CSR, they can request a certificate as anyone, including a domain admin user. - Therefore, if a certificate template allows requesters to specify a subjectAltName and - StrongCertificateBindingEnforcement is not set to 2, it could potentially be vulnerable to ESC9. However, - the presence of CT_FLAG_NO_SECURITY_EXTENSION in msPKI-Enrollment-Flag is a clear indicator of a template - being vulnerable to ESC9. -#> + .OUTPUTS + The script outputs an array of custom objects representing the matching ADCS objects and their associated information. + .EXAMPLE + $Targets = Get-Target + $ADCSObjects = Get-ADCSObject -Targets $Targets + $SafeUsers = '-512$|-519$|-544$|-18$|-517$|-500$|-516$|-521$|-498$|-9$|-526$|-527$|S-1-5-10' + $ClientAuthEKUs = '1\.3\.6\.1\.5\.5\.7\.3\.2|1\.3\.6\.1\.5\.2\.3\.4|1\.3\.6\.1\.4\.1\.311\.20\.2\.2|2\.5\.29\.37\.0' + $Results = Find-ESC1 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -ClientAuthEKUs $ClientAuthEKUs + $Results + #> [CmdletBinding()] param( [Parameter(Mandatory)] [Microsoft.ActiveDirectory.Management.ADEntity[]]$ADCSObjects, [Parameter(Mandatory)] + [string]$SafeUsers, + [Parameter(Mandatory)] + $ClientAuthEKUs, + [int]$Mode = 0, + [Parameter(Mandatory)] [string]$UnsafeUsers, [switch]$SkipRisk ) - - # Import the required module - Import-Module ActiveDirectory - - # Get the configuration naming context - $configNC = (Get-ADRootDSE).configurationNamingContext - - # Define the path to the Certificate Templates container - $path = "CN=Certificate Templates,CN=Public Key Services,CN=Services,$configNC" - - # Get all certificate templates - $templates = Get-ADObject -Filter * -SearchBase $path -Properties msPKI-Enrollment-Flag, msPKI-Certificate-Name-Flag - - foreach ($template in $templates) { - # Check if msPKI-Enrollment-Flag contains the CT_FLAG_NO_SECURITY_EXTENSION (0x80000) flag - if ($template.'msPKI-Enrollment-Flag' -band 0x80000) { - # Check if msPKI-Certificate-Name-Flag contains the CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT_ALT_NAME (0x2) flag - if ($template.'msPKI-Certificate-Name-Flag' -band 0x2) { - # Output the template name - Write-Output "Template Name: $($template.Name), Vulnerable to ESC9" + $ADCSObjects | Where-Object { + ($_.objectClass -eq 'pKICertificateTemplate') -and + ($_.pkiExtendedKeyUsage -match $ClientAuthEKUs) -and + ($_.'msPKI-Enrollment-Flag' -band 0x80000) + } | ForEach-Object { + foreach ($entry in $_.nTSecurityDescriptor.Access) { + $Principal = New-Object System.Security.Principal.NTAccount($entry.IdentityReference) + if ($Principal -match '^(S-1|O:)') { + $SID = $Principal + } else { + $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value + } + if ( ($SID -notmatch $SafeUsers) -and ( ($entry.ActiveDirectoryRights -match 'ExtendedRight') -or ($entry.ActiveDirectoryRights -match 'GenericAll') ) ) { + $Issue = [pscustomobject]@{ + Forest = $_.CanonicalName.split('/')[0] + Name = $_.Name + DistinguishedName = $_.DistinguishedName + IdentityReference = $entry.IdentityReference + IdentityReferenceSID = $SID + ActiveDirectoryRights = $entry.ActiveDirectoryRights + Enabled = $_.Enabled + EnabledOn = $_.EnabledOn + Issue = @" +This Client Authentication template has the szOID_NTDS_CA_SECURITY_EXT security +extension disabled. Certificates issued from this template will not enforce +strong certificate binding. Depending on the current Certificate Binding +Enforcement level ESC6 status, it may be possible to request and receive +certificates that rely on weak (aka attacker-controllable) binding methods. + +An attacker can abuse this weakness by: +1. Getting access to a user or computer account. +2. Modifying the user's userPrincipalName attribute (or the computer's dNSHostName attribute) to match a higher-privileged account. +3. Requesting a client authentication certificate from a template that w/ szOID_NTDS_CA_SECURITY_EXT disabled. +4. Using the client authentication certificiate to authenticate as the higher-privileged account. +5. Profiting. + +More info: + - ESC9 description: https://github.com/ly4k/Certipy/wiki/06-%E2%80%90-Privilege-Escalation#esc9-no-security-extension-on-certificate-template + - Strong Mapping/Enforcement Mode: https://support.microsoft.com/en-us/topic/kb5014754-certificate-based-authentication-changes-on-windows-domain-controllers-ad2c23b0-15d8-4340-a468-4d4f3b188f16 + +"@ + Fix = @" +# Enable Manager Approval +`$Object = '$($_.DistinguishedName)' +Get-ADObject `$Object | Set-ADObject -Replace @{'msPKI-Enrollment-Flag' = 2} +"@ + Revert = @" +# Disable Manager Approval +`$Object = '$($_.DistinguishedName)' +Get-ADObject `$Object | Set-ADObject -Replace @{'msPKI-Enrollment-Flag' = 0} +"@ + Technique = 'ESC9' + } + + if ( $Mode -in @(1, 3, 4) ) { + Update-ESC9Remediation -Issue $Issue + } + if ($SkipRisk -eq $false) { + Set-RiskRating -ADCSObjects $ADCSObjects -Issue $Issue -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers + } + $Issue } } } - - # AND / OR / ALSO - - Import-Module ActiveDirectory - - $templates = Get-ADObject -Filter { ObjectClass -eq 'pKICertificateTemplate' } -Properties * - foreach ($template in $templates) { - $name = $template.Name - - $subjectNameFlag = $template.'msPKI-Cert-Template-OID' - $subjectType = $template.'msPKI-Certificate-Application-Policy' - $enrollmentFlag = $template.'msPKI-Enrollment-Flag' - $certificateNameFlag = $template.'msPKI-Certificate-Name-Flag' - - # Check if the template is vulnerable to ESC9 - if ($subjectNameFlag -eq 'Supply in the request' -and - ($subjectType -eq 'User' -or $subjectType -eq 'Computer') -and - # 0x200 means a certificate needs to include a template name certificate extension - # 0x220 instructs the client to perform auto-enrollment for the specified template - ($enrollmentFlag -eq 0x200 -or $enrollmentFlag -eq 0x220) -and - # 0x2 instructs the client to supply subject information in the certificate request (CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT). - # This means that any user who is allowed to enroll in a certificate with this setting can request a certificate as any - # user in the network, including a privileged user. - # 0x3 instructs the client to supply both the subject and subject alternate name information in the certificate request - ($certificateNameFlag -eq 0x2 -or $certificateNameFlag -eq 0x3)) { - - # Print the template name and the vulnerability - Write-Output "$name is vulnerable to ESC9" - } else { - # Print the template name and the status - Write-Output "$name is not vulnerable to ESC9" - } - } - } diff --git a/Private/Format-Result.ps1 b/Private/Format-Result.ps1 index 4b2565a..65e321d 100644 --- a/Private/Format-Result.ps1 +++ b/Private/Format-Result.ps1 @@ -40,10 +40,11 @@ function Format-Result { ESC6 = 'ESC6 - EDITF_ATTRIBUTESUBJECTALTNAME2 Flag Enabled' ESC7 = 'ESC7 - Non-standard PKI Admins' ESC8 = 'ESC8 - HTTP/S Enrollment Enabled' + ESC9 = 'ESC9 - szOID_NTDS_CA_SECURITY_EXT Extension Disabled on Template' ESC11 = 'ESC11 - IF_ENFORCEENCRYPTICERTREQUEST Flag Disabled' ESC13 = 'ESC13 - Vulnerable Certificate Template - Group-Linked' 'ESC15/EKUwu' = 'ESC15 - Vulnerable Certificate Template - Schema V1' - ESC16 = 'ESC16 - szOID_NTDS_CA_SECURITY_EXT Extension Disabled' + ESC16 = 'ESC16 - szOID_NTDS_CA_SECURITY_EXT Extension Disabled on CA' } $RiskTable = @{ @@ -72,7 +73,7 @@ function Format-Result { Format-Table Technique, @{l = 'CA Name'; e = { $_.Name } }, @{l = 'Risk'; e = { $_.RiskName } }, Issue -Wrap | Write-HostColorized -PatternColorMap $RiskTable -CaseSensitive } - { $_ -in @('ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC13', 'ESC15/EKUwu') } { + { $_ -in @('ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC9', 'ESC13', 'ESC15/EKUwu') } { $Issue | Format-Table Technique, @{l = 'Template Name'; e = { $_.Name } }, @{l = 'Risk'; e = { $_.RiskName } }, Enabled, Issue -Wrap | Write-HostColorized -PatternColorMap $RiskTable -CaseSensitive @@ -90,7 +91,7 @@ function Format-Result { Format-List Technique, @{l = 'CA Name'; e = { $_.Name } }, @{l = 'Risk'; e = { $_.RiskName } }, DistinguishedName, Issue, Fix, @{l = 'Risk Score'; e = { $_.RiskValue } }, @{l = 'Risk Score Detail'; e = { $_.RiskScoring -join "`n" } } | Write-HostColorized -PatternColorMap $RiskTable -CaseSensitive } - { $_ -in @('ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC13', 'ESC15/EKUwu') } { + { $_ -in @('ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC9', 'ESC13', 'ESC15/EKUwu') } { $Issue | Format-List Technique, @{l = 'Template Name'; e = { $_.Name } }, @{l = 'Risk'; e = { $_.RiskName } }, DistinguishedName, Enabled, EnabledOn, Issue, Fix, @{l = 'Risk Score'; e = { $_.RiskValue } }, @{l = 'Risk Score Detail'; e = { $_.RiskScoring -join "`n" } } | Write-HostColorized -PatternColorMap $RiskTable -CaseSensitive diff --git a/Private/Invoke-Scans.ps1 b/Private/Invoke-Scans.ps1 index 55d37e7..bd0aa26 100644 --- a/Private/Invoke-Scans.ps1 +++ b/Private/Invoke-Scans.ps1 @@ -5,12 +5,12 @@ function Invoke-Scans { .PARAMETER Scans Specifies the type of scans to perform. Multiple scan options can be provided as an array. The default value is 'All'. - The available scan options are: 'Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC8', 'ESC11', + The available scan options are: 'Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC8', 'ESC9', 'ESC11', 'ESC13', 'ESC15, 'EKUwu', 'ESC16', 'All', 'PromptMe'. .NOTES - The script requires the following functions to be defined: Find-AuditingIssue, Find-ESC1, Find-ESC2, Find-ESC3C1, - Find-ESC3C2, Find-ESC4, Find-ESC5, Find-ESC6, Find-ESC8, Find-ESC11, Find-ESC13, Find-ESC15, Find-ESC16 + Find-ESC3C2, Find-ESC4, Find-ESC5, Find-ESC6, Find-ESC8, Find-ESC9, Find-ESC11, Find-ESC13, Find-ESC15, Find-ESC16 - The script uses Out-GridView or Out-ConsoleGridView for interactive selection when the 'PromptMe' scan option is chosen. - The script returns a hash table containing the results of the scans. @@ -48,7 +48,7 @@ function Invoke-Scans { [string]$SafeUsers, [Parameter(Mandatory)] [string]$SafeOwners, - [ValidateSet('Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC7', 'ESC8', 'ESC11', 'ESC13', 'ESC15', 'EKUwu', 'ESC16', 'All', 'PromptMe')] + [ValidateSet('Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC7', 'ESC8', 'ESC9', 'ESC11', 'ESC13', 'ESC15', 'EKUwu', 'ESC16', 'All', 'PromptMe')] [array]$Scans = 'All', [Parameter(Mandatory)] [string]$UnsafeUsers, @@ -109,6 +109,10 @@ function Invoke-Scans { Write-Host 'Identifying HTTP-based certificate enrollment interfaces (ESC8)...' [array]$ESC8 = Find-ESC8 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers } + ESC9 { + Write-Host 'Identifying AD CS templates with szOID_NTDS_CA_SECURITY_EXT disabled (ESC9)...' + [array]$ESC9 = Find-ESC9 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -ClientAuthEKUs $ClientAuthEkus -Mode $Mode -UnsafeUsers $UnsafeUsers + } ESC11 { Write-Host 'Identifying Issuing CAs with IF_ENFORCEENCRYPTICERTREQUEST disabled (ESC11)...' [array]$ESC11 = Find-ESC11 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers @@ -149,6 +153,8 @@ function Invoke-Scans { [array]$ESC7 = Find-ESC7 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers -SafeUsers $SafeUsers Write-Host 'Identifying HTTP-based certificate enrollment interfaces (ESC8)...' [array]$ESC8 = Find-ESC8 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers + Write-Host 'Identifying AD CS templates with szOID_NTDS_CA_SECURITY_EXT disabled (ESC9)...' + [array]$ESC9 = Find-ESC9 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -ClientAuthEKUs $ClientAuthEkus -Mode $Mode -UnsafeUsers $UnsafeUsers Write-Host 'Identifying Certificate Authorities with IF_ENFORCEENCRYPTICERTREQUEST disabled (ESC11)...' [array]$ESC11 = Find-ESC11 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers Write-Host 'Identifying AD CS templates with dangerous ESC13 configurations...' @@ -160,11 +166,11 @@ function Invoke-Scans { } } - [array]$AllIssues = $AuditingIssues + $ESC1 + $ESC2 + $ESC3 + $ESC4 + $ESC5 + $ESC6 + $ESC7 + $ESC8 + $ESC11 + $ESC13 + $ESC15 + $ESC16 + [array]$AllIssues = $AuditingIssues + $ESC1 + $ESC2 + $ESC3 + $ESC4 + $ESC5 + $ESC6 + $ESC7 + $ESC8 + $ESC9 + $ESC11 + $ESC13 + $ESC15 + $ESC16 # If these are all empty = no issues found, exit if ($AllIssues.Count -lt 1) { - Write-Host "`n$(Get-Date) : No ADCS issues were found." -ForegroundColor Green + Write-Host "`n$(Get-Date) : No ADCS issues were found. :)" -ForegroundColor Green break } @@ -180,6 +186,7 @@ function Invoke-Scans { ESC6 = $ESC6 ESC7 = $ESC7 ESC8 = $ESC8 + ESC9 = $ESC9 ESC11 = $ESC11 ESC13 = $ESC13 ESC15 = $ESC15 diff --git a/Private/Set-RiskRating.ps1 b/Private/Set-RiskRating.ps1 index e83ff78..2ed8edf 100644 --- a/Private/Set-RiskRating.ps1 +++ b/Private/Set-RiskRating.ps1 @@ -64,6 +64,20 @@ function Set-RiskRating { $RiskScoring += 'HTTP Enrollment: +2' } + # Modifiers that rely on the existence of other ESCs + if ($Issue.Technique -eq 'ESC6') { + [array]$ESC9 = Find-ESC9 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -ClientAuthEKUs $ClientAuthEkus -UnsafeUsers $UnsafeUsers + [array]$ESC16 = Find-ESC16 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers + $ESC9and16 = $ESC9 + $ESC16 + if ($ESC9and16) { + $RiskValue += 2 + $RiskScoring += "Additional risky configurations exist which make this issue more severe: +2" + foreach($otherIssue in $ESC9and16) { + $RiskScoring += " - $($otherIssue.Technique) exists on $($otherIssue.Name)" + } + } # end if ($ESC9and16) + } + # TODO Check NtAuthCertificates for CA thumbprint. If found, +2, else -1 # TODO Check if NTLMv1 is allowed. } @@ -90,7 +104,7 @@ function Set-RiskRating { # ESC1 and ESC4 templates are more dangerous than other templates because they can result in immediate compromise. if ($Issue.Technique -in @('ESC1', 'ESC4')) { $RiskValue += 1 - $RiskScoring += 'ESC1/4: +1' + $RiskScoring += "$($Issue.Technique) +1" } if ($Issue.IdentityReferenceSID -match $UnsafeUsers) { @@ -103,8 +117,8 @@ function Set-RiskRating { $RiskScoring += 'Group: +1' } - # Safe users and managed service accounts are inherently safer than other principals - except in ESC3 Condition 2! - if ($Issue.Technique -eq 'ESC3' -and $Issue.Condition -eq 2) { + # Safe users and managed service accounts are inherently safer than other principals - except in ESC3 Condition 2 and ESC9! + if (($Issue.Technique -eq 'ESC9') -or ($Issue.Technique -eq 'ESC3' -and $Issue.Condition -eq 2)) { if ($Issue.IdentityReferenceSID -match $SafeUsers) { # Safe Users are admins. Authenticating as an admin is bad. $RiskValue += 2 @@ -294,8 +308,8 @@ function Set-RiskRating { $RiskValue += $OtherTemplateRisk } - # Disabled ESC1, ESC2, ESC3, ESC4, and ESC15 templates are more dangerous if there's an ESC5 on one or more CA objects - if ($Issue.Technique -match 'ESC1|ESC2|ESC3|ESC4' -and $Issue.Enabled -eq $false ) { + # Disabled ESC1, ESC2, ESC3, ESC4, ESC9, and ESC15 templates are more dangerous if there's an ESC5 on one or more CA objects + if ($Issue.Technique -match 'ESC1|ESC2|ESC3|ESC4|ESC9' -and $Issue.Enabled -eq $false ) { $ESC5 = Find-ESC5 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers -DangerousRights $DangerousRights -SafeOwners '-519$' -SafeObjectTypes $SafeObjectTypes -SkipRisk | Where-Object { $_.objectClass -eq 'pKIEnrollmentService' } $ESC5Names = @(($ESC5 | Select-Object -Property Name -Unique).Name) @@ -356,6 +370,18 @@ function Set-RiskRating { } } + # ESC9/ESC16 are much more dangerous when ESC6 exists + if ($Issue.Technique -match 'ESC9|ESC16') { + $ESC6 = Find-ESC6 -ADCSObjects $ADCSObjects -UnsafeUsers $UnsafeUsers -SkipRisk + if ($ESC6) { + $RiskValue += 2 + $RiskScoring += "One or more CAs have an ESC6: +2" + foreach($ca in $ESC6.CAFullName) { + $RiskScoring += " - ESC6 exists on $ca" + } + } # end if ($ESC6) + } + # Convert Value to Name $RiskName = switch ($RiskValue) { { $_ -le 1 } { 'Informational' } diff --git a/Private/Update-ESC9Remediation.ps1 b/Private/Update-ESC9Remediation.ps1 new file mode 100644 index 0000000..4324413 --- /dev/null +++ b/Private/Update-ESC9Remediation.ps1 @@ -0,0 +1,110 @@ +function Update-ESC9Remediation { + <# + .SYNOPSIS + This function asks the user a set of questions to provide the most appropriate remediation for ESC9 issues. + + .DESCRIPTION + This function takes a single ESC9 issue as input then asks a series of questions to determine the correct + remediation. + + Questions: + 1. Does the identified principal need to enroll in this template? [Yes/No/Unsure] + 2. Is this certificate widely used and/or frequently requested? [Yes/No/Unsure] + + Depending on answers to these questions, the Issue and Fix attributes on the Issue object are updated. + + TODO: More questions: + Should the identified principal be able to request certs that include a SAN or SANs? + + .PARAMETER Issue + A pscustomobject that includes all pertinent information about the ESC1 issue. + + .OUTPUTS + This function updates ESC9 remediations customized to the user's needs. + + .EXAMPLE + $Targets = Get-Target + $ADCSObjects = Get-ADCSObject -Targets $Targets + $SafeUsers = '-512$|-519$|-544$|-18$|-517$|-500$|-516$|-521$|-498$|-9$|-526$|-527$|S-1-5-10' + $ESC9Issues = Find-ESC9 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers + foreach ($issue in $ESC9Issues) { Update-ESC9Remediation -Issue $Issue } + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [object]$Issue + ) + + $Header = "`n[!] ESC9 Issue detected in $($Issue.Name)" + Write-Host $Header -ForegroundColor Yellow + Write-Host $('-' * $Header.Length) -ForegroundColor Yellow + Write-Host @" +The $($Issue.Name) template has the szOID_NTDS_CA_SECURITY_EXT security extension +disabled. Certificates issued from this template will not enforce strong +certificate binding. Manager approval is not required for a certificate to be issued. +To provide the most appropriate remediation for this issue, Locksmith will now +ask you a few questions. +"@ + + $Enroll = '' + do { + $Enroll = Read-Host "`nDoes $($Issue.IdentityReference) need to Enroll in the $($Issue.Name) template? [y/n/unsure]" + } while ( ($Enroll -ne 'y') -and ($Enroll -ne 'n') -and ($Enroll -ne 'unsure')) + + if ($Enroll -eq 'y') { + $Frequent = '' + do { + $Frequent = Read-Host "`nIs the $($Issue.Name) certificate frequently requested? [y/n/unsure]" + } while ( ($Frequent -ne 'y') -and ($Frequent -ne 'n') -and ($Frequent -ne 'unsure')) + + if ($Frequent -ne 'n') { + $Issue.Fix = @" +# Locksmith cannot currently determine the best remediation course. +# Remediation Options: +# 1. If $($Issue.IdentityReference) is a group, remove its Enroll/AutoEnroll rights and grant those rights +# to a smaller group or a single user/service account. + +# 2. Enable Manager Approval +`$Object = '$($_.DistinguishedName)' +Get-ADObject `$Object | Set-ADObject -Replace @{'msPKI-Enrollment-Flag' = 2} + +# 3. Enable the szOID_NTDS_CA_SECURITY_EXT security extension. +TODO +"@ + + $Issue.Revert = @" +# 1. Replace Enroll/AutoEnroll rights from the smaller group/single user/service account and grant those rights +# back to $($Issue.IdentityReference). + +# 2. Disable Manager Approval +`$Object = '$($_.DistinguishedName)' +Get-ADObject `$Object | Set-ADObject -Replace @{'msPKI-Enrollment-Flag' = 0} + +# 3. Disable the szOID_NTDS_CA_SECURITY_EXT security extension. +TODO +"@ + } + } elseif ($Enroll -eq 'n') { + $Issue.Fix = @" +<# + 1. Open the Certification Templates Console: certtmpl.msc + 2. Double-click the $($Issue.Name) template to open its Properties page. + 3. Select the Security tab. + 4. Select the entry for $($Issue.IdentityReference). + 5. Uncheck the "Enroll" and/or "Autoenroll" boxes. + 6. Click OK. +#> +"@ + + $Issue.Revert = @" +<# + 1. Open the Certification Templates Console: certtmpl.msc + 2. Double-click the $($Issue.Name) template to open its Properties page. + 3. Select the Security tab. + 4. Select the entry for $($Issue.IdentityReference). + 5. Check the "Enroll" and/or "Autoenroll" boxes depending on your specific needs. + 6. Click OK. +#> +"@ + } # end if ($Enroll -eq 'y')/elseif ($Enroll -eq 'n') +} diff --git a/Public/Invoke-Locksmith.ps1 b/Public/Invoke-Locksmith.ps1 index 5ba780e..92cc1de 100644 --- a/Public/Invoke-Locksmith.ps1 +++ b/Public/Invoke-Locksmith.ps1 @@ -95,6 +95,7 @@ function Invoke-Locksmith { 'ESC6', 'ESC7', 'ESC8', + 'ESC9', 'ESC11', 'ESC13', 'ESC15', @@ -285,6 +286,7 @@ function Invoke-Locksmith { $ESC6 = $Results['ESC6'] $ESC7 = $Results['ESC7'] $ESC8 = $Results['ESC8'] + $ESC9 = $Results['ESC9'] $ESC11 = $Results['ESC11'] $ESC13 = $Results['ESC13'] $ESC15 = $Results['ESC15'] @@ -309,6 +311,7 @@ function Invoke-Locksmith { Format-Result -Issue $ESC6 -Mode 0 Format-Result -Issue $ESC7 -Mode 0 Format-Result -Issue $ESC8 -Mode 0 + Format-Result -Issue $ESC9 -Mode 0 Format-Result -Issue $ESC11 -Mode 0 Format-Result -Issue $ESC13 -Mode 0 Format-Result -Issue $ESC15 -Mode 0 @@ -340,6 +343,7 @@ Invoke-Locksmith -Mode 1 Format-Result -Issue $ESC6 -Mode 1 Format-Result -Issue $ESC7 -Mode 1 Format-Result -Issue $ESC8 -Mode 1 + Format-Result -Issue $ESC9 -Mode 1 Format-Result -Issue $ESC11 -Mode 1 Format-Result -Issue $ESC13 -Mode 1 Format-Result -Issue $ESC15 -Mode 1 From efec13b00d2fc923f7019484520e945f3d2d769e Mon Sep 17 00:00:00 2001 From: Jake Hildreth Date: Sun, 25 May 2025 09:17:36 -0400 Subject: [PATCH 09/11] Risk and remediation updates. 1. All "remediation updaters" now highlight the question being asked. 2. Complete rewrite of ESC7 to bring inline with all other principal-based issues. 3. Improved risk scoring for ESC1, ESC6, ESC7, ESC9, and ESC16 4. ESC4 and ESC7 now properly mark risk when principal is an admin/manager. --- Invoke-Locksmith.ps1 | 290 ++++++++++++++++++++++------- Locksmith.psd1 | 2 +- Private/Find-ESC1.ps1 | 8 +- Private/Find-ESC4.ps1 | 8 +- Private/Find-ESC7.ps1 | 110 +++++------ Private/Set-RiskRating.ps1 | 49 +++-- Private/Update-ESC1Remediation.ps1 | 4 +- Private/Update-ESC4Remediation.ps1 | 18 +- Private/Update-ESC7Remediation.ps1 | 95 ++++++++++ Private/Update-ESC9Remediation.ps1 | 4 +- 10 files changed, 438 insertions(+), 150 deletions(-) create mode 100644 Private/Update-ESC7Remediation.ps1 diff --git a/Invoke-Locksmith.ps1 b/Invoke-Locksmith.ps1 index 73c1048..7bc5de2 100644 --- a/Invoke-Locksmith.ps1 +++ b/Invoke-Locksmith.ps1 @@ -299,12 +299,14 @@ Get-ADObject `$Object | Set-ADObject -Replace @{'msPKI-Enrollment-Flag' = 0} Technique = 'ESC1' } - if ( $Mode -in @(1, 3, 4) ) { - Update-ESC1Remediation -Issue $Issue - } if ($SkipRisk -eq $false) { Set-RiskRating -ADCSObjects $ADCSObjects -Issue $Issue -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers } + + if ( $Mode -in @(1, 3, 4) ) { + Update-ESC1Remediation -Issue $Issue + } + $Issue } } @@ -1144,12 +1146,14 @@ Set-Acl -Path 'AD:$($_.DistinguishedName)' -AclObject `$ACL Technique = 'ESC4' } - if ( $Mode -in @(1, 3, 4) ) { - Update-ESC4Remediation -Issue $Issue - } if ($SkipRisk -eq $false) { Set-RiskRating -ADCSObjects $ADCSObjects -Issue $Issue -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers } + + if ( $Mode -in @(1, 3, 4) ) { + Update-ESC4Remediation -Issue $Issue + } + $Issue } } @@ -1569,74 +1573,80 @@ function Find-ESC7 { [switch]$SkipRisk ) process { - $ADCSObjects | Where-Object { - ($_.objectClass -eq 'pKIEnrollmentService') -and $_.CAHostDistinguishedName -and - ( ($_.CAAdministrator) -or ($_.CertificateManager) ) + Write-Output $ADCSObjects -PipelineVariable object | Where-Object { + ($object.objectClass -eq 'pKIEnrollmentService') -and $object.CAHostDistinguishedName -and + ( ($object.CAAdministrator) -or ($object.CertificateManager) ) } | ForEach-Object { - $UnsafeCAAdministrators = Write-Output $_.CAAdministrator -PipelineVariable admin | ForEach-Object { + Write-Output $object.CAAdministrator -PipelineVariable admin | ForEach-Object { $SID = Convert-IdentityReferenceToSid -Object $admin if ($SID -notmatch $SafeUsers) { - $admin - } - } - $UnsafeCertificateManagers = Write-Output $_.CertificateManager -PipelineVariable manager | ForEach-Object { - $SID = Convert-IdentityReferenceToSid -Object $manager - if ($SID -notmatch $SafeUsers) { - $manager - } - } - if ($UnsafeCAAdministrators -or $UnsafeCertificateManagers) { - $Issue = [pscustomobject]@{ - Forest = $_.CanonicalName.split('/')[0] - Name = $_.Name - DistinguishedName = $_.DistinguishedName - CAAdministrator = $_.CAAdministrator - CertificateManager = $_.CertificateManager - Issue = $null - Fix = $null - Revert = $null - Technique = 'ESC7' - } - if ($UnsafeCAAdministrators) { - $Issue.Issue = $Issue.Issue + @" -Unexpected principals are granted "CA Administrator" rights on this Certification Authority. -Unsafe CA Administrators: $($UnsafeCAAdministrators -join ', '). + $Issue = [pscustomobject]@{ + Forest = $object.CanonicalName.split('/')[0] + Name = $object.Name + DistinguishedName = $object.DistinguishedName + IdentityReference = $admin + IdentityReferenceSID = $SID + Right = 'CA Administrator' + Issue = @" +$admin has been granted CA Administrator rights on this Certification Authority (CA). + +$admin has full control over this CA. -"@ - $Issue.Fix = $Issue.Fix + @" -Revoke CA Administrator rights from $($UnsafeCAAdministrators -join ', ') - -"@ - $Issue.Revert = $Issue.Revert + @" -Reinstate CA Administrator rights for $($UnsafeCAAdministrators -join ', ') +More info: + - https://posts.specterops.io/certified-pre-owned-d95910965cd2 "@ - } - if ($UnsafeCertificateManagers) { - $Issue.Issue = $Issue.Issue + @" -expected principals are granted "Certificate Manager" rights on this Certification Authority. -Unexpected Principals: $($UnsafeCertificateManagers -join ', ') + Fix = "Revoke CA Administrator rights from ${admin}." + Revert = "Restore CA Administrator rights to ${admin}." + Technique = 'ESC7' + } -"@ - $Issue.Fix = $Issue.Fix + @" -Revoke Certificate Manager rights from $($UnsafeCertificateManagers -join ', ') + if ($SkipRisk -eq $false) { + Set-RiskRating -ADCSObjects $ADCSObjects -Issue $Issue -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers + } -"@ - $Issue.Revert = $Issue.Revert + @" -Reinstate Certificate Manager rights for $($UnsafeCertificateManagers -join ', ') + if ( $Mode -in @(1, 3, 4) ) { + Update-ESC7Remediation -Issue $Issue + } -"@ - } - if ($SkipRisk -eq $false) { - Set-RiskRating -ADCSObjects $ADCSObjects -Issue $Issue -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers + $Issue } - $Issue.Issue = $Issue.Issue + @" + } + + Write-Output $object.CertificateManager -PipelineVariable admin | ForEach-Object { + $SID = Convert-IdentityReferenceToSid -Object $admin + if ($SID -notmatch $SafeUsers) { + $Issue = [pscustomobject]@{ + Forest = $object.CanonicalName.split('/')[0] + Name = $object.Name + DistinguishedName = $object.DistinguishedName + IdentityReference = $admin + IdentityReferenceSID = $SID + Right = 'Certificate Manager' + Issue = @" +$admin has been granted Certificate Manager rights on this Certification Authority (CA). + +$admin can approve pending certificate requests on this CA. More info: - https://posts.specterops.io/certified-pre-owned-d95910965cd2 "@ - $Issue + Fix = "Revoke Certificate Manager rights from ${admin}." + Revert = "Restore Certificate Manager rights to ${admin}." + Technique = 'ESC7' + } + + if ($SkipRisk -eq $false) { + Set-RiskRating -ADCSObjects $ADCSObjects -Issue $Issue -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers + } + + if ( $Mode -in @(1, 3, 4) ) { + Update-ESC7Remediation -Issue $Issue + } + + $Issue + } } } } @@ -3375,6 +3385,29 @@ function Set-RiskRating { $RiskScoring += 'HTTP Enrollment: +2' } + if ($Issue.Technique -eq 'ESC7') { + # If an Issue can be tied to a principal, the principal's objectClass impacts the Issue's risk + $SID = $Issue.IdentityReferenceSID.ToString() + $IdentityReferenceObjectClass = Get-ADObject -Filter { objectSid -eq $SID } | Select-Object objectClass + + if ($Issue.IdentityReferenceSID -match $UnsafeUsers) { + # Authenticated Users, Domain Users, Domain Computers etc. are very risky + $RiskValue += 2 + $RiskScoring += 'Very Large Group: +2' + } + elseif ($IdentityReferenceObjectClass -eq 'group') { + # Groups are riskier than individual principals + $RiskValue += 1 + $RiskScoring += 'Group: +1' + } + elseif ($Issue.IdentityReferenceSID -notmatch $UnsafeUsers -and + $Issue.IdentityReferenceSID -notmatch $SafeUsers -and + $IdentityReferenceObjectClass -notlike '*ManagedServiceAccount') { + $RiskValue += 1 + $RiskScoring += 'Unprivileged Principal: +1' + } + } + # Modifiers that rely on the existence of other ESCs if ($Issue.Technique -eq 'ESC6') { [array]$ESC9 = Find-ESC9 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -ClientAuthEKUs $ClientAuthEkus -UnsafeUsers $UnsafeUsers @@ -3409,16 +3442,17 @@ function Set-RiskRating { } } - # The principal's objectClass impacts the Issue's risk - $SID = $Issue.IdentityReferenceSID.ToString() - $IdentityReferenceObjectClass = Get-ADObject -Filter { objectSid -eq $SID } | Select-Object objectClass - # ESC1 and ESC4 templates are more dangerous than other templates because they can result in immediate compromise. if ($Issue.Technique -in @('ESC1', 'ESC4')) { $RiskValue += 1 $RiskScoring += "$($Issue.Technique) +1" } + # If an Issue can be tied to a principal, the principal's objectClass impacts the Issue's risk + $SID = $Issue.IdentityReferenceSID.ToString() + $IdentityReferenceObjectClass = Get-ADObject -Filter { objectSid -eq $SID } | Select-Object objectClass + + if ($Issue.IdentityReferenceSID -match $UnsafeUsers) { # Authenticated Users, Domain Users, Domain Computers etc. are very risky $RiskValue += 2 @@ -3429,6 +3463,12 @@ function Set-RiskRating { $RiskValue += 1 $RiskScoring += 'Group: +1' } + elseif ($Issue.IdentityReferenceSID -notmatch $UnsafeUsers -and + $Issue.IdentityReferenceSID -notmatch $SafeUsers -and + $IdentityReferenceObjectClass -notlike '*ManagedServiceAccount') { + $RiskValue += 1 + $RiskScoring += 'Unprivileged Principal: +1' + } # Safe users and managed service accounts are inherently safer than other principals - except in ESC3 Condition 2 and ESC9! if (($Issue.Technique -eq 'ESC9') -or ($Issue.Technique -eq 'ESC3' -and $Issue.Condition -eq 2)) { @@ -4090,13 +4130,13 @@ function Update-ESC1Remediation { $Enroll = '' do { - $Enroll = Read-Host "`nDoes $($Issue.IdentityReference) need to Enroll in the $($Issue.Name) template? [y/n/unsure]" + $Enroll = Read-Host "`n[?] Does $($Issue.IdentityReference) need to Enroll in the $($Issue.Name) template? [y/n/unsure]" } while ( ($Enroll -ne 'y') -and ($Enroll -ne 'n') -and ($Enroll -ne 'unsure')) if ($Enroll -eq 'y') { $Frequent = '' do { - $Frequent = Read-Host "`nIs the $($Issue.Name) certificate frequently requested? [y/n/unsure]" + $Frequent = Read-Host "`n[?] Is the $($Issue.Name) certificate frequently requested? [y/n/unsure]" } while ( ($Frequent -ne 'y') -and ($Frequent -ne 'n') -and ($Frequent -ne 'unsure')) if ($Frequent -ne 'n') { @@ -4162,8 +4202,10 @@ function Update-ESC4Remediation { .DESCRIPTION This function takes a single ESC4 issue as input. It then prompts the user if the principal with the ESC4 rights administers the template in question. - If the principal is an admin of the template, the Issue attribute is updated to indicate this configuration is - expected, and the Fix attribute for the issue is updated to indicate no remediation is needed. + If the principal is an admin of the template: + - the Issue attribute is updated to indicate this configuration is expected + - the Fix attribute for the issue is updated to indicate no remediation is needed + - the Risk Value, Risk Name, and Risk Scoring Details are updated to indicate no risk If the the principal is not an admin of the template AND the rights assigned is GenericAll, Locksmith will ask if Enroll or AutoEnroll rights are needed. Depending on the answers to the listed questions, the Fix attribute is updated accordingly. @@ -4198,12 +4240,20 @@ function Update-ESC4Remediation { $Admin = '' do { - $Admin = Read-Host "`nDoes $($Issue.IdentityReference) administer and/or maintain the $($Issue.Name) template? [y/n]" + $Admin = Read-Host "`n[?] Does $($Issue.IdentityReference) administer and/or maintain the $($Issue.Name) template? [y/n]" } while ( ($Admin -ne 'y') -and ($Admin -ne 'n') ) if ($Admin -eq 'y') { - $Issue.Issue = "$($Issue.IdentityReference) has $($Issue.ActiveDirectoryRights) rights on this template, but this is expected." + $Issue.Issue = @" +$($Issue.IdentityReference) has $($Issue.ActiveDirectoryRights) rights on this template, but this is expected. + +More info: + - https://posts.specterops.io/certified-pre-owned-d95910965cd2 +"@ $Issue.Fix = "No immediate remediation required." + $Issue | Add-Member -NotePropertyName RiskValue -NotePropertyValue 0 -Force + $Issue | Add-Member -NotePropertyName RiskName -NotePropertyValue 'Informational' -Force + $Issue | Add-Member -NotePropertyName RiskScoring -NotePropertyValue "$($Issue.IdentityReference) administers this template" -Force } elseif ($Issue.Issue -match 'GenericAll') { $RightsToRestore = 0 @@ -4313,6 +4363,102 @@ Set-Acl -Path `$Path -AclObject `$ACL } # end elseif ($Issue.Issue -match 'GenericAll') } +function Update-ESC7Remediation { + <# + .SYNOPSIS + This function asks the user a set of questions to provide the most appropriate remediation for ESC7 issues. + + .DESCRIPTION + This function takes a single ESC7 issue as input. It then prompts the user if the principal with the ESC7 rights + administers the Certification Authority (CA) in question. + If the principal is an admin of the CA: + - the Issue attribute is updated to indicate this configuration is expected + - the Fix attribute for the issue is updated to indicate no remediation is needed + - the Risk Value and Risk Scoring are set to + If the the principal is not an admin of the CA, + Depending on the answers to the listed questions, the Fix attribute is updated accordingly. + + .PARAMETER Issue + A pscustomobject that includes all pertinent information about the ESC7 issue. + + .OUTPUTS + This function updates ESC7 remediations customized to the user's needs. + + .EXAMPLE + $Targets = Get-Target + $ADCSObjects = Get-ADCSObject -Targets $Targets + $DangerousRights = @('GenericAll', 'WriteProperty', 'WriteOwner', 'WriteDacl') + $SafeOwners = '-512$|-519$|-544$|-18$|-517$|-500$' + $SafeUsers = '-512$|-519$|-544$|-18$|-517$|-500$|-516$|-521$|-498$|-9$|-526$|-527$|S-1-5-10' + $SafeObjectTypes = '0e10c968-78fb-11d2-90d4-00c04f79dc55|a05b8cc2-17bc-4802-a710-e7c15ab866a2' + $ESC7Issues = Find-ESC7 -ADCSObjects $ADCSObjects -DangerousRights $DangerousRights -SafeOwners $SafeOwners -SafeUsers $SafeUsers -SafeObjectTypes $SafeObjectTypes -Mode 1 + foreach ($issue in $ESC7Issues) { Update-ESC7Remediation -Issue $Issue } + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [object]$Issue + ) + + if ($Issue.Right -eq 'CA Administrator') { + $Header = "`n[!] ESC7 Issue detected on $($Issue.Name)" + Write-Host $Header -ForegroundColor Yellow + Write-Host $('-' * $Header.Length) -ForegroundColor Yellow + Write-Host "$($Issue.IdentityReference) has CA Administrator rights on this Certification Authority (CA).`n" + Write-Host 'To provide the most appropriate remediation for this issue, Locksmith will now ask you a few questions.' + + $Admin = '' + do { + $Admin = Read-Host "`n[?] Does $($Issue.IdentityReference) administer and/or maintain the $($Issue.Name) CA? [y/n]" + } while ( ($Admin -ne 'y') -and ($Admin -ne 'n') ) + + if ($Admin -eq 'y') { + $Issue.Issue = @" +$($Issue.IdentityReference) has CA Administrator rights on this CA, but this is expected. + +Note: +These rights grant $($Issue.IdentityReference) control of the forest. +This principal should be considered a Tier 0/control plane object and protected as such. + +More info: + - https://posts.specterops.io/certified-pre-owned-d95910965cd2 + +"@ + $Issue.Fix = "No immediate remediation required." + $Issue | Add-Member -NotePropertyName RiskValue -NotePropertyValue 0 -Force + $Issue | Add-Member -NotePropertyName RiskName -NotePropertyValue 'Informational' -Force + $Issue | Add-Member -NotePropertyName RiskScoring -NotePropertyValue "$($Issue.IdentityReference) administers this CA" -Force + } + } + + if ($Issue.Right -eq 'Certificate Manager') { + $Header = "`n[!] ESC7 Issue detected on $($Issue.Name)" + Write-Host $Header -ForegroundColor Yellow + Write-Host $('-' * $Header.Length) -ForegroundColor Yellow + Write-Host "$($Issue.IdentityReference) has Certificate Manager rights on this Certification Authority (CA).`n" + Write-Host 'To provide the most appropriate remediation for this issue, Locksmith will now ask you a few questions.' + + $Admin = '' + do { + $Admin = Read-Host "`n[?] Does $($Issue.IdentityReference) need to approve pending certificate requests on the $($Issue.Name) CA? [y/n]" + } while ( ($Admin -ne 'y') -and ($Admin -ne 'n') ) + + if ($Admin -eq 'y') { + $Issue.Issue = @" +$($Issue.IdentityReference) has Certificate Manager rights on this CA, but this is expected. + +More info: + - https://posts.specterops.io/certified-pre-owned-d95910965cd2 + +"@ + $Issue.Fix = "No immediate remediation required." + $Issue | Add-Member -NotePropertyName RiskValue -NotePropertyValue 0 -Force + $Issue | Add-Member -NotePropertyName RiskName -NotePropertyValue 'Informational' -Force + $Issue | Add-Member -NotePropertyName RiskScoring -NotePropertyValue "$($Issue.IdentityReference) approves pending certificate requests on this CA" -Force + } + } +} + function Update-ESC9Remediation { <# .SYNOPSIS @@ -4363,13 +4509,13 @@ ask you a few questions. $Enroll = '' do { - $Enroll = Read-Host "`nDoes $($Issue.IdentityReference) need to Enroll in the $($Issue.Name) template? [y/n/unsure]" + $Enroll = Read-Host "`n[?] Does $($Issue.IdentityReference) need to Enroll in the $($Issue.Name) template? [y/n/unsure]" } while ( ($Enroll -ne 'y') -and ($Enroll -ne 'n') -and ($Enroll -ne 'unsure')) if ($Enroll -eq 'y') { $Frequent = '' do { - $Frequent = Read-Host "`nIs the $($Issue.Name) certificate frequently requested? [y/n/unsure]" + $Frequent = Read-Host "`n[?] Is the $($Issue.Name) certificate frequently requested? [y/n/unsure]" } while ( ($Frequent -ne 'y') -and ($Frequent -ne 'n') -and ($Frequent -ne 'unsure')) if ($Frequent -ne 'n') { @@ -4809,7 +4955,7 @@ function Invoke-Locksmith { [System.Management.Automation.PSCredential]$Credential ) - $Version = '2025.5.24' + $Version = '2025.5.25' $LogoPart1 = @' _ _____ _______ _ _ _______ _______ _____ _______ _ _ | | | | |____/ |______ | | | | | |_____| diff --git a/Locksmith.psd1 b/Locksmith.psd1 index bdf4476..6836c9d 100644 --- a/Locksmith.psd1 +++ b/Locksmith.psd1 @@ -8,7 +8,7 @@ FunctionsToExport = 'Invoke-Locksmith' GUID = 'b1325b42-8dc4-4f17-aa1f-dcb5984ca14a' HelpInfoURI = 'https://raw.githubusercontent.com/jakehildreth/Locksmith/main/en-US/' - ModuleVersion = '2025.5.24' + ModuleVersion = '2025.5.25' PowerShellVersion = '5.1' PrivateData = @{ PSData = @{ diff --git a/Private/Find-ESC1.ps1 b/Private/Find-ESC1.ps1 index d54c1c9..abb6fd0 100644 --- a/Private/Find-ESC1.ps1 +++ b/Private/Find-ESC1.ps1 @@ -92,12 +92,14 @@ Get-ADObject `$Object | Set-ADObject -Replace @{'msPKI-Enrollment-Flag' = 0} Technique = 'ESC1' } - if ( $Mode -in @(1, 3, 4) ) { - Update-ESC1Remediation -Issue $Issue - } if ($SkipRisk -eq $false) { Set-RiskRating -ADCSObjects $ADCSObjects -Issue $Issue -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers } + + if ( $Mode -in @(1, 3, 4) ) { + Update-ESC1Remediation -Issue $Issue + } + $Issue } } diff --git a/Private/Find-ESC4.ps1 b/Private/Find-ESC4.ps1 index 0aff874..0d13347 100644 --- a/Private/Find-ESC4.ps1 +++ b/Private/Find-ESC4.ps1 @@ -173,12 +173,14 @@ Set-Acl -Path 'AD:$($_.DistinguishedName)' -AclObject `$ACL Technique = 'ESC4' } - if ( $Mode -in @(1, 3, 4) ) { - Update-ESC4Remediation -Issue $Issue - } if ($SkipRisk -eq $false) { Set-RiskRating -ADCSObjects $ADCSObjects -Issue $Issue -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers } + + if ( $Mode -in @(1, 3, 4) ) { + Update-ESC4Remediation -Issue $Issue + } + $Issue } } diff --git a/Private/Find-ESC7.ps1 b/Private/Find-ESC7.ps1 index dee7cf2..2976147 100644 --- a/Private/Find-ESC7.ps1 +++ b/Private/Find-ESC7.ps1 @@ -34,74 +34,80 @@ [switch]$SkipRisk ) process { - $ADCSObjects | Where-Object { - ($_.objectClass -eq 'pKIEnrollmentService') -and $_.CAHostDistinguishedName -and - ( ($_.CAAdministrator) -or ($_.CertificateManager) ) + Write-Output $ADCSObjects -PipelineVariable object | Where-Object { + ($object.objectClass -eq 'pKIEnrollmentService') -and $object.CAHostDistinguishedName -and + ( ($object.CAAdministrator) -or ($object.CertificateManager) ) } | ForEach-Object { - $UnsafeCAAdministrators = Write-Output $_.CAAdministrator -PipelineVariable admin | ForEach-Object { + Write-Output $object.CAAdministrator -PipelineVariable admin | ForEach-Object { $SID = Convert-IdentityReferenceToSid -Object $admin if ($SID -notmatch $SafeUsers) { - $admin - } - } - $UnsafeCertificateManagers = Write-Output $_.CertificateManager -PipelineVariable manager | ForEach-Object { - $SID = Convert-IdentityReferenceToSid -Object $manager - if ($SID -notmatch $SafeUsers) { - $manager - } - } - if ($UnsafeCAAdministrators -or $UnsafeCertificateManagers) { - $Issue = [pscustomobject]@{ - Forest = $_.CanonicalName.split('/')[0] - Name = $_.Name - DistinguishedName = $_.DistinguishedName - CAAdministrator = $_.CAAdministrator - CertificateManager = $_.CertificateManager - Issue = $null - Fix = $null - Revert = $null - Technique = 'ESC7' - } - if ($UnsafeCAAdministrators) { - $Issue.Issue = $Issue.Issue + @" -Unexpected principals are granted "CA Administrator" rights on this Certification Authority. -Unsafe CA Administrators: $($UnsafeCAAdministrators -join ', '). + $Issue = [pscustomobject]@{ + Forest = $object.CanonicalName.split('/')[0] + Name = $object.Name + DistinguishedName = $object.DistinguishedName + IdentityReference = $admin + IdentityReferenceSID = $SID + Right = 'CA Administrator' + Issue = @" +$admin has been granted CA Administrator rights on this Certification Authority (CA). -"@ - $Issue.Fix = $Issue.Fix + @" -Revoke CA Administrator rights from $($UnsafeCAAdministrators -join ', ') +$admin has full control over this CA. -"@ - $Issue.Revert = $Issue.Revert + @" -Reinstate CA Administrator rights for $($UnsafeCAAdministrators -join ', ') +More info: + - https://posts.specterops.io/certified-pre-owned-d95910965cd2 "@ - } - if ($UnsafeCertificateManagers) { - $Issue.Issue = $Issue.Issue + @" -expected principals are granted "Certificate Manager" rights on this Certification Authority. -Unexpected Principals: $($UnsafeCertificateManagers -join ', ') + Fix = "Revoke CA Administrator rights from ${admin}." + Revert = "Restore CA Administrator rights to ${admin}." + Technique = 'ESC7' + } -"@ - $Issue.Fix = $Issue.Fix + @" -Revoke Certificate Manager rights from $($UnsafeCertificateManagers -join ', ') + if ($SkipRisk -eq $false) { + Set-RiskRating -ADCSObjects $ADCSObjects -Issue $Issue -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers + } -"@ - $Issue.Revert = $Issue.Revert + @" -Reinstate Certificate Manager rights for $($UnsafeCertificateManagers -join ', ') + if ( $Mode -in @(1, 3, 4) ) { + Update-ESC7Remediation -Issue $Issue + } -"@ - } - if ($SkipRisk -eq $false) { - Set-RiskRating -ADCSObjects $ADCSObjects -Issue $Issue -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers + $Issue } - $Issue.Issue = $Issue.Issue + @" + } + + Write-Output $object.CertificateManager -PipelineVariable admin | ForEach-Object { + $SID = Convert-IdentityReferenceToSid -Object $admin + if ($SID -notmatch $SafeUsers) { + $Issue = [pscustomobject]@{ + Forest = $object.CanonicalName.split('/')[0] + Name = $object.Name + DistinguishedName = $object.DistinguishedName + IdentityReference = $admin + IdentityReferenceSID = $SID + Right = 'Certificate Manager' + Issue = @" +$admin has been granted Certificate Manager rights on this Certification Authority (CA). + +$admin can approve pending certificate requests on this CA. More info: - https://posts.specterops.io/certified-pre-owned-d95910965cd2 "@ - $Issue + Fix = "Revoke Certificate Manager rights from ${admin}." + Revert = "Restore Certificate Manager rights to ${admin}." + Technique = 'ESC7' + } + + if ($SkipRisk -eq $false) { + Set-RiskRating -ADCSObjects $ADCSObjects -Issue $Issue -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers + } + + if ( $Mode -in @(1, 3, 4) ) { + Update-ESC7Remediation -Issue $Issue + } + + $Issue + } } } } diff --git a/Private/Set-RiskRating.ps1 b/Private/Set-RiskRating.ps1 index 2ed8edf..c5e7d54 100644 --- a/Private/Set-RiskRating.ps1 +++ b/Private/Set-RiskRating.ps1 @@ -64,6 +64,27 @@ function Set-RiskRating { $RiskScoring += 'HTTP Enrollment: +2' } + if ($Issue.Technique -eq 'ESC7') { + # If an Issue can be tied to a principal, the principal's objectClass impacts the Issue's risk + $SID = $Issue.IdentityReferenceSID.ToString() + $IdentityReferenceObjectClass = Get-ADObject -Filter { objectSid -eq $SID } | Select-Object objectClass + + if ($Issue.IdentityReferenceSID -match $UnsafeUsers) { + # Authenticated Users, Domain Users, Domain Computers etc. are very risky + $RiskValue += 2 + $RiskScoring += 'Very Large Group: +2' + } elseif ($IdentityReferenceObjectClass -eq 'group') { + # Groups are riskier than individual principals + $RiskValue += 1 + $RiskScoring += 'Group: +1' + } elseif ($Issue.IdentityReferenceSID -notmatch $UnsafeUsers -and + $Issue.IdentityReferenceSID -notmatch $SafeUsers -and + $IdentityReferenceObjectClass -notlike '*ManagedServiceAccount') { + $RiskValue += 1 + $RiskScoring += 'Unprivileged Principal: +1' + } + } + # Modifiers that rely on the existence of other ESCs if ($Issue.Technique -eq 'ESC6') { [array]$ESC9 = Find-ESC9 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -ClientAuthEKUs $ClientAuthEkus -UnsafeUsers $UnsafeUsers @@ -72,7 +93,7 @@ function Set-RiskRating { if ($ESC9and16) { $RiskValue += 2 $RiskScoring += "Additional risky configurations exist which make this issue more severe: +2" - foreach($otherIssue in $ESC9and16) { + foreach ($otherIssue in $ESC9and16) { $RiskScoring += " - $($otherIssue.Technique) exists on $($otherIssue.Name)" } } # end if ($ESC9and16) @@ -97,16 +118,17 @@ function Set-RiskRating { } } - # The principal's objectClass impacts the Issue's risk - $SID = $Issue.IdentityReferenceSID.ToString() - $IdentityReferenceObjectClass = Get-ADObject -Filter { objectSid -eq $SID } | Select-Object objectClass - # ESC1 and ESC4 templates are more dangerous than other templates because they can result in immediate compromise. if ($Issue.Technique -in @('ESC1', 'ESC4')) { $RiskValue += 1 $RiskScoring += "$($Issue.Technique) +1" } + # If an Issue can be tied to a principal, the principal's objectClass impacts the Issue's risk + $SID = $Issue.IdentityReferenceSID.ToString() + $IdentityReferenceObjectClass = Get-ADObject -Filter { objectSid -eq $SID } | Select-Object objectClass + + if ($Issue.IdentityReferenceSID -match $UnsafeUsers) { # Authenticated Users, Domain Users, Domain Computers etc. are very risky $RiskValue += 2 @@ -115,6 +137,11 @@ function Set-RiskRating { # Groups are riskier than individual principals $RiskValue += 1 $RiskScoring += 'Group: +1' + } elseif ($Issue.IdentityReferenceSID -notmatch $UnsafeUsers -and + $Issue.IdentityReferenceSID -notmatch $SafeUsers -and + $IdentityReferenceObjectClass -notlike '*ManagedServiceAccount') { + $RiskValue += 1 + $RiskScoring += 'Unprivileged Principal: +1' } # Safe users and managed service accounts are inherently safer than other principals - except in ESC3 Condition 2 and ESC9! @@ -144,7 +171,7 @@ function Set-RiskRating { foreach ($name in $ESC3C2Names) { $OtherTemplateRisk = 0 $Principals = @() - foreach ($esc in $($ESC3C2 | Where-Object Name -eq $name) ) { + foreach ($esc in $($ESC3C2 | Where-Object Name -EQ $name) ) { if ($CheckedESC3C2Templates.GetEnumerator().Name -contains $esc.Name) { $Principals = $CheckedESC3C2Templates.$($esc.Name) } else { @@ -184,13 +211,13 @@ function Set-RiskRating { # Default 'User' and 'Machine' templates are more dangerous $ESC15 = Find-ESC15 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -UnsafeUsers $UnsafeUsers -SkipRisk | Where-Object { $_.Enabled -eq $true } - $ESC15Names = @(($ESC15 | Where-Object Name -in @('Machine', 'User')).Name) + $ESC15Names = @(($ESC15 | Where-Object Name -In @('Machine', 'User')).Name) if ($ESC15Names) { $CheckedESC15Templates = @{} foreach ($name in $ESC15Names) { $OtherTemplateRisk = 0 $Principals = @() - foreach ($esc in $($ESC15 | Where-Object Name -eq $name) ) { + foreach ($esc in $($ESC15 | Where-Object Name -EQ $name) ) { if ($CheckedESC15Templates.GetEnumerator().Name -contains $esc.Name) { $Principals = $CheckedESC15Templates.$($esc.Name) } else { @@ -242,7 +269,7 @@ function Set-RiskRating { foreach ($name in $ESC2Names) { $OtherTemplateRisk = 0 $Principals = @() - foreach ($esc in $($ESC2 | Where-Object Name -eq $name) ) { + foreach ($esc in $($ESC2 | Where-Object Name -EQ $name) ) { if ($CheckedESC2Templates.GetEnumerator().Name -contains $esc.Name) { $Principals = $CheckedESC2Templates.$($esc.Name) } else { @@ -278,7 +305,7 @@ function Set-RiskRating { foreach ($name in $ESC3C1Names) { $OtherTemplateRisk = 0 $Principals = @() - foreach ($esc in $($ESC3C1 | Where-Object Name -eq $name) ) { + foreach ($esc in $($ESC3C1 | Where-Object Name -EQ $name) ) { if ($CheckedESC3C1Templates.GetEnumerator().Name -contains $esc.Name) { $Principals = $CheckedESC3C1Templates.$($esc.Name) } else { @@ -376,7 +403,7 @@ function Set-RiskRating { if ($ESC6) { $RiskValue += 2 $RiskScoring += "One or more CAs have an ESC6: +2" - foreach($ca in $ESC6.CAFullName) { + foreach ($ca in $ESC6.CAFullName) { $RiskScoring += " - ESC6 exists on $ca" } } # end if ($ESC6) diff --git a/Private/Update-ESC1Remediation.ps1 b/Private/Update-ESC1Remediation.ps1 index ca90228..1c11002 100644 --- a/Private/Update-ESC1Remediation.ps1 +++ b/Private/Update-ESC1Remediation.ps1 @@ -44,13 +44,13 @@ function Update-ESC1Remediation { $Enroll = '' do { - $Enroll = Read-Host "`nDoes $($Issue.IdentityReference) need to Enroll in the $($Issue.Name) template? [y/n/unsure]" + $Enroll = Read-Host "`n[?] Does $($Issue.IdentityReference) need to Enroll in the $($Issue.Name) template? [y/n/unsure]" } while ( ($Enroll -ne 'y') -and ($Enroll -ne 'n') -and ($Enroll -ne 'unsure')) if ($Enroll -eq 'y') { $Frequent = '' do { - $Frequent = Read-Host "`nIs the $($Issue.Name) certificate frequently requested? [y/n/unsure]" + $Frequent = Read-Host "`n[?] Is the $($Issue.Name) certificate frequently requested? [y/n/unsure]" } while ( ($Frequent -ne 'y') -and ($Frequent -ne 'n') -and ($Frequent -ne 'unsure')) if ($Frequent -ne 'n') { diff --git a/Private/Update-ESC4Remediation.ps1 b/Private/Update-ESC4Remediation.ps1 index d4262e1..0759dc5 100644 --- a/Private/Update-ESC4Remediation.ps1 +++ b/Private/Update-ESC4Remediation.ps1 @@ -6,8 +6,10 @@ function Update-ESC4Remediation { .DESCRIPTION This function takes a single ESC4 issue as input. It then prompts the user if the principal with the ESC4 rights administers the template in question. - If the principal is an admin of the template, the Issue attribute is updated to indicate this configuration is - expected, and the Fix attribute for the issue is updated to indicate no remediation is needed. + If the principal is an admin of the template: + - the Issue attribute is updated to indicate this configuration is expected + - the Fix attribute for the issue is updated to indicate no remediation is needed + - the Risk Value, Risk Name, and Risk Scoring Details are updated to indicate no risk If the the principal is not an admin of the template AND the rights assigned is GenericAll, Locksmith will ask if Enroll or AutoEnroll rights are needed. Depending on the answers to the listed questions, the Fix attribute is updated accordingly. @@ -42,12 +44,20 @@ function Update-ESC4Remediation { $Admin = '' do { - $Admin = Read-Host "`nDoes $($Issue.IdentityReference) administer and/or maintain the $($Issue.Name) template? [y/n]" + $Admin = Read-Host "`n[?] Does $($Issue.IdentityReference) administer and/or maintain the $($Issue.Name) template? [y/n]" } while ( ($Admin -ne 'y') -and ($Admin -ne 'n') ) if ($Admin -eq 'y') { - $Issue.Issue = "$($Issue.IdentityReference) has $($Issue.ActiveDirectoryRights) rights on this template, but this is expected." + $Issue.Issue = @" +$($Issue.IdentityReference) has $($Issue.ActiveDirectoryRights) rights on this template, but this is expected. + +More info: + - https://posts.specterops.io/certified-pre-owned-d95910965cd2 +"@ $Issue.Fix = "No immediate remediation required." + $Issue | Add-Member -NotePropertyName RiskValue -NotePropertyValue 0 -Force + $Issue | Add-Member -NotePropertyName RiskName -NotePropertyValue 'Informational' -Force + $Issue | Add-Member -NotePropertyName RiskScoring -NotePropertyValue "$($Issue.IdentityReference) administers this template" -Force } elseif ($Issue.Issue -match 'GenericAll') { $RightsToRestore = 0 while ($RightsToRestore -notin 1..5) { diff --git a/Private/Update-ESC7Remediation.ps1 b/Private/Update-ESC7Remediation.ps1 new file mode 100644 index 0000000..550445f --- /dev/null +++ b/Private/Update-ESC7Remediation.ps1 @@ -0,0 +1,95 @@ +function Update-ESC7Remediation { + <# + .SYNOPSIS + This function asks the user a set of questions to provide the most appropriate remediation for ESC7 issues. + + .DESCRIPTION + This function takes a single ESC7 issue as input. It then prompts the user if the principal with the ESC7 rights + administers the Certification Authority (CA) in question. + If the principal is an admin of the CA: + - the Issue attribute is updated to indicate this configuration is expected + - the Fix attribute for the issue is updated to indicate no remediation is needed + - the Risk Value and Risk Scoring are set to + If the the principal is not an admin of the CA, + Depending on the answers to the listed questions, the Fix attribute is updated accordingly. + + .PARAMETER Issue + A pscustomobject that includes all pertinent information about the ESC7 issue. + + .OUTPUTS + This function updates ESC7 remediations customized to the user's needs. + + .EXAMPLE + $Targets = Get-Target + $ADCSObjects = Get-ADCSObject -Targets $Targets + $DangerousRights = @('GenericAll', 'WriteProperty', 'WriteOwner', 'WriteDacl') + $SafeOwners = '-512$|-519$|-544$|-18$|-517$|-500$' + $SafeUsers = '-512$|-519$|-544$|-18$|-517$|-500$|-516$|-521$|-498$|-9$|-526$|-527$|S-1-5-10' + $SafeObjectTypes = '0e10c968-78fb-11d2-90d4-00c04f79dc55|a05b8cc2-17bc-4802-a710-e7c15ab866a2' + $ESC7Issues = Find-ESC7 -ADCSObjects $ADCSObjects -DangerousRights $DangerousRights -SafeOwners $SafeOwners -SafeUsers $SafeUsers -SafeObjectTypes $SafeObjectTypes -Mode 1 + foreach ($issue in $ESC7Issues) { Update-ESC7Remediation -Issue $Issue } + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [object]$Issue + ) + + if ($Issue.Right -eq 'CA Administrator') { + $Header = "`n[!] ESC7 Issue detected on $($Issue.Name)" + Write-Host $Header -ForegroundColor Yellow + Write-Host $('-' * $Header.Length) -ForegroundColor Yellow + Write-Host "$($Issue.IdentityReference) has CA Administrator rights on this Certification Authority (CA).`n" + Write-Host 'To provide the most appropriate remediation for this issue, Locksmith will now ask you a few questions.' + + $Admin = '' + do { + $Admin = Read-Host "`n[?] Does $($Issue.IdentityReference) administer and/or maintain the $($Issue.Name) CA? [y/n]" + } while ( ($Admin -ne 'y') -and ($Admin -ne 'n') ) + + if ($Admin -eq 'y') { + $Issue.Issue = @" +$($Issue.IdentityReference) has CA Administrator rights on this CA, but this is expected. + +Note: +These rights grant $($Issue.IdentityReference) control of the forest. +This principal should be considered a Tier 0/control plane object and protected as such. + +More info: + - https://posts.specterops.io/certified-pre-owned-d95910965cd2 + +"@ + $Issue.Fix = "No immediate remediation required." + $Issue | Add-Member -NotePropertyName RiskValue -NotePropertyValue 0 -Force + $Issue | Add-Member -NotePropertyName RiskName -NotePropertyValue 'Informational' -Force + $Issue | Add-Member -NotePropertyName RiskScoring -NotePropertyValue "$($Issue.IdentityReference) administers this CA" -Force + } + } + + if ($Issue.Right -eq 'Certificate Manager') { + $Header = "`n[!] ESC7 Issue detected on $($Issue.Name)" + Write-Host $Header -ForegroundColor Yellow + Write-Host $('-' * $Header.Length) -ForegroundColor Yellow + Write-Host "$($Issue.IdentityReference) has Certificate Manager rights on this Certification Authority (CA).`n" + Write-Host 'To provide the most appropriate remediation for this issue, Locksmith will now ask you a few questions.' + + $Admin = '' + do { + $Admin = Read-Host "`n[?] Does $($Issue.IdentityReference) need to approve pending certificate requests on the $($Issue.Name) CA? [y/n]" + } while ( ($Admin -ne 'y') -and ($Admin -ne 'n') ) + + if ($Admin -eq 'y') { + $Issue.Issue = @" +$($Issue.IdentityReference) has Certificate Manager rights on this CA, but this is expected. + +More info: + - https://posts.specterops.io/certified-pre-owned-d95910965cd2 + +"@ + $Issue.Fix = "No immediate remediation required." + $Issue | Add-Member -NotePropertyName RiskValue -NotePropertyValue 0 -Force + $Issue | Add-Member -NotePropertyName RiskName -NotePropertyValue 'Informational' -Force + $Issue | Add-Member -NotePropertyName RiskScoring -NotePropertyValue "$($Issue.IdentityReference) approves pending certificate requests on this CA" -Force + } + } +} diff --git a/Private/Update-ESC9Remediation.ps1 b/Private/Update-ESC9Remediation.ps1 index 4324413..6cd16c7 100644 --- a/Private/Update-ESC9Remediation.ps1 +++ b/Private/Update-ESC9Remediation.ps1 @@ -48,13 +48,13 @@ ask you a few questions. $Enroll = '' do { - $Enroll = Read-Host "`nDoes $($Issue.IdentityReference) need to Enroll in the $($Issue.Name) template? [y/n/unsure]" + $Enroll = Read-Host "`n[?] Does $($Issue.IdentityReference) need to Enroll in the $($Issue.Name) template? [y/n/unsure]" } while ( ($Enroll -ne 'y') -and ($Enroll -ne 'n') -and ($Enroll -ne 'unsure')) if ($Enroll -eq 'y') { $Frequent = '' do { - $Frequent = Read-Host "`nIs the $($Issue.Name) certificate frequently requested? [y/n/unsure]" + $Frequent = Read-Host "`n[?] Is the $($Issue.Name) certificate frequently requested? [y/n/unsure]" } while ( ($Frequent -ne 'y') -and ($Frequent -ne 'n') -and ($Frequent -ne 'unsure')) if ($Frequent -ne 'n') { From 4e40e54ad580d8141a453bd5f711191864e1f608 Mon Sep 17 00:00:00 2001 From: Jake Hildreth Date: Sun, 25 May 2025 09:38:19 -0400 Subject: [PATCH 10/11] fixed small grammar error --- Invoke-Locksmith.ps1 | 2 +- Private/Find-ESC3C1.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Invoke-Locksmith.ps1 b/Invoke-Locksmith.ps1 index 7bc5de2..92a82d8 100644 --- a/Invoke-Locksmith.ps1 +++ b/Invoke-Locksmith.ps1 @@ -847,7 +847,7 @@ function Find-ESC3C1 { $($entry.IdentityReference) can use this template to request an Enrollment Agent certificate without Manager Approval. -The resulting certificate can be used to enroll in any template that requires +The resulting certificate can be used to enroll in any template that allows an Enrollment Agent to submit the request. More info: diff --git a/Private/Find-ESC3C1.ps1 b/Private/Find-ESC3C1.ps1 index 514fcc2..633120a 100644 --- a/Private/Find-ESC3C1.ps1 +++ b/Private/Find-ESC3C1.ps1 @@ -60,7 +60,7 @@ $($entry.IdentityReference) can use this template to request an Enrollment Agent certificate without Manager Approval. -The resulting certificate can be used to enroll in any template that requires +The resulting certificate can be used to enroll in any template that allows an Enrollment Agent to submit the request. More info: From fec7548d6a8a0c29b6a7b179251686886834154c Mon Sep 17 00:00:00 2001 From: Jake Hildreth Date: Mon, 26 May 2025 07:28:19 -0400 Subject: [PATCH 11/11] Fresh Build in preparation for release. --- Invoke-Locksmith.ps1 | 2 +- Locksmith.psd1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Invoke-Locksmith.ps1 b/Invoke-Locksmith.ps1 index 92a82d8..dbd8022 100644 --- a/Invoke-Locksmith.ps1 +++ b/Invoke-Locksmith.ps1 @@ -4955,7 +4955,7 @@ function Invoke-Locksmith { [System.Management.Automation.PSCredential]$Credential ) - $Version = '2025.5.25' + $Version = '2025.5.26' $LogoPart1 = @' _ _____ _______ _ _ _______ _______ _____ _______ _ _ | | | | |____/ |______ | | | | | |_____| diff --git a/Locksmith.psd1 b/Locksmith.psd1 index 6836c9d..b8ae79d 100644 --- a/Locksmith.psd1 +++ b/Locksmith.psd1 @@ -8,7 +8,7 @@ FunctionsToExport = 'Invoke-Locksmith' GUID = 'b1325b42-8dc4-4f17-aa1f-dcb5984ca14a' HelpInfoURI = 'https://raw.githubusercontent.com/jakehildreth/Locksmith/main/en-US/' - ModuleVersion = '2025.5.25' + ModuleVersion = '2025.5.26' PowerShellVersion = '5.1' PrivateData = @{ PSData = @{