Forum Discussion
TomWechsler
Apr 03, 2024MVP
Active Directory Advanced Threat Hunting - Tracing the cause of account lockouts and password errors
Dear Microsoft Active Directory friends,
In this article we are going on a "search for clues" :-). In the life of an IT administrator, you have certainly often had to reset a user's password or remove an account lockout.
Now the question arises on which system the account was locked or on which system the password was entered incorrectly.
In order to determine this information with PowerShell, some preparations must be made. "Advanced Audit Policy Configuration" must be configured in the group policies.
This article from Microsoft provides a good starting point:
https://learn.microsoft.com/en-us/defender-for-identity/deploy/event-collection-overview
In my example, I have adapted the Default Domain Controls Policy.
Before we begin, here is some important information about MITRE techniques:
Account Access Removal:
https://attack.mitre.org/techniques/T1531/
User Account:
https://attack.mitre.org/datasources/DS0002/
Brute Force: Password Spraying:
https://attack.mitre.org/techniques/T1110/003/
Account lockouts are logged in the Windows event logs with the ID 4740. We will therefore focus on this event ID first.
The start of the PowerShell script looks like this:
#Prep work for lockouts, Account lockout Event ID
$LockOutID = 4740
#Find the PDC
(Get-ADDomain).PDCEmulator
$PDCEmulator = (Get-ADDomain).PDCEmulator
#Connect to the PDC
Enter-PSSession -ComputerName $PDCEmulator
#Query event log
Get-WinEvent -ComputerName $PDCEmulator -FilterHashtable @{
LogName = 'Security'
ID = $LockOutID
}
#Parse the event and assign to a variable
$events = Get-WinEvent -ComputerName $PDCEmulator -FilterHashtable @{
LogName = 'Security'
ID = $LockOutID
}
#Examine some properties
$events[0].Message
#Regex?
$events[0].Message -match 'Caller Computer Name:\s+(?<caller>[^\s]+)'
$Matches.caller
#Cool, but not as easy as:
$events[0].Properties
$events[0].Properties[1].Value
#For all events:
ForEach($event in $events){
[pscustomobject]@{
UserName = $event.Properties[0].Value
CallerComputer = $event.Properties[1].Value
TimeStamp = $event.TimeCreated
}
}
#And we'll make that a function
Function Get-ADUserLockouts {
[CmdletBinding(
DefaultParameterSetName = 'All'
)]
Param (
[Parameter(
ValueFromPipeline = $true,
ParameterSetName = 'ByUser'
)]
[Microsoft.ActiveDirectory.Management.ADUser]$Identity
)
Begin{
$LockOutID = 4740
$PDCEmulator = (Get-ADDomain).PDCEmulator
}
Process {
If($PSCmdlet.ParameterSetName -eq 'All'){
#Query event log
$events = Get-WinEvent -ComputerName $PDCEmulator -FilterHashtable @{
LogName = 'Security'
ID = $LockOutID
}
}ElseIf($PSCmdlet.ParameterSetName -eq 'ByUser'){
$user = Get-ADUser $Identity
#Query event log
$events = Get-WinEvent -ComputerName $PDCEmulator -FilterHashtable @{
LogName = 'Security'
ID = $LockOutID
} | Where-Object {$_.Properties[0].Value -eq $user.SamAccountName}
}
ForEach($event in $events){
[pscustomobject]@{
UserName = $event.Properties[0].Value
CallerComputer = $event.Properties[1].Value
TimeStamp = $event.TimeCreated
}
}
}
End{}
}
#Usage
Get-ADUserLockouts
#Single user
Get-ADUser 'jesse.pinkman' | Get-ADUserLockouts
Now we come to the incorrectly entered passwords. These events are logged in the Windows event logs with the ID 4625.
#Prep work for bad passwords - Event ID
$badPwId = 4625
#Get the events from the PDC
$events = Get-WinEvent -ComputerName $PDCEmulator -FilterHashtable @{
LogName = 'Security'
ID = $badPwId
}
#Correlate the logon types
$LogonType = @{
'2' = 'Interactive'
'3' = 'Network'
'4' = 'Batch'
'5' = 'Service'
'7' = 'Unlock'
'8' = 'Networkcleartext'
'9' = 'NewCredentials'
'10' = 'RemoteInteractive'
'11' = 'CachedInteractive'
}
#Format the properties
ForEach($event in $events){
[pscustomobject]@{
TargetAccount = $event.properties.Value[5]
LogonType = $LogonType["$($event.properties.Value[10])"]
CallingComputer = $event.Properties.Value[13]
IPAddress = $event.Properties.Value[19]
TimeStamp = $event.TimeCreated
}
}
#Bring it all together in a function
Function Get-ADUserBadPasswords {
[CmdletBinding(
DefaultParameterSetName = 'All'
)]
Param (
[Parameter(
ValueFromPipeline = $true,
ParameterSetName = 'ByUser'
)]
[Microsoft.ActiveDirectory.Management.ADUser]$Identity
)
Begin {
$badPwId = 4625
$PDCEmulator = (Get-ADDomain).PDCEmulator
$LogonType = @{
'2' = 'Interactive'
'3' = 'Network'
'4' = 'Batch'
'5' = 'Service'
'7' = 'Unlock'
'8' = 'Networkcleartext'
'9' = 'NewCredentials'
'10' = 'RemoteInteractive'
'11' = 'CachedInteractive'
}
}
Process {
If($PSCmdlet.ParameterSetName -eq 'All'){
#Query event log
$events = Get-WinEvent -ComputerName $PDCEmulator -FilterHashtable @{
LogName = 'Security'
ID = $badPwId
}
}ElseIf($PSCmdlet.ParameterSetName -eq 'ByUser'){
$user = Get-ADUser $Identity
#Query event log
$events = Get-WinEvent -ComputerName $PDCEmulator -FilterHashtable @{
LogName = 'Security'
ID = $badPwId
} | Where-Object {$_.Properties[5].Value -eq $user.SamAccountName}
}
ForEach($event in $events){
[pscustomobject]@{
TargetAccount = $event.properties.Value[5]
LogonType = $LogonType["$($event.properties.Value[10])"]
CallingComputer = $event.Properties.Value[13]
IPAddress = $event.Properties.Value[19]
TimeStamp = $event.TimeCreated
}
}
}
End{}
}
#Usage
Get-ADUserBadPasswords | Format-Table
#Single account
Get-ADUser administrator | Get-ADUserBadPasswords | Format-Table
I hope that this information is helpful to you and that you have been given a good "little" foundation. This article/information is by no means complete and exhaustive. But I still hope that this information is helpful to you.
Thank you for taking the time to read the article.
Happy Hunting, Tom Wechsler
P.S. All scripts (#PowerShell, Azure CLI, #Terraform, #ARM) that I use can be found on github! https://github.com/tomwechsler
- Joachim_OtahalIron Contributor
In my experience with large security logs adding "ProviderName" speeds up the query significantly:
-FilterHashtable @{ProviderName="Microsoft-Windows-Security-Auditing";LogName="Security";ID=4625}
If only failed logins are needed I add "Keywords=4503599627370496" aka "Audit Fail":
-FilterHashtable @{ProviderName="Microsoft-Windows-Security-Auditing";LogName="Security";ID=4625;Keywords=4503599627370496}
Only -FilterXPath syntax is still faster, and the "[Provider[@Name='Microsoft-Windows-Security-Auditing']]" helps speeding that up too. But I had not yet the need to use -FilterXPath for those 4740/4625 security events, only for NTLM logging where the amount of log entries is huge.