Tracking down bad password attempts with PowerShell

7 minute read

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