Skip to main content

Where did a User’s Account Get Locked Out?

Updated: May 15, 2015
When this article was originally published, two extra carriage returns were add causing the code to malfunction.  The code below is correct.  

My client for this week’s PowerShell class had a really interesting question. They needed to know where an account is being locked out at. OK, interesting. Apparently users hop around clients and forget to log off, leading to eventual lock out of their accounts. The accounts can be unlocked, but are then relocked after Active Directory replication.
This problem is solved in two parts. The first one is to modify the event auditing on the network. The second part is resolved with PowerShell.
The first part involves creating a group policy that will encompass your Domain Controllers. In this GPO, make these changes.
  • Expand Computer Configuration \ Policies \ Windows Settings \ Security Settings \ Advanced Audit Policy Configuration \ Audit Policies \ Account Management
  • Double click User Account Management
  • Check Configure the following audit events.
  • Check Success
This will allow the domain controllers to audit event 4740 which tells use that an account with locked out. Your users will see something like the image below when their account is locked out.
clip_image001
Once the above GPO has been applied, you will now begin to record lock outs. All accounts currently locked out will not have entries in the Security log until they report another lock out.
The second part is to use PowerShell to parse through all the Security logs on the domain controllers and tell you which client a user’s account was locked out on. This script is designed to be dot sourced or turned into a module. It is heavily commented so it can be used for instruction as well as an actually script in production.
Function Extract-UserName
{
param (
   $StringData
)
   <#
    Extract the username from the log file.
     The following line of code performs several tasks.  It receives
     a multi line string that is the message data from the event.  This data
     is saved in the variable $StringData.  Since this is a [STRING] object
     we have access to the methods Split, Replace, and Trim.

    - The first operation splits the string into an array of single lines.
      This is accomplished with the "Split" method.  The `n tells the
      method to split on each new line.

    - The next operation take the contents in array index [10]

    - The "Replace" method is used to replace the part of the string that we
      do not want.  In this case, we are replacing "Account Name:" with $NULL.

    - The "Trim" method removes all leading and trailing spaces and leaves
      us with just the username.
   #>
   ((($StringData.Split("`n"))[10]).Replace("Account Name:",$Null)).Trim()
}

Function Extract-ComputerName
{
param (
   $StringData
)
   # Extract the computer name from the log file.
   Write-Output ((($StringData.Split("`n"))[13]).Replace("Caller Computer Name:",$Null)).Trim()
}

<#
.SYNOPSIS
Discovers the client that a user account was locked out on.

.DESCRIPTION
Returns the client machine which has locked out a user account.
See the NOTEs section for setup information.

.PARAMETER $User
The Name of the User Account that you want to discover the client
that locked this account out.

.PARAMETER $DaysToSearch
This is the number of days to look back in the security logs.
The default setting is 14 days.

.EXAMPLE
Find-LockingClients -user jyoder

UserName                      LockingClient                TimeLocked
--------                      -------------                ----------
jyoder                        LON-CL1                      8/27/2012
7:02:06 PM
jyoder                        LON-CL1                      8/28/2012
8:53:42 PM

Returns the clients where the user account -jyoder was locked out.

.EXAMPLE
Find-LockingClients -User jyoder -DaysToSearch 7

Returns the clients where the user account -jyoder was locked out.
This will search the security logs for the past 7 days. 
The default is 14 days.


.NOTES
For the cmdlet to work, advanced auditing needs to be configured
on the domain controllers. This can be configured locally or in
Group Policy.  It must be done for all Domain Controllers.

Category: Account Management
Subcategory: User Account Management
Audit for: Success

