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