Disclaimer: I do not accept responsibility for any issues arising from scripts being run without adequate understanding. It is the user's responsibility to review and assess any code before execution. More information

PowerShell Account Lockout Troubleshooting Without Domain Admin Rights


Domain Admin privileges are not required to troubleshoot account lockouts, you actually on require read access to the Event Log which can we completed with Event Log Reader, giving more access violates least privilege principles and creates security risks.

The solution: use Event Log Readers group membership to query Event ID 4740 on the PDC Emulator.

Why Query the PDC Emulator

According to Microsoft documentation, account lockout is processed on the PDC emulator. When authentication failures occur, the domain controller closest to the user redirects the authentication request to the DC with the PDC emulator FSMO role, which is responsible for processing account locks.

The PDC emulator role retains specific functions including: password changes are replicated preferentially to the PDC emulator, authentication failures are forwarded to the PDC emulator before bad password failure messages are reported, and account lockout is processed on the PDC emulator.

This makes the PDC Emulator the authoritative source for Event ID 4740 (account lockout) events.

Required Permissions

Members of the Event Log Readers group can read event logs from domain controllers without administrative privileges. This built-in group provides sufficient access to query security logs for lockout events.

You will also need to add a security key to the registry of the Domain Controller - in this case the PDC, however we will cover the option to add to all Domain Controllers if required, the local setting is a registry key below:

HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\Eventlog\Security\
Value Name:CustomSD
Value Type: REG_SZ  
Value Data: O:BAG:SYD:(A;;CCLCSDRCWDWO;;;SY)(A;;CCLC;;;BA)(A;;CC;;;ER)(A;;CC;;;S-1-5-80-818380073-2995186456-1411405591-3990468014-3617507088)(A;;0x1;;;S-1-5-80-818380073-2995186456-1411405591-3990468014-3617507088)

If you wish to deploy this to all domain controllers via GPO you can use the "Default Domain Controllers" policy with the instructions below:

  • Open Group Policy Management Console
  • Edit Default Domain Controllers Policy (or create new GPO)
  • Navigate to: Computer Configuration > Windows Settings > Security Settings > Local Policies > Security Options
  • Configure: "Event log: Security log SDDL"
  • Add the SDDL string including Event Log Readers from above!

Technical Implementation

Core Function Parameters

