Tracking down bad password attempts with PowerShell
Before you read through this post, I heavily encourage you to read my previous post on Tracking down account lockout sources because I’m going to be referring back to a lot of what I did previously, but tweaking it for finding bad password attempts. You definitely don’t have to refer back if you are familiar with parsing event logs with PowerShell, but I’ll point out the times where I go more into depth in the previous post.
If you are dealing with password lockouts, it can also be useful to see where the bad password attempts are coming from and what types of logons they are. The good news is that the event logs contain all that information! We just need to write ourselves a handy function to access that information.
Background
Like before, lets cover the metadata for the event first.
The Event
In an Active Directory environment whenever an authentication failure occurs, EventID 4625
is generated and the event is forwarded to the PDC Emulator. This event contains a plethura of useful information that we’ll be taking a look at.
The Command
Same as before, we’ll be using the Get-WinEvent
cmdlet. You can refer back to the previous post for a basic example, or read on to see how we’ll use it to find the bad password attempts.
Building a tool
Get-WinEvent refresher
So, like before, we’re going to use the -FilterHashtable
parameter to filter to the left (I time this vs Where-Object
in the last post) and look for the bad password events in the security logs:
Get-WinEvent -FilterHashtable @{
LogName = 'Security'
ID = 4625
}
And again, since we are in AD we’ll need to find the PDC Emulator:
$PDCEmulator = (Get-ADDomain).PDCEmulator
Get-WinEvent -ComputerName $PDCEmulator -FilterHashtable @{
LogName = 'Security'
ID = 4625
}
The Event Object
When looking at one of the returned events from that command, we get quite a lot of useful information:
An account failed to log on.
Subject:
Security ID: S-1-5-18
Account Name: DC01$
Account Domain: techsnipsdemo
Logon ID: 0x3E7
Logon Type: 7
Account For Which Logon Failed:
Security ID: S-1-0-0
Account Name: Administrator
Account Domain: techsnipsdemo
Failure Information:
Failure Reason: Unknown user name or bad password.
Status: 0xC000006D
Sub Status: 0xC000006A
Process Information:
Caller Process ID: 0x49c
Caller Process Name: C:\Windows\System32\svchost.exe
Network Information:
Workstation Name: DC01
Source Network Address: 127.0.0.1
Source Port: 0
Detailed Authentication Information:
Logon Process: User32
Authentication Package: Negotiate
Transited Services: -
Package Name (NTLM only): -
Key Length: 0
Now lets break out some unintelligible regex!! Ha, just kidding. Instead, we can look at the Properties
property and see a list of all the property values:
Value
-----
S-1-5-18
DC01$
techsnipsdemo
999
S-1-0-0
Administrator
techsnipsdemo
-1073741715
%%2313
-1073741718
7
User32
Negotiate
DC01
-
-
0
1180
C:\Windows\System32\svchost.exe
127.0.0.1
0
Now we’re going to grab values based on the index in that array, but as Mathias Jessen pointed out to me, you can access them via XPath, but that is a little out of scope for this post.
As an example of what I mean, if we want to get the logon type, target account, and the network info we can create a custom PS Object:
[pscustomobject]@{
LogonType = $event.Properties.Value[10]
TargetAccount = $event.Properties.Value[5]
WorkstationName = $event.Properties.Value[13]
NetworkAddress = $event.Properties.Value[19]
}
Logon types
Something to note is that the logon type is just a number. What is a logon type of 7 anyway? Microsoft has a nice table for that information, here’s the PowerShell version of it:
$LogonType = @{
'2' = 'Interactive'
'3' = 'Network'
'4' = 'Batch'
'5' = 'Service'
'7' = 'Unlock'
'8' = 'Networkcleartext'
'9' = 'NewCredentials'
'10' = 'RemoteInteractive'
'11' = 'CachedInteractive'
}
So in this case, that example event was an attempt to unlock DC01 locally. I can blame myself for that one!
Bringing it together in a function
My favorite part about writing tools is making them easily reusable as functions:
Function Get-ADUserBadPasswords {
[CmdletBinding(
DefaultParameterSetName = 'All'
)]
Param (
[Parameter(
ValueFromPipeline = $true,
ParameterSetName = 'ByUser'
)]
[Microsoft.ActiveDirectory.Management.ADUser]$Identity
,
[string]$DomainController = (Get-ADDomain).PDCEmulator
,
[datetime]$StartTime
,
[datetime]$EndTime
)
Begin {
$LogonType = @{
'2' = 'Interactive'
'3' = 'Network'
'4' = 'Batch'
'5' = 'Service'
'7' = 'Unlock'
'8' = 'Networkcleartext'
'9' = 'NewCredentials'
'10' = 'RemoteInteractive'
'11' = 'CachedInteractive'
}
$filterHt = @{
LogName = 'Security'
ID = 4625
}
if ($PSBoundParameters.ContainsKey('StartTime')){
$filterHt['StartTime'] = $StartTime
}
if ($PSBoundParameters.ContainsKey('EndTime')){
$filterHt['EndTime'] = $EndTime
}
# Query the event log just once instead of for each user if using the pipeline
$events = Get-WinEvent -ComputerName $DomainController -FilterHashtable $filterHt
}
Process {
if ($PSCmdlet.ParameterSetName -eq 'ByUser'){
$user = Get-ADUser $Identity
# Filter for the user
$output = $events | Where-Object {$_.Properties[5].Value -eq $user.SamAccountName}
} else {
$output = $events
}
foreach ($event in $output){
[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{}
}
You’ll notice that I added a parameter set to this one so that you can specify a specific user, if you want to. It does depend on the Where-Object
cmdlet, so it wouldn’t be any faster that filtering on your own, just more convenient.
I also moved the selection of the PDC Emulator to the param block so that you could specify a specific DC if you wanted to. This function doesn’t verify that the computer is a DC, so it will just search the event logs of any server you point it at.
Much like the Get-ADUserLockouts
from the previous post, I also collect all events in the Begin{}
block in case multiple users are passed through the pipeline so that it doesn’t have to reach out to get all events for each passed user.
Usage
To get some really simple data, I’d try running the plain command and piping it to Format-Table
:
Get-ADUserBadPasswords | Format-Table
TargetAccount LogonType CallingComputer IPAddress TimeStamp
------------- --------- --------------- --------- ---------
Administrator Unlock DC01 127.0.0.1 5/8/2019 1:56:21 PM
Administrator Unlock DC01 127.0.0.1 5/8/2019 1:56:18 PM
Since we added pipeline functionality, we can use that to look at a notoriously fat-fingered department:
Get-ADUser -Filter {Department -eq 'Executive Department'} | Get-ADUserBadPasswords
Or we could simply check our own account:
Get-ADUserBadPasswords -Identity 'theposhwolf'
Conclusion
I hope you find this useful and it saves you some time! Its a tool that I’ve used many times for tracking down bad password attempts and whatnot.
I’ve also uploaded this to my Utilities repo so be sure to check it out there as well as the other tools I’ve got up there.
If you’ve got any feedback, let me know with a comment, tweet, email, or whatever. I’m happy to assist if you have trouble using any of the tools I write.
Leave a Comment