Tracking down account lockout sources with PowerShell

7 minute read

Update: I had a question about checking other DCs beyond just the PDC, according to Microsoft:

Account lockout is processed on the PDC emulator.

I can’t say for certain that account lockouts will always happen on the PDC and no where else, but in a perfect world that should hold true.

Original post:

One very frustrating task to accomplish for a sysadmin is tracking down why an account has been locked out. Especially when a user asks you to unlock their account 2 minutes after the last time they asked. It is clearly your fault, definitely not the fact that they never changed the password on their iPad that is syncing with Exchange. The good news is that even though it can take a bit of time to filter through logs, if you do some leg work ahead of time with PowerShell we can cut down the time to a few seconds.

Background

Before we dive into building the tool, I want to make sure we are on the same page.

The event

Whenever an account is lockedout, EventID 4740 is generated on the authenticating domain controller and copied to the PDC Emulator. Inside that event, there are a number of useful bits of information. Obviously the date, time, and account that was locked out, but it also includes information about where the lockout originated from. Specifically the Caller Computer as it calls it, and we can grab all of that information with PowerShell!

The command

To retrieve event logs from a remote computer that allows remote event log management, we’ll use the Get-WinEvent cmdlet. At a bare minimum, we need to include the logname that we are querying. In this case, the security log:

Get-WinEvent -LogName Security

That’ll list out all the recent events in the security log.

Building a tool

So now that we have all of that information, lets build ourselves a tool to do the work for us!

Filtering to the left

In PowerShell, the further you can filter to the left, the more efficient your commands will be. In this specific instance, we can use the Get-WinEvent cmdlet to filter for certain event IDs in a certain log using the -FilterHashtable parameter. To find account lockouts, this would look like:

Get-WinEvent -FilterHashtable @{
    LogName = 'Security'
    ID = 4740
}

Or if we were running this remotely, we’d need to find the PDC Emulator first:

$PDCEmulator = (Get-ADDomain).PDCEmulator
Get-WinEvent -ComputerName $PDCEmulator -FilterHashtable @{
    LogName = 'Security'
    ID = 4740
}

This is more efficient than doing something like:

Get-WinEvent -LogName 'Security' | Where-Object {$_.ID -eq 4740}

Because the second example returns all the events and passes them down the pipeline to be filtered by Where-Object. But hey, you definitely shouldn’t believe me just because I say that, lets run some tests:

Measure-Command {
    Get-WinEvent -LogName 'Security' | Where-Object {$_.ID -eq 4740}
}

On my lab DC this took 13 minutes, 20 seconds. Filtering to the left, we can measure the following:

Measure-Command {
    Get-WinEvent -FilterHashtable @{
        LogName = 'Security'
        ID = 4740
    }
}

This took half a second. That is 1/1600 the amount of time. That is why you should filter to the left whenever possible.

The event object

My apologies for the sidetrack, that is important info! Lets take a look at the event object that gets returned as well. It has the expected metadata such as a timestamp, ID, level, and message. If we take a look at the message property, we see something like:

A user account was locked out.

Subject:
        Security ID:            S-1-5-18
        Account Name:           DC01$
        Account Domain:         techsnipsdemo
        Logon ID:               0x3E7

Account That Was Locked Out:
        Security ID:            S-1-5-21-3887150854-3870875727-2903
        Account Name:           jesse.pinkman

Additional Information:
        Caller Computer Name:   ALPHAWOLF

That is pretty sweet! We get to see the account that was locked out and where it came from. Though, believe it or not, I’m not going to recommend regex here. The event object also has another property: properties:

Value
-----
jesse.pinkman
ALPHAWOLF
S-1-5-21-3887150854-3870875727-2903
S-1-5-18
DC01$
techsnipsdemo
999

So we don’t even need to worry about parsing the string, we just need to know what index each property is.

Looping for all events

The last step, before finalizing this with a function, is to loop through each event and create the appropriate output object. I’ll choose to do that with a foreach loop and create a custom object to format the output:

$events = Get-WinEvent -FilterHashtable @{
    LogName = 'Security'
    ID = $LockOutID
}
foreach ($event in $events){
    [pscustomobject]@{
        UserName = $event.Properties[0].Value
        CallerComputer = $event.Properties[1].Value
        TimeStamp = $event.TimeCreated
    }
}

Making this into a function

We do, of course, need to functionitize this! I’ll call it Get-ADUserLockouts:

Function Get-ADUserLockouts {
    [CmdletBinding(
        DefaultParameterSetName = 'All'
    )]
    param (
        [Parameter(
            ValueFromPipeline = $true,
            ParameterSetName = 'ByUser'
        )]
        [Microsoft.ActiveDirectory.Management.ADUser]$Identity
        ,
        [datetime]$StartTime
        ,
        [datetime]$EndTime
    )
    Begin{
        $filterHt = @{
            LogName = 'Security'
            ID = 4740
        }
        if ($PSBoundParameters.ContainsKey('StartTime')){
            $filterHt['StartTime'] = $StartTime
        }
        if ($PSBoundParameters.ContainsKey('EndTime')){
            $filterHt['EndTime'] = $EndTime
        }
        $PDCEmulator = (Get-ADDomain).PDCEmulator
        # Query the event log just once instead of for each user if using the pipeline
        $events = Get-WinEvent -ComputerName $PDCEmulator -FilterHashtable $filterHt
    }
    Process {
        if ($PSCmdlet.ParameterSetName -eq 'ByUser'){
            $user = Get-ADUser $Identity
            # Filter the events
            $output = $events | Where-Object {$_.Properties[0].Value -eq $user.SamAccountName}
        } else {
            $output = $events
        }
        foreach ($event in $output){
            [pscustomobject]@{
                UserName = $event.Properties[0].Value
                CallerComputer = $event.Properties[1].Value
                TimeStamp = $event.TimeCreated
            }
        }
    }
    End{}
}

You’ll notice that I also added a couple of parameter sets to it so that you can filter for certain users as well as filter for certain times as far left as possible.

And don’t forget that you can also find this code in my Github Utilities repo. Open an issue there if you find a bug with this code, or maybe you’d like to suggest an improvement.

Usage

If you run the cmdlet by itself, you’ll simply return all of the lockout events with their source:

Get-ADUserLockouts
UserName      CallerComputer TimeStamp
--------      -------------- ---------
jesse.pinkman ALPHAWOLF      4/24/2019 1:03:24 PM
walter.white  EX01           4/22/2019 8:25:53 AM

Though you could use the pipeline if you want to pull users from AD:

Get-ADUser -Filter {Department -eq 'IT'} | Get-ADUserLockouts

Or even filter for lockouts from a certain date:

Get-ADUserLockouts -StartTime (Get-Date).AddDays(-2) -EndTime (Get-Date).AddDays(-1)

There’s a lot you can do! And this will save you time over manually searching the event viewer, trust me.

Conclusion

I hope this has been helpful for you! This is a tool that I have use many times and easily impressed callers during my time on the helpdesk when I was quickly able to tell them why they were being locked out.

Be sure to check out my other posts here on my blog and the other tools I’ve got in my Utilities repository.

If you got feedback, leave me a comment, tweet at me, send me an email, whatever works for you!

Leave a Comment