function Get-AccountLogonPAM {
    param(
        [Parameter(Mandatory=$true)]
        [string]$samAccountName,
        
        [Parameter(Mandatory=$false)]
        [ValidateRange(1, 365)]
        [int]$DaysBack = 7
    )

PDC Detection

Automatically identify the PDC Emulator:

$domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
$PDC = $domain.PdcRoleOwner.Name

Server-Side XML Filtering

Critical for performance with large event logs (30+ million entries):

$StartDate = (Get-Date).AddDays(-$DaysBack)
$StartDateUTC = $StartDate.ToUniversalTime()
$StartDateString = $StartDateUTC.ToString('yyyy-MM-ddTHH:mm:ss.fffZ')

$XMLFilter = @"
<QueryList>
  <Query Id="0" Path="Security">
    <Select Path="Security">
      *[System[(EventID=4740) and TimeCreated[@SystemTime&gt;='$StartDateString']]]
      and
      *[EventData[Data[@Name='TargetUserName'] and (text()='$samAccountName')]]
    </Select>
  </Query>
</QueryList>
"@

$events = Get-WinEvent -ComputerName $PDC -FilterXml $XMLFilter

Event Data Extraction

Parse Event 4740 structured data from the PDC:

$eventXML = [xml]$event.ToXml()
$eventData = @{}
foreach ($data in $eventXML.Event.EventData.Data) {
    $eventData[$data.Name] = $data.'#text'
}

$lockoutSource = $eventData['CallerComputer'] ?? 
                 $eventData['WorkstationName'] ?? 
                 "Unknown"

Now lets move on to the script for this that can be manually run or installed as a module for all users of that server.

Script : LockoutAccountTools.ps1

function Get-AccountLogonPAM {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, Position=0)]
        [ValidateNotNullOrEmpty()]
        [string]$samAccountName,
        
        [Parameter(Mandatory=$false, Position=1)]
        [ValidateRange(1, 365)]
        [int]$DaysBack = 7
    )
    
    try {
        # Calculate date range
        $StartDate = (Get-Date).AddDays(-$DaysBack)
        $StartDateUTC = $StartDate.ToUniversalTime()
        $StartDateString = $StartDateUTC.ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
        
        # Get PDC Emulator
        $domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
        $PDC = $domain.PdcRoleOwner.Name
        
        if (-not $PDC) {
            throw "Unable to identify the Primary Domain Controller"
        }
        
        Write-Host "PDC: $PDC" -ForegroundColor Green
        Write-Host "Searching: $samAccountName (last $DaysBack days)" -ForegroundColor Yellow
        
        # XML filter for server-side filtering
        $XMLFilter = @"
<QueryList>
  <Query Id="0" Path="Security">
    <Select Path="Security">
      *[System[(EventID=4740) and TimeCreated[@SystemTime&gt;='$StartDateString']]]
      and
      *[EventData[Data[@Name='TargetUserName'] and (text()='$samAccountName')]]
    </Select>
  </Query>
</QueryList>
"@
        
        # Query events
        try {
            $events = Get-WinEvent -ComputerName $PDC -FilterXml $XMLFilter -ErrorAction Stop
        }
        catch {
            # Fallback to hashtable filter
            $FilterHashtable = @{
                LogName = 'Security'
                ID = 4740
                StartTime = $StartDate
            }
            
            $events = Get-WinEvent -ComputerName $PDC -FilterHashtable $FilterHashtable -ErrorAction Stop |
                Where-Object {
                    $_.Message -like "*$samAccountName*" -or 
                    ($_.Properties[0].Value -eq $samAccountName)
                }
        }
        
        if ($events) {
            Write-Host "Found $($events.Count) lockout event(s)" -ForegroundColor Green
            
            foreach ($event in $events) {
                # Parse event data
                $eventXML = [xml]$event.ToXml()
                $eventData = @{}
                foreach ($data in $eventXML.Event.EventData.Data) {
                    $eventData[$data.Name] = $data.'#text'
                }
                
                # Extract lockout source
                $lockoutSource = if ($eventData['CallerComputer']) {
                    $eventData['CallerComputer']
                } elseif ($eventData['WorkstationName']) {
                    $eventData['WorkstationName']
                } else {
                    if ($event.Message -match 'Caller Computer Name:\s+(\S+)') {
                        $Matches[1]
                    } else {
                        "Unknown"
                    }
                }
                
                # Output results
                [PSCustomObject]@{
                    'DateTime'         = $event.TimeCreated
                    'Username'         = "$($eventData['TargetDomainName'])\$($eventData['TargetUserName'])"
                    'LockoutComputer'  = $lockoutSource
                    'DC'               = $event.MachineName
                    'RecordID'         = $event.RecordId
                }
            }
        }
        else {
            Write-Host "No lockout events found for $samAccountName in last $DaysBack day(s)" -ForegroundColor Yellow
        }
    }
    catch {
        if ($_.Exception.Message -like "*Access is denied*") {
            Write-Error "Access Denied: Ensure membership in 'Event Log Readers' group on $PDC"
        }
        elseif ($_.Exception.Message -like "*RPC server is unavailable*") {
            Write-Error "Cannot connect to $PDC. Check connectivity and firewall rules."
        }
        else {
            Write-Error "Error: $_"
        }
    }
}

Usage Examples

# Last 24 hours (fastest)
Get-AccountLogonPAM -samAccountName "lockout.larry" -DaysBack 1

# Default 7 days
Get-AccountLogonPAM -samAccountName "lockout.larry"

# Extended search
Get-AccountLogonPAM -samAccountName "lockout.larry" -DaysBack 30

Module Installation

Save as AccountTools.psm1 in:

  • System-wide: C:\Program Files\WindowsPowerShell\Modules\AccountTools\
  • Current user: $HOME\Documents\WindowsPowerShell\Modules\AccountTools\

Key Points

  • Event Log Readers group membership is sufficient - no admin rights needed
  • PDC Emulator is authoritative for lockout events (Event ID 4740)
  • XML filtering at server level prevents downloading entire event logs
  • DaysBack parameter essential for performance with large logs
  • Script handles 30+ million event log entries efficiently
Results

When the script runs with the basic parameters the results will look like this:


Previous Post Next Post

نموذج الاتصال