diff --git a/Build/Build-Module.ps1 b/Build/Build-Module.ps1 index 220b846..410bcfd 100644 --- a/Build/Build-Module.ps1 +++ b/Build/Build-Module.ps1 @@ -1,4 +1,9 @@ -if (Get-Module -Name 'PSPublishModule' -ListAvailable) { +param ( + # A CalVer string if you need to manually override the default yyyy.M version string. + [string]$CalVer +) + +if (Get-Module -Name 'PSPublishModule' -ListAvailable) { Write-Information 'PSPublishModule is installed.' } else { Write-Information 'PSPublishModule is not installed. Attempting installation.' @@ -17,7 +22,7 @@ Import-Module -Name PSPublishModule -Force Build-Module -ModuleName 'Locksmith' { # Usual defaults as per standard module $Manifest = [ordered] @{ - ModuleVersion = (Get-Date -Format yyyy.M) + ModuleVersion = if ($Calver) {$CalVer} else {(Get-Date -Format yyyy.M)} CompatiblePSEditions = @('Desktop', 'Core') GUID = 'b1325b42-8dc4-4f17-aa1f-dcb5984ca14a' Author = 'Jake Hildreth' @@ -30,6 +35,13 @@ Build-Module -ModuleName 'Locksmith' { } New-ConfigurationManifest @Manifest + # See [PR26](https://github.com/EvotecIT/PSPublishModule/pull/26) for notes about using placeholders and + # built-in placeholders for common module metadata. + # New-ConfigurationPlaceHolder -CustomReplacement @( + # @{ Find = '{CustomName}'; Replace = 'SpecialCase' } + # @{ Find = '{CustomVersion}'; Replace = '1.0.0' } + # ) + # Add standard module dependencies (directly, but can be used with loop as well) #New-ConfigurationModule -Type RequiredModule -Name 'PSSharedGoods' -Guid 'Auto' -Version 'Latest' diff --git a/Invoke-Locksmith.ps1 b/Invoke-Locksmith.ps1 index b9f45d8..f293862 100644 --- a/Invoke-Locksmith.ps1 +++ b/Invoke-Locksmith.ps1 @@ -421,10 +421,9 @@ function Find-ESC3Condition2 { $ADCSObjects | Where-Object { ($_.objectClass -eq 'pKICertificateTemplate') -and ($_.pkiExtendedKeyUsage -match $ClientAuthEKU) -and - ($_.'msPKI-Certificate-Name-Flag' -band 1) -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) ) + ($_.'msPKI-RA-Application-Policies' -match '1.3.6.1.4.1.311.20.2.1') -and + ($_.'msPKI-RA-Signature' -eq 1) } | ForEach-Object { foreach ($entry in $_.nTSecurityDescriptor.Access) { $Principal = New-Object System.Security.Principal.NTAccount($entry.IdentityReference) @@ -535,12 +534,14 @@ function Find-ESC4 { [int]$Mode ) $ADCSObjects | ForEach-Object { - $Principal = New-Object System.Security.Principal.NTAccount($_.nTSecurityDescriptor.Owner) - if ($Principal -match '^(S-1|O:)') { - $SID = $Principal - } - else { - $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value + if ($_.Name -ne '' -and $null -ne $_.Name) { + $Principal = [System.Security.Principal.NTAccount]::New($_.nTSecurityDescriptor.Owner) + if ($Principal -match '^(S-1|O:)') { + $SID = $Principal + } + else { + $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value + } } if ( ($_.objectClass -eq 'pKICertificateTemplate') -and ($SID -notmatch $SafeOwners) ) { @@ -685,12 +686,14 @@ function Find-ESC5 { $SafeObjectTypes ) $ADCSObjects | ForEach-Object { - $Principal = New-Object System.Security.Principal.NTAccount($_.nTSecurityDescriptor.Owner) - if ($Principal -match '^(S-1|O:)') { - $SID = $Principal - } - else { - $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value + if ($_.Name -ne '' -and $null -ne $_.Name) { + $Principal = New-Object System.Security.Principal.NTAccount($_.nTSecurityDescriptor.Owner) + if ($Principal -match '^(S-1|O:)') { + $SID = $Principal + } + else { + $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value + } } if ( ($_.objectClass -ne 'pKICertificateTemplate') -and ($SID -notmatch $SafeOwners) ) { @@ -854,20 +857,31 @@ function Find-ESC8 { $ADCSObjects | Where-Object { $_.CAEnrollmentEndpoint } | ForEach-Object { - $Issue = [pscustomobject]@{ - Forest = $_.CanonicalName.split('/')[0] - Name = $_.Name - DistinguishedName = $_.DistinguishedName - CAEnrollmentEndpoint = $_.CAEnrollmentEndpoint - Issue = 'HTTP enrollment is enabled.' - Fix = '[TODO]' - Revert = '[TODO]' - Technique = 'ESC8' - } - if ($_.CAEnrollmentEndpoint -match '^https') { - $Issue.Issue = 'HTTPS enrollment is enabled.' + foreach ($endpoint in $_.CAEnrollmentEndpoint) { + $Issue = [pscustomobject]@{ + Forest = $_.CanonicalName.split('/')[0] + Name = $_.Name + DistinguishedName = $_.DistinguishedName + CAEnrollmentEndpoint = $endpoint.URL + AuthType = $endpoint.Auth + Issue = 'An HTTP enrollment endpoint is available.' + Fix = @' +Disable HTTP access and enforce HTTPS. +Enable EPA. +Disable NTLM authentication (if possible.) +'@ + Revert = '[TODO]' + Technique = 'ESC8' + } + if ($endpoint.URL -match '^https:') { + $Issue.Issue = 'An HTTPS enrollment endpoint is available.' + $Issue.Fix = @' +Ensure EPA is enabled. +Disable NTLM authentication (if possible.) +'@ + } + $Issue } - $Issue } } } @@ -1038,7 +1052,7 @@ function Format-Result { } 1 { if ($Issue.Technique -eq 'ESC8') { - $Issue | Format-List Technique, Name, DistinguishedName, CAEnrollmentEndpoint, Issue, Fix + $Issue | Format-List Technique, Name, DistinguishedName, CAEnrollmentEndpoint, AuthType, Issue, Fix } else { $Issue | Format-List Technique, Name, DistinguishedName, Issue, Fix @@ -1614,7 +1628,7 @@ function Invoke-Scans { .EXAMPLE # Perform all scans - Invoke-Scans + Invoke-Scans .EXAMPLE # Perform only the 'Auditing' and 'ESC1' scans @@ -1626,6 +1640,8 @@ function Invoke-Scans { #> [CmdletBinding()] + [OutputType([hashtable])] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', 'Invoke-Scans', Justification = 'Performing multiple scans.')] param ( # Could split Scans and PromptMe into separate parameter sets. [Parameter()] @@ -1957,12 +1973,72 @@ function Set-AdditionalCAProperty { $ForestGC ) + begin { + $CAEnrollmentEndpoint = @() + $code = @" +using System.Net; +using System.Security.Cryptography.X509Certificates; +public class TrustAllCertsPolicy : ICertificatePolicy { + public bool CheckValidationResult(ServicePoint srvPoint, X509Certificate certificate, WebRequest request, int certificateProblem) { + return true; + } +} +"@ + Add-Type -TypeDefinition $code -Language CSharp + [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy + } + process { $ADCSObjects | Where-Object objectClass -Match 'pKIEnrollmentService' | ForEach-Object { - [string]$CAEnrollmentEndpoint = $_.'msPKI-Enrollment-Servers' | Select-String 'http.*' | ForEach-Object { $_.Matches[0].Value } + #[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 { + $Auth = 'NTLM' + $FullURL = "http$URL" + $Request = [System.Net.WebRequest]::Create($FullURL) + $Cache = [System.Net.CredentialCache]::New() + $Cache.Add([System.Uri]::new($FullURL), $Auth, [System.Net.CredentialCache]::DefaultNetworkCredentials) + $Request.Credentials = $Cache + $Request.Timeout = 3000 + $Request.GetResponse() | Out-Null + $CAEnrollmentEndpoint += @{ + 'URL' = $FullURL + 'Auth' = $Auth + } + } + catch { + try { + $FullURL = "https$URL" + $Request = [System.Net.WebRequest]::Create($FullURL) + + $Request.GetResponse() | Out-Null + $CAEnrollmentEndpoint += @{ + 'URL' = $FullURL + 'Auth' = $Auth + } + } + catch { + try { + $Auth = 'Negotiate' + $FullURL = "https$URL" + $Request = [System.Net.WebRequest]::Create($FullURL) + $Cache = [System.Net.CredentialCache]::New() + $Cache.Add([System.Uri]::new($FullURL), 'Negotiate', [System.Net.CredentialCache]::DefaultNetworkCredentials) + $Request.Credentials = $Cache + $Request.GetResponse() | Out-Null + $CAEnrollmentEndpoint += @{ + 'URL' = $FullURL + 'Auth' = $Auth + } + } + catch { + } + } + } + } [string]$CAFullName = "$($_.dNSHostName)\$($_.Name)" $CAHostname = $_.dNSHostName.split('.')[0] - # $CAName = $_.Name 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 @@ -1972,7 +2048,7 @@ function Set-AdditionalCAProperty { $CAHostFQDN = (Get-ADObject -Filter { (Name -eq $CAHostName) -and (objectclass -eq 'computer') } -Properties DnsHostname -Server $ForestGC).DnsHostname } $ping = Test-Connection -ComputerName $CAHostFQDN -Quiet -Count 1 - if ($ping) { + if ($ping) { try { if ($Credential) { $CertutilAudit = Invoke-Command -ComputerName $CAHostname -Credential $Credential -ScriptBlock { param($CAFullName); certutil -config $CAFullName -getreg CA\AuditFilter } -ArgumentList $CAFullName @@ -2035,6 +2111,7 @@ function Set-AdditionalCAProperty { } function Set-Severity { + [OutputType([string])] [CmdletBinding()] param( [Parameter(Mandatory = $true)] @@ -2162,9 +2239,11 @@ function Test-IsMemberOfProtectedUsers { Active Directory user object, user SID, SamAccountName, etc .OUTPUTS - True, False + Boolean #> + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', 'Test-IsMemberOfProtectedUsers', Justification = 'The name of the group we are checking is plural.')] + [OutputType([Boolean])] [CmdletBinding()] param ( # User parameter accepts any input that is valid for Get-ADUser @@ -2243,12 +2322,9 @@ function Test-IsRecentVersion { Published at: 01/28/2024 12:47:18 Install Module: Install-Module -Name Locksmith Standalone Script: https://github.com/trimarcjake/locksmith/releases/download/v2.6/Invoke-Locksmith.zip - - .NOTES - Author: Sam Erde - Date: 02/10/2024 #> [CmdletBinding()] + [OutputType([boolean])] param ( # Check a specific version number from the script [Parameter(Mandatory)] @@ -2493,7 +2569,7 @@ function Invoke-Locksmith { 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 + Locksmith uses the Active Directory (AD) Powershell (PS) module to identify 7 misconfigurations commonly found in Enterprise mode AD CS installations. .COMPONENT @@ -2555,17 +2631,30 @@ function Invoke-Locksmith { [CmdletBinding()] param ( - [string]$Forest, - [string]$InputPath, + #[string]$Forest, # Not used yet + #[string]$InputPath, # Not used yet + + # The mode to run Locksmith in. Defaults to 0. + [Parameter()] + [ValidateSet(0, 1, 2, 3, 4)] [int]$Mode = 0, + + # The scans to run. Defaults to 'All'. [Parameter()] [ValidateSet('Auditing', 'ESC1', 'ESC2', 'ESC3', 'ESC4', 'ESC5', 'ESC6', 'ESC8', 'All', 'PromptMe')] [array]$Scans = 'All', - [string]$OutputPath = (Get-Location).Path, + + # The directory to save the output in (defaults to the current working directory). + [Parameter()] + [ValidateScript({ Test-Path -Path $_ -PathType Container })] + [string]$OutputPath = $PWD, + + # The credential to use for working with ADCS. + [Parameter()] [System.Management.Automation.PSCredential]$Credential ) - $Version = '2024.8' + $Version = '2024.10' $LogoPart1 = @" _ _____ _______ _ _ _______ _______ _____ _______ _ _ | | | | |____/ |______ | | | | | |_____| @@ -2595,10 +2684,13 @@ function Invoke-Locksmith { # 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; + break } ### Initial variables + # For output filenames + [string]$FilePrefix = "Locksmith $(Get-Date -Format 'yyyy-MM-dd hh-mm-ss')" + # Extended Key Usages for client authentication. A requirement for ESC1 $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' @@ -2648,18 +2740,20 @@ function Invoke-Locksmith { ### Generated variables # $Dictionary = New-Dictionary + + $Forest = Get-ADForest $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' + # $DNSRoot = [string]($Forest.RootDomain | Get-ADDomain).DNSRoot + $EnterpriseAdminsSID = ([string]($Forest.RootDomain | Get-ADDomain).DomainSID) + '-519' $PreferredOwner = [System.Security.Principal.SecurityIdentifier]::New($EnterpriseAdminsSID) - # $DomainSIDs = (Get-ADForest).Domains | ForEach-Object { (Get-ADDomain $_).DomainSID.Value } + # $DomainSIDs = $Forest.Domains | ForEach-Object { (Get-ADDomain $_).DomainSID.Value } # Add SIDs of (probably) Safe Users to $SafeUsers Get-ADGroupMember $EnterpriseAdminsSID | ForEach-Object { $SafeUsers += '|' + $_.SID.Value } - (Get-ADForest).Domains | ForEach-Object { + $Forest.Domains | ForEach-Object { $DomainSID = (Get-ADDomain $_).DomainSID.Value <# -517 = Cert Publishers @@ -2762,7 +2856,7 @@ function Invoke-Locksmith { Format-Result $ESC8 '1' } 2 { - $Output = 'ADCSIssues.CSV' + $Output = Join-Path -Path $OutputPath -ChildPath "$FilePrefix ADCSIssues.CSV" Write-Host "Writing AD CS issues to $Output..." try { $AllIssues | Select-Object Forest, Technique, Name, Issue | Export-Csv -NoTypeInformation $Output @@ -2773,7 +2867,7 @@ function Invoke-Locksmith { } } 3 { - $Output = 'ADCSRemediation.CSV' + $Output = Join-Path -Path $OutputPath -ChildPath "$FilePrefix ADCSRemediation.CSV" Write-Host "Writing AD CS issues to $Output..." try { $AllIssues | Select-Object Forest, Technique, Name, DistinguishedName, Issue, Fix | Export-Csv -NoTypeInformation $Output diff --git a/Locksmith.psd1 b/Locksmith.psd1 index 13596d7..7d3dcd3 100644 --- a/Locksmith.psd1 +++ b/Locksmith.psd1 @@ -7,7 +7,7 @@ Description = 'A small tool to find and fix common misconfigurations in Active Directory Certificate Services.' FunctionsToExport = @('*') GUID = 'b1325b42-8dc4-4f17-aa1f-dcb5984ca14a' - ModuleVersion = '2024.8' + ModuleVersion = '2024.10' PowerShellVersion = '5.1' PrivateData = @{ PSData = @{ diff --git a/Private/Find-ESC3Condition2.ps1 b/Private/Find-ESC3Condition2.ps1 index 1ab9503..c8ab537 100644 --- a/Private/Find-ESC3Condition2.ps1 +++ b/Private/Find-ESC3Condition2.ps1 @@ -33,10 +33,9 @@ $ADCSObjects | Where-Object { ($_.objectClass -eq 'pKICertificateTemplate') -and ($_.pkiExtendedKeyUsage -match $ClientAuthEKU) -and - ($_.'msPKI-Certificate-Name-Flag' -band 1) -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) ) + ($_.'msPKI-RA-Application-Policies' -match '1.3.6.1.4.1.311.20.2.1') -and + ($_.'msPKI-RA-Signature' -eq 1) } | ForEach-Object { foreach ($entry in $_.nTSecurityDescriptor.Access) { $Principal = New-Object System.Security.Principal.NTAccount($entry.IdentityReference) diff --git a/Private/Find-ESC4.ps1 b/Private/Find-ESC4.ps1 index 5079ab0..1f103be 100644 --- a/Private/Find-ESC4.ps1 +++ b/Private/Find-ESC4.ps1 @@ -75,11 +75,13 @@ [int]$Mode ) $ADCSObjects | ForEach-Object { - $Principal = New-Object System.Security.Principal.NTAccount($_.nTSecurityDescriptor.Owner) - if ($Principal -match '^(S-1|O:)') { - $SID = $Principal - } else { - $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value + if ($_.Name -ne '' -and $null -ne $_.Name) { + $Principal = [System.Security.Principal.NTAccount]::New($_.nTSecurityDescriptor.Owner) + if ($Principal -match '^(S-1|O:)') { + $SID = $Principal + } else { + $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value + } } if ( ($_.objectClass -eq 'pKICertificateTemplate') -and ($SID -notmatch $SafeOwners) ) { diff --git a/Private/Find-ESC5.ps1 b/Private/Find-ESC5.ps1 index fcaf9f5..25f3d31 100644 --- a/Private/Find-ESC5.ps1 +++ b/Private/Find-ESC5.ps1 @@ -73,11 +73,13 @@ $SafeObjectTypes ) $ADCSObjects | ForEach-Object { - $Principal = New-Object System.Security.Principal.NTAccount($_.nTSecurityDescriptor.Owner) - if ($Principal -match '^(S-1|O:)') { - $SID = $Principal - } else { - $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value + if ($_.Name -ne '' -and $null -ne $_.Name) { + $Principal = New-Object System.Security.Principal.NTAccount($_.nTSecurityDescriptor.Owner) + if ($Principal -match '^(S-1|O:)') { + $SID = $Principal + } else { + $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value + } } if ( ($_.objectClass -ne 'pKICertificateTemplate') -and ($SID -notmatch $SafeOwners) ) { diff --git a/Private/Find-ESC8.ps1 b/Private/Find-ESC8.ps1 index 04f9589..6d8137e 100644 --- a/Private/Find-ESC8.ps1 +++ b/Private/Find-ESC8.ps1 @@ -37,20 +37,31 @@ $ADCSObjects | Where-Object { $_.CAEnrollmentEndpoint } | ForEach-Object { - $Issue = [pscustomobject]@{ - Forest = $_.CanonicalName.split('/')[0] - Name = $_.Name - DistinguishedName = $_.DistinguishedName - CAEnrollmentEndpoint = $_.CAEnrollmentEndpoint - Issue = 'HTTP enrollment is enabled.' - Fix = '[TODO]' - Revert = '[TODO]' - Technique = 'ESC8' + foreach ($endpoint in $_.CAEnrollmentEndpoint) { + $Issue = [pscustomobject]@{ + Forest = $_.CanonicalName.split('/')[0] + Name = $_.Name + DistinguishedName = $_.DistinguishedName + CAEnrollmentEndpoint = $endpoint.URL + AuthType = $endpoint.Auth + Issue = 'An HTTP enrollment endpoint is available.' + Fix = @' +Disable HTTP access and enforce HTTPS. +Enable EPA. +Disable NTLM authentication (if possible.) +'@ + Revert = '[TODO]' + Technique = 'ESC8' + } + if ($endpoint.URL -match '^https:') { + $Issue.Issue = 'An HTTPS enrollment endpoint is available.' + $Issue.Fix = @' +Ensure EPA is enabled. +Disable NTLM authentication (if possible.) +'@ + } + $Issue } - if ($_.CAEnrollmentEndpoint -match '^https') { - $Issue.Issue = 'HTTPS enrollment is enabled.' - } - $Issue } } } diff --git a/Private/Format-Result.ps1 b/Private/Format-Result.ps1 index c573258..7104e27 100644 --- a/Private/Format-Result.ps1 +++ b/Private/Format-Result.ps1 @@ -50,7 +50,7 @@ function Format-Result { } 1 { if ($Issue.Technique -eq 'ESC8') { - $Issue | Format-List Technique, Name, DistinguishedName, CAEnrollmentEndpoint, Issue, Fix + $Issue | Format-List Technique, Name, DistinguishedName, CAEnrollmentEndpoint, AuthType, Issue, Fix } else { $Issue | Format-List Technique, Name, DistinguishedName, Issue, Fix if(($Issue.Technique -eq "DETECT" -or $Issue.Technique -eq "ESC6") -and (Get-RestrictedAdminModeSetting)){ diff --git a/Private/Invoke-Scans.ps1 b/Private/Invoke-Scans.ps1 index 4f9f20d..75ed69e 100644 --- a/Private/Invoke-Scans.ps1 +++ b/Private/Invoke-Scans.ps1 @@ -28,7 +28,7 @@ function Invoke-Scans { .EXAMPLE # Perform all scans - Invoke-Scans + Invoke-Scans .EXAMPLE # Perform only the 'Auditing' and 'ESC1' scans @@ -40,6 +40,8 @@ function Invoke-Scans { #> [CmdletBinding()] + [OutputType([hashtable])] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', 'Invoke-Scans', Justification='Performing multiple scans.')] param ( # Could split Scans and PromptMe into separate parameter sets. [Parameter()] diff --git a/Private/Set-AdditionalCAProperty.ps1 b/Private/Set-AdditionalCAProperty.ps1 index c32e27f..a397a47 100644 --- a/Private/Set-AdditionalCAProperty.ps1 +++ b/Private/Set-AdditionalCAProperty.ps1 @@ -35,12 +35,69 @@ $ForestGC ) + begin { + $CAEnrollmentEndpoint = @() + $code= @" +using System.Net; +using System.Security.Cryptography.X509Certificates; +public class TrustAllCertsPolicy : ICertificatePolicy { + public bool CheckValidationResult(ServicePoint srvPoint, X509Certificate certificate, WebRequest request, int certificateProblem) { + return true; + } +} +"@ + Add-Type -TypeDefinition $code -Language CSharp + [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy + } + process { $ADCSObjects | Where-Object objectClass -Match 'pKIEnrollmentService' | ForEach-Object { - [string]$CAEnrollmentEndpoint = $_.'msPKI-Enrollment-Servers' | Select-String 'http.*' | ForEach-Object { $_.Matches[0].Value } + #[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 { + $Auth = 'NTLM' + $FullURL = "http$URL" + $Request = [System.Net.WebRequest]::Create($FullURL) + $Cache = [System.Net.CredentialCache]::New() + $Cache.Add([System.Uri]::new($FullURL), $Auth, [System.Net.CredentialCache]::DefaultNetworkCredentials) + $Request.Credentials = $Cache + $Request.Timeout = 3000 + $Request.GetResponse() | Out-Null + $CAEnrollmentEndpoint += @{ + 'URL' = $FullURL + 'Auth' = $Auth + } + } catch { + try { + $FullURL = "https$URL" + $Request = [System.Net.WebRequest]::Create($FullURL) + + $Request.GetResponse() | Out-Null + $CAEnrollmentEndpoint += @{ + 'URL' = $FullURL + 'Auth' = $Auth + } + } catch { + try { + $Auth = 'Negotiate' + $FullURL = "https$URL" + $Request = [System.Net.WebRequest]::Create($FullURL) + $Cache = [System.Net.CredentialCache]::New() + $Cache.Add([System.Uri]::new($FullURL), 'Negotiate', [System.Net.CredentialCache]::DefaultNetworkCredentials) + $Request.Credentials = $Cache + $Request.GetResponse() | Out-Null + $CAEnrollmentEndpoint += @{ + 'URL' = $FullURL + 'Auth' = $Auth + } + } catch { + } + } + } + } [string]$CAFullName = "$($_.dNSHostName)\$($_.Name)" $CAHostname = $_.dNSHostName.split('.')[0] - # $CAName = $_.Name 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 @@ -49,7 +106,7 @@ $CAHostFQDN = (Get-ADObject -Filter { (Name -eq $CAHostName) -and (objectclass -eq 'computer') } -Properties DnsHostname -Server $ForestGC).DnsHostname } $ping = Test-Connection -ComputerName $CAHostFQDN -Quiet -Count 1 - if ($ping) { + if ($ping) { try { if ($Credential) { $CertutilAudit = Invoke-Command -ComputerName $CAHostname -Credential $Credential -ScriptBlock { param($CAFullName); certutil -config $CAFullName -getreg CA\AuditFilter } -ArgumentList $CAFullName diff --git a/Private/Set-Severity.ps1 b/Private/Set-Severity.ps1 index 620dac3..76e1362 100644 --- a/Private/Set-Severity.ps1 +++ b/Private/Set-Severity.ps1 @@ -1,4 +1,5 @@ function Set-Severity { + [OutputType([string])] [CmdletBinding()] param( [Parameter(Mandatory = $true)] diff --git a/Private/Test-IsMemberOfProtectedUsers.ps1 b/Private/Test-IsMemberOfProtectedUsers.ps1 index f23f1b5..4d3935b 100644 --- a/Private/Test-IsMemberOfProtectedUsers.ps1 +++ b/Private/Test-IsMemberOfProtectedUsers.ps1 @@ -24,9 +24,11 @@ function Test-IsMemberOfProtectedUsers { Active Directory user object, user SID, SamAccountName, etc .OUTPUTS - True, False + Boolean #> + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', 'Test-IsMemberOfProtectedUsers', Justification='The name of the group we are checking is plural.')] + [OutputType([Boolean])] [CmdletBinding()] param ( # User parameter accepts any input that is valid for Get-ADUser diff --git a/Private/Test-IsRecentVersion.ps1 b/Private/Test-IsRecentVersion.ps1 index 0400918..9d1b7f4 100644 --- a/Private/Test-IsRecentVersion.ps1 +++ b/Private/Test-IsRecentVersion.ps1 @@ -30,12 +30,9 @@ function Test-IsRecentVersion { Published at: 01/28/2024 12:47:18 Install Module: Install-Module -Name Locksmith Standalone Script: https://github.com/trimarcjake/locksmith/releases/download/v2.6/Invoke-Locksmith.zip - - .NOTES - Author: Sam Erde - Date: 02/10/2024 #> [CmdletBinding()] + [OutputType([boolean])] param ( # Check a specific version number from the script [Parameter(Mandatory)] diff --git a/Public/Invoke-Locksmith.ps1 b/Public/Invoke-Locksmith.ps1 index 29e9fe2..3882deb 100644 --- a/Public/Invoke-Locksmith.ps1 +++ b/Public/Invoke-Locksmith.ps1 @@ -4,7 +4,7 @@ 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 + Locksmith uses the Active Directory (AD) Powershell (PS) module to identify 7 misconfigurations commonly found in Enterprise mode AD CS installations. .COMPONENT @@ -66,17 +66,30 @@ [CmdletBinding()] param ( - [string]$Forest, - [string]$InputPath, - [int]$Mode = 0, + #[string]$Forest, # Not used yet + #[string]$InputPath, # Not used yet + + # The mode to run Locksmith in. Defaults to 0. + [Parameter()] + [ValidateSet(0,1,2,3,4)] + [int]$Mode = 0, + + # The scans to run. Defaults to 'All'. [Parameter()] [ValidateSet('Auditing','ESC1','ESC2','ESC3','ESC4','ESC5','ESC6','ESC8','All','PromptMe')] [array]$Scans = 'All', - [string]$OutputPath = (Get-Location).Path, + + # The directory to save the output in (defaults to the current working directory). + [Parameter()] + [ValidateScript({Test-Path -Path $_ -PathType Container})] + [string]$OutputPath = $PWD, + + # The credential to use for working with ADCS. + [Parameter()] [System.Management.Automation.PSCredential]$Credential ) - $Version = '2024.8' + $Version = '' $LogoPart1 = @" _ _____ _______ _ _ _______ _______ _____ _______ _ _ | | | | |____/ |______ | | | | | |_____| @@ -105,10 +118,13 @@ # 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; + break } ### Initial variables + # For output filenames + [string]$FilePrefix = "Locksmith $(Get-Date -format 'yyyy-MM-dd hh-mm-ss')" + # Extended Key Usages for client authentication. A requirement for ESC1 $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' @@ -158,18 +174,20 @@ ### Generated variables # $Dictionary = New-Dictionary + + $Forest = Get-ADForest $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' + # $DNSRoot = [string]($Forest.RootDomain | Get-ADDomain).DNSRoot + $EnterpriseAdminsSID = ([string]($Forest.RootDomain | Get-ADDomain).DomainSID) + '-519' $PreferredOwner = [System.Security.Principal.SecurityIdentifier]::New($EnterpriseAdminsSID) - # $DomainSIDs = (Get-ADForest).Domains | ForEach-Object { (Get-ADDomain $_).DomainSID.Value } + # $DomainSIDs = $Forest.Domains | ForEach-Object { (Get-ADDomain $_).DomainSID.Value } # Add SIDs of (probably) Safe Users to $SafeUsers Get-ADGroupMember $EnterpriseAdminsSID | ForEach-Object { $SafeUsers += '|' + $_.SID.Value } - (Get-ADForest).Domains | ForEach-Object { + $Forest.Domains | ForEach-Object { $DomainSID = (Get-ADDomain $_).DomainSID.Value <# -517 = Cert Publishers @@ -270,7 +288,7 @@ Format-Result $ESC8 '1' } 2 { - $Output = 'ADCSIssues.CSV' + $Output = Join-Path -Path $OutputPath -ChildPath "$FilePrefix ADCSIssues.CSV" Write-Host "Writing AD CS issues to $Output..." try { $AllIssues | Select-Object Forest, Technique, Name, Issue | Export-Csv -NoTypeInformation $Output @@ -280,7 +298,7 @@ } } 3 { - $Output = 'ADCSRemediation.CSV' + $Output = Join-Path -Path $OutputPath -ChildPath "$FilePrefix ADCSRemediation.CSV" Write-Host "Writing AD CS issues to $Output..." try { $AllIssues | Select-Object Forest, Technique, Name, DistinguishedName, Issue, Fix | Export-Csv -NoTypeInformation $Output