diff --git a/NTLM/Get-KerbEncryptionUsage.ps1 b/NTLM/Get-KerbEncryptionUsage.ps1 new file mode 100644 index 0000000..92f3a7c --- /dev/null +++ b/NTLM/Get-KerbEncryptionUsage.ps1 @@ -0,0 +1,345 @@ +<# +.SYNOPSIS +Retrieves ticket and session key encryption types +.DESCRIPTION +Searches the Security Event Log for instances of Event Id 4769 and Event Id 4768 to create a list of encryption types used in Kerberos tickets +.EXAMPLE +Get-KerbEncryptionUsage # This will list all requests seen in the the past 30 days +.EXAMPLE +Get-KerbEncryptionUsage -Encryption RC4 -EncryptionUsage Ticket # This will list all requests that used RC4 in the Ticket encryption +.EXAMPLE +Get-KerbEncryptionUsage -Searchscope AllKdcs -Since (Get-Date).AddDays(-7) # This will list all requests querying all KDCs for events in the past 7 days + +.PARAMETER Encryption +Specifies the encryption type to be queried +.PARAMETER Since +Specifies the earliest point to be queried from +.PARAMETER SearchScope +Specifies whether the query should be the local machine or all KDCs +.PARAMETER EncryptionUsage +Specifies where to check for encryption usage. Ticket, SessionKey, Either or Both + +.NOTES +Author: Will Aftring (wiaftrin) + +When specifying AllKdcs, to pull the event log results remote Event Log reading must be enabled. + +Copyright (c) Microsoft Corporation. All rights reserved. + +#> + +[CmdletBinding()] +param( + [ValidateSet("RC4", "DES", "AES-SHA1", "AES128-SHA96", "AES256-SHA96", "All")] + [string]$Encryption = "All", + [DateTime]$Since = $(Get-Date).AddDays(-30), + [ValidateSet("This", "AllKdcs")] + [string]$SearchScope = "This", + [ValidateSet("Ticket", "SessionKey", "Either", "Both")] + [string]$EncryptionUsage = "Either" +) + +#region Classes +class EncryptionType { + [string]$Name + [int]$Value + EncryptionType([string]$name, [int]$value) { + $this.Name = $name + $this.Value = $value + } + [string]ToDataString() { + return "Data='0x{0:x}'" -f $this.Value + } + + [string]ToString() { + return $this.Name + } + + [bool]Equals([object]$other) { + if ($null -eq $other -or $this.GetType() -ne $other.GetType()) { + return $false + } + $EType = [EncryptionType]$other + return $EType.Name -eq $this.Name -and $EType.Value -eq $this.Value + } +} + +enum RequestType { + AS + TGS +} + +class KerbRequest { + hidden [long]$RecordId + [string]$MachineName + [DateTime]$Time + [string]$Requestor + [string]$Source + [string]$Target + [RequestType]$Type + [EncryptionType]$Ticket + [EncryptionType]$SessionKey + + KerbRequest([long]$id, [string]$m, [datetime]$tc, [string]$r, [string]$s, [string]$t, [RequestType]$rt, [EncryptionType]$te, [EncryptionType]$se) { + $this.RecordId = $id + $this.MachineName = $m + $this.Time = $tc + if ($r.StartsWith("::ffff:")) { + $r = $r.Replace("::ffff:", "") + } + $this.Requestor = $r + $this.Source = $s + $this.Target = $t + $this.Type = $rt + $this.Ticket = $te + $this.SessionKey = $se + } + + [string] GetEvtFilter() { + return @" + + + + + +"@ + } + + [System.Diagnostics.Eventing.Reader.EventLogRecord] ShowEvent() { + $query = $this.GetEvtFilter() + if ($this.MachineName.ToUpper() -eq "$ENV:COMPUTERNAME`.$ENV:USERDNSDOMAIN") { + return Get-WinEvent -FilterXPath $query -LogName Security + } else { + return Get-WinEvent -FilterXPath $query -LogName Security -ComputerName $this.MachineName + } + } +} + +#endregion + + + +#region Globals + +$script:DES_CRC = [EncryptionType]::new("DES-CRC", 0x1) +$script:DES_MD5 = [EncryptionType]::new("DES-MD5", 0x3) +$script:RC4 = [EncryptionType]::new("RC4", 0x17) +$script:AES128 = [EncryptionType]::new("AES128-SHA96", 0x11) +$script:AES256 = [EncryptionType]::new("AES256-SHA96", 0x12) +$script:AES128_SHA256 = [EncryptionType]::new("AES128-SHA256", 0x13) +$script:AES256_SHA384 = [EncryptionType]::new("AES256-SHA384", 0x14) +$script:UnknownEType = [EncryptionType]::new("Unknown", 0xFF) + +$script:EncryptionTypes = @( + $script:DES_CRC + $script:DES_MD5 + $script:RC4 + $script:AES128 + $script:AES256 + $script:AES128_SHA256 + $script:AES256_SHA384 + $script:UnknownEType +) + +<# + The new properties counts are 21 for 4769 and 24 for 4668. Meaning if we have a lower + property count then we are reading the old event data. +#> +$script:MIN_PROPERTY_COUNT = 21 + +$script:XPathQuery = @" + + + + + + + + +"@ + +#endregion + +#region Functions +function Get-KdcEventLog { + param( + [string]$KDCName = $null, + [string]$Query + ) + Write-Debug "Query:`n$Query to KDC '$KDCName'" + Write-Verbose "Attempting to query $KDCName" + $Results = $null + try { + if ([string]::IsNullOrEmpty($KDCName)) { + $Results = Get-WinEvent -FilterXPath $Query -LogName Security -ErrorAction Stop + } + else { + $Results = Get-WinEvent -ComputerName $KDCName -FilterXPath $Query -LogName Security -ErrorAction Stop + } + } + catch { + if ($_.FullyQualifiedErrorId -eq "NoMatchingEventsFound,Microsoft.PowerShell.Commands.GetWinEventCommand") { + $RealKdcName = if ($null -eq $KDCName) { "$ENV:COMPUTERNAME" } else { $KDCName } + Write-Warning "No events found on $RealKdcName" + } + else { + throw $_ + } + } + + Write-Debug "$Results" + return $Results +} + +function Check-ETypeUsage { + param( + [string]$UsageMode, + [EncryptionType]$TicketEtype, + [EncryptionType]$SKEtype, + [EncryptionType]$SearchEtype + ) + + if ("Both" -eq $EncryptionUsage) { + return $($TicketEtype -eq $SKEtype -and $SearchEtype -eq $TicketEtype) + } + elseif ("Ticket" -eq $EncryptionUsage) { + return $($TicketEtype -eq $SearchEtype) + } + elseif ("SessionKey" -eq $EncryptionUsage) { + return $($SKEtype -eq $SearchEtype) + } + else { + return $($SKEtype -eq $SearchEtype -or $TicketEtype -eq $SearchEtype) + } +} + +function Get-EncryptionType { + param( + [Parameter(Mandatory = $true, ParameterSetName = "Name")] + [string]$Name, + [Parameter(Mandatory = $true, ParameterSetName = "Value")] + [int]$Value + ) + + foreach ($etype in $script:EncryptionTypes) { + if (($PSCmdlet.ParameterSetName -eq "Name" -and $etype.Name -eq $Name) ` + -or ($PSCmdlet.ParameterSetName -eq "Value" -and $etype.Value -eq $Value)) { + return $etype + } + } + + return $script:UnknownEType +} + +function Get-EncryptionTypes { + return $script:EncryptionTypes +} + + +function Get-KerbEncryptionUsage { + [CmdletBinding()] + param( + [ValidateSet("RC4", "DES", "AES-SHA1", "AES128-SHA96", "AES256-SHA96", "All")] + [string]$Encryption = "All", + [DateTime]$Since = $(Get-Date).AddDays(-30), + [ValidateSet("This", "AllKdcs")] + [string]$SearchScope = "This", + [ValidateSet("Ticket", "SessionKey", "Either", "Both")] + [string]$EncryptionUsage = "Either" + ) + + + $Events = [System.Collections.ArrayList]::new() + if ("AllKdcs" -eq $SearchScope) { + + Get-ADDomainController -Filter * | ForEach-Object { + $KDCName = $_.HostName + try { + [Array]$KdcResult = $(Get-KdcEventLog -KDCName $KDCName -Query $script:XPathQuery) + + if ($null -ne $KdcResult -and 0 -ne $KdcResult.Count) { + $Events.AddRange($KdcResult) + } + } + catch { + Write-Error "Failed to get event logs from $KDCName with result: $_" + } + } + } + else { + try { + [Array]$LocalResult = $(Get-KdcEventLog -Query $script:XPathQuery) + + if ($null -ne $LocalResult -and 0 -ne $LocalResult.Count) { + $Events.AddRange($LocalResult) + } + } + catch { + Write-Error "Failed to get event logs from $KDCName with result: $_" + } + } + + # Validate we are working with the correct version + if ($accounts.Count -gt 0 -and $accounts[0].Properties.Count -lt $script:MIN_PROPERTY_COUNT) { + Write-Error "Attempting to run script on Windows Version $([System.Environment]::OSVersion.Version) which doesn't have the new event metadata. +Please install the most recent Windows Updates available for this machine and attempt again." + return + } + + + Write-Verbose "Total events: $($Events.Count)" + $Events | ForEach-Object { + $ShowRequest = $true + $T = $null + $SK = $null + $R = $null + $Target = $null + $IP = $null + + if ($_.Id -eq 4769) { + $Target = $_.Properties[2].Value + $T = Get-EncryptionType -Value $_.Properties[5].Value + $SK = Get-EncryptionType -Value $_.Properties[20].Value + $R = [RequestType]::TGS + $IP = $_.Properties[6].Value + } + else { + $Target = $_.Properties[3].Value + $T = Get-EncryptionType -Value $_.Properties[7].Value + $SK = Get-EncryptionType -Value $_.Properties[22].Value + $R = [RequestType]::AS + $IP = $_.Properties[9].Value + } + + if ("DES" -eq $Encryption) { + $D1 = Check-ETypeUsage -UsageMode $EncryptionUsage -TicketEtype $T -SKEtype $SK -SearchEtype $script:DES_CRC + $D2 = Check-ETypeUsage -UsageMode $EncryptionUsage -TicketEtype $T -SKEtype $SK -SearchEtype $script:DES_MD5 + $ShowRequest = $D1 -or $D2 + } + elseif ("AES-SHA1" -eq $Encryption) { + $A1 = Check-ETypeUsage -UsageMode $EncryptionUsage -TicketEtype $T -SKEtype $SK -SearchEtype $script:AES128 + $A2 = Check-ETypeUsage -UsageMode $EncryptionUsage -TicketEtype $T -SKEtype $SK -SearchEtype $script:AES256 + $ShowRequest = $A1 -or $A2 + } + elseif ("All" -ne $Encryption) { + $Etype = Get-EncryptionType -Name $Encryption + $ShowRequest = $(Check-ETypeUsage -UsageMode $EncryptionUsage -TicketEtype $T -SKEtype $SK -SearchEtype $EType) + } + + if ($ShowRequest) { + [KerbRequest]::new($_.RecordId, $_.MachineName, $_.TimeCreated, $IP, $_.Properties[0].Value, $Target, $R, $T, $SK) + } + } +} + +#endregion + +if ($MyInvocation.InvocationName -ne ".") { + Get-KerbEncryptionUsage @PSBoundParameters +} + +#endregion \ No newline at end of file diff --git a/NTLM/List-AccountKeys.ps1 b/NTLM/List-AccountKeys.ps1 new file mode 100644 index 0000000..f1642df --- /dev/null +++ b/NTLM/List-AccountKeys.ps1 @@ -0,0 +1,268 @@ +<# +.SYNOPSIS +Retrieves the observed Account Key types +.DESCRIPTION +Searches the Security Event Logs for intstances of Event Id 4769 and Event Id 4768 to determine which account keys are used. + +.EXAMPLE +List-AccountKeys # This will list all accounts and their key types found in the past 90 days + +.PARAMETER Since +Specifies the earliest time to be searched since +.PARAMETER SearchScope +Specifies whether we should search all KDCs in the domain or just the local machine + +.NOTES +Author: Will Aftring (wiaftrin) + +When specifying AllKdcs, to pull the event log results remote Event Log reading must be enabled. + +Copyright (c) Microsoft Corporation. All rights reserved. + +#> + + +[CmdletBinding()] +param( + [DateTime]$Since = $(Get-Date).AddDays(-30), + [ValidateSet("DES", "RC4", "AES-SHA1", "All")] + [string]$ContainsKeyType = "All", + [ValidateSet("DES", "RC4", "AES-SHA1", "None")] + [string]$NotContainsKeyType = "None", + [ValidateSet("This", "AllKdcs")] + [string]$SearchScope = "This" +) + +<# + N.B(wiaftrin): On Windows Server 2022 the AES-SHA1 keys are aggregated into a single string. + On Windows Server 2025+, the keys are called out individually. +#> + +# AES-SHA1 on 2022- +$script:AES_SHA1_FILTER_2022 = "AES-SHA1" +#AES-SHA1 on 2025+ +$script:AES_SHA1_FILTER_2025 = "SHA96" + +enum AccountType { + User + Machine + Service +} + +class Account { + hidden [long]$RecordId + [string]$MachineName + [datetime]$Time + [string]$Name + [AccountType]$Type + [string]$Keys + + Account([long]$id, [string]$m, [datetime]$tc, [string]$name, [AccountType]$ct, [string]$ckeys) { + $this.RecordId = $id + $this.MachineName = $m + $this.Time = $tc + $this.Name = $name + $this.Type = $ct + $tmp = [System.Collections.ArrayList]::new() + + $ckeys.Split(",").Trim() | ForEach-Object { + if ($_ -eq $script:AES_SHA1_FILTER_2022) { + $tmp.Add("AES128-SHA96") + $tmp.Add("AES256-SHA96") + } + else { + $tmp.Add($_) + } + } + + $this.Keys = $tmp -join '; ' # Using ; for CSV to be more CSV friendly + } + + [string] GetEvtFilter() { + return @" + + + + + +"@ + } + + [System.Diagnostics.Eventing.Reader.EventLogRecord] ShowEvent() { + $query = $this.GetEvtFilter() + if ($this.MachineName.ToUpper() -eq "$ENV:COMPUTERNAME`.$ENV:USERDNSDOMAIN") { + return Get-WinEvent -FilterXPath $query -LogName Security + } else { + return Get-WinEvent -FilterXPath $query -LogName Security -ComputerName $this.MachineName + } + } +} + +#endregion + +#region Globals + +$script:XPathQuery = @" + + + + + + + + +"@ + +<# + The new properties counts are 21 for 4769 and 24 for 4668. Meaning if we have a lower + property count then we are reading the old event data. +#> +$script:MIN_PROPERTY_COUNT = 21 + +$script:KeyFilter = "" +$script:NotKeyFilter = "" + +#endregion + +#region Functions + +function Get-AccountsFromKDC { + param( + [string]$KDCName = $null, + [string]$Query + ) + Write-Debug "Query:`n$Query to KDC '$KDCName'" + Write-Verbose "Attempting to query $KDCName" + $Results = $null + try { + if ([string]::IsNullOrEmpty($KDCName)) { + $Results = Get-WinEvent -FilterXPath $Query -LogName Security -ErrorAction Stop + } + else { + $Results = Get-WinEvent -ComputerName $KDCName -FilterXPath $Query -LogName Security -ErrorAction Stop + } + } + catch { + if ($_.FullyQualifiedErrorId -eq "NoMatchingEventsFound,Microsoft.PowerShell.Commands.GetWinEventCommand") { + $RealKdcName = if ($null -eq $KDCName) { "$ENV:COMPUTERNAME" } else { $KDCName } + Write-Warning "No events found on $RealKdcName" + } + else { + throw $_ + } + } + return $Results +} + +function List-AccountKeys { + [CmdletBinding()] + param( + [DateTime]$Since = $(Get-Date).AddDays(-30), + [ValidateSet("DES", "RC4", "AES-SHA1", "All")] + [string]$ContainsKeyType = "All", + [ValidateSet("DES", "RC4", "AES-SHA1", "None")] + [string]$NotContainsKeyType = "None", + [ValidateSet("This", "AllKdcs")] + [string]$SearchScope = "This" + ) + + if ("All" -ne $ContainsKeyType) { + # translate AES-SHA1 into either + if ("AES-SHA1" -eq $ContainsKeyType) { + $script:KeyFilter = $script:AES_SHA1_FILTER + } + elseif ("DES" -eq $ContainsKeyType) { + $script:KeyFilter = "DES" + } + else { + $script:KeyFilter = $ContainsKeyType + } + } + + if ("None" -ne $NotContainsKeyType) { + if ("AES-SHA1" -eq $NotContainsKeyType) { + $script:NotKeyFilter = $script:AES_SHA1_FILTER + } + elseif ("DES" -eq $ContainsKeyType) { + $script:NotKeyFilter = "DES" + } + else { + $script:NotKeyFilter = $NotContainsKeyType + } + } + + $accounts = [System.Collections.ArrayList]::new() + if ("This" -eq $SearchScope) { + [Array]$LocalResult = $(Get-AccountsFromKDC -Query $script:XPathQuery) + + if ($null -ne $LocalResult -and 0 -ne $LocalResult.Count) { + $accounts.AddRange($LocalResult) + } + } + else { + Get-ADDomainController -Filter * | ForEach-Object { + $KDCName = $_.HostName + + try { + [Array]$KdcResult = $(Get-AccountsFromKDC -KDCName $KDCName -Query $script:XPathQuery) + + if ($null -ne $KdcResult -and 0 -ne $KdcResult.Count) { + $accounts.AddRange($KdcResult) + } + } + catch { + Write-Error "Failed to get event logs from $KDCName with result: $_" + } + } + } + + # Validate we are working with the correct version + if ($accounts.Count -gt 0 -and $accounts[0].Properties.Count -lt $script:MIN_PROPERTY_COUNT) { + Write-Error "Attempting to run script on Windows Version $([System.Environment]::OSVersion.Version) which doesn't have the new event metadata. +Please install the most recent Windows Updates available for this machine and attempt again." + return + } + + Write-Verbose "Accounts returned: $($accounts.Count)" + + $accounts | ForEach-Object { + $KDC = $_.MachineName + [string]$keys = $_.Properties[16].Value + if (-not [string]::IsNullOrEmpty($script:NotKeyFilter)) { + if ($keys.Contains($script:NotKeyFilter)) { + continue + } + } + + if (-not [string]::IsNullOrEmpty($script:KeyFilter)) { + if (-not $keys.Contains($script:KeyFilter)) { + continue + } + } + + if (4769 -eq $_.Id) { + $type = [AccountType]::Service + $target = $_.Properties[2].Value + if ($target.EndsWith("$")) { + $type = [AccountType]::Machine + } + [Account]::new($_.RecordId, $KDC, $_.TimeCreated, $target, $type, $keys) + } + else { + $target = $_.Properties[0].Value + $type = if ($target.EndsWith("$")) {[AccountType]::Machine } else { [AccountType]::User } + [Account]::new($_.RecordId, $KDC, $_.TimeCreated, $target, $type, $keys) + } + } +} + +#endregion + + +if ($MyInvocation.InvocationName -ne ".") { + List-AccountKeys @PSBoundParameters +} \ No newline at end of file diff --git a/NTLM/get-oldpasswordusers.ps1 b/NTLM/get-oldpasswordusers.ps1 new file mode 100644 index 0000000..3f91072 --- /dev/null +++ b/NTLM/get-oldpasswordusers.ps1 @@ -0,0 +1,45 @@ +# Set the number of days to check for unchanged passwords +$daysThreshold = 1000 + +# Prompt the user to select an Organizational Unit (OU) +$selectedOU = Read-Host "Enter the DistinguishedName of an OU (EX: OU=SUBNAME,OU=Name,DC=DOMAIN,DC=LOCAL). Leave blank to search the entire domain." + +# Get the current date +$currentDate = Get-Date + +# Calculate the date X days ago +$thresholdDate = $currentDate.AddDays(-$daysThreshold) + +# Get users from Active Directory +if ([string]::IsNullOrEmpty($selectedOU)) { + $users = Get-ADUser -Filter * -Properties PasswordLastSet, Enabled, DisplayName, CannotChangePassword +} else { + $users = Get-ADUser -Filter * -SearchBase $selectedOU -Properties PasswordLastSet, Enabled, DisplayName, CannotChangePassword +} + +# Iterate through each user and check if their password has not been changed in X days +$results = @() +foreach ($user in $users) { + if ($user.Enabled -eq $true) { + $passwordLastSet = $user.PasswordLastSet + + # If PasswordLastSet is null or empty, skip the user + if (-not $passwordLastSet) { + continue + } + + # Compare the last password change date with the threshold date + if ($passwordLastSet -lt $thresholdDate) { + $result = [PSCustomObject]@{ + DisplayName = $user.DisplayName + SAMAccountName = $user.SamAccountName + LastPasswordSet = $passwordLastSet + CanChangePassword = -not $user.CannotChangePassword + } + $results += $result + } + } +} + +# Output the results in a table format +$results | Sort LastPasswordSet | Format-Table -AutoSize \ No newline at end of file