diff --git a/.gitignore b/.gitignore index 244cf0d..cc65fe4 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ ADCSRemediation.CSV Artefacts/* Examples/Output/* Ignore/* +Invoke-RevertLocksmith.ps1 Lib/Core/* Lib/Default/* Lib/Standard/* diff --git a/Build/Build-Module.ps1 b/Build/Build-Module.ps1 index 16a9367..5bf5cf0 100644 --- a/Build/Build-Module.ps1 +++ b/Build/Build-Module.ps1 @@ -17,12 +17,13 @@ Import-Module -Name PSPublishModule -Force Build-Module -ModuleName 'Locksmith' { # Usual defaults as per standard module $Manifest = [ordered] @{ - ModuleVersion = '2023.11' + ModuleVersion = '2023.12' CompatiblePSEditions = @('Desktop', 'Core') GUID = 'b1325b42-8dc4-4f17-aa1f-dcb5984ca14a' Author = 'Jake Hildreth' Copyright = "(c) 2022 - $((Get-Date).Year). All rights reserved." Description = 'A small tool to find and fix common misconfigurations in Active Directory Certificate Services.' + ProjectUri = 'https://github.com/TrimarcJake/Locksmith' PowerShellVersion = '5.1' Tags = @('Windows', 'Locksmith', 'CA', 'PKI', 'ActiveDirectory', 'CertificateServices','ADCS') } @@ -40,8 +41,7 @@ Build-Module -ModuleName 'Locksmith' { # those modules are builtin in PowerShell so no need to install them # could as well be ignored with New-ConfigurationModuleSkip 'Microsoft.PowerShell.Utility' - 'Microsoft.PowerShell.LocalAccounts', - 'Microsoft.PowerShell.Utility' + 'Microsoft.PowerShell.LocalAccounts' 'Microsoft.PowerShell.Management' 'CimCmdlets' 'Dism' @@ -54,8 +54,8 @@ Build-Module -ModuleName 'Locksmith' { # Ignore missing modules or cmdlets during build process New-ConfigurationModuleSkip -IgnoreFunctionName @('Out-ConsoleGridView') -IgnoreModuleName @('Microsoft.PowerShell.ConsoleGuiTools') - # Tells the script to exclude Out-ConsoleGridView command from functions if the module is not available to be loaded - New-ConfigurationCommand -CommandName @('Out-ConsoleGridView') -ModuleName 'Microsoft.PowerShell.ConsoleGuiTools' + # Tells the script to exclude commands from functions if the module is not available to be loaded + # New-ConfigurationCommand -CommandName @('') -ModuleName @('') # Populate arrays or remove empty example. $ConfigurationFormat = [ordered] @{ RemoveComments = $false diff --git a/Images/locksmith-sticker.png b/Images/locksmith-sticker.png new file mode 100644 index 0000000..9cba729 Binary files /dev/null and b/Images/locksmith-sticker.png differ diff --git a/Invoke-Locksmith.ps1 b/Invoke-Locksmith.ps1 index ea48e06..eb1326b 100644 --- a/Invoke-Locksmith.ps1 +++ b/Invoke-Locksmith.ps1 @@ -1,10 +1,9 @@ -param ( +param ( [int]$Mode, [Parameter()] - [ValidateSet('Auditing','ESC1','ESC2','ESC3','ESC4','ESC5','ESC6','ESC8','All','PromptMe')] - [array]$Scans = 'All' + [ValidateSet('Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC8', 'All', 'PromptMe')] + [array]$Scans = 'All' ) - function ConvertFrom-IdentityReference { [CmdletBinding()] param( @@ -15,12 +14,12 @@ function ConvertFrom-IdentityReference { $Principal = New-Object System.Security.Principal.NTAccount($Object) if ($Principal -match '^(S-1|O:)') { $SID = $Principal - } else { + } + else { $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value } return $SID } - function Export-RevertScript { [CmdletBinding()] param( @@ -47,7 +46,6 @@ function Export-RevertScript { } } } - function Find-AuditingIssue { [CmdletBinding()] param( @@ -59,9 +57,9 @@ function Find-AuditingIssue { ($_.AuditFilter -ne '127') } | ForEach-Object { $Issue = New-Object -TypeName pscustomobject - $Issue | Add-Member -MemberType NoteProperty -Name 'Forest' -Value $_.CanonicalName.split('/')[0] -Force - $Issue | Add-Member -MemberType NoteProperty -Name 'Name' -Value $_.Name -Force - $Issue | Add-Member -MemberType NoteProperty -Name 'DistinguishedName' -Value $_.DistinguishedName -Force + $Issue | Add-Member -MemberType NoteProperty -Name 'Forest' -Value $_.CanonicalName.split('/')[0] -Force + $Issue | Add-Member -MemberType NoteProperty -Name 'Name' -Value $_.Name -Force + $Issue | Add-Member -MemberType NoteProperty -Name 'DistinguishedName' -Value $_.DistinguishedName -Force if ($_.AuditFilter -match 'CA Unavailable') { $Issue | Add-Member -MemberType NoteProperty -Name 'Issue' -Value $_.AuditFilter -Force $Issue | Add-Member -MemberType NoteProperty -Name 'Fix' -Value 'N/A' -Force @@ -69,7 +67,7 @@ function Find-AuditingIssue { $Issue | Add-Member -MemberType NoteProperty -Name 'Technique' -Value 'DETECT' -Force } else { - $Issue | Add-Member -MemberType NoteProperty -Name 'Issue' -Value "Auditing is not fully enabled. Current value is $($_.AuditFilter)" -Force + $Issue | Add-Member -MemberType NoteProperty -Name 'Issue' -Value "Auditing is not fully enabled on $($_.CAFullName). Current value is $($_.AuditFilter)" -Force $Issue | Add-Member -MemberType NoteProperty -Name 'Fix' ` -Value "certutil.exe -config `'$($_.CAFullname)`' -setreg `'CA\AuditFilter`' 127; Invoke-Command -ComputerName `'$($_.dNSHostName)`' -ScriptBlock { Get-Service -Name `'certsvc`' | Restart-Service -Force }" -Force $Issue | Add-Member -MemberType NoteProperty -Name 'Revert' ` @@ -94,14 +92,15 @@ function Find-ESC1 { ($_.objectClass -eq 'pKICertificateTemplate') -and ($_.pkiExtendedKeyUsage -match $ClientAuthEKUs) -and ($_.'msPKI-Certificate-Name-Flag' -eq 1) -and - ($_.'msPKI-Enrollment-Flag' -ne 2) -and + !($_.'msPKI-Enrollment-Flag' -band 2) -and ( ($_.'msPKI-RA-Signature' -eq 0) -or ($null -eq $_.'msPKI-RA-Signature') ) } | 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 { + } + else { $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value } if ( ($SID -notmatch $SafeUsers) -and ($entry.ActiveDirectoryRights -match 'ExtendedRight') ) { @@ -136,16 +135,17 @@ function Find-ESC2 { ) $ADCSObjects | Where-Object { ($_.ObjectClass -eq 'pKICertificateTemplate') -and - ( (!$_.pkiExtendedKeyUsage) -or ($_.pkiExtendedKeyUsage -match '2.5.29.37.0') )-and + ( (!$_.pkiExtendedKeyUsage) -or ($_.pkiExtendedKeyUsage -match '2.5.29.37.0') ) -and ($_.'msPKI-Certificate-Name-Flag' -eq 1) -and - ($_.'msPKI-Enrollment-Flag' -ne 2) -and + !($_.'msPKI-Enrollment-Flag' -band 2) -and ( ($_.'msPKI-RA-Signature' -eq 0) -or ($null -eq $_.'msPKI-RA-Signature') ) } | 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 { + } + else { $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value } if ( ($SID -notmatch $SafeUsers) -and ($entry.ActiveDirectoryRights -match 'ExtendedRight') ) { @@ -181,14 +181,15 @@ function Find-ESC3Condition1 { $ADCSObjects | Where-Object { ($_.objectClass -eq 'pKICertificateTemplate') -and ($_.pkiExtendedKeyUsage -match $EnrollmentAgentEKU) -and - ($_.'msPKI-Enrollment-Flag' -ne 2) -and + !($_.'msPKI-Enrollment-Flag' -band 2) -and ( ($_.'msPKI-RA-Signature' -eq 0) -or ($null -eq $_.'msPKI-RA-Signature') ) } | 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 { + } + else { $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value } if ( ($SID -notmatch $SafeUsers) -and ($entry.ActiveDirectoryRights -match 'ExtendedRight') ) { @@ -225,7 +226,7 @@ function Find-ESC3Condition2 { ($_.objectClass -eq 'pKICertificateTemplate') -and ($_.pkiExtendedKeyUsage -match $ClientAuthEKU) -and ($_.'msPKI-Certificate-Name-Flag' -eq 1) -and - ($_.'msPKI-Enrollment-Flag' -ne 2) -and + !($_.'msPKI-Enrollment-Flag' -band 2) -and ($_.'msPKI-RA-Application-Policies' -eq '1.3.6.1.4.1.311.20.2.1') -and ( ($_.'msPKI-RA-Signature' -eq 1) ) } | ForEach-Object { @@ -233,7 +234,8 @@ function Find-ESC3Condition2 { $Principal = New-Object System.Security.Principal.NTAccount($entry.IdentityReference) if ($Principal -match '^(S-1|O:)') { $SID = $Principal - } else { + } + else { $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value } if ( ($SID -notmatch $SafeUsers) -and ($entry.ActiveDirectoryRights -match 'ExtendedRight') ) { @@ -274,10 +276,12 @@ function Find-ESC4 { $Principal = New-Object System.Security.Principal.NTAccount($_.nTSecurityDescriptor.Owner) if ($Principal -match '^(S-1|O:)') { $SID = $Principal - } else { + } + else { $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value } - if ( ($_.objectClass -eq 'pKICertificateTemplate') -and ($SID -notmatch $SafeOwners) ) { + + if ( ($_.objectClass -eq 'pKICertificateTemplate') -and ($SID -match $UnsafeOwners) ) { $Issue = New-Object -TypeName pscustomobject $Issue | Add-Member -MemberType NoteProperty -Name Forest -Value $_.CanonicalName.split('/')[0] -Force $Issue | Add-Member -MemberType NoteProperty -Name Name -Value $_.Name -Force @@ -286,14 +290,14 @@ function Find-ESC4 { $Issue | Add-Member -MemberType NoteProperty -Name ActiveDirectoryRights -Value $entry.ActiveDirectoryRights -Force $Issue | Add-Member -MemberType NoteProperty -Name Issue ` -Value "$($_.nTSecurityDescriptor.Owner) has Owner rights on this template" -Force - $Issue | Add-Member -MemberType NoteProperty -Name Fix -Value '[TODO]' -Force - $Issue | Add-Member -MemberType NoteProperty -Name Revert -Value '[TODO]' -Force + $Issue | Add-Member -MemberType NoteProperty -Name Fix -Value "`$Owner = New-Object System.Security.Principal.SecurityIdentifier(`'$PreferredOwner`'); `$ACL = Get-Acl -Path `'AD:$($_.DistinguishedName)`'; `$ACL.SetOwner(`$Owner); Set-ACL -Path `'AD:$($_.DistinguishedName)`' -AclObject `$ACL" -Force + $Issue | Add-Member -MemberType NoteProperty -Name Revert -Value "`$Owner = New-Object System.Security.Principal.SecurityIdentifier(`'$($_.nTSecurityDescriptor.Owner)`'); `$ACL = Get-Acl -Path `'AD:$($_.DistinguishedName)`'; `$ACL.SetOwner(`$Owner); Set-ACL -Path `'AD:$($_.DistinguishedName)`' -AclObject `$ACL" -Force $Issue | Add-Member -MemberType NoteProperty -Name Technique -Value 'ESC4' $Severity = Set-Severity -Issue $Issue $Issue | Add-Member -MemberType NoteProperty -Name Severity -Value $Severity $Issue } - if ( ($_.objectClass -eq 'pKICertificateTemplate') -and ($SID -match $UnsafeOwners) ) { + elseif ( ($_.objectClass -eq 'pKICertificateTemplate') -and ($SID -notmatch $SafeOwners) ) { $Issue = New-Object -TypeName pscustomobject $Issue | Add-Member -MemberType NoteProperty -Name Forest -Value $_.CanonicalName.split('/')[0] -Force $Issue | Add-Member -MemberType NoteProperty -Name Name -Value $_.Name -Force @@ -302,25 +306,27 @@ function Find-ESC4 { $Issue | Add-Member -MemberType NoteProperty -Name ActiveDirectoryRights -Value $entry.ActiveDirectoryRights -Force $Issue | Add-Member -MemberType NoteProperty -Name Issue ` -Value "$($_.nTSecurityDescriptor.Owner) has Owner rights on this template" -Force - $Issue | Add-Member -MemberType NoteProperty -Name Fix -Value "`$Owner = New-Object System.Security.Principal.SecurityIdentifier(`'$PreferredOwner`'); `$ACL = Get-Acl -Path `'AD:$($_.DistinguishedName)`'; `$ACL.SetOwner(`$Owner); Set-ACL -Path `'AD:$($_.DistinguishedName)`' -AclObject `$ACL" -Force - $Issue | Add-Member -MemberType NoteProperty -Name Revert -Value "`$Owner = New-Object System.Security.Principal.SecurityIdentifier(`'$($_.nTSecurityDescriptor.Owner)`'); `$ACL = Get-Acl -Path `'AD:$($_.DistinguishedName)`'; `$ACL.SetOwner(`$Owner); Set-ACL -Path `'AD:$($_.DistinguishedName)`' -AclObject `$ACL" -Force + $Issue | Add-Member -MemberType NoteProperty -Name Fix -Value '[TODO]' -Force + $Issue | Add-Member -MemberType NoteProperty -Name Revert -Value '[TODO]' -Force $Issue | Add-Member -MemberType NoteProperty -Name Technique -Value 'ESC4' $Severity = Set-Severity -Issue $Issue $Issue | Add-Member -MemberType NoteProperty -Name Severity -Value $Severity $Issue } + foreach ($entry in $_.nTSecurityDescriptor.Access) { $Principal = New-Object System.Security.Principal.NTAccount($entry.IdentityReference) if ($Principal -match '^(S-1|O:)') { $SID = $Principal - } else { + } + else { $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value } if ( ($_.objectClass -eq 'pKICertificateTemplate') -and ($SID -notmatch $SafeUsers) -and ($entry.ActiveDirectoryRights -match $DangerousRights) -and ($entry.ActiveDirectoryRights.ObjectType -notmatch $SafeObjectTypes) - ) { + ) { $Issue = New-Object -TypeName pscustomobject $Issue | Add-Member -MemberType NoteProperty -Name Forest -Value $_.CanonicalName.split('/')[0] -Force $Issue | Add-Member -MemberType NoteProperty -Name Name -Value $_.Name -Force @@ -356,13 +362,14 @@ function Find-ESC5 { $Principal = New-Object System.Security.Principal.NTAccount($_.nTSecurityDescriptor.Owner) if ($Principal -match '^(S-1|O:)') { $SID = $Principal - } else { + } + else { $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value } - if ( ($_.objectClass -ne 'pKICertificateTemplate') -and + if ( ($_.objectClass -ne 'pKICertificateTemplate') -and ($SID -notmatch $SafeOwners) -and - ($entry.ActiveDirectoryRights.ObjectType -notmatch $SafeObjectTypes) - ) { + ($entry.ActiveDirectoryRights.ObjectType -notmatch $SafeObjectTypes) + ) { $Issue = New-Object -TypeName pscustomobject $Issue | Add-Member -MemberType NoteProperty -Name Forest -Value $_.CanonicalName.split('/')[0] -Force $Issue | Add-Member -MemberType NoteProperty -Name Name -Value $_.Name -Force @@ -398,7 +405,8 @@ function Find-ESC5 { $Principal = New-Object System.Security.Principal.NTAccount($entry.IdentityReference) if ($Principal -match '^(S-1|O:)') { $SID = $Principal - } else { + } + else { $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value } if ( ($_.objectClass -ne 'pKICertificateTemplate') -and @@ -422,7 +430,6 @@ function Find-ESC5 { } } } - function Find-ESC6 { [CmdletBinding()] param( @@ -458,7 +465,6 @@ function Find-ESC6 { } } } - function Find-ESC8 { [CmdletBinding()] param( @@ -479,7 +485,8 @@ function Find-ESC8 { $Issue['CAEnrollmentEndpoint'] = $_.CAEnrollmentEndpoint $Issue['Fix'] = 'TBD - Remediate by doing 1, 2, and 3' $Issue['Revert'] = 'TBD' - } else { + } + else { $Issue['Issue'] = 'HTTPS enrollment is enabled.' $Issue['CAEnrollmentEndpoint'] = $_.CAEnrollmentEndpoint $Issue['Fix'] = 'TBD - Remediate by doing 1, 2, and 3' @@ -492,7 +499,6 @@ function Find-ESC8 { } } } - function Format-Result { [CmdletBinding()] param( @@ -523,9 +529,10 @@ function Format-Result { 1 { if ($Issue.Technique -eq 'ESC8') { $Issue | Format-List Technique, Name, DistinguishedName, CAEnrollmentEndpoint, Issue, Fix - } else { + } + else { $Issue | Format-List Technique, Name, DistinguishedName, Issue, Fix - if(($Issue.Technique -eq "DETECT" -or $Issue.Technique -eq "ESC6") -and (Get-RestrictedAdminModeSetting)){ + if (($Issue.Technique -eq "DETECT" -or $Issue.Technique -eq "ESC6") -and (Get-RestrictedAdminModeSetting)) { Write-Warning "Restricted Admin Mode appears to be configured. Certutil.exe may not work from this host, therefore you may need to execute the 'Fix' commands on the CA server itself" } } @@ -533,7 +540,6 @@ function Format-Result { } } } - function Get-ADCSObject { [CmdletBinding()] param( @@ -542,16 +548,16 @@ function Get-ADCSObject { [System.Management.Automation.PSCredential]$Credential ) foreach ( $forest in $Targets ) { - if ($Credential){ + if ($Credential) { $ADRoot = (Get-ADRootDSE -Credential $Credential -Server $forest).defaultNamingContext Get-ADObject -Filter * -SearchBase "CN=Public Key Services,CN=Services,CN=Configuration,$ADRoot" -SearchScope 2 -Properties * -Credential $Credential - } else { + } + else { $ADRoot = (Get-ADRootDSE -Server $forest).defaultNamingContext Get-ADObject -Filter * -SearchBase "CN=Public Key Services,CN=Services,CN=Configuration,$ADRoot" -SearchScope 2 -Properties * } } } - function Get-CAHostObject { [CmdletBinding()] param ( @@ -566,29 +572,30 @@ function Get-CAHostObject { $ADCSObjects | Where-Object objectClass -Match 'pKIEnrollmentService' | ForEach-Object { Get-ADObject $_.CAHostDistinguishedName -Properties * -Server $ForestGC -Credential $Credential } - } else { + } + else { $ADCSObjects | Where-Object objectClass -Match 'pKIEnrollmentService' | ForEach-Object { Get-ADObject $_.CAHostDistinguishedName -Properties * -Server $ForestGC } } } } - function Get-RestrictedAdminModeSetting { $Path = 'HKLM:SYSTEM\CurrentControlSet\Control\Lsa' try { $RAM = (Get-ItemProperty -Path $Path).DisableRestrictedAdmin $Creds = (Get-ItemProperty -Path $Path).DisableRestrictedAdminOutboundCreds - if ($RAM -eq '0' -and $Creds -eq '1'){ + if ($RAM -eq '0' -and $Creds -eq '1') { return $true - } else { + } + else { return $false } - } catch { + } + catch { return $false } } - function Get-Target { param ( [string]$Forest, @@ -601,594 +608,245 @@ function Get-Target { } elseif ($InputPath) { $Targets = Get-Content $InputPath - } else { - if ($Credential){ + } + else { + if ($Credential) { $Targets = (Get-ADForest -Credential $Credential).Name - } else { + } + else { $Targets = (Get-ADForest).Name } } return $Targets } - -function Invoke-Locksmith { - <# - .SYNOPSIS - Finds the most common malconfigurations of Active Directory Certificate Services (AD CS). - - .DESCRIPTION - Locksmith uses the Active Directory (AD) Powershell (PS) module to identify 6 misconfigurations - commonly found in Enterprise mode AD CS installations. - - .COMPONENT - Locksmith requires the AD PS module to be installed in the scope of the Current User. - If Locksmith does not identify the AD PS module as installed, it will attempt to - install the module. If module installation does not complete successfully, - Locksmith will fail. - - .PARAMETER Mode - Specifies sets of common script execution modes. - - -Mode 0 - Finds any malconfigurations and displays them in the console. - No attempt is made to fix identified issues. - - -Mode 1 - Finds any malconfigurations and displays them in the console. - Displays example Powershell snippet that can be used to resolve the issue. - No attempt is made to fix identified issues. - - -Mode 2 - Finds any malconfigurations and writes them to a series of CSV files. - No attempt is made to fix identified issues. - - -Mode 3 - Finds any malconfigurations and writes them to a series of CSV files. - Creates code snippets to fix each issue and writes them to an environment-specific custom .PS1 file. - No attempt is made to fix identified issues. - - -Mode 4 - Finds any malconfigurations and creates code snippets to fix each issue. - Attempts to fix all identified issues. This mode may require high-privileged access. - - .PARAMETER Scans - Specify which scans you want to run. Available scans: 'All' or Auditing, ESC1, ESC2, ESC3, ESC4, ESC5, ESC6, ESC8, or 'PromptMe' - - -Scans All - Run all scans (default) - - -Scans PromptMe - Presents a grid view of the available scan types that can be selected and run them after you click OK. - - .PARAMETER OutputPath - Specify the path where you want to save reports and mitigation scripts. - - .INPUTS - None. You cannot pipe objects to Invoke-Locksmith.ps1. - - .OUTPUTS - Output types: - 1. Console display of identified issues - 2. Console display of identified issues and their fixes - 3. CSV containing all identified issues - 4. CSV containing all identified issues and their fixes - - .NOTES - Windows PowerShell cmdlet Restart-Service requires RunAsAdministrator - #> - +function Invoke-Scans { [CmdletBinding()] param ( - [string]$Forest, - [string]$InputPath, - [int]$Mode = 0, + # Could split Scans and PromptMe into separate parameter sets. [Parameter()] - [ValidateSet('Auditing','ESC1','ESC2','ESC3','ESC4','ESC5','ESC6','ESC8','All','PromptMe')] - [array]$Scans = 'All', - [string]$OutputPath = (Get-Location).Path, - [System.Management.Automation.PSCredential]$Credential + [ValidateSet('Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC8', 'All', 'PromptMe')] + [array]$Scans = 'All' ) - $Version = '2023.11' - $Logo = @" - _ _____ _______ _ _ _______ _______ _____ _______ _ _ - | | | | |____/ |______ | | | | | |_____| - |_____ |_____| |_____ | \_ ______| | | | __|__ | | | - .--. .--. .--. - /.-. '----------. /.-. '----------. /.-. '----------. - \'-' .---'-''-'-' \'-' .--'--''-'-' \'-' .--'--'-''-' - '--' '--' '--' - v$Version + # Is this needed? + if ($Scans -eq $IsNullOrEmpty) { + $Scans = 'All' + } -"@ - $Logo + if ( $Scans -eq 'PromptMe' ) { + $GridViewTitle = 'Select the tests to run and press Enter or click OK to continue...' - # Check if ActiveDirectory PowerShell module is available, and attempt to install if not found - if (-not(Get-Module -Name 'ActiveDirectory' -ListAvailable)) { - if (Test-IsElevated) { - $OS = (Get-CimInstance -ClassName Win32_OperatingSystem).ProductType - # 1 - workstation, 2 - domain controller, 3 - non-dc server - if ($OS -gt 1) { - # Attempt to install ActiveDirectory PowerShell module for Windows Server OSes, works with Windows Server 2012 R2 through Windows Server 2022 - Install-WindowsFeature -Name RSAT-AD-PowerShell - } else { - # Attempt to install ActiveDirectory PowerShell module for Windows Desktop OSes - Add-WindowsCapability -Name Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0 -Online - } + # Check for Out-GridView or Out-ConsoleGridView + if ((Get-Command Out-ConsoleGridView -ErrorAction SilentlyContinue) -and ($PSVersionTable.PSVersion.Major -ge 7)) { + [array]$Scans = ($Dictionary | Select-Object Name, Category, Subcategory | Out-ConsoleGridView -OutputMode Multiple -Title $GridViewTitle).Name | Sort-Object -Property Name + } + elseif (Get-Command -Name Out-GridView -ErrorAction SilentlyContinue) { + [array]$Scans = ($Dictionary | Select-Object Name, Category, Subcategory | Out-GridView -PassThru -Title $GridViewTitle).Name | Sort-Object -Property Name } else { - Write-Warning -Message "The ActiveDirectory PowerShell module is required for Locksmith, but is not installed. Please launch an elevated PowerShell session to have this module installed for you automatically." - # The goal here is to exit the script without closing the PowerShell window. Need to test. - Return + # To Do: Check for admin and prompt to install features/modules or revert to 'All'. + Write-Information "Out-GridView and Out-ConsoleGridView were not found on your system. Defaulting to `'All`'." + $Scans = 'All' } } - # Exit if running in restricted admin mode without explicit credentials - if (!$Credential -and (Get-RestrictedAdminModeSetting)) { - Write-Warning "Restricted Admin Mode appears to be in place, re-run with the '-Credential domain\user' option" - break; + switch ( $Scans ) { + Auditing { + Write-Host 'Identifying auditing issues...' + [array]$AuditingIssues = Find-AuditingIssue -ADCSObjects $ADCSObjects + } + ESC1 { + Write-Host 'Identifying AD CS templates with dangerous ESC1 configurations...' + [array]$ESC1 = Find-ESC1 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers + } + ESC2 { + Write-Host 'Identifying AD CS templates with dangerous ESC2 configurations...' + [array]$ESC2 = Find-ESC2 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers + } + ESC3 { + Write-Host 'Identifying AD CS templates with dangerous ESC3 configurations...' + [array]$ESC3 = Find-ESC3Condition1 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers + [array]$ESC3 += Find-ESC3Condition2 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers + } + ESC4 { + Write-Host 'Identifying AD CS template and other objects with poor access control (ESC4)...' + [array]$ESC4 = Find-ESC4 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -DangerousRights $DangerousRights -SafeOwners $SafeOwners + } + ESC5 { + Write-Host 'Identifying AD CS template and other objects with poor access control (ESC5)...' + [array]$ESC5 = Find-ESC5 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -DangerousRights $DangerousRights -SafeOwners $SafeOwners + } + ESC6 { + Write-Host 'Identifying AD CS template and other objects with poor access control (ESC6)...' + [array]$ESC6 = Find-ESC6 -ADCSObjects $ADCSObjects + } + ESC8 { + Write-Host 'Identifying HTTP-based certificate enrollment interfaces (ESC8)...' + [array]$ESC8 = Find-ESC8 -ADCSObjects $ADCSObjects + } + All { + Write-Host 'Identifying auditing issues...' + [array]$AuditingIssues = Find-AuditingIssue -ADCSObjects $ADCSObjects + Write-Host 'Identifying AD CS templates with dangerous ESC1 configurations...' + [array]$ESC1 = Find-ESC1 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers + Write-Host 'Identifying AD CS templates with dangerous ESC2 configurations...' + [array]$ESC2 = Find-ESC2 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers + Write-Host 'Identifying AD CS templates with dangerous ESC3 configurations...' + [array]$ESC3 = Find-ESC3Condition1 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers + [array]$ESC3 += Find-ESC3Condition2 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers + Write-Host 'Identifying AD CS template and other objects with poor access control (ESC4)...' + [array]$ESC4 = Find-ESC4 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -DangerousRights $DangerousRights -SafeOwners $SafeOwners + Write-Host 'Identifying AD CS template and other objects with poor access control (ESC5)...' + [array]$ESC5 = Find-ESC5 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -DangerousRights $DangerousRights -SafeOwners $SafeOwners + Write-Host 'Identifying AD CS template and other objects with poor access control (ESC6)...' + [array]$ESC6 = Find-ESC6 -ADCSObjects $ADCSObjects + Write-Host 'Identifying HTTP-based certificate enrollment interfaces (ESC8)...' + [array]$ESC8 = Find-ESC8 -ADCSObjects $ADCSObjects + } } - # Initial variables - $AllDomainsCertPublishersSIDs = @() - $AllDomainsDomainAdminSIDs = @() - $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' - $DangerousRights = 'GenericAll|WriteDacl|WriteOwner|WriteProperty' - $EnrollmentAgentEKU = '1\.3\.6\.1\.4\.1\.311\.20\.2\.1' - $SafeObjectTypes = '0e10c968-78fb-11d2-90d4-00c04f79dc55|a05b8cc2-17bc-4802-a710-e7c15ab866a2' - $SafeOwners = '-512$|-519$|-544$|-18$|-517$|-500$' - $SafeUsers = '-512$|-519$|-544$|-18$|-517$|-500$|-516$|-9$|-526$|-527$|S-1-5-10' - $UnsafeOwners = 'S-1-1-0|-11$|-513$|-515$' - $UnsafeUsers = 'S-1-1-0|-11$|-513$|-515$' - - # Generated variables - $Dictionary = New-Dictionary - $ForestGC = $(Get-ADDomainController -Discover -Service GlobalCatalog -ForceDiscover | Select-Object -ExpandProperty Hostname) + ":3268" - $DNSRoot = [string]((Get-ADForest).RootDomain | Get-ADDomain).DNSRoot - $EnterpriseAdminsSID = ([string]((Get-ADForest).RootDomain | Get-ADDomain).DomainSID) + '-519' - $PreferredOwner = New-Object System.Security.Principal.SecurityIdentifier($EnterpriseAdminsSID) - $DomainSIDs = (Get-ADForest).Domains | ForEach-Object { (Get-ADDomain $_).DomainSID.Value } - $DomainSIDs | ForEach-Object { - $AllDomainsCertPublishersSIDs += $_ + '-517' - $AllDomainsDomainAdminSIDs += $_ + '-512' - } + [array]$AllIssues = $AuditingIssues + $ESC1 + $ESC2 + $ESC3 + $ESC4 + $ESC5 + $ESC6 + $ESC8 - # Add SIDs of (probably) Safe Users to $SafeUsers - Get-ADGroupMember $EnterpriseAdminsSID | ForEach-Object { - $SafeUsers += '|' + $_.SID.Value - } - - (Get-ADForest).Domains | ForEach-Object { - $DomainSID = (Get-ADDomain $_).DomainSID.Value - $SafeGroupRIDs = @('-517','-512') - $SafeGroupSIDs = @('S-1-5-32-544') - foreach ($rid in $SafeGroupRIDs ) { - $SafeGroupSIDs += $DomainSID + $rid - } - foreach ($sid in $SafeGroupSIDs) { - $users += (Get-ADGroupMember $sid -Server $_ -Recursive).SID.Value - } - foreach ($user in $users) { - $SafeUsers += '|' + $user - } - } - - if (!$Credential -and (Get-RestrictedAdminModeSetting)) { - Write-Warning "Restricted Admin Mode appears to be in place, re-run with the '-Credential domain\user' option" - break; - } - - if ($Credential) { - $Targets = Get-Target -Credential $Credential - } else { - $Targets = Get-Target - } - - Write-Host "Gathering AD CS Objects from $($Targets)..." - if ($Credential) { - $ADCSObjects = Get-ADCSObject -Targets $Targets -Credential $Credential - Set-AdditionalCAProperty -ADCSObjects $ADCSObjects -Credential $Credential - $ADCSObjects += Get-CAHostObject -ADCSObjects $ADCSObjects -Credential $Credential - $CAHosts = Get-CAHostObject -ADCSObjects $ADCSObjects -Credential $Credential - $CAHosts | ForEach-Object { $SafeUsers += '|' + $_.Name } - } else { - $ADCSObjects = Get-ADCSObject -Targets $Targets - Set-AdditionalCAProperty -ADCSObjects $ADCSObjects - $ADCSObjects += Get-CAHostObject -ADCSObjects $ADCSObjects - $CAHosts = Get-CAHostObject -ADCSObjects $ADCSObjects - $CAHosts | ForEach-Object { $SafeUsers += '|' + $_.Name } - } - - if ( $Scans ) { - # If the Scans parameter was used, Invoke-Scans with the specified checks. - $Results = Invoke-Scans -Scans $Scans - # Re-hydrate the findings arrays from the Results hash table - $AllIssues = $Results['AllIssues'] - $AuditingIssues = $Results['AuditingIssues'] - $ESC1 = $Results['ESC1'] - $ESC2 = $Results['ESC2'] - $ESC3 = $Results['ESC3'] - $ESC4 = $Results['ESC4'] - $ESC5 = $Results['ESC5'] - $ESC6 = $Results['ESC6'] - $ESC8 = $Results['ESC8'] - } - - # If these are all empty = no issues found, exit - if ($null -eq $Results) { - Write-Host "`n$(Get-Date) : No ADCS issues were found." -ForegroundColor Green - break - } - - switch ($Mode) { - 0 { - Format-Result $AuditingIssues '0' - Format-Result $ESC1 '0' - Format-Result $ESC2 '0' - Format-Result $ESC3 '0' - Format-Result $ESC4 '0' - Format-Result $ESC5 '0' - Format-Result $ESC6 '0' - Format-Result $ESC8 '0' - } - 1 { - Format-Result $AuditingIssues '1' - Format-Result $ESC1 '1' - Format-Result $ESC2 '1' - Format-Result $ESC3 '1' - Format-Result $ESC4 '1' - Format-Result $ESC5 '1' - Format-Result $ESC6 '1' - Format-Result $ESC8 '1' - } - 2 { - $Output = 'ADCSIssues.CSV' - Write-Host "Writing AD CS issues to $Output..." - try { - $AllIssues | Select-Object Forest, Technique, Name, Issue | Export-Csv -NoTypeInformation $Output - Write-Host "$Output created successfully!" - } catch { - Write-Host 'Ope! Something broke.' - } - } - 3 { - $Output = 'ADCSRemediation.CSV' - Write-Host "Writing AD CS issues to $Output..." - try { - $AllIssues | Select-Object Forest, Technique, Name, DistinguishedName, Issue, Fix | Export-Csv -NoTypeInformation $Output - Write-Host "$Output created successfully!" - } catch { - Write-Host 'Ope! Something broke.' - } - } - 4 { - Write-Host 'Creating a script to revert any changes made by Locksmith...' - try { Export-RevertScript -AuditingIssues $AuditingIssues -ESC1 $ESC1 -ESC2 $ESC2 -ESC6 $ESC6 } catch {} - Write-Host 'Executing Mode 4 - Attempting to fix all identified issues!' - if ($AuditingIssues) { - $AuditingIssues | ForEach-Object { - $FixBlock = [scriptblock]::Create($_.Fix) - Write-Host "Attempting to fully enable AD CS auditing on $($_.Name)..." - Write-Host "This should have little impact on your environment.`n" - Write-Host 'Command(s) to be run:' - Write-Host 'PS> ' -NoNewline - Write-Host "$($_.Fix)`n" -ForegroundColor Cyan - try { - $WarningError = $null - Write-Warning 'If you continue, this script will attempt to fix this issue.' -WarningAction Inquire -ErrorVariable WarningError - if (!$WarningError) { - try { - Invoke-Command -ScriptBlock $FixBlock - } catch { - Write-Error 'Could not modify AD CS auditing. Are you a local admin on this host?' - } - } - } catch { - Write-Host 'SKIPPED!' -ForegroundColor Yellow - } - Read-Host -Prompt 'Press enter to continue...' - } - } - if ($ESC1) { - $ESC1 | ForEach-Object { - $FixBlock = [scriptblock]::Create($_.Fix) - Write-Host "Attempting to enable Manager Approval on the $($_.Name) template...`n" - Write-Host 'Command(s) to be run:' - Write-Host 'PS> ' -NoNewline - Write-Host "$($_.Fix)`n" -ForegroundColor Cyan - try { - $WarningError = $null - Write-Warning "This could cause some services to stop working until certificates are approved.`nIf you continue this script will attempt to fix this issues." -WarningAction Inquire -ErrorVariable WarningError - if (!$WarningError) { - try { - Invoke-Command -ScriptBlock $FixBlock - } catch { - Write-Error 'Could not enable Manager Approval. Are you an Active Directory or AD CS admin?' - } - } - } catch { - Write-Host 'SKIPPED!' -ForegroundColor Yellow - } - Read-Host -Prompt 'Press enter to continue...' - } - } - if ($ESC2) { - $ESC2 | ForEach-Object { - $FixBlock = [scriptblock]::Create($_.Fix) - Write-Host "Attempting to enable Manager Approval on the $($_.Name) template...`n" - Write-Host 'Command(s) to be run:' - Write-Host 'PS> ' -NoNewline - Write-Host "$($_.Fix)`n" -ForegroundColor Cyan - try { - $WarningError = $null - Write-Warning "This could cause some services to stop working until certificates are approved.`nIf you continue, this script will attempt to fix this issue." -WarningAction Inquire -ErrorVariable WarningError - if (!$WarningError) { - try { - Invoke-Command -ScriptBlock $FixBlock - } catch { - Write-Error 'Could not enable Manager Approval. Are you an Active Directory or AD CS admin?' - } - } - } catch { - Write-Host 'SKIPPED!' -ForegroundColor Yellow - } - Read-Host -Prompt 'Press enter to continue...' - } - } - if ($ESC6) { - $ESC6 | ForEach-Object { - $FixBlock = [scriptblock]::Create($_.Fix) - Write-Host "Attempting to disable the EDITF_ATTRIBUTESUBJECTALTNAME2 flag on $($_.Name)...`n" - Write-Host 'Command(s) to be run:' - Write-Host 'PS> ' -NoNewline - Write-Host "$($_.Fix)`n" -ForegroundColor Cyan - try { - $WarningError = $null - Write-Warning "This could cause some services to stop working.`nIf you continue this script will attempt to fix this issues." -WarningAction Inquire -ErrorVariable WarningError - if (!$WarningError) { - try { - Invoke-Command -ScriptBlock $FixBlock - } catch { - Write-Error 'Could not disable the EDITF_ATTRIBUTESUBJECTALTNAME2 flag. Are you an Active Directory or AD CS admin?' - } - } - } catch { - Write-Host 'SKIPPED!' -ForegroundColor Yellow - } - Read-Host -Prompt 'Press enter to continue...' - } - } - } - } -} - -function Invoke-Scans { - [CmdletBinding()] - param ( - # Could split Scans and PromptMe into separate parameter sets. - [Parameter()] - [ValidateSet('Auditing','ESC1','ESC2','ESC3','ESC4','ESC5','ESC6','ESC8','All','PromptMe')] - [array]$Scans = 'All' - ) - - # Is this needed? - if ($Scans -eq $IsNullOrEmpty) { - $Scans = 'All' - } - - if ( $Scans -eq 'PromptMe' ) { - $GridViewTitle = 'Select the tests to run and press Enter or click OK to continue...' - - # Check for Out-GridView or Out-ConsoleGridView - if ((Get-Command Out-ConsoleGridView -ErrorAction SilentlyContinue) -and ($PSVersionTable.PSVersion.Major -ge 7)) { - [array]$Scans = ($Dictionary | Select-Object Name,Category,Subcategory | Out-ConsoleGridView -OutputMode Multiple -Title $GridViewTitle).Name | Sort-Object -Property Name - } - elseif (Get-Command -Name Out-GridView -ErrorAction SilentlyContinue) { - [array]$Scans = ($Dictionary | Select-Object Name,Category,Subcategory | Out-GridView -PassThru -Title $GridViewTitle).Name | Sort-Object -Property Name - } - else { - # To Do: Check for admin and prompt to install features/modules or revert to 'All'. - Write-Information "Out-GridView and Out-ConsoleGridView were not found on your system. Defaulting to `'All`'." - $Scans = 'All' - } - } - - switch ( $Scans ) { - Auditing { - Write-Host 'Identifying auditing issues...' - [array]$AuditingIssues = Find-AuditingIssue -ADCSObjects $ADCSObjects - } - ESC1 { - Write-Host 'Identifying AD CS templates with dangerous ESC1 configurations...' - [array]$ESC1 = Find-ESC1 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers - } - ESC2 { - Write-Host 'Identifying AD CS templates with dangerous ESC2 configurations...' - [array]$ESC2 = Find-ESC2 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers - } - ESC3 { - Write-Host 'Identifying AD CS templates with dangerous ESC3 configurations...' - [array]$ESC3 = Find-ESC3Condition1 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers - [array]$ESC3 += Find-ESC3Condition2 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers - } - ESC4 { - Write-Host 'Identifying AD CS template and other objects with poor access control (ESC4)...' - [array]$ESC4 = Find-ESC4 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -DangerousRights $DangerousRights -SafeOwners $SafeOwners - } - ESC5 { - Write-Host 'Identifying AD CS template and other objects with poor access control (ESC5)...' - [array]$ESC5 = Find-ESC5 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -DangerousRights $DangerousRights -SafeOwners $SafeOwners - } - ESC6 { - Write-Host 'Identifying AD CS template and other objects with poor access control (ESC6)...' - [array]$ESC6 = Find-ESC6 -ADCSObjects $ADCSObjects - } - ESC8 { - Write-Host 'Identifying HTTP-based certificate enrollment interfaces (ESC8)...' - [array]$ESC8 = Find-ESC8 -ADCSObjects $ADCSObjects - } - All { - Write-Host 'Identifying auditing issues...' - [array]$AuditingIssues = Find-AuditingIssue -ADCSObjects $ADCSObjects - Write-Host 'Identifying AD CS templates with dangerous ESC1 configurations...' - [array]$ESC1 = Find-ESC1 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers - Write-Host 'Identifying AD CS templates with dangerous ESC2 configurations...' - [array]$ESC2 = Find-ESC2 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers - Write-Host 'Identifying AD CS templates with dangerous ESC3 configurations...' - [array]$ESC3 = Find-ESC3Condition1 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers - [array]$ESC3 += Find-ESC3Condition2 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers - Write-Host 'Identifying AD CS template and other objects with poor access control (ESC4)...' - [array]$ESC4 = Find-ESC4 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -DangerousRights $DangerousRights -SafeOwners $SafeOwners - Write-Host 'Identifying AD CS template and other objects with poor access control (ESC5)...' - [array]$ESC5 = Find-ESC5 -ADCSObjects $ADCSObjects -SafeUsers $SafeUsers -DangerousRights $DangerousRights -SafeOwners $SafeOwners - Write-Host 'Identifying AD CS template and other objects with poor access control (ESC6)...' - [array]$ESC6 = Find-ESC6 -ADCSObjects $ADCSObjects - Write-Host 'Identifying HTTP-based certificate enrollment interfaces (ESC8)...' - [array]$ESC8 = Find-ESC8 -ADCSObjects $ADCSObjects - } - } - - [array]$AllIssues = $AuditingIssues + $ESC1 + $ESC2 + $ESC3 + $ESC4 + $ESC5 + $ESC6 + $ESC8 - - # If these are all empty = no issues found, exit - if ((!$AuditingIssues) -and (!$ESC1) -and (!$ESC2) -and (!$ESC3) -and (!$ESC4) -and (!$ESC5) -and (!$ESC6) -and (!$ESC8) ) { - Write-Host "`n$(Get-Date) : No ADCS issues were found." -ForegroundColor Green - break + # If these are all empty = no issues found, exit + if ((!$AuditingIssues) -and (!$ESC1) -and (!$ESC2) -and (!$ESC3) -and (!$ESC4) -and (!$ESC5) -and (!$ESC6) -and (!$ESC8) ) { + Write-Host "`n$(Get-Date) : No ADCS issues were found." -ForegroundColor Green + break } # Return a hash table of array names (keys) and arrays (values) so they can be directly referenced with other functions Return @{ - AllIssues = $AllIssues + AllIssues = $AllIssues AuditingIssues = $AuditingIssues - ESC1 = $ESC1 - ESC2 = $ESC2 - ESC3 = $ESC3 - ESC4 = $ESC4 - ESC5 = $ESC5 - ESC6 = $ESC6 - ESC8 = $ESC8 + ESC1 = $ESC1 + ESC2 = $ESC2 + ESC3 = $ESC3 + ESC4 = $ESC4 + ESC5 = $ESC5 + ESC6 = $ESC6 + ESC8 = $ESC8 } } -function New-Dictionary { - <# - .SYNOPSIS - Create a dictionary of the escalation paths and insecure configurations that Locksmith scans for. +<# +.SYNOPSIS +Create a dictionary of the escalation paths and insecure configurations that Locksmith scans for. - .DESCRIPTION - The New-Dictionary function is used to instantiate an array of objects that contain the names, definitions, - descriptions, code used to find, code used to fix, and reference URLs. This is invoked by the module's main function. +.DESCRIPTION +The New-Dictionary function is used to instantiate an array of objects that contain the names, definitions, +descriptions, code used to find, code used to fix, and reference URLs. This is invoked by the module's main function. - .NOTES +.NOTES - VulnerableConfigurationItem Class Definition: - Version Update each time the class definition or the dictionary below is changed. - Name The short name of the vulnerable configuration item (VCI). - Category The high level category of VCI types, including escalation path, server configuration, GPO setting, etc. - Subcategory The subcategory of vulnerable configuration item types. - Summary A summary of the vulnerability and how it can be abused. - FindIt The name of the function that is used to look for the VCI, stored as an invokable scriptblock. - FixIt The name of the function that is used to fix the VCI, stored as an invokable scriptblock. - ReferenceUrls An array of URLs that are used as references to learn more about the VCI. - #> + VulnerableConfigurationItem Class Definition: + Version Update each time the class definition or the dictionary below is changed. + Name The short name of the vulnerable configuration item (VCI). + Category The high level category of VCI types, including escalation path, server configuration, GPO setting, etc. + Subcategory The subcategory of vulnerable configuration item types. + Summary A summary of the vulnerability and how it can be abused. + FindIt The name of the function that is used to look for the VCI, stored as an invokable scriptblock. + FixIt The name of the function that is used to fix the VCI, stored as an invokable scriptblock. + ReferenceUrls An array of URLs that are used as references to learn more about the VCI. +#> + +function New-Dictionary { class VulnerableConfigurationItem { - static [string] $Version = '2023.10.01.000' - [string]$Name - [ValidateSet('Escalation Path','Server Configuration','GPO Setting')][string]$Category - [string]$Subcategory - [string]$Summary - [scriptblock]$FindIt - [scriptblock]$FixIt - [uri[]]$ReferenceUrls -} + static [string] $Version = '2023.10.01.000' + [string]$Name + [ValidateSet('Escalation Path', 'Server Configuration', 'GPO Setting')][string]$Category + [string]$Subcategory + [string]$Summary + [scriptblock]$FindIt + [scriptblock]$FixIt + [uri[]]$ReferenceUrls + } -[VulnerableConfigurationItem[]]$Dictionary = @( - [VulnerableConfigurationItem]@{ - Name = 'ESC1' - Category = 'Escalation Path' - Subcategory = 'Misconfigured Certificate Templates' - Summary = '' - FindIt = {Find-ESC1} - FixIt = {Write-Output "Add code to fix the vulnerable configuration."} - ReferenceUrls = 'https://posts.specterops.io/certified-pre-owned-d95910965cd2#:~:text=Misconfigured%20Certificate%20Templates%20%E2%80%94%20ESC1' - }, - [VulnerableConfigurationItem]@{ - Name = 'ESC2' - Category = 'Escalation Path' - Subcategory = 'Misconfigured Certificate Templates' - Summary = '' - FindIt = {Find-ESC2} - FixIt = {Write-Output 'Add code to fix the vulnerable configuration.'} - ReferenceUrls = 'https://posts.specterops.io/certified-pre-owned-d95910965cd2#:~:text=Misconfigured%20Certificate%20Templates%20%E2%80%94%20ESC2' - }, - [VulnerableConfigurationItem]@{ - Name = 'ESC3' - Category = 'Escalation Path' - Subcategory = 'Enrollment Agent Templates' - Summary = '' - FindIt = { - Find-ESC3Condition1 - Find-ESC3Condition2 + [VulnerableConfigurationItem[]]$Dictionary = @( + [VulnerableConfigurationItem]@{ + Name = 'ESC1' + Category = 'Escalation Path' + Subcategory = 'Misconfigured Certificate Templates' + Summary = '' + FindIt = { Find-ESC1 } + FixIt = { Write-Output "Add code to fix the vulnerable configuration." } + ReferenceUrls = 'https://posts.specterops.io/certified-pre-owned-d95910965cd2#:~:text=Misconfigured%20Certificate%20Templates%20%E2%80%94%20ESC1' + }, + [VulnerableConfigurationItem]@{ + Name = 'ESC2' + Category = 'Escalation Path' + Subcategory = 'Misconfigured Certificate Templates' + Summary = '' + FindIt = { Find-ESC2 } + FixIt = { Write-Output 'Add code to fix the vulnerable configuration.' } + ReferenceUrls = 'https://posts.specterops.io/certified-pre-owned-d95910965cd2#:~:text=Misconfigured%20Certificate%20Templates%20%E2%80%94%20ESC2' + }, + [VulnerableConfigurationItem]@{ + Name = 'ESC3' + Category = 'Escalation Path' + Subcategory = 'Enrollment Agent Templates' + Summary = '' + FindIt = { + Find-ESC3Condition1 + Find-ESC3Condition2 + } + FixIt = { Write-Output 'Add code to fix the vulnerable configuration.' } + ReferenceUrls = 'https://posts.specterops.io/certified-pre-owned-d95910965cd2#:~:text=Enrollment%20Agent%20Templates%20%E2%80%94%20ESC3' + }, + [VulnerableConfigurationItem]@{ + Name = 'ESC4'; + Category = 'Escalation Path' + Subcategory = 'Vulnerable Certificate Template Access Control' + Summary = '' + FindIt = { Find-ESC4 } + FixIt = { Write-Output 'Add code to fix the vulnerable configuration.' } + ReferenceUrls = 'https://posts.specterops.io/certified-pre-owned-d95910965cd2#:~:text=Vulnerable%20Certificate%20Template%20Access%20Control%20%E2%80%94%20ESC4' + }, + [VulnerableConfigurationItem]@{ + Name = 'ESC5'; + Category = 'Escalation Path' + Subcategory = 'Vulnerable PKI Object Access Control' + Summary = '' + FindIt = { Find-ESC5 } + FixIt = { Write-Output 'Add code to fix the vulnerable configuration.' } + ReferenceUrls = 'https://posts.specterops.io/certified-pre-owned-d95910965cd2#:~:text=Vulnerable%20PKI%20Object%20Access%20Control%20%E2%80%94%20ESC5' + }, + [VulnerableConfigurationItem]@{ + Name = 'ESC6' + Category = 'Escalation Path' + Subcategory = 'EDITF_ATTRIBUTESUBJECTALTNAME2' + Summary = '' + FindIt = { Find-ESC6 } + FixIt = { Write-Output 'Add code to fix the vulnerable configuration.' } + ReferenceUrls = 'https://posts.specterops.io/certified-pre-owned-d95910965cd2#:~:text=EDITF_ATTRIBUTESUBJECTALTNAME2%20%E2%80%94%20ESC6' + }, + [VulnerableConfigurationItem]@{ + Name = 'ESC7' + Category = 'Escalation Path' + Subcategory = 'Vulnerable Certificate Authority Access Control' + Summary = '' + FindIt = { Write-Output 'We have not created Find-ESC7 yet.' } + 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' + }, + [VulnerableConfigurationItem]@{ + Name = 'ESC8' + Category = 'Escalation Path' + Subcategory = 'NTLM Relay to AD CS HTTP Endpoints' + Summary = '' + FindIt = { Find-ESC8 } + FixIt = { Write-Output 'Add code to fix the vulnerable configuration.' } + ReferenceUrls = 'https://posts.specterops.io/certified-pre-owned-d95910965cd2#:~:text=NTLM%20Relay%20to%20AD%20CS%20HTTP%20Endpoints' + }, + [VulnerableConfigurationItem]@{ + Name = 'Auditing' + Category = 'Server Configuration' + Subcategory = 'Gaps in auditing on certificate authorities and AD CS objects.' + Summary = '' + FindIt = { Find-AuditingIssue } + FixIt = { Write-Output 'Add code to fix the vulnerable configuration.' } + ReferenceUrls = @('https://github.com/TrimarcJake/Locksmith', 'https://techcommunity.microsoft.com/t5/ask-the-directory-services-team/designing-and-implementing-a-pki-part-i-design-and-planning/ba-p/396953') } - FixIt = {Write-Output 'Add code to fix the vulnerable configuration.'} - ReferenceUrls = 'https://posts.specterops.io/certified-pre-owned-d95910965cd2#:~:text=Enrollment%20Agent%20Templates%20%E2%80%94%20ESC3' - }, - [VulnerableConfigurationItem]@{ - Name = 'ESC4'; - Category = 'Escalation Path' - Subcategory = 'Vulnerable Certificate Template Access Control' - Summary = '' - FindIt = {Find-ESC4} - FixIt = {Write-Output 'Add code to fix the vulnerable configuration.'} - ReferenceUrls = 'https://posts.specterops.io/certified-pre-owned-d95910965cd2#:~:text=Vulnerable%20Certificate%20Template%20Access%20Control%20%E2%80%94%20ESC4' - }, - [VulnerableConfigurationItem]@{ - Name = 'ESC5'; - Category = 'Escalation Path' - Subcategory = 'Vulnerable PKI Object Access Control' - Summary = '' - FindIt = {Find-ESC5} - FixIt = {Write-Output 'Add code to fix the vulnerable configuration.'} - ReferenceUrls = 'https://posts.specterops.io/certified-pre-owned-d95910965cd2#:~:text=Vulnerable%20PKI%20Object%20Access%20Control%20%E2%80%94%20ESC5' - }, - [VulnerableConfigurationItem]@{ - Name = 'ESC6' - Category = 'Escalation Path' - Subcategory = 'EDITF_ATTRIBUTESUBJECTALTNAME2' - Summary = '' - FindIt = {Find-ESC6} - FixIt = {Write-Output 'Add code to fix the vulnerable configuration.'} - ReferenceUrls = 'https://posts.specterops.io/certified-pre-owned-d95910965cd2#:~:text=EDITF_ATTRIBUTESUBJECTALTNAME2%20%E2%80%94%20ESC6' - }, - [VulnerableConfigurationItem]@{ - Name = 'ESC7' - Category = 'Escalation Path' - Subcategory = 'Vulnerable Certificate Authority Access Control' - Summary = '' - FindIt = {Write-Output 'We have not created Find-ESC7 yet.'} - 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' - }, - [VulnerableConfigurationItem]@{ - Name = 'ESC8' - Category = 'Escalation Path' - Subcategory = 'NTLM Relay to AD CS HTTP Endpoints' - Summary = '' - FindIt = {Find-ESC8} - FixIt = {Write-Output 'Add code to fix the vulnerable configuration.'} - ReferenceUrls = 'https://posts.specterops.io/certified-pre-owned-d95910965cd2#:~:text=NTLM%20Relay%20to%20AD%20CS%20HTTP%20Endpoints' - }, - [VulnerableConfigurationItem]@{ - Name = 'Auditing' - Category = 'Server Configuration' - Subcategory = 'Gaps in auditing on certificate authorities and AD CS objects.' - Summary = '' - FindIt = {Find-AuditingIssue} - FixIt = {Write-Output 'Add code to fix the vulnerable configuration.'} - ReferenceUrls = @('https://github.com/TrimarcJake/Locksmith','https://techcommunity.microsoft.com/t5/ask-the-directory-services-team/designing-and-implementing-a-pki-part-i-design-and-planning/ba-p/396953') - } -) -Return $Dictionary + ) + Return $Dictionary } function New-OutputPath { @@ -1200,7 +858,6 @@ function New-OutputPath { New-Item -Path $ForestPath -ItemType Directory -Force | Out-Null } } - function Set-AdditionalCAProperty { [CmdletBinding(SupportsShouldProcess)] param ( @@ -1219,7 +876,8 @@ function Set-AdditionalCAProperty { if ($Credential) { $CAHostDistinguishedName = (Get-ADObject -Filter { (Name -eq $CAHostName) -and (objectclass -eq 'computer') } -Server $ForestGC -Credential $Credential).DistinguishedName $CAHostFQDN = (Get-ADObject -Filter { (Name -eq $CAHostName) -and (objectclass -eq 'computer') } -Properties DnsHostname -Server $ForestGC -Credential $Credential).DnsHostname - } else { + } + else { $CAHostDistinguishedName = (Get-ADObject -Filter { (Name -eq $CAHostName) -and (objectclass -eq 'computer') } -Server $ForestGC ).DistinguishedName $CAHostFQDN = (Get-ADObject -Filter { (Name -eq $CAHostName) -and (objectclass -eq 'computer') } -Properties DnsHostname -Server $ForestGC).DnsHostname } @@ -1228,22 +886,27 @@ function Set-AdditionalCAProperty { try { if ($Credential) { $CertutilAudit = Invoke-Command -ComputerName $CAHostname -Credential $Credential -ScriptBlock { param($CAFullName); certutil -config $CAFullName -getreg CA\AuditFilter } -ArgumentList $CAFullName - } else { + } + else { $CertutilAudit = certutil -config $CAFullName -getreg CA\AuditFilter } - } catch { + } + catch { $AuditFilter = 'Failure' } try { if ($Credential) { $CertutilFlag = Invoke-Command -ComputerName $CAHostname -Credential $Credential -ScriptBlock { param($CAFullName); certutil -config $CAFullName -getreg policy\EditFlags } -ArgumentList $CAFullName - } else { + } + else { $CertutilFlag = certutil -config $CAFullName -getreg policy\EditFlags } - } catch { + } + catch { $AuditFilter = 'Failure' } - } else { + } + else { $AuditFilter = 'CA Unavailable' $SANFlag = 'CA Unavailable' } @@ -1251,11 +914,13 @@ function Set-AdditionalCAProperty { try { [string]$AuditFilter = $CertutilAudit | Select-String 'AuditFilter REG_DWORD = ' | Select-String '\(' $AuditFilter = $AuditFilter.split('(')[1].split(')')[0] - } catch { + } + catch { try { [string]$AuditFilter = $CertutilAudit | Select-String 'AuditFilter REG_DWORD = ' $AuditFilter = $AuditFilter.split('=')[1].trim() - } catch { + } + catch { $AuditFilter = 'Never Configured' } } @@ -1264,7 +929,8 @@ function Set-AdditionalCAProperty { [string]$SANFlag = $CertutilFlag | Select-String ' EDITF_ATTRIBUTESUBJECTALTNAME2 -- 40000 \(' if ($SANFlag) { $SANFlag = 'Yes' - } else { + } + else { $SANFlag = 'No' } } @@ -1277,7 +943,6 @@ function Set-AdditionalCAProperty { } } } - function Set-Severity { [CmdletBinding()] param( @@ -1306,9 +971,10 @@ function Set-Severity { if (($SID -notmatch $SafeUsers -and $SID -notmatch $SafeOwners) -and ($Finding.ActiveDirectoryRights -match $DangerousRights)) { return 'Critical' } - } catch { + } + catch { Write-Error "Could not determine issue severity for issue: $($Issue.Issue)" - return 'Unknown Failure' + return 'Unknown Failure' } } } @@ -1329,7 +995,7 @@ function Test-IsADAdmin { ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole("Domain Admin") -or ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole("Administrators") -or ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole("Enterprise Admins") - ) { + ) { Return $true } else { @@ -1359,7 +1025,6 @@ function Test-IsElevated { $principal = New-Object Security.Principal.WindowsPrincipal $identity $principal.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) } - function Test-IsLocalAccountSession { <# .SYNOPSIS @@ -1380,4 +1045,421 @@ function Test-IsLocalAccountSession { } } +function Invoke-Locksmith { + <# + .SYNOPSIS + Finds the most common malconfigurations of Active Directory Certificate Services (AD CS). + + .DESCRIPTION + Locksmith uses the Active Directory (AD) Powershell (PS) module to identify 6 misconfigurations + commonly found in Enterprise mode AD CS installations. + + .COMPONENT + Locksmith requires the AD PS module to be installed in the scope of the Current User. + If Locksmith does not identify the AD PS module as installed, it will attempt to + install the module. If module installation does not complete successfully, + Locksmith will fail. + + .PARAMETER Mode + Specifies sets of common script execution modes. + + -Mode 0 + Finds any malconfigurations and displays them in the console. + No attempt is made to fix identified issues. + + -Mode 1 + Finds any malconfigurations and displays them in the console. + Displays example Powershell snippet that can be used to resolve the issue. + No attempt is made to fix identified issues. + + -Mode 2 + Finds any malconfigurations and writes them to a series of CSV files. + No attempt is made to fix identified issues. + + -Mode 3 + Finds any malconfigurations and writes them to a series of CSV files. + Creates code snippets to fix each issue and writes them to an environment-specific custom .PS1 file. + No attempt is made to fix identified issues. + + -Mode 4 + Finds any malconfigurations and creates code snippets to fix each issue. + Attempts to fix all identified issues. This mode may require high-privileged access. + + .PARAMETER Scans + Specify which scans you want to run. Available scans: 'All' or Auditing, ESC1, ESC2, ESC3, ESC4, ESC5, ESC6, ESC8, or 'PromptMe' + + -Scans All + Run all scans (default) + + -Scans PromptMe + Presents a grid view of the available scan types that can be selected and run them after you click OK. + + .PARAMETER OutputPath + Specify the path where you want to save reports and mitigation scripts. + + .INPUTS + None. You cannot pipe objects to Invoke-Locksmith.ps1. + + .OUTPUTS + Output types: + 1. Console display of identified issues + 2. Console display of identified issues and their fixes + 3. CSV containing all identified issues + 4. CSV containing all identified issues and their fixes + + .NOTES + Windows PowerShell cmdlet Restart-Service requires RunAsAdministrator + #> + + [CmdletBinding()] + param ( + [string]$Forest, + [string]$InputPath, + [int]$Mode = 0, + [Parameter()] + [ValidateSet('Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC8', 'All', 'PromptMe')] + [array]$Scans = 'All', + [string]$OutputPath = (Get-Location).Path, + [System.Management.Automation.PSCredential]$Credential + ) + + $Version = '2023.12' + $LogoPart1 = @" + _ _____ _______ _ _ _______ _______ _____ _______ _ _ + | | | | |____/ |______ | | | | | |_____| + |_____ |_____| |_____ | \_ ______| | | | __|__ | | | +"@ + $LogoPart2 = @" + .--. .--. .--. + /.-. '----------. /.-. '----------. /.-. '----------. + \'-' .---'-''-'-' \'-' .--'--''-'-' \'-' .--'--'-''-' + '--' '--' '--' +"@ + $VersionBanner = " v$Version" + + Write-Host $LogoPart1 -ForegroundColor Magenta + Write-Host $LogoPart2 -ForegroundColor White + Write-Host $VersionBanner -ForegroundColor Red + + # Check if ActiveDirectory PowerShell module is available, and attempt to install if not found + if (-not(Get-Module -Name 'ActiveDirectory' -ListAvailable)) { + if (Test-IsElevated) { + $OS = (Get-CimInstance -ClassName Win32_OperatingSystem).ProductType + # 1 - workstation, 2 - domain controller, 3 - non-dc server + if ($OS -gt 1) { + # Attempt to install ActiveDirectory PowerShell module for Windows Server OSes, works with Windows Server 2012 R2 through Windows Server 2022 + Install-WindowsFeature -Name RSAT-AD-PowerShell + } + else { + # Attempt to install ActiveDirectory PowerShell module for Windows Desktop OSes + Add-WindowsCapability -Name Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0 -Online + } + } + else { + Write-Warning -Message "The ActiveDirectory PowerShell module is required for Locksmith, but is not installed. Please launch an elevated PowerShell session to have this module installed for you automatically." + # The goal here is to exit the script without closing the PowerShell window. Need to test. + Return + } + } + + # Exit if running in restricted admin mode without explicit credentials + if (!$Credential -and (Get-RestrictedAdminModeSetting)) { + Write-Warning "Restricted Admin Mode appears to be in place, re-run with the '-Credential domain\user' option" + break; + } + + # Initial variables + $AllDomainsCertPublishersSIDs = @() + $AllDomainsDomainAdminSIDs = @() + $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' + $DangerousRights = 'GenericAll|WriteDacl|WriteOwner|WriteProperty' + $EnrollmentAgentEKU = '1\.3\.6\.1\.4\.1\.311\.20\.2\.1' + $SafeObjectTypes = '0e10c968-78fb-11d2-90d4-00c04f79dc55|a05b8cc2-17bc-4802-a710-e7c15ab866a2' + $SafeOwners = '-512$|-519$|-544$|-18$|-517$|-500$' + $SafeUsers = '-512$|-519$|-544$|-18$|-517$|-500$|-516$|-9$|-526$|-527$|S-1-5-10' + $UnsafeOwners = 'S-1-1-0|-11$|-513$|-515$' + $UnsafeUsers = 'S-1-1-0|-11$|-513$|-515$' + + # Generated variables + $Dictionary = New-Dictionary + $ForestGC = $(Get-ADDomainController -Discover -Service GlobalCatalog -ForceDiscover | Select-Object -ExpandProperty Hostname) + ":3268" + $DNSRoot = [string]((Get-ADForest).RootDomain | Get-ADDomain).DNSRoot + $EnterpriseAdminsSID = ([string]((Get-ADForest).RootDomain | Get-ADDomain).DomainSID) + '-519' + $PreferredOwner = New-Object System.Security.Principal.SecurityIdentifier($EnterpriseAdminsSID) + $DomainSIDs = (Get-ADForest).Domains | ForEach-Object { (Get-ADDomain $_).DomainSID.Value } + $DomainSIDs | ForEach-Object { + $AllDomainsCertPublishersSIDs += $_ + '-517' + $AllDomainsDomainAdminSIDs += $_ + '-512' + } + + # Add SIDs of (probably) Safe Users to $SafeUsers + Get-ADGroupMember $EnterpriseAdminsSID | ForEach-Object { + $SafeUsers += '|' + $_.SID.Value + } + + (Get-ADForest).Domains | ForEach-Object { + $DomainSID = (Get-ADDomain $_).DomainSID.Value + $SafeGroupRIDs = @('-517', '-512') + $SafeGroupSIDs = @('S-1-5-32-544') + foreach ($rid in $SafeGroupRIDs ) { + $SafeGroupSIDs += $DomainSID + $rid + } + foreach ($sid in $SafeGroupSIDs) { + $users += (Get-ADGroupMember $sid -Server $_ -Recursive).SID.Value + } + foreach ($user in $users) { + $SafeUsers += '|' + $user + } + } + + if ($Credential) { + $Targets = Get-Target -Credential $Credential + } + else { + $Targets = Get-Target + } + + Write-Host "Gathering AD CS Objects from $($Targets)..." + if ($Credential) { + $ADCSObjects = Get-ADCSObject -Targets $Targets -Credential $Credential + Set-AdditionalCAProperty -ADCSObjects $ADCSObjects -Credential $Credential + $ADCSObjects += Get-CAHostObject -ADCSObjects $ADCSObjects -Credential $Credential + $CAHosts = Get-CAHostObject -ADCSObjects $ADCSObjects -Credential $Credential + $CAHosts | ForEach-Object { $SafeUsers += '|' + $_.Name } + } + else { + $ADCSObjects = Get-ADCSObject -Targets $Targets + Set-AdditionalCAProperty -ADCSObjects $ADCSObjects + $ADCSObjects += Get-CAHostObject -ADCSObjects $ADCSObjects + $CAHosts = Get-CAHostObject -ADCSObjects $ADCSObjects + $CAHosts | ForEach-Object { $SafeUsers += '|' + $_.Name } + } + + if ( $Scans ) { + # If the Scans parameter was used, Invoke-Scans with the specified checks. + $Results = Invoke-Scans -Scans $Scans + # Re-hydrate the findings arrays from the Results hash table + $AllIssues = $Results['AllIssues'] + $AuditingIssues = $Results['AuditingIssues'] + $ESC1 = $Results['ESC1'] + $ESC2 = $Results['ESC2'] + $ESC3 = $Results['ESC3'] + $ESC4 = $Results['ESC4'] + $ESC5 = $Results['ESC5'] + $ESC6 = $Results['ESC6'] + $ESC8 = $Results['ESC8'] + } + + # If these are all empty = no issues found, exit + if ($null -eq $Results) { + Write-Host "`n$(Get-Date) : No ADCS issues were found.`n" -ForegroundColor Green + Write-Host 'Thank you for using ' -NoNewline + Write-Host "❤ Locksmith ❤ `n" -ForegroundColor Magenta + break + } + + switch ($Mode) { + 0 { + Format-Result $AuditingIssues '0' + Format-Result $ESC1 '0' + Format-Result $ESC2 '0' + Format-Result $ESC3 '0' + Format-Result $ESC4 '0' + Format-Result $ESC5 '0' + Format-Result $ESC6 '0' + Format-Result $ESC8 '0' + } + 1 { + Format-Result $AuditingIssues '1' + Format-Result $ESC1 '1' + Format-Result $ESC2 '1' + Format-Result $ESC3 '1' + Format-Result $ESC4 '1' + Format-Result $ESC5 '1' + Format-Result $ESC6 '1' + Format-Result $ESC8 '1' + } + 2 { + $Output = 'ADCSIssues.CSV' + Write-Host "Writing AD CS issues to $Output..." + try { + $AllIssues | Select-Object Forest, Technique, Name, Issue | Export-Csv -NoTypeInformation $Output + Write-Host "$Output created successfully!`n" + } + catch { + Write-Host 'Ope! Something broke.' + } + } + 3 { + $Output = 'ADCSRemediation.CSV' + Write-Host "Writing AD CS issues to $Output..." + try { + $AllIssues | Select-Object Forest, Technique, Name, DistinguishedName, Issue, Fix | Export-Csv -NoTypeInformation $Output + Write-Host "$Output created successfully!`n" + } + catch { + Write-Host 'Ope! Something broke.' + } + } + 4 { + Write-Host "`nExecuting Mode 4 - Attempting to fix all identified issues!`n" -ForegroundColor Green + Write-Host 'Creating a script (' -NoNewline + Write-Host 'Invoke-RevertLocksmith.ps1' -ForegroundColor White -NoNewline + Write-Host ") which can be used to revert any changes made by Locksmith...`n" + try { + Export-RevertScript -AuditingIssues $AuditingIssues -ESC1 $ESC1 -ESC2 $ESC2 -ESC6 $ESC6 + } + catch { + } + if ($AuditingIssues) { + $AuditingIssues | ForEach-Object { + $FixBlock = [scriptblock]::Create($_.Fix) + Write-Host 'ISSUE:' -ForegroundColor White + Write-Host "Auditing is not fully enabled on Certification Authority `"$($_.Name)`".`n" + Write-Host 'TECHNIQUE:' -ForegroundColor White + Write-Host "$($_.Technique)`n" + Write-Host 'ACTION TO BE PEFORMED:' -ForegroundColor White + Write-Host "Locksmith will attempt to fully enable auditing on Certification Authority `"$($_.Name)`".`n" + Write-Host 'COMMAND(S) TO BE RUN:' + Write-Host 'PS> ' -NoNewline + Write-Host "$($_.Fix)`n" -ForegroundColor Cyan + Write-Host 'OPERATIONAL IMPACT:' -ForegroundColor White + Write-Host "This change should have little to no impact on the AD CS environment.`n" -ForegroundColor Green + Write-Host "If you continue, Locksmith will attempt to fix this issue.`n" -ForegroundColor Yellow + Write-Host "Continue with this operation? [Y] Yes " -NoNewline + Write-Host "[N] " -ForegroundColor Yellow -NoNewline + Write-Host "No: " -NoNewline + $WarningError = '' + $WarningError = Read-Host + if ($WarningError -like 'y') { + try { + Invoke-Command -ScriptBlock $FixBlock + } + catch { + Write-Error 'Could not modify AD CS auditing. Are you a local admin on the CA host?' + } + } + else { + Write-Host "SKIPPED!`n" -ForegroundColor Yellow + } + } + } + if ($ESC1) { + $ESC1 | ForEach-Object { + $FixBlock = [scriptblock]::Create($_.Fix) + Write-Host 'ISSUE:' -ForegroundColor White + Write-Host "Security Principals can enroll in `"$($_.Name)`" template using a Subject Alternative Name without Manager Approval.`n" + Write-Host 'TECHNIQUE:' -ForegroundColor White + Write-Host "$($_.Technique)`n" + Write-Host 'ACTION TO BE PEFORMED:' -ForegroundColor White + Write-Host "Locksmith will attempt to enable Manager Approval on the `"$($_.Name)`" template.`n" + Write-Host 'CCOMMAND(S) TO BE RUN:' + Write-Host 'PS> ' -NoNewline + Write-Host "$($_.Fix)`n" -ForegroundColor Cyan + Write-Host 'OPERATIONAL IMPACT:' -ForegroundColor White + Write-Host "WARNING: This change could cause some services to stop working until certificates are approved.`n" -ForegroundColor Yellow + Write-Host "If you continue, Locksmith will attempt to fix this issue.`n" -ForegroundColor Yellow + Write-Host "Continue with this operation? [Y] Yes " -NoNewline + Write-Host "[N] " -ForegroundColor Yellow -NoNewline + Write-Host "No: " -NoNewline + $WarningError = '' + $WarningError = Read-Host + if ($WarningError -like 'y') { + try { + Invoke-Command -ScriptBlock $FixBlock + } + catch { + Write-Error 'Could not enable Manager Approval. Are you an Active Directory or AD CS admin?' + } + } + else { + Write-Host "SKIPPED!`n" -ForegroundColor Yellow + } + } + } + if ($ESC2) { + $ESC2 | ForEach-Object { + $FixBlock = [scriptblock]::Create($_.Fix) + Write-Host 'ISSUE:' -ForegroundColor White + Write-Host "Security Principals can enroll in `"$($_.Name)`" template and create a Subordinate Certification Authority without Manager Approval.`n" + Write-Host 'TECHNIQUE:' -ForegroundColor White + Write-Host "$($_.Technique)`n" + Write-Host 'ACTION TO BE PEFORMED:' -ForegroundColor White + Write-Host "Locksmith will attempt to enable Manager Approval on the `"$($_.Name)`" template.`n" + Write-Host 'COMMAND(S) TO BE RUN:' -ForegroundColor White + Write-Host 'PS> ' -NoNewline + Write-Host "$($_.Fix)`n" -ForegroundColor Cyan + Write-Host 'OPERATIONAL IMPACT:' -ForegroundColor White + Write-Host "WARNING: This change could cause some services to stop working until certificates are approved.`n" -ForegroundColor Yellow + Write-Host "If you continue, Locksmith will attempt to fix this issue.`n" -ForegroundColor Yellow + Write-Host "Continue with this operation? [Y] Yes " -NoNewline + Write-Host "[N] " -ForegroundColor Yellow -NoNewline + Write-Host "No: " -NoNewline + $WarningError = '' + $WarningError = Read-Host + if ($WarningError -like 'y') { + try { + Invoke-Command -ScriptBlock $FixBlock + } + catch { + Write-Error 'Could not enable Manager Approval. Are you an Active Directory or AD CS admin?' + } + } + else { + Write-Host "SKIPPED!`n" -ForegroundColor Yellow + } + } + } + if ($ESC6) { + $ESC6 | ForEach-Object { + $FixBlock = [scriptblock]::Create($_.Fix) + Write-Host 'ISSUE:' -ForegroundColor White + Write-Host "The Certification Authority `"$($_.Name)`" has the dangerous EDITF_ATTRIBUTESUBJECTALTNAME2 flag enabled.`n" + Write-Host 'TECHNIQUE:' -ForegroundColor White + Write-Host "$($_.Technique)`n" + Write-Host 'ACTION TO BE PEFORMED:' -ForegroundColor White + Write-Host "Locksmith will attempt to disable the EDITF_ATTRIBUTESUBJECTALTNAME2 flag on Certifiction Authority `"$($_.Name)`".`n" + Write-Host 'COMMAND(S) TO BE RUN' -ForegroundColor White + Write-Host 'PS> ' -NoNewline + Write-Host "$($_.Fix)`n" -ForegroundColor Cyan + $WarningError = 'n' + Write-Host 'OPERATIONAL IMPACT:' -ForegroundColor White + Write-Host "WARNING: This change could cause some services to stop working.`n" -ForegroundColor Yellow + Write-Host "If you continue, Locksmith will attempt to fix this issue.`n" -ForegroundColor Yellow + Write-Host "Continue with this operation? [Y] Yes " -NoNewline + Write-Host "[N] " -ForegroundColor Yellow -NoNewline + Write-Host "No: " -NoNewline + $WarningError = '' + $WarningError = Read-Host + if ($WarningError -like 'y') { + try { + Invoke-Command -ScriptBlock $FixBlock + } + catch { + Write-Error 'Could not disable the EDITF_ATTRIBUTESUBJECTALTNAME2 flag. Are you an Active Directory or AD CS admin?' + } + } + else { + Write-Host "SKIPPED!`n" -ForegroundColor Yellow + } + } + } + + Write-Host "Mode 4 Complete! There are no more issues that Locksmith can automatically resolve.`n" -ForegroundColor Green + Write-Host 'If you experience any operational impact from using Locksmith Mode 4, use ' -NoNewline + Write-Host 'Invoke-RevertLocksmith.ps1 ' -ForegroundColor White + Write-Host "to revert all changes made by Locksmith. It can be found in the current working directory.`n" + Write-Host @" +REMINDER: Locksmith cannot automatically resolve all AD CS issues at this time. +There may be more AD CS issues remaining in your environment. +Use Locksmith in Modes 0-3 to further investigate your environment +or reach out to the Locksmith team for assistance. We'd love to help`n +"@ -ForegroundColor Yellow + } + } + Write-Host 'Thank you for using ' -NoNewline + Write-Host "❤ Locksmith ❤`n" -ForegroundColor Magenta +} + + Invoke-Locksmith -Mode $Mode -Scans $Scans diff --git a/Locksmith.psd1 b/Locksmith.psd1 index 9370d37..726b94c 100644 --- a/Locksmith.psd1 +++ b/Locksmith.psd1 @@ -1,4 +1,4 @@ -@{ +@{ AliasesToExport = @('*') Author = 'Jake Hildreth' CmdletsToExport = @() @@ -7,14 +7,15 @@ Description = 'A small tool to find and fix common misconfigurations in Active Directory Certificate Services.' FunctionsToExport = @('*') GUID = 'b1325b42-8dc4-4f17-aa1f-dcb5984ca14a' - ModuleVersion = '2023.11' + ModuleVersion = '2023.12' PowerShellVersion = '5.1' PrivateData = @{ PSData = @{ - ExternalModuleDependencies = @('ActiveDirectory', 'ServerManager', 'Microsoft.PowerShell.Utility', 'Microsoft.PowerShell.LocalAccounts', 'Microsoft.PowerShell.Utility', 'Microsoft.PowerShell.Management', 'CimCmdlets', 'Dism') Tags = @('Windows', 'Locksmith', 'CA', 'PKI', 'ActiveDirectory', 'CertificateServices', 'ADCS') + ProjectUri = 'https://github.com/TrimarcJake/Locksmith' + ExternalModuleDependencies = @('ActiveDirectory', 'ServerManager', 'Microsoft.PowerShell.Utility', 'Microsoft.PowerShell.LocalAccounts', 'Microsoft.PowerShell.Management', 'CimCmdlets', 'Dism') } } - RequiredModules = @('ActiveDirectory', 'ServerManager', 'Microsoft.PowerShell.Utility', 'Microsoft.PowerShell.LocalAccounts', 'Microsoft.PowerShell.Utility', 'Microsoft.PowerShell.Management', 'CimCmdlets', 'Dism') + RequiredModules = @('ActiveDirectory', 'ServerManager', 'Microsoft.PowerShell.Utility', 'Microsoft.PowerShell.LocalAccounts', 'Microsoft.PowerShell.Management', 'CimCmdlets', 'Dism') RootModule = 'Locksmith.psm1' } \ No newline at end of file diff --git a/Locksmith.psm1 b/Locksmith.psm1 index 0869fc0..f9b4d34 100644 --- a/Locksmith.psm1 +++ b/Locksmith.psm1 @@ -1,4 +1,4 @@ -# Get public and private function definition files. +# Get public and private function definition files. $Public = @( Get-ChildItem -Path $PSScriptRoot\Public\*.ps1 -ErrorAction SilentlyContinue -Recurse ) $Private = @( Get-ChildItem -Path $PSScriptRoot\Private\*.ps1 -ErrorAction SilentlyContinue -Recurse ) $Classes = @( Get-ChildItem -Path $PSScriptRoot\Classes\*.ps1 -ErrorAction SilentlyContinue -Recurse ) diff --git a/Private/Find-AuditingIssue.ps1 b/Private/Find-AuditingIssue.ps1 index 2f8c082..560f056 100644 --- a/Private/Find-AuditingIssue.ps1 +++ b/Private/Find-AuditingIssue.ps1 @@ -19,7 +19,7 @@ $Issue | Add-Member -MemberType NoteProperty -Name 'Technique' -Value 'DETECT' -Force } else { - $Issue | Add-Member -MemberType NoteProperty -Name 'Issue' -Value "Auditing is not fully enabled. Current value is $($_.AuditFilter)" -Force + $Issue | Add-Member -MemberType NoteProperty -Name 'Issue' -Value "Auditing is not fully enabled on $($_.CAFullName). Current value is $($_.AuditFilter)" -Force $Issue | Add-Member -MemberType NoteProperty -Name 'Fix' ` -Value "certutil.exe -config `'$($_.CAFullname)`' -setreg `'CA\AuditFilter`' 127; Invoke-Command -ComputerName `'$($_.dNSHostName)`' -ScriptBlock { Get-Service -Name `'certsvc`' | Restart-Service -Force }" -Force $Issue | Add-Member -MemberType NoteProperty -Name 'Revert' ` diff --git a/Private/Find-ESC1.ps1 b/Private/Find-ESC1.ps1 index bbc99ac..f311a75 100644 --- a/Private/Find-ESC1.ps1 +++ b/Private/Find-ESC1.ps1 @@ -10,7 +10,7 @@ ($_.objectClass -eq 'pKICertificateTemplate') -and ($_.pkiExtendedKeyUsage -match $ClientAuthEKUs) -and ($_.'msPKI-Certificate-Name-Flag' -eq 1) -and - ($_.'msPKI-Enrollment-Flag' -ne 2) -and + !($_.'msPKI-Enrollment-Flag' -band 2) -and ( ($_.'msPKI-RA-Signature' -eq 0) -or ($null -eq $_.'msPKI-RA-Signature') ) } | ForEach-Object { foreach ($entry in $_.nTSecurityDescriptor.Access) { diff --git a/Private/Find-ESC2.ps1 b/Private/Find-ESC2.ps1 index 919f9df..b23d712 100644 --- a/Private/Find-ESC2.ps1 +++ b/Private/Find-ESC2.ps1 @@ -10,7 +10,7 @@ ($_.ObjectClass -eq 'pKICertificateTemplate') -and ( (!$_.pkiExtendedKeyUsage) -or ($_.pkiExtendedKeyUsage -match '2.5.29.37.0') )-and ($_.'msPKI-Certificate-Name-Flag' -eq 1) -and - ($_.'msPKI-Enrollment-Flag' -ne 2) -and + !($_.'msPKI-Enrollment-Flag' -band 2) -and ( ($_.'msPKI-RA-Signature' -eq 0) -or ($null -eq $_.'msPKI-RA-Signature') ) } | ForEach-Object { foreach ($entry in $_.nTSecurityDescriptor.Access) { @@ -40,4 +40,4 @@ } } } -} \ No newline at end of file +} diff --git a/Private/Find-ESC3Condition1.ps1 b/Private/Find-ESC3Condition1.ps1 index a19b484..15b4b4e 100644 --- a/Private/Find-ESC3Condition1.ps1 +++ b/Private/Find-ESC3Condition1.ps1 @@ -9,7 +9,7 @@ $ADCSObjects | Where-Object { ($_.objectClass -eq 'pKICertificateTemplate') -and ($_.pkiExtendedKeyUsage -match $EnrollmentAgentEKU) -and - ($_.'msPKI-Enrollment-Flag' -ne 2) -and + !($_.'msPKI-Enrollment-Flag' -band 2) -and ( ($_.'msPKI-RA-Signature' -eq 0) -or ($null -eq $_.'msPKI-RA-Signature') ) } | ForEach-Object { foreach ($entry in $_.nTSecurityDescriptor.Access) { @@ -39,4 +39,4 @@ } } } -} \ No newline at end of file +} diff --git a/Private/Find-ESC3Condition2.ps1 b/Private/Find-ESC3Condition2.ps1 index 5b8ae0a..cf3680f 100644 --- a/Private/Find-ESC3Condition2.ps1 +++ b/Private/Find-ESC3Condition2.ps1 @@ -10,7 +10,7 @@ ($_.objectClass -eq 'pKICertificateTemplate') -and ($_.pkiExtendedKeyUsage -match $ClientAuthEKU) -and ($_.'msPKI-Certificate-Name-Flag' -eq 1) -and - ($_.'msPKI-Enrollment-Flag' -ne 2) -and + !($_.'msPKI-Enrollment-Flag' -band 2) -and ($_.'msPKI-RA-Application-Policies' -eq '1.3.6.1.4.1.311.20.2.1') -and ( ($_.'msPKI-RA-Signature' -eq 1) ) } | ForEach-Object { @@ -41,4 +41,4 @@ } } } -} \ No newline at end of file +} diff --git a/Private/Find-ESC4.ps1 b/Private/Find-ESC4.ps1 index 129fdbe..bf66bfe 100644 --- a/Private/Find-ESC4.ps1 +++ b/Private/Find-ESC4.ps1 @@ -17,7 +17,8 @@ } else { $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value } - if ( ($_.objectClass -eq 'pKICertificateTemplate') -and ($SID -notmatch $SafeOwners) ) { + + if ( ($_.objectClass -eq 'pKICertificateTemplate') -and ($SID -match $UnsafeOwners) ) { $Issue = New-Object -TypeName pscustomobject $Issue | Add-Member -MemberType NoteProperty -Name Forest -Value $_.CanonicalName.split('/')[0] -Force $Issue | Add-Member -MemberType NoteProperty -Name Name -Value $_.Name -Force @@ -26,14 +27,13 @@ $Issue | Add-Member -MemberType NoteProperty -Name ActiveDirectoryRights -Value $entry.ActiveDirectoryRights -Force $Issue | Add-Member -MemberType NoteProperty -Name Issue ` -Value "$($_.nTSecurityDescriptor.Owner) has Owner rights on this template" -Force - $Issue | Add-Member -MemberType NoteProperty -Name Fix -Value '[TODO]' -Force - $Issue | Add-Member -MemberType NoteProperty -Name Revert -Value '[TODO]' -Force + $Issue | Add-Member -MemberType NoteProperty -Name Fix -Value "`$Owner = New-Object System.Security.Principal.SecurityIdentifier(`'$PreferredOwner`'); `$ACL = Get-Acl -Path `'AD:$($_.DistinguishedName)`'; `$ACL.SetOwner(`$Owner); Set-ACL -Path `'AD:$($_.DistinguishedName)`' -AclObject `$ACL" -Force + $Issue | Add-Member -MemberType NoteProperty -Name Revert -Value "`$Owner = New-Object System.Security.Principal.SecurityIdentifier(`'$($_.nTSecurityDescriptor.Owner)`'); `$ACL = Get-Acl -Path `'AD:$($_.DistinguishedName)`'; `$ACL.SetOwner(`$Owner); Set-ACL -Path `'AD:$($_.DistinguishedName)`' -AclObject `$ACL" -Force $Issue | Add-Member -MemberType NoteProperty -Name Technique -Value 'ESC4' $Severity = Set-Severity -Issue $Issue $Issue | Add-Member -MemberType NoteProperty -Name Severity -Value $Severity $Issue - } - if ( ($_.objectClass -eq 'pKICertificateTemplate') -and ($SID -match $UnsafeOwners) ) { + } elseif ( ($_.objectClass -eq 'pKICertificateTemplate') -and ($SID -notmatch $SafeOwners) ) { $Issue = New-Object -TypeName pscustomobject $Issue | Add-Member -MemberType NoteProperty -Name Forest -Value $_.CanonicalName.split('/')[0] -Force $Issue | Add-Member -MemberType NoteProperty -Name Name -Value $_.Name -Force @@ -42,13 +42,14 @@ $Issue | Add-Member -MemberType NoteProperty -Name ActiveDirectoryRights -Value $entry.ActiveDirectoryRights -Force $Issue | Add-Member -MemberType NoteProperty -Name Issue ` -Value "$($_.nTSecurityDescriptor.Owner) has Owner rights on this template" -Force - $Issue | Add-Member -MemberType NoteProperty -Name Fix -Value "`$Owner = New-Object System.Security.Principal.SecurityIdentifier(`'$PreferredOwner`'); `$ACL = Get-Acl -Path `'AD:$($_.DistinguishedName)`'; `$ACL.SetOwner(`$Owner); Set-ACL -Path `'AD:$($_.DistinguishedName)`' -AclObject `$ACL" -Force - $Issue | Add-Member -MemberType NoteProperty -Name Revert -Value "`$Owner = New-Object System.Security.Principal.SecurityIdentifier(`'$($_.nTSecurityDescriptor.Owner)`'); `$ACL = Get-Acl -Path `'AD:$($_.DistinguishedName)`'; `$ACL.SetOwner(`$Owner); Set-ACL -Path `'AD:$($_.DistinguishedName)`' -AclObject `$ACL" -Force + $Issue | Add-Member -MemberType NoteProperty -Name Fix -Value '[TODO]' -Force + $Issue | Add-Member -MemberType NoteProperty -Name Revert -Value '[TODO]' -Force $Issue | Add-Member -MemberType NoteProperty -Name Technique -Value 'ESC4' $Severity = Set-Severity -Issue $Issue $Issue | Add-Member -MemberType NoteProperty -Name Severity -Value $Severity $Issue } + foreach ($entry in $_.nTSecurityDescriptor.Access) { $Principal = New-Object System.Security.Principal.NTAccount($entry.IdentityReference) if ($Principal -match '^(S-1|O:)') { @@ -78,4 +79,4 @@ } } } -} \ No newline at end of file +} diff --git a/Private/Find-ESC5.ps1 b/Private/Find-ESC5.ps1 index cddfd9f..8471086 100644 --- a/Private/Find-ESC5.ps1 +++ b/Private/Find-ESC5.ps1 @@ -17,10 +17,7 @@ } else { $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value } - if ( ($_.objectClass -ne 'pKICertificateTemplate') -and - ($SID -notmatch $SafeOwners) -and - ($entry.ActiveDirectoryRights.ObjectType -notmatch $SafeObjectTypes) - ) { + if ( ($_.objectClass -ne 'pKICertificateTemplate') -and ($SID -match $UnsafeOwners) ) { $Issue = New-Object -TypeName pscustomobject $Issue | Add-Member -MemberType NoteProperty -Name Forest -Value $_.CanonicalName.split('/')[0] -Force $Issue | Add-Member -MemberType NoteProperty -Name Name -Value $_.Name -Force @@ -28,15 +25,17 @@ $Issue | Add-Member -MemberType NoteProperty -Name IdentityReference -Value $entry.IdentityReference -Force $Issue | Add-Member -MemberType NoteProperty -Name ActiveDirectoryRights -Value $entry.ActiveDirectoryRights -Force $Issue | Add-Member -MemberType NoteProperty -Name Issue ` - -Value "$($_.nTSecurityDescriptor.Owner) has Owner rights on this object" -Force - $Issue | Add-Member -MemberType NoteProperty -Name Fix -Value '[TODO]' -Force - $Issue | Add-Member -MemberType NoteProperty -Name Revert -Value '[TODO]' -Force + -Value "$($_.nTSecurityDescriptor.Owner) has Owner rights on this template" -Force + $Issue | Add-Member -MemberType NoteProperty -Name Fix -Value "`$Owner = New-Object System.Security.Principal.SecurityIdentifier(`'$PreferredOwner`'); `$ACL = Get-Acl -Path `'AD:$($_.DistinguishedName)`'; `$ACL.SetOwner(`$Owner); Set-ACL -Path `'AD:$($_.DistinguishedName)`' -AclObject `$ACL" -Force + $Issue | Add-Member -MemberType NoteProperty -Name Revert -Value "`$Owner = New-Object System.Security.Principal.SecurityIdentifier(`'$($_.nTSecurityDescriptor.Owner)`'); `$ACL = Get-Acl -Path `'AD:$($_.DistinguishedName)`'; `$ACL.SetOwner(`$Owner); Set-ACL -Path `'AD:$($_.DistinguishedName)`' -AclObject `$ACL" -Force $Issue | Add-Member -MemberType NoteProperty -Name Technique -Value 'ESC5' $Severity = Set-Severity -Issue $Issue $Issue | Add-Member -MemberType NoteProperty -Name Severity -Value $Severity $Issue - } - if ( ($_.objectClass -ne 'pKICertificateTemplate') -and ($SID -match $UnsafeOwners) ) { + } elseif ( ($_.objectClass -ne 'pKICertificateTemplate') -and + ($SID -notmatch $SafeOwners) -and + ($entry.ActiveDirectoryRights.ObjectType -notmatch $SafeObjectTypes) + ) { $Issue = New-Object -TypeName pscustomobject $Issue | Add-Member -MemberType NoteProperty -Name Forest -Value $_.CanonicalName.split('/')[0] -Force $Issue | Add-Member -MemberType NoteProperty -Name Name -Value $_.Name -Force @@ -44,14 +43,15 @@ $Issue | Add-Member -MemberType NoteProperty -Name IdentityReference -Value $entry.IdentityReference -Force $Issue | Add-Member -MemberType NoteProperty -Name ActiveDirectoryRights -Value $entry.ActiveDirectoryRights -Force $Issue | Add-Member -MemberType NoteProperty -Name Issue ` - -Value "$($_.nTSecurityDescriptor.Owner) has Owner rights on this template" -Force - $Issue | Add-Member -MemberType NoteProperty -Name Fix -Value "`$Owner = New-Object System.Security.Principal.SecurityIdentifier(`'$PreferredOwner`'); `$ACL = Get-Acl -Path `'AD:$($_.DistinguishedName)`'; `$ACL.SetOwner(`$Owner); Set-ACL -Path `'AD:$($_.DistinguishedName)`' -AclObject `$ACL" -Force - $Issue | Add-Member -MemberType NoteProperty -Name Revert -Value "`$Owner = New-Object System.Security.Principal.SecurityIdentifier(`'$($_.nTSecurityDescriptor.Owner)`'); `$ACL = Get-Acl -Path `'AD:$($_.DistinguishedName)`'; `$ACL.SetOwner(`$Owner); Set-ACL -Path `'AD:$($_.DistinguishedName)`' -AclObject `$ACL" -Force + -Value "$($_.nTSecurityDescriptor.Owner) has Owner rights on this object" -Force + $Issue | Add-Member -MemberType NoteProperty -Name Fix -Value '[TODO]' -Force + $Issue | Add-Member -MemberType NoteProperty -Name Revert -Value '[TODO]' -Force $Issue | Add-Member -MemberType NoteProperty -Name Technique -Value 'ESC5' $Severity = Set-Severity -Issue $Issue $Issue | Add-Member -MemberType NoteProperty -Name Severity -Value $Severity $Issue } + foreach ($entry in $_.nTSecurityDescriptor.Access) { $Principal = New-Object System.Security.Principal.NTAccount($entry.IdentityReference) if ($Principal -match '^(S-1|O:)') { @@ -79,4 +79,4 @@ } } } -} \ No newline at end of file +} diff --git a/Private/Test-IsProtectedUser.ps1 b/Private/Test-IsProtectedUser.ps1 new file mode 100644 index 0000000..2a1793f --- /dev/null +++ b/Private/Test-IsProtectedUser.ps1 @@ -0,0 +1,69 @@ +function Test-IsMemberOfProtectedUsers { +<# + .SYNOPSIS + Check to see if a user is a member of the Protected Users group. + + .DESCRIPTION + This function checks to see if a specified user or the current user is a member of the Protected Users group in AD. + + .PARAMETER User + The user that will be checked for membership in the Protected Users group. This parameter accepts input from the pipeline. + + .EXAMPLE + This example will check if JaneDoe is a member of the Protected Users group. + + Test-IsMemberOfProtectedUsers -User JaneDoe + + .EXAMPLE + This example will check if the current user is a member of the Protected Users group. + + Test-IsMemberOfProtectedUsers + + .INPUTS + Active Directory user object, user SID, SamAccountName, etc + + .OUTPUTS + Boolean + + .NOTES + Membership in Active Directory's Protect Users group can have implications for anything that relies on NTLM authentication. + +#> + + [CmdletBinding()] + param ( + # User parameter accepts any input that is valid for Get-ADUser + [Parameter( + ValueFromPipeline = $true + )] + $User + ) + + Import-Module ActiveDirectory + + # Use the currently logged in user if none is specified + # Get the user from Active Directory + if (-not($User)) { + $CurrentUser = ([System.Security.Principal.WindowsIdentity]::GetCurrent().Name).Split('\')[-1] + $CheckUser = Get-ADUser $CurrentUser + } + else { + $CheckUser = Get-ADUser $User + } + + # Get the Protected Users group by SID instead of by its name to ensure compatibility with any locale or language. + $DomainSID = (Get-ADDomain).DomainSID.Value + $ProtectedUsersSID = "$DomainSID-525" + + # Get members of the Protected Users group for the current domain. Recuse in case groups are nested in it. + $ProtectedUsers = Get-ADGroupMember -Identity $ProtectedUsersSID -Recursive | Select-Object -Unique + + # Check if the current user is in the 'Protected Users' group + if ($ProtectedUsers -contains $CheckUser) { + Write-Verbose "$($CheckUser.Name) ($($CheckUser.DistinguishedName)) is a member of the Protected Users group." + $true + } else { + Write-Verbose "$($CheckUser.Name) ($($CheckUser.DistinguishedName)) is not a member of the Protected Users group." + $false + } +} diff --git a/Public/Invoke-Locksmith.ps1 b/Public/Invoke-Locksmith.ps1 index cf62fdb..443c9cf 100644 --- a/Public/Invoke-Locksmith.ps1 +++ b/Public/Invoke-Locksmith.ps1 @@ -76,19 +76,23 @@ [System.Management.Automation.PSCredential]$Credential ) - $Version = '2023.11' - $Logo = @" + $Version = '2023.12' + $LogoPart1 = @" _ _____ _______ _ _ _______ _______ _____ _______ _ _ | | | | |____/ |______ | | | | | |_____| |_____ |_____| |_____ | \_ ______| | | | __|__ | | | +"@ + $LogoPart2 = @" .--. .--. .--. /.-. '----------. /.-. '----------. /.-. '----------. \'-' .---'-''-'-' \'-' .--'--''-'-' \'-' .--'--'-''-' '--' '--' '--' - v$Version - "@ - $Logo + $VersionBanner = " v$Version" + + Write-Host $LogoPart1 -ForegroundColor Magenta + Write-Host $LogoPart2 -ForegroundColor White + Write-Host $VersionBanner -ForegroundColor Red # Check if ActiveDirectory PowerShell module is available, and attempt to install if not found if (-not(Get-Module -Name 'ActiveDirectory' -ListAvailable)) { @@ -160,11 +164,6 @@ } } - if (!$Credential -and (Get-RestrictedAdminModeSetting)) { - Write-Warning "Restricted Admin Mode appears to be in place, re-run with the '-Credential domain\user' option" - break; - } - if ($Credential) { $Targets = Get-Target -Credential $Credential } else { @@ -203,7 +202,9 @@ # If these are all empty = no issues found, exit if ($null -eq $Results) { - Write-Host "`n$(Get-Date) : No ADCS issues were found." -ForegroundColor Green + Write-Host "`n$(Get-Date) : No ADCS issues were found.`n" -ForegroundColor Green + Write-Host 'Thank you for using ' -NoNewline + Write-Host "❤ Locksmith ❤ `n" -ForegroundColor Magenta break } @@ -233,7 +234,7 @@ Write-Host "Writing AD CS issues to $Output..." try { $AllIssues | Select-Object Forest, Technique, Name, Issue | Export-Csv -NoTypeInformation $Output - Write-Host "$Output created successfully!" + Write-Host "$Output created successfully!`n" } catch { Write-Host 'Ope! Something broke.' } @@ -243,108 +244,156 @@ Write-Host "Writing AD CS issues to $Output..." try { $AllIssues | Select-Object Forest, Technique, Name, DistinguishedName, Issue, Fix | Export-Csv -NoTypeInformation $Output - Write-Host "$Output created successfully!" + Write-Host "$Output created successfully!`n" } catch { Write-Host 'Ope! Something broke.' } } 4 { - Write-Host 'Creating a script to revert any changes made by Locksmith...' + Write-Host "`nExecuting Mode 4 - Attempting to fix all identified issues!`n" -ForegroundColor Green + Write-Host 'Creating a script (' -NoNewline + Write-Host 'Invoke-RevertLocksmith.ps1' -ForegroundColor White -NoNewline + Write-Host ") which can be used to revert any changes made by Locksmith...`n" try { Export-RevertScript -AuditingIssues $AuditingIssues -ESC1 $ESC1 -ESC2 $ESC2 -ESC6 $ESC6 } catch {} - Write-Host 'Executing Mode 4 - Attempting to fix all identified issues!' if ($AuditingIssues) { $AuditingIssues | ForEach-Object { $FixBlock = [scriptblock]::Create($_.Fix) - Write-Host "Attempting to fully enable AD CS auditing on $($_.Name)..." - Write-Host "This should have little impact on your environment.`n" - Write-Host 'Command(s) to be run:' + Write-Host 'ISSUE:' -ForegroundColor White + Write-Host "Auditing is not fully enabled on Certification Authority `"$($_.Name)`".`n" + Write-Host 'TECHNIQUE:' -ForegroundColor White + Write-Host "$($_.Technique)`n" + Write-Host 'ACTION TO BE PEFORMED:' -ForegroundColor White + Write-Host "Locksmith will attempt to fully enable auditing on Certification Authority `"$($_.Name)`".`n" + Write-Host 'COMMAND(S) TO BE RUN:' Write-Host 'PS> ' -NoNewline Write-Host "$($_.Fix)`n" -ForegroundColor Cyan - try { - $WarningError = $null - Write-Warning 'If you continue, this script will attempt to fix this issue.' -WarningAction Inquire -ErrorVariable WarningError - if (!$WarningError) { - try { - Invoke-Command -ScriptBlock $FixBlock - } catch { - Write-Error 'Could not modify AD CS auditing. Are you a local admin on this host?' - } + Write-Host 'OPERATIONAL IMPACT:' -ForegroundColor White + Write-Host "This change should have little to no impact on the AD CS environment.`n" -ForegroundColor Green + Write-Host "If you continue, Locksmith will attempt to fix this issue.`n" -ForegroundColor Yellow + Write-Host "Continue with this operation? [Y] Yes " -NoNewline + Write-Host "[N] " -ForegroundColor Yellow -NoNewline + Write-Host "No: " -NoNewLine + $WarningError = '' + $WarningError = Read-Host + if ($WarningError -like 'y') { + try { + Invoke-Command -ScriptBlock $FixBlock + } catch { + Write-Error 'Could not modify AD CS auditing. Are you a local admin on the CA host?' } - } catch { - Write-Host 'SKIPPED!' -ForegroundColor Yellow + } else { + Write-Host "SKIPPED!`n" -ForegroundColor Yellow } - Read-Host -Prompt 'Press enter to continue...' } } if ($ESC1) { $ESC1 | ForEach-Object { $FixBlock = [scriptblock]::Create($_.Fix) - Write-Host "Attempting to enable Manager Approval on the $($_.Name) template...`n" - Write-Host 'Command(s) to be run:' + Write-Host 'ISSUE:' -ForegroundColor White + Write-Host "Security Principals can enroll in `"$($_.Name)`" template using a Subject Alternative Name without Manager Approval.`n" + Write-Host 'TECHNIQUE:' -ForegroundColor White + Write-Host "$($_.Technique)`n" + Write-Host 'ACTION TO BE PEFORMED:' -ForegroundColor White + Write-Host "Locksmith will attempt to enable Manager Approval on the `"$($_.Name)`" template.`n" + Write-Host 'CCOMMAND(S) TO BE RUN:' Write-Host 'PS> ' -NoNewline Write-Host "$($_.Fix)`n" -ForegroundColor Cyan - try { - $WarningError = $null - Write-Warning "This could cause some services to stop working until certificates are approved.`nIf you continue this script will attempt to fix this issues." -WarningAction Inquire -ErrorVariable WarningError - if (!$WarningError) { - try { - Invoke-Command -ScriptBlock $FixBlock - } catch { - Write-Error 'Could not enable Manager Approval. Are you an Active Directory or AD CS admin?' - } + Write-Host 'OPERATIONAL IMPACT:' -ForegroundColor White + Write-Host "WARNING: This change could cause some services to stop working until certificates are approved.`n" -ForegroundColor Yellow + Write-Host "If you continue, Locksmith will attempt to fix this issue.`n" -ForegroundColor Yellow + Write-Host "Continue with this operation? [Y] Yes " -NoNewline + Write-Host "[N] " -ForegroundColor Yellow -NoNewline + Write-Host "No: " -NoNewLine + $WarningError = '' + $WarningError = Read-Host + if ($WarningError -like 'y') { + try { + Invoke-Command -ScriptBlock $FixBlock + } catch { + Write-Error 'Could not enable Manager Approval. Are you an Active Directory or AD CS admin?' } - } catch { - Write-Host 'SKIPPED!' -ForegroundColor Yellow + } else { + Write-Host "SKIPPED!`n" -ForegroundColor Yellow } - Read-Host -Prompt 'Press enter to continue...' + } } if ($ESC2) { $ESC2 | ForEach-Object { $FixBlock = [scriptblock]::Create($_.Fix) - Write-Host "Attempting to enable Manager Approval on the $($_.Name) template...`n" - Write-Host 'Command(s) to be run:' + Write-Host 'ISSUE:' -ForegroundColor White + Write-Host "Security Principals can enroll in `"$($_.Name)`" template and create a Subordinate Certification Authority without Manager Approval.`n" + Write-Host 'TECHNIQUE:' -ForegroundColor White + Write-Host "$($_.Technique)`n" + Write-Host 'ACTION TO BE PEFORMED:' -ForegroundColor White + Write-Host "Locksmith will attempt to enable Manager Approval on the `"$($_.Name)`" template.`n" + Write-Host 'COMMAND(S) TO BE RUN:' -ForegroundColor White Write-Host 'PS> ' -NoNewline Write-Host "$($_.Fix)`n" -ForegroundColor Cyan - try { - $WarningError = $null - Write-Warning "This could cause some services to stop working until certificates are approved.`nIf you continue, this script will attempt to fix this issue." -WarningAction Inquire -ErrorVariable WarningError - if (!$WarningError) { - try { - Invoke-Command -ScriptBlock $FixBlock - } catch { - Write-Error 'Could not enable Manager Approval. Are you an Active Directory or AD CS admin?' - } + Write-Host 'OPERATIONAL IMPACT:' -ForegroundColor White + Write-Host "WARNING: This change could cause some services to stop working until certificates are approved.`n" -ForegroundColor Yellow + Write-Host "If you continue, Locksmith will attempt to fix this issue.`n" -ForegroundColor Yellow + Write-Host "Continue with this operation? [Y] Yes " -NoNewline + Write-Host "[N] " -ForegroundColor Yellow -NoNewline + Write-Host "No: " -NoNewLine + $WarningError = '' + $WarningError = Read-Host + if ($WarningError -like 'y') { + try { + Invoke-Command -ScriptBlock $FixBlock + } catch { + Write-Error 'Could not enable Manager Approval. Are you an Active Directory or AD CS admin?' } - } catch { - Write-Host 'SKIPPED!' -ForegroundColor Yellow + } else { + Write-Host "SKIPPED!`n" -ForegroundColor Yellow } - Read-Host -Prompt 'Press enter to continue...' } } if ($ESC6) { $ESC6 | ForEach-Object { $FixBlock = [scriptblock]::Create($_.Fix) - Write-Host "Attempting to disable the EDITF_ATTRIBUTESUBJECTALTNAME2 flag on $($_.Name)...`n" - Write-Host 'Command(s) to be run:' + Write-Host 'ISSUE:' -ForegroundColor White + Write-Host "The Certification Authority `"$($_.Name)`" has the dangerous EDITF_ATTRIBUTESUBJECTALTNAME2 flag enabled.`n" + Write-Host 'TECHNIQUE:' -ForegroundColor White + Write-Host "$($_.Technique)`n" + Write-Host 'ACTION TO BE PEFORMED:' -ForegroundColor White + Write-Host "Locksmith will attempt to disable the EDITF_ATTRIBUTESUBJECTALTNAME2 flag on Certifiction Authority `"$($_.Name)`".`n" + Write-Host 'COMMAND(S) TO BE RUN' -ForegroundColor White Write-Host 'PS> ' -NoNewline Write-Host "$($_.Fix)`n" -ForegroundColor Cyan - try { - $WarningError = $null - Write-Warning "This could cause some services to stop working.`nIf you continue this script will attempt to fix this issues." -WarningAction Inquire -ErrorVariable WarningError - if (!$WarningError) { - try { - Invoke-Command -ScriptBlock $FixBlock - } catch { - Write-Error 'Could not disable the EDITF_ATTRIBUTESUBJECTALTNAME2 flag. Are you an Active Directory or AD CS admin?' - } + $WarningError = 'n' + Write-Host 'OPERATIONAL IMPACT:' -ForegroundColor White + Write-Host "WARNING: This change could cause some services to stop working.`n" -ForegroundColor Yellow + Write-Host "If you continue, Locksmith will attempt to fix this issue.`n" -ForegroundColor Yellow + Write-Host "Continue with this operation? [Y] Yes " -NoNewline + Write-Host "[N] " -ForegroundColor Yellow -NoNewline + Write-Host "No: " -NoNewLine + $WarningError = '' + $WarningError = Read-Host + if ($WarningError -like 'y') { + try { + Invoke-Command -ScriptBlock $FixBlock + } catch { + Write-Error 'Could not disable the EDITF_ATTRIBUTESUBJECTALTNAME2 flag. Are you an Active Directory or AD CS admin?' } - } catch { - Write-Host 'SKIPPED!' -ForegroundColor Yellow + } else { + Write-Host "SKIPPED!`n" -ForegroundColor Yellow } - Read-Host -Prompt 'Press enter to continue...' } } + + Write-Host "Mode 4 Complete! There are no more issues that Locksmith can automatically resolve.`n" -ForegroundColor Green + Write-Host 'If you experience any operational impact from using Locksmith Mode 4, use ' -NoNewline + Write-Host 'Invoke-RevertLocksmith.ps1 ' -ForegroundColor White + Write-Host "to revert all changes made by Locksmith. It can be found in the current working directory.`n" + Write-Host @" +REMINDER: Locksmith cannot automatically resolve all AD CS issues at this time. +There may be more AD CS issues remaining in your environment. +Use Locksmith in Modes 0-3 to further investigate your environment +or reach out to the Locksmith team for assistance. We'd love to help`n +"@ -ForegroundColor Yellow } } + Write-Host 'Thank you for using ' -NoNewline + Write-Host "❤ Locksmith ❤`n" -ForegroundColor Magenta }