diff --git a/Invoke-Locksmith.ps1 b/Invoke-Locksmith.ps1 index 64e6476..e295ee3 100644 --- a/Invoke-Locksmith.ps1 +++ b/Invoke-Locksmith.ps1 @@ -263,7 +263,12 @@ function Find-ESC1 { else { $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value } - if ( ($SID -notmatch $SafeUsers) -and ( ($entry.ActiveDirectoryRights -match 'ExtendedRight') -or ($entry.ActiveDirectoryRights -match 'GenericAll') ) ) { + if ( + ($SID -notmatch $SafeUsers) -and + ( ( ($entry.ActiveDirectoryRights -match 'ExtendedRight') -and + ( $entry.ObjectType -match '0e10c968-78fb-11d2-90d4-00c04f79dc55|00000000-0000-0000-0000-000000000000' ) ) -or + ($entry.ActiveDirectoryRights -match 'GenericAll') ) + ) { $Issue = [pscustomobject]@{ Forest = $_.CanonicalName.split('/')[0] Name = $_.Name @@ -458,7 +463,12 @@ function Find-ESC13 { else { $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value } - if ( ($SID -notmatch $SafeUsers) -and ($entry.ActiveDirectoryRights -match 'ExtendedRight') ) { + if ( + ($SID -notmatch $SafeUsers) -and + ( ( ($entry.ActiveDirectoryRights -match 'ExtendedRight') -and + ( $entry.ObjectType -match '0e10c968-78fb-11d2-90d4-00c04f79dc55|00000000-0000-0000-0000-000000000000' ) ) -or + ($entry.ActiveDirectoryRights -match 'GenericAll') ) + ) { $Issue = [pscustomobject]@{ Forest = $_.CanonicalName.split('/')[0] Name = $_.Name @@ -551,7 +561,12 @@ function Find-ESC15 { else { $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value } - if ( ($SID -notmatch $SafeUsers) -and ( ($entry.ActiveDirectoryRights -match 'ExtendedRight') -or ($entry.ActiveDirectoryRights -match 'GenericAll') ) ) { + if ( + ($SID -notmatch $SafeUsers) -and + ( ( ($entry.ActiveDirectoryRights -match 'ExtendedRight') -and + ( $entry.ObjectType -match '0e10c968-78fb-11d2-90d4-00c04f79dc55|00000000-0000-0000-0000-000000000000' ) ) -or + ($entry.ActiveDirectoryRights -match 'GenericAll') ) + ) { $Issue = [pscustomobject]@{ Forest = $_.CanonicalName.split('/')[0] Name = $_.Name @@ -646,7 +661,7 @@ function Find-ESC16 { if ($_.DisableExtensionList -eq 'Yes') { $Issue.Issue = @" The Certification Authority (CA) $($_.CAFullName) has the szOID_NTDS_CA_SECURITY_EXT security extension disabled. When -this extension is disabled, every certificate issued by this CA will be unable to to reliably map a certificate to a +this extension is disabled, every certificate issued from this template will be unable to reliably map a certificate to a user or computer account's SID for authentication. More info: @@ -729,7 +744,12 @@ function Find-ESC2 { else { $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value } - if ( ($SID -notmatch $SafeUsers) -and ( ($entry.ActiveDirectoryRights -match 'ExtendedRight') -or ($entry.ActiveDirectoryRights -match 'GenericAll') ) ) { + if ( + ($SID -notmatch $SafeUsers) -and + ( ( ($entry.ActiveDirectoryRights -match 'ExtendedRight') -and + ( $entry.ObjectType -match '0e10c968-78fb-11d2-90d4-00c04f79dc55|00000000-0000-0000-0000-000000000000' ) ) -or + ($entry.ActiveDirectoryRights -match 'GenericAll') ) + ) { $Issue = [pscustomobject]@{ Forest = $_.CanonicalName.split('/')[0] Name = $_.Name @@ -833,7 +853,12 @@ function Find-ESC3C1 { else { $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value } - if ( ($SID -notmatch $SafeUsers) -and ( ($entry.ActiveDirectoryRights -match 'ExtendedRight') -or ($entry.ActiveDirectoryRights -match 'GenericAll') ) ) { + if ( + ($SID -notmatch $SafeUsers) -and + ( ( ($entry.ActiveDirectoryRights -match 'ExtendedRight') -and + ( $entry.ObjectType -match '0e10c968-78fb-11d2-90d4-00c04f79dc55|00000000-0000-0000-0000-000000000000' ) ) -or + ($entry.ActiveDirectoryRights -match 'GenericAll') ) + ) { $Issue = [pscustomobject]@{ Forest = $_.CanonicalName.split('/')[0] Name = $_.Name @@ -926,7 +951,12 @@ function Find-ESC3C2 { else { $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value } - if ( ($SID -notmatch $SafeUsers) -and ( ($entry.ActiveDirectoryRights -match 'ExtendedRight') -or ($entry.ActiveDirectoryRights -match 'GenericAll') ) ) { + if ( + ($SID -notmatch $SafeUsers) -and + ( ( ($entry.ActiveDirectoryRights -match 'ExtendedRight') -and + ( $entry.ObjectType -match '0e10c968-78fb-11d2-90d4-00c04f79dc55|00000000-0000-0000-0000-000000000000' ) ) -or + ($entry.ActiveDirectoryRights -match 'GenericAll') ) + ) { $Issue = [pscustomobject]@{ Forest = $_.CanonicalName.split('/')[0] Name = $_.Name @@ -1808,7 +1838,12 @@ function Find-ESC9 { else { $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value } - if ( ($SID -notmatch $SafeUsers) -and ( ($entry.ActiveDirectoryRights -match 'ExtendedRight') -or ($entry.ActiveDirectoryRights -match 'GenericAll') ) ) { + if ( + ($SID -notmatch $SafeUsers) -and + ( ( ($entry.ActiveDirectoryRights -match 'ExtendedRight') -and + ( $entry.ObjectType -match '0e10c968-78fb-11d2-90d4-00c04f79dc55|00000000-0000-0000-0000-000000000000' ) ) -or + ($entry.ActiveDirectoryRights -match 'GenericAll') ) + ) { $Issue = [pscustomobject]@{ Forest = $_.CanonicalName.split('/')[0] Name = $_.Name @@ -2053,20 +2088,20 @@ function Get-CAHostObject { if ($Credential) { $ADCSObjects | Where-Object objectClass -Match 'pKIEnrollmentService' | ForEach-Object { if ($_.CAHostDistinguishedName) { - Get-ADObject $_.CAHostDistinguishedName -Properties * -Server $ForestGC -Credential $Credential + Get-ADObject $_.CAHostDistinguishedName -Properties * -Server $ForestGC -Credential $Credential } else { - Write-Warning "Get-CAHostObject: Unable to get information from $($_.DisplayName)" + Write-Warning "Get-CAHostObject: Unable to get information from $($_.DisplayName)" } } } else { $ADCSObjects | Where-Object objectClass -Match 'pKIEnrollmentService' | ForEach-Object { if ($_.CAHostDistinguishedName) { - Get-ADObject -Identity $_.CAHostDistinguishedName -Properties * -Server $ForestGC + Get-ADObject -Identity $_.CAHostDistinguishedName -Properties * -Server $ForestGC } else { - Write-Warning "Get-CAHostObject: Unable to get information from $($_.DisplayName)" + Write-Warning "Get-CAHostObject: Unable to get information from $($_.DisplayName)" } } } @@ -2232,7 +2267,7 @@ function Install-RSATADPowerShell { 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 + return } } function Invoke-Remediation { @@ -2964,7 +2999,7 @@ function New-Dictionary { ReferenceUrls = @('https://github.com/jakehildreth/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 { @@ -3039,7 +3074,7 @@ function Set-AdditionalCAProperty { begin { if (-not ([System.Management.Automation.PSTypeName]'TrustAllCertsPolicy') ) { if ($PSVersionTable.PSEdition -eq 'Desktop') { - $code = @" + $code = @' using System.Net; using System.Security.Cryptography.X509Certificates; public class TrustAllCertsPolicy : ICertificatePolicy { @@ -3047,12 +3082,12 @@ function Set-AdditionalCAProperty { return true; } } -"@ +'@ Add-Type -TypeDefinition $code -Language CSharp [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy } else { - Add-Type @" + Add-Type @' using System.Net; using System.Security.Cryptography.X509Certificates; using System.Net.Security; @@ -3061,7 +3096,7 @@ function Set-AdditionalCAProperty { return true; } } -"@ +'@ # Set the ServerCertificateValidationCallback [System.Net.ServicePointManager]::ServerCertificateValidationCallback = [TrustAllCertsPolicy]::TrustAllCerts } @@ -3071,8 +3106,7 @@ function Set-AdditionalCAProperty { process { $ADCSObjects | Where-Object objectClass -Match 'pKIEnrollmentService' | ForEach-Object { $CAEnrollmentEndpoint = @() - #[array]$CAEnrollmentEndpoint = $_.'msPKI-Enrollment-Servers' | Select-String 'http.*' | ForEach-Object { $_.Matches[0].Value } - foreach ($directory in @("certsrv/", "$($_.Name)_CES_Kerberos/service.svc", "$($_.Name)_CES_Kerberos/service.svc/CES", "ADPolicyProvider_CEP_Kerberos/service.svc", "certsrv/mscep/")) { + foreach ($directory in @('certsrv/', "$($_.Name)_CES_Kerberos/service.svc", "$($_.Name)_CES_Kerberos/service.svc/CES", 'ADPolicyProvider_CEP_Kerberos/service.svc', 'certsrv/mscep/')) { $URL = "://$($_.dNSHostName)/$directory" try { $Auth = 'NTLM' @@ -3081,7 +3115,7 @@ function Set-AdditionalCAProperty { $Cache = [System.Net.CredentialCache]::New() $Cache.Add([System.Uri]::new($FullURL), $Auth, [System.Net.CredentialCache]::DefaultNetworkCredentials) $Request.Credentials = $Cache - $Request.Timeout = 1000 + $Request.Timeout = 100 $Request.GetResponse() | Out-Null $CAEnrollmentEndpoint += @{ 'URL' = $FullURL @@ -3096,7 +3130,7 @@ function Set-AdditionalCAProperty { $Cache = [System.Net.CredentialCache]::New() $Cache.Add([System.Uri]::new($FullURL), $Auth, [System.Net.CredentialCache]::DefaultNetworkCredentials) $Request.Credentials = $Cache - $Request.Timeout = 1000 + $Request.Timeout = 100 $Request.GetResponse() | Out-Null $CAEnrollmentEndpoint += @{ 'URL' = $FullURL @@ -3111,7 +3145,7 @@ function Set-AdditionalCAProperty { $Cache = [System.Net.CredentialCache]::New() $Cache.Add([System.Uri]::new($FullURL), $Auth, [System.Net.CredentialCache]::DefaultNetworkCredentials) $Request.Credentials = $Cache - $Request.Timeout = 1000 + $Request.Timeout = 100 $Request.GetResponse() | Out-Null $CAEnrollmentEndpoint += @{ 'URL' = $FullURL @@ -3119,6 +3153,7 @@ function Set-AdditionalCAProperty { } } catch { + Write-Debug "There may have been an error or something nothing found. $_" } } } @@ -3134,10 +3169,10 @@ function Set-AdditionalCAProperty { $CAHostFQDN = (Get-ADObject -Filter { (Name -eq $CAHostName) -and (objectclass -eq 'computer') } -Properties DnsHostname -Server $ForestGC).DnsHostname } $ping = if ($CAHostFQDN) { - Test-Connection -ComputerName $CAHostFQDN -Count 1 -Quiet + Test-Connection -ComputerName $CAHostFQDN -Count 1 -Quiet } else { - Write-Warning "Unable to resolve $($_.Name) Fully Qualified Domain Name (FQDN)" + Write-Warning "Unable to resolve $($_.Name) Fully Qualified Domain Name (FQDN)" } if ($ping) { try { @@ -3303,18 +3338,20 @@ function Set-AdditionalTemplateProperty { [Microsoft.ActiveDirectory.Management.ADEntity[]]$ADCSObjects ) - $ADCSObjects | Where-Object objectClass -Match 'pKICertificateTemplate' -PipelineVariable template | ForEach-Object { - # Write-Host "[?] Checking if template `"$($template.Name)`" is Enabled on any Certification Authority." -ForegroundColor Blue - $Enabled = $false - $EnabledOn = @() - foreach ($ca in ($ADCSObjects | Where-Object objectClass -EQ 'pKIEnrollmentService')) { - if ($ca.certificateTemplates -contains $template.Name) { - $Enabled = $true - $EnabledOn += $ca.Name - } + process { + $ADCSObjects | Where-Object objectClass -Match 'pKICertificateTemplate' -PipelineVariable template | ForEach-Object { + # Write-Host "[?] Checking if template `"$($template.Name)`" is Enabled on any Certification Authority." -ForegroundColor Blue + $Enabled = $false + $EnabledOn = @() + foreach ($ca in ($ADCSObjects | Where-Object objectClass -EQ 'pKIEnrollmentService')) { + if ($ca.certificateTemplates -contains $template.Name) { + $Enabled = $true + $EnabledOn += $ca.Name + } - $template | Add-Member -NotePropertyName Enabled -NotePropertyValue $Enabled -Force - $template | Add-Member -NotePropertyName EnabledOn -NotePropertyValue $EnabledOn -Force + $template | Add-Member -NotePropertyName Enabled -NotePropertyValue $Enabled -Force + $template | Add-Member -NotePropertyName EnabledOn -NotePropertyValue $EnabledOn -Force + } } } } @@ -3706,6 +3743,10 @@ function Set-RiskRating { $Principals += $OtherIssue.IdentityReference.Value $OtherIssueRisk += 1 } + else { + $Principals += $OtherIssue.IdentityReference.Value + $OtherIssueRisk += 0.1 + } $CheckedESC5Templates.$($OtherIssue.Name) = $Principals } # forech ($OtherIssue) if ($OtherIssueRisk -ge 2) { @@ -3727,23 +3768,23 @@ function Set-RiskRating { switch ($Issue.objectClass) { # Being able to modify Root CA Objects is very bad. 'certificationAuthority' { - $RiskValue += 2; $RiskScoring += 'Root Certification Authority bject: +2' + $RiskValue += 2; $RiskScoring += 'Root Certification Authority bject: +2' } # Being able to modify Issuing CA Objects is also very bad. 'pKIEnrollmentService' { - $RiskValue += 2; $RiskScoring += 'Issuing Certification Authority Object: +2' + $RiskValue += 2; $RiskScoring += 'Issuing Certification Authority Object: +2' } # Being able to modify CA Hosts? Yeah... very bad. 'computer' { - $RiskValue += 2; $RiskScoring += 'Certification Authority Host Computer: +2' + $RiskValue += 2; $RiskScoring += 'Certification Authority Host Computer: +2' } # Being able to modify OIDs could result in ESC13 vulns. 'msPKI-Enterprise-Oid' { - $RiskValue += 1; $RiskScoring += 'OID: +1' + $RiskValue += 1; $RiskScoring += 'OID: +1' } # Being able to modify PKS containers is bad. 'container' { - $RiskValue += 1; $RiskScoring += 'Container: +1' + $RiskValue += 1; $RiskScoring += 'Container: +1' } } } @@ -3764,19 +3805,19 @@ function Set-RiskRating { # Convert Value to Name $RiskName = switch ($RiskValue) { { $_ -le 1 } { - 'Informational' + 'Informational' } 2 { - 'Low' + 'Low' } 3 { - 'Medium' + 'Medium' } 4 { - 'High' + 'High' } { $_ -ge 5 } { - 'Critical' + 'Critical' } } @@ -3837,9 +3878,9 @@ function Test-IsADAdmin { #> if ( # Need to test to make sure this checks domain groups and not local groups, particularly for 'Administrators' (reference SID instead of name?). - ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole("Domain Admins") -or - ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole("Administrators") -or - ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole("Enterprise Admins") + ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole("Domain Admins") -or + ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole("Administrators") -or + ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole("Enterprise Admins") ) { return $true } @@ -3887,7 +3928,7 @@ function Test-IsLocalAccountSession { $CurrentSID = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value $LocalSIDs = (Get-LocalUser).SID.Value if ($CurrentSID -in $LocalSIDs) { - Return $true + return $true } } @@ -3932,41 +3973,48 @@ function Test-IsMemberOfProtectedUsers { $User ) - Import-Module ActiveDirectory - - # Use the currently logged in user if none is specified - # Get the user from Active Directory - if (-not($User)) { - # These two are different types. Fixed by referencing $CheckUser.SID later, but should fix here by using one type. - $CurrentUser = ([System.Security.Principal.WindowsIdentity]::GetCurrent().Name).Split('\')[-1] - $CheckUser = Get-ADUser $CurrentUser -Properties primaryGroupID - } - else { - $CheckUser = Get-ADUser $User -Properties primaryGroupID + begin { + Import-Module ActiveDirectory } - # 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" + process { + # Use the currently logged in user if none is specified + # Get the user from Active Directory + if (-not($User)) { + # These two are different types. Fixed by referencing $CheckUser.SID later, but should fix here by using one type. + $CurrentUser = ([System.Security.Principal.WindowsIdentity]::GetCurrent().Name).Split('\')[-1] + $CheckUser = Get-ADUser $CurrentUser -Properties primaryGroupID + } + else { + $CheckUser = Get-ADUser $User -Properties primaryGroupID + } - # 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 + # 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" - # Check if the current user is in the 'Protected Users' group - if ($ProtectedUsers.SID.Value -contains $CheckUser.SID) { - Write-Verbose "$($CheckUser.Name) ($($CheckUser.DistinguishedName)) is a member of the Protected Users group." - $true - } - else { - # Check if the user's PGID (primary group ID) is set to the Protected Users group RID (525). - if ( $CheckUser.primaryGroupID -eq '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.SID.Value -contains $CheckUser.SID) { + 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 + # Check if the user's PGID (primary group ID) is set to the Protected Users group RID (525). + if ( $CheckUser.primaryGroupID -eq '525' ) { + $true + } + else { + Write-Verbose "$($CheckUser.Name) ($($CheckUser.DistinguishedName)) is not a member of the Protected Users group." + $false + } } } + + end { + } } function Test-IsRecentVersion { @@ -4335,7 +4383,7 @@ Set-Acl -Path `$Path -AclObject `$ACL "@ } 4 { - break + break } 5 { $Issue.Fix = @" @@ -4587,7 +4635,7 @@ TODO #> -Function Write-HostColorized { +function Write-HostColorized { <# .SYNOPSIS Colors portions of the default host output that match given patterns. @@ -4706,10 +4754,10 @@ Function Write-HostColorized { # We precompile them for better performance with many input objects. [System.Text.RegularExpressions.RegexOptions] $reOpts = if ($CaseSensitive) { - 'Compiled, ExplicitCapture' + 'Compiled, ExplicitCapture' } else { - 'Compiled, ExplicitCapture, IgnoreCase' + 'Compiled, ExplicitCapture, IgnoreCase' } # Transform the dictionary: @@ -4731,20 +4779,20 @@ Function Write-HostColorized { } $colorArgs = @{ } if ($fg) { - $colorArgs['ForegroundColor'] = [ConsoleColor] $fg + $colorArgs['ForegroundColor'] = [ConsoleColor] $fg } if ($bg) { - $colorArgs['BackgroundColor'] = [ConsoleColor] $bg + $colorArgs['BackgroundColor'] = [ConsoleColor] $bg } # Consolidate the patterns into a single pattern with alternation ('|'), # escape the patterns if -SimpleMatch was passsed. $re = New-Object regex -Args ` $(if ($SimpleMatch) { - ($entry.Key | ForEach-Object { [regex]::Escape($_) }) -join '|' + ($entry.Key | ForEach-Object { [regex]::Escape($_) }) -join '|' } else { - ($entry.Key | ForEach-Object { '({0})' -f $_ }) -join '|' + ($entry.Key | ForEach-Object { '({0})' -f $_ }) -join '|' }), $reOpts @@ -4753,7 +4801,7 @@ Function Write-HostColorized { } } catch { - throw + throw } # Construct the arguments to pass to Out-String. @@ -4776,7 +4824,7 @@ Function Write-HostColorized { foreach ($m in $entry.Key.Matches($_)) { @{ Index = $m.Index; Text = $m.Value; ColorArgs = $entry.Value } if ($WholeLine) { - break patternLoop + break patternLoop } } } @@ -4955,7 +5003,7 @@ function Invoke-Locksmith { [System.Management.Automation.PSCredential]$Credential ) - $Version = '2025.5.26' + $Version = '2025.9.8' $LogoPart1 = @' _ _____ _______ _ _ _______ _______ _____ _______ _ _ | | | | |____/ |______ | | | | | |_____| @@ -4997,7 +5045,7 @@ function Invoke-Locksmith { # GenericAll, WriteDacl, and WriteOwner all permit full control of an AD object. # WriteProperty may or may not permit full control depending the specific property and AD object type. - $DangerousRights = 'GenericAll|WriteDacl|WriteOwner|WriteProperty' + $DangerousRights = 'GenericAll|Write' # Extended Key Usage for client authentication. A requirement for ESC3. $EnrollmentAgentEKU = '1\.3\.6\.1\.4\.1\.311\.20\.2\.1' @@ -5162,11 +5210,13 @@ function Invoke-Locksmith { [!] You ran Locksmith in Mode 0 which only provides an high-level overview of issues identified in the environment. For more details including: - - DistinguishedName of impacted object(s) - - Remediation guidance and/or code + - Detailed Risk Rating + - General remediation guidance and/or code for all issues + - Custom remediation guidance and/or code for some issues! - Revert guidance and/or code (in case remediation breaks something!) + - Distinguished Name of impacted object(s) -Run Locksmith in Mode 1! +Try Mode 1! # Module version Invoke-Locksmith -Mode 1 @@ -5229,7 +5279,8 @@ Invoke-Locksmith -Mode 1 } } Write-Host 'Thank you for using ' -NoNewline - Write-Host "Locksmith <3`n" -ForegroundColor Magenta + Write-Host 'Locksmith <3 ' -ForegroundColor Magenta -NoNewline + Write-Host "(https://github.com/jakehildreth/Locksmith)`n" } diff --git a/Locksmith.psd1 b/Locksmith.psd1 index b8ae79d..9222068 100644 --- a/Locksmith.psd1 +++ b/Locksmith.psd1 @@ -8,7 +8,7 @@ FunctionsToExport = 'Invoke-Locksmith' GUID = 'b1325b42-8dc4-4f17-aa1f-dcb5984ca14a' HelpInfoURI = 'https://raw.githubusercontent.com/jakehildreth/Locksmith/main/en-US/' - ModuleVersion = '2025.5.26' + ModuleVersion = '2025.9.8' PowerShellVersion = '5.1' PrivateData = @{ PSData = @{ diff --git a/Locksmith.psm1 b/Locksmith.psm1 index 0869fc0..af7557e 100644 --- a/Locksmith.psm1 +++ b/Locksmith.psm1 @@ -53,7 +53,7 @@ $Assembly = @( } ) $FoundErrors = @( - Foreach ($Import in @($Assembly)) { + foreach ($Import in @($Assembly)) { try { Write-Verbose -Message $Import.FullName Add-Type -Path $Import.Fullname -ErrorAction Stop @@ -77,10 +77,10 @@ $FoundErrors = @( } } #Dot source the files - Foreach ($Import in @($Classes + $Enums + $Private + $Public)) { - Try { + foreach ($Import in @($Classes + $Enums + $Private + $Public)) { + try { . $Import.Fullname - } Catch { + } catch { Write-Error -Message "Failed to import functions from $($import.Fullname): $_" $true } diff --git a/Private/Find-ESC1.ps1 b/Private/Find-ESC1.ps1 index abb6fd0..8903cbd 100644 --- a/Private/Find-ESC1.ps1 +++ b/Private/Find-ESC1.ps1 @@ -56,7 +56,12 @@ } else { $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value } - if ( ($SID -notmatch $SafeUsers) -and ( ($entry.ActiveDirectoryRights -match 'ExtendedRight') -or ($entry.ActiveDirectoryRights -match 'GenericAll') ) ) { + if ( + ($SID -notmatch $SafeUsers) -and + ( ( ($entry.ActiveDirectoryRights -match 'ExtendedRight') -and + ( $entry.ObjectType -match '0e10c968-78fb-11d2-90d4-00c04f79dc55|00000000-0000-0000-0000-000000000000' ) ) -or + ($entry.ActiveDirectoryRights -match 'GenericAll') ) + ) { $Issue = [pscustomobject]@{ Forest = $_.CanonicalName.split('/')[0] Name = $_.Name diff --git a/Private/Find-ESC13.ps1 b/Private/Find-ESC13.ps1 index 222c598..20c578b 100644 --- a/Private/Find-ESC13.ps1 +++ b/Private/Find-ESC13.ps1 @@ -56,7 +56,12 @@ function Find-ESC13 { } else { $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value } - if ( ($SID -notmatch $SafeUsers) -and ($entry.ActiveDirectoryRights -match 'ExtendedRight') ) { + if ( + ($SID -notmatch $SafeUsers) -and + ( ( ($entry.ActiveDirectoryRights -match 'ExtendedRight') -and + ( $entry.ObjectType -match '0e10c968-78fb-11d2-90d4-00c04f79dc55|00000000-0000-0000-0000-000000000000' ) ) -or + ($entry.ActiveDirectoryRights -match 'GenericAll') ) + ) { $Issue = [pscustomobject]@{ Forest = $_.CanonicalName.split('/')[0] Name = $_.Name diff --git a/Private/Find-ESC15.ps1 b/Private/Find-ESC15.ps1 index ba99bb7..1e5c45b 100644 --- a/Private/Find-ESC15.ps1 +++ b/Private/Find-ESC15.ps1 @@ -44,7 +44,12 @@ function Find-ESC15 { } else { $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value } - if ( ($SID -notmatch $SafeUsers) -and ( ($entry.ActiveDirectoryRights -match 'ExtendedRight') -or ($entry.ActiveDirectoryRights -match 'GenericAll') ) ) { + if ( + ($SID -notmatch $SafeUsers) -and + ( ( ($entry.ActiveDirectoryRights -match 'ExtendedRight') -and + ( $entry.ObjectType -match '0e10c968-78fb-11d2-90d4-00c04f79dc55|00000000-0000-0000-0000-000000000000' ) ) -or + ($entry.ActiveDirectoryRights -match 'GenericAll') ) + ) { $Issue = [pscustomobject]@{ Forest = $_.CanonicalName.split('/')[0] Name = $_.Name diff --git a/Private/Find-ESC16.ps1 b/Private/Find-ESC16.ps1 index 0f02ecd..b70f42e 100644 --- a/Private/Find-ESC16.ps1 +++ b/Private/Find-ESC16.ps1 @@ -40,7 +40,7 @@ if ($_.DisableExtensionList -eq 'Yes') { $Issue.Issue = @" The Certification Authority (CA) $($_.CAFullName) has the szOID_NTDS_CA_SECURITY_EXT security extension disabled. When -this extension is disabled, every certificate issued by this CA will be unable to to reliably map a certificate to a +this extension is disabled, every certificate issued by this CA will be unable to reliably map a certificate to a user or computer account's SID for authentication. More info: diff --git a/Private/Find-ESC2.ps1 b/Private/Find-ESC2.ps1 index 471e6ac..a49e2ff 100644 --- a/Private/Find-ESC2.ps1 +++ b/Private/Find-ESC2.ps1 @@ -46,7 +46,12 @@ } else { $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value } - if ( ($SID -notmatch $SafeUsers) -and ( ($entry.ActiveDirectoryRights -match 'ExtendedRight') -or ($entry.ActiveDirectoryRights -match 'GenericAll') ) ) { + if ( + ($SID -notmatch $SafeUsers) -and + ( ( ($entry.ActiveDirectoryRights -match 'ExtendedRight') -and + ( $entry.ObjectType -match '0e10c968-78fb-11d2-90d4-00c04f79dc55|00000000-0000-0000-0000-000000000000' ) ) -or + ($entry.ActiveDirectoryRights -match 'GenericAll') ) + ) { $Issue = [pscustomobject]@{ Forest = $_.CanonicalName.split('/')[0] Name = $_.Name diff --git a/Private/Find-ESC3C1.ps1 b/Private/Find-ESC3C1.ps1 index 633120a..fce534f 100644 --- a/Private/Find-ESC3C1.ps1 +++ b/Private/Find-ESC3C1.ps1 @@ -46,7 +46,12 @@ } else { $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value } - if ( ($SID -notmatch $SafeUsers) -and ( ($entry.ActiveDirectoryRights -match 'ExtendedRight') -or ($entry.ActiveDirectoryRights -match 'GenericAll') ) ) { + if ( + ($SID -notmatch $SafeUsers) -and + ( ( ($entry.ActiveDirectoryRights -match 'ExtendedRight') -and + ( $entry.ObjectType -match '0e10c968-78fb-11d2-90d4-00c04f79dc55|00000000-0000-0000-0000-000000000000' ) ) -or + ($entry.ActiveDirectoryRights -match 'GenericAll') ) + ) { $Issue = [pscustomobject]@{ Forest = $_.CanonicalName.split('/')[0] Name = $_.Name diff --git a/Private/Find-ESC3C2.ps1 b/Private/Find-ESC3C2.ps1 index 3e36604..868de4c 100644 --- a/Private/Find-ESC3C2.ps1 +++ b/Private/Find-ESC3C2.ps1 @@ -47,7 +47,12 @@ } else { $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value } - if ( ($SID -notmatch $SafeUsers) -and ( ($entry.ActiveDirectoryRights -match 'ExtendedRight') -or ($entry.ActiveDirectoryRights -match 'GenericAll') ) ) { + if ( + ($SID -notmatch $SafeUsers) -and + ( ( ($entry.ActiveDirectoryRights -match 'ExtendedRight') -and + ( $entry.ObjectType -match '0e10c968-78fb-11d2-90d4-00c04f79dc55|00000000-0000-0000-0000-000000000000' ) ) -or + ($entry.ActiveDirectoryRights -match 'GenericAll') ) + ) { $Issue = [pscustomobject]@{ Forest = $_.CanonicalName.split('/')[0] Name = $_.Name diff --git a/Private/Find-ESC9.ps1 b/Private/Find-ESC9.ps1 index 2d3c2b1..3c25c9b 100644 --- a/Private/Find-ESC9.ps1 +++ b/Private/Find-ESC9.ps1 @@ -56,7 +56,12 @@ } else { $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value } - if ( ($SID -notmatch $SafeUsers) -and ( ($entry.ActiveDirectoryRights -match 'ExtendedRight') -or ($entry.ActiveDirectoryRights -match 'GenericAll') ) ) { + if ( + ($SID -notmatch $SafeUsers) -and + ( ( ($entry.ActiveDirectoryRights -match 'ExtendedRight') -and + ( $entry.ObjectType -match '0e10c968-78fb-11d2-90d4-00c04f79dc55|00000000-0000-0000-0000-000000000000' ) ) -or + ($entry.ActiveDirectoryRights -match 'GenericAll') ) + ) { $Issue = [pscustomobject]@{ Forest = $_.CanonicalName.split('/')[0] Name = $_.Name diff --git a/Private/Set-AdditionalCAProperty.ps1 b/Private/Set-AdditionalCAProperty.ps1 index 4f872ce..224f496 100644 --- a/Private/Set-AdditionalCAProperty.ps1 +++ b/Private/Set-AdditionalCAProperty.ps1 @@ -71,7 +71,6 @@ process { $ADCSObjects | Where-Object objectClass -Match 'pKIEnrollmentService' | ForEach-Object { $CAEnrollmentEndpoint = @() - #[array]$CAEnrollmentEndpoint = $_.'msPKI-Enrollment-Servers' | Select-String 'http.*' | ForEach-Object { $_.Matches[0].Value } foreach ($directory in @('certsrv/', "$($_.Name)_CES_Kerberos/service.svc", "$($_.Name)_CES_Kerberos/service.svc/CES", 'ADPolicyProvider_CEP_Kerberos/service.svc', 'certsrv/mscep/')) { $URL = "://$($_.dNSHostName)/$directory" try { @@ -81,7 +80,7 @@ $Cache = [System.Net.CredentialCache]::New() $Cache.Add([System.Uri]::new($FullURL), $Auth, [System.Net.CredentialCache]::DefaultNetworkCredentials) $Request.Credentials = $Cache - $Request.Timeout = 1000 + $Request.Timeout = 100 $Request.GetResponse() | Out-Null $CAEnrollmentEndpoint += @{ 'URL' = $FullURL @@ -95,7 +94,7 @@ $Cache = [System.Net.CredentialCache]::New() $Cache.Add([System.Uri]::new($FullURL), $Auth, [System.Net.CredentialCache]::DefaultNetworkCredentials) $Request.Credentials = $Cache - $Request.Timeout = 1000 + $Request.Timeout = 100 $Request.GetResponse() | Out-Null $CAEnrollmentEndpoint += @{ 'URL' = $FullURL @@ -109,7 +108,7 @@ $Cache = [System.Net.CredentialCache]::New() $Cache.Add([System.Uri]::new($FullURL), $Auth, [System.Net.CredentialCache]::DefaultNetworkCredentials) $Request.Credentials = $Cache - $Request.Timeout = 1000 + $Request.Timeout = 100 $Request.GetResponse() | Out-Null $CAEnrollmentEndpoint += @{ 'URL' = $FullURL diff --git a/Private/Set-RiskRating.ps1 b/Private/Set-RiskRating.ps1 index c5e7d54..8c93809 100644 --- a/Private/Set-RiskRating.ps1 +++ b/Private/Set-RiskRating.ps1 @@ -364,6 +364,10 @@ function Set-RiskRating { $Principals += $OtherIssue.IdentityReference.Value $OtherIssueRisk += 1 } + else { + $Principals += $OtherIssue.IdentityReference.Value + $OtherIssueRisk += 0.1 + } $CheckedESC5Templates.$($OtherIssue.Name) = $Principals } # forech ($OtherIssue) if ($OtherIssueRisk -ge 2) { diff --git a/Public/Invoke-Locksmith.ps1 b/Public/Invoke-Locksmith.ps1 index 92cc1de..cc6c054 100644 --- a/Public/Invoke-Locksmith.ps1 +++ b/Public/Invoke-Locksmith.ps1 @@ -157,7 +157,7 @@ function Invoke-Locksmith { # GenericAll, WriteDacl, and WriteOwner all permit full control of an AD object. # WriteProperty may or may not permit full control depending the specific property and AD object type. - $DangerousRights = 'GenericAll|WriteDacl|WriteOwner|WriteProperty' + $DangerousRights = 'GenericAll|Write' # Extended Key Usage for client authentication. A requirement for ESC3. $EnrollmentAgentEKU = '1\.3\.6\.1\.4\.1\.311\.20\.2\.1' @@ -320,11 +320,13 @@ function Invoke-Locksmith { [!] You ran Locksmith in Mode 0 which only provides an high-level overview of issues identified in the environment. For more details including: - - DistinguishedName of impacted object(s) - - Remediation guidance and/or code + - Detailed Risk Rating + - General remediation guidance and/or code for all issues + - Custom remediation guidance and/or code for some issues! - Revert guidance and/or code (in case remediation breaks something!) + - Distinguished Name of impacted object(s) -Run Locksmith in Mode 1! +Try Mode 1! # Module version Invoke-Locksmith -Mode 1 @@ -385,5 +387,6 @@ Invoke-Locksmith -Mode 1 } } Write-Host 'Thank you for using ' -NoNewline - Write-Host "Locksmith <3`n" -ForegroundColor Magenta + Write-Host 'Locksmith <3 ' -ForegroundColor Magenta -NoNewline + Write-Host "(https://github.com/jakehildreth/Locksmith)`n" }