#>
Function Find-LockingClients
{
Param(
    [cmdletbinding()]
    [Parameter(Mandatory=$True)][String]$User = $(Read-Host "Provide a user name"),
    [int]$DaysToSearch = 14


)
   # Get a list of all of the Domain Controllers
   $DCs = Get-ADComputer -Filter * `
    -SearchBase "OU=Domain Controllers,$((Get-ADDomain).distinguishedName)"



   # Set up the number of days to search in the past.
   $StartTime = (Get-Date).AddDays(-($DaysToSearch))

   # Create the hash for the event logs.
   $LogHash = @{LogName = 'Security';StartTime = $StartTime; ID=4740}


   # Store each relevant event in the variable $Events.
    ForEach($Item in $DCs)
     {

      # Error handling just in case on the Domain Controllers
      # is offline.
      Try
       {
          Write-Host "Gathering events from"$Item.Name-ForegroundColor Green `
           -BackgroundColor DarkMagenta
          $Events = Get-WinEvent -FilterHashtable $LogHash `
          -ComputerName $Item.Name -ErrorAction Stop |
          Sort-Object -Property TimeCreated
       }
      Catch
       {

           Write-host "Domain Controller"$Item.Name" if offline" `
           -ForegroundColor Red `
           -BackgroundColor DarkRed
       }
      Finally
       {
            Write-Host $Item.Name"completed" -ForegroundColor Green `
            -BackgroundColor DarkMagenta
       }
     }



   # Set up the array to hold multiple sets of data from the event logs.
   $EventArray = @()

   ForEach ($Event in $Events)
    {
       # Send the event message data to the functions to extract
       # Username and Computer Name.
       $UserName = Extract-UserName $Event.Message
       $ComputerName = Extract-ComputerName $Event.Message

     
       # Write the data to the object.
       $Obj = New-Object -TypeName PSObject
       $Obj |Add-Member -MemberType NoteProperty -Name "UserName" -Value $UserName
       $Obj |Add-Member -MemberType NoteProperty -Name "LockingClient" -Value $ComputerName
       $Obj |Add-Member -MemberType NoteProperty -Name "TimeLocked" -Value $Event.TimeCreated
     
       # Add the individual object to the array of objects.
       $Obj | Where-Object {$_.Username -eq $User}

    }

   # Filter for the user specified in the parameter $User and
   # Send the output into the Pipeline.
  #$EventArray |Where-Object {$_.UserName -eq $User}


}










Comments

Unknown said…
Can you use get-qadcomputer instead of get-adcomputer?
Owen,

I do not utilize the Quest cmdlets. If the object produced has a property called NAME that holds the computer name, then I do not see why not. Also, GET-ADDomainControler could work to extract the names of the domain controllers.

Jason
Anonymous said…
Filtering the specific eventid in eventviewer would have been much easier, no? :)

Thanks for the group policy thing. I was looking all over the place for this.
It would be easier, if you only had one Domain Controller. What if you had 10, 20, or even 100 Domain Controllers? This method touches all of them by executing it only once.
Unknown said…
What would be returned if you were to be locked by say a Mac, or iPhone or other tablet?
Dennis,

I do not have access to Apple Tech on my networks. If they are active directory integrated, go ahead and lock an account on one and see what is in the event log. I'm interested in hearing what you find.

Jason
Unknown said…
After a quick test, I tried to access wireless via a Mac and it showed up blank. I will test further tomorrow when in the office from a login window (via centrify) and an ipad. Thanks
Unknown said…
How does this script decided whether a DC is online or offline? I keep getting messages that the DC is offline but I know for a fact it is online.

Dennis,

The call to the remote computer is in the Get-WinEvent cmdlet. Doing a quick test on my computer to a non-existent server, and RPC error was returned.

Try this:
Enable Windows Remoting on your servers.

Change -------

$Events = Get-WinEvent -FilterHashtable $LogHash `
-ComputerName $Item.Name-ErrorAction Stop |
Sort-Object -Property TimeCreated

To -----------

$Events = Invoke-Command -ScriptBlock {
Param ($LogHash)
Get-WinEvent -FilterHashtable $LogHash `|
Sort-Object -Property TimeCreated } -ComputerName $Item.Name -EA Stop


I do not have a setup to test this. This should remove the RPC technology and utilize Windows PowerShell Remoting for the connection to the remote system. Search my blog of the post on enabling PowerShell Remoting with Group Policy.
Unknown said…
Thanks for the post.

What parameters do I need to supply?
I have tried:
Full name
"Full Name"
SamID
Domain\SamID
nothing
In all cases, I get the error:
[PS] E:\>Extract-UserName
You cannot call a method on a null-valued expression.
At E:\SCripts\PS\Functions\Extract-UserName.ps1:25 char:37
+ Write-Output ((($StringData.Split <<<< ("`n"))[10]).Replace("Account
+ CategoryInfo : InvalidOperation: (Split:String) [], RuntimeException
+ FullyQualifiedErrorId : InvokeMethodOnNull


What am I missing?

thanks in advance.
Lars,

Extract-UserName is a supporting function. Take a look further down the code. You will see a cmdlet called Find-LockingClients. it also has a help file.

Jason
Ken Elmer said…
I have tweaked the powershell script to use Invoke-Command to run the Get-WinEvent part on each domain controller simultaneously and also run as a straight script instead of dot sourced. Much quicker with 8 DCs.
Bill Curtis said…
I keep getting this message when I run it:
The Value parameter is required for a member of type "NoteProperty". Specify the Value parameter when adding members of
this type.
Value:
Bill,

Thank you for sending the error. The source of the problem came from two extra carriage returns injected when I copied the code over. I've fixed the formatting in the blog to prevent that from occurring again. Please copy the above code and try again.

Jason
Anonymous said…
Awesome functions! Thanks for this!
Unknown said…
I know this is an old post, hoping someone still receives comments. I ran as admin, set execution policy, copy the script into power shell and it simply stares back at me with a blinking cursor after the last line. Sorry, newbie here, what am I doing wrong?
Anthony,

This creates a PowerShell Script Cmdlet in memory. just type Find-LockingClients and press Enter.

Remember, nothing will get reported if you just turned on the auditing. If auditing was not enabled until today, you will not receive information on events from yesterday.

Jason
Bill Curtis said…
I was working on another script utilizing the event log and I found similarities to this one. I thought I would share this since I don't work with the split command very well. It will also reduce the need for the supporting functions.

There is a property named ReplacementStrings that will contain the username, computername, Domain name, and some related SIDS. Using this, you can get the names without needing to parse the message.

$a = get-eventlog -logname security -instanceid 4740 -newest 1
$a.replacementstrings[0] will be the username.
$a.replacementstrings[1] will be the computer that locked it out.
$a.replacementstrings[5] will be the domain the account was in that locked it out.

Thought you would find this interesting.

-Bill

Popular posts from this blog

Adding a Comment to a GPO with PowerShell

As I'm writing this article, I'm also writing a customization for a PowerShell course I'm teaching next week in Phoenix.  This customization deals with Group Policy and PowerShell.  For those of you who attend my classes may already know this, but I sit their and try to ask the questions to myself that others may ask as I present the material.  I finished up my customization a few hours ago and then I realized that I did not add in how to put a comment on a GPO.  This is a feature that many Group Policy Administrators may not be aware of. This past summer I attended a presentation at TechEd on Group Policy.  One organization in the crowd had over 5,000 Group Policies.  In an environment like that, the comment section can be priceless.  I always like to write in the comment section why I created the policy so I know its purpose next week after I've completed 50 other tasks and can't remember what I did 5 minutes ago. In the Group Policy module for PowerShell V3, th

Return duplicate values from a collection with PowerShell

If you have a collection of objects and you want to remove any duplicate items, it is fairly simple. # Create a collection with duplicate values $Set1 = 1 , 1 , 2 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2   # Remove the duplicate values. $Set1 | Select-Object -Unique 1 2 3 4 5 6 7 What if you want only the duplicate values and nothing else? # Create a collection with duplicate values $Set1 = 1 , 1 , 2 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2   #Create a second collection with duplicate values removed. $Set2 = $Set1 | Select-Object -Unique   # Return only the duplicate values. ( Compare-Object -ReferenceObject $Set2 -DifferenceObject $Set1 ) . InputObject | Select-Object – Unique 1 2 This works with objects as well as numbers.  The first command creates a collection with 2 duplicates of both 1 and 2.   The second command creates another collection with the duplicates filtered out.  The Compare-Object cmdlet will first find items that are diffe

How to list all the AD LDS instances on a server

AD LDS allows you to provide directory services to applications that are free of the confines of Active Directory.  To list all the AD LDS instances on a server, follow this procedure: Log into the server in question Open a command prompt. Type dsdbutil and press Enter Type List Instances and press Enter . You will receive a list of the instance name, both the LDAP and SSL port numbers, the location of the database, and its status.