Notice: Due to size constraints and loading performance considerations, scripts referenced in blog posts are not attached directly. To request access, please complete the following form: Script Request Form Note: A Google account is required to access the form.
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

NetLogon : Tracing the "bad passwords" for the "account lockout"

Account lockouts are a persistent challenge in Active Directory environments. Traditional manual investigation involves checking multiple domain controllers, searching through netlogon files, and correlating authentication failures across the infrastructure. This process is time-consuming, error-prone, and often misses critical details buried in gigabytes of log data.

The lockout will depend on your organisation password policy, if for example you have a lockout after 4 attempts this is now it flows:

Bad Password, Bad Password, Bad Password, Lockout

This means if you look at that from errors in netlogon.log that looks like this:

0xC000006A > 0xC000006A > 0xC000006A > 0xC0000234

This means if you are looking for the lockout event of 4740, you are skipping the bad passwords and going straight for the the 4th bad attempt which means a lockout, for many scenarios this "Caller Computer" will record the correct computer, but sometimes it will not have the caller computer or simply be blank, this is where this investigation comes in.

I developed a PowerShell solution that automates this entire process, providing comprehensive lockout analysis in minutes rather than hours. The script systematically searches all domain controllers for authentication failures, focusing specifically on the error codes that matter: bad passwords (0xC000006A) and actual lockouts (0xC0000234).

Website Visuals

The information comes later but the is the website you can utilise if you cannot read JSON files, this is is the website that requires the JSON file from the lockout script (which is below)


Once you provide the JSON file it will confirm this as below then use the analyse button:


This will then show you the results as below where you can see "lee.user" has entered a bad password on WrkDevice25665:

How the Script Works

The core challenge in lockout analysis is that authentication failures are scattered across multiple domain controllers in different log files. Manual investigation requires:

  1. Identifying all domain controllers
  2. Accessing netlogon.log and netlogon.bak on each DC
  3. Searching through hundreds of thousands of log entries
  4. Correlating timestamps and error codes
  5. Distinguishing between different failure types

The script automates each step:

Basic Usage

.\LockoutAnalyzer.ps1 -Username lee.croucher -InputTupe sam

The script accepts either SAM account names or User Principal Names:

.\LockoutAnalyzer.ps1 -Username lee.croucher@bear.local -InputType upn

The Search Process

The script begins by querying Active Directory to resolve the username and gather account details. It then discovers all domain controllers in the environment:

$DCs = Get-ADDomainController -Filter * | 
       Select-Object -ExpandProperty HostName

For each domain controller, the script systematically searches both current and archived netlogon files. The key insight is understanding that authentication failures appear in netlogon logs with specific patterns that differ from successful authentications.

Pattern Recognition

Traditional approaches often capture all authentication events, creating noise in the analysis. The script focuses exclusively on authentication failures using precise regex patterns:

$matches = $content | Where-Object {
    ($_ -match "\[LOGON\].*$escapedSearchUser.*Returns 0xC000006A") -or
    ($_ -match "\[LOGON\].*$escapedSearchUser.*Returns 0xC0000234") -or
    ($_ -match "\[LOGON\].*\(null\)\\$SamAccountName.*Returns 0xC000006A") -or
    ($_ -match "\[LOGON\].*\(null\)\\$SamAccountName.*Returns 0xC0000234")
}

The script searches for both domain\username and (null)\username patterns because failed authentication attempts often appear with null domain context in the logs.

Error Code Classification

Each matched event is categorized based on the Windows authentication error code:

if ($match -match "Returns 0xC000006A") { $eventType = "bad-password" }  
elseif ($match -match "Returns 0xC0000234") { $eventType = "lockout" }
  • 0xC000006A: Wrong password (bad-password events)
  • 0xC0000234: Account locked out (lockout events)

JSON File Output

The script builds a comprehensive data structure containing user information, statistics, and detailed event logs:

$result = @{
    Success = $true
    UserInfo = @{
        Name = $userInfo.Name
        SamAccountName = $userInfo.SamAccountName
        UserPrincipalName = $userInfo.UserPrincipalName
        Enabled = $userInfo.Enabled
        LockedOut = $userInfo.LockedOut
        BadLogonCount = $userInfo.BadLogonCount
    }
    Analysis = @{
        TotalEvents = $searchResults.TotalEvents
        LockoutEvents = $searchResults.LockoutEvents
        BadPasswordEvents = $searchResults.BadPasswordEvents
        DomainControllers = $searchResults.DomainControllers
        Events = @($searchResults.Events)
    }
}

The output is saved as structured JSON, enabling programmatic consumption and integration with other tools.

Web Interface for Analysis

While the PowerShell script provides complete functionality, interpreting raw JSON output can be cumbersome for daily operations. I created a web interface that transforms the script's output into an intuitive dashboard.

Website Process

The web interface uses a simple file-based approach:

  1. Run the PowerShell script to generate LockoutResults.json
  2. Upload the JSON file to the web interface
  3. View formatted results with visual indicators

Key Features

The interface provides several advantages over raw script output:

  • Visual Event Classification: Events are color-coded and categorized with clear icons. Bad password attempts appear with warning indicators, while lockout events are highlighted as critical issues.
  • Timeline Presentation: Authentication failures are sorted chronologically, making it easy to Script identify patterns and escalation sequences.
  • Domain Controller Mapping: Each event clearly identifies which domain controller logged the failure, essential for understanding authentication flow and potential infrastructure issues.
  • Account Status Overview: Current account state is displayed with status indicators, showing whether the account is currently locked, enabled, and the current bad logon count.

File Processing Logic

The interface handles JSON parsing with error checking and data validation:

const data = JSON.parse(jsonContent);

// Ensure events is always an array for consistent processing
let events = data.Analysis.Events;
if (!Array.isArray(events)) {
    events = events ? [events] : [];
}

This approach handles edge cases where PowerShell might return a single object instead of an array when only one event is found.

Script : LockoutAnalyzer.ps1

[CmdletBinding()]
param(
    [Parameter(Mandatory=$true)]
    [string]$Username,
    
    [Parameter(Mandatory=$false)]
    [string]$InputType = "sam",
    
    [string[]]$ExcludedDCs = @()
)

# Create log and output files in same directory as script
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$logFile = Join-Path $scriptDir "LockoutAnalysis.log"
$jsonOutputFile = Join-Path $scriptDir "LockoutResults.json"

function Write-ScriptLog {
    param([string]$Message)
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $logEntry = "[$timestamp] $Message"
    try {
        $logEntry | Out-File -FilePath $logFile -Append -Encoding UTF8
        Write-Output $logEntry
    } catch {
        # Ignore logging errors
    }
}

Write-ScriptLog "=== Lockout Analysis Started ==="
Write-ScriptLog "Username: $Username, InputType: $InputType"

try {
    Import-Module ActiveDirectory -ErrorAction Stop
    Write-ScriptLog "Active Directory module loaded"
} catch {
    Write-ScriptLog "ERROR: Active Directory module failed to load"
    $errorResult = @{
        Success = $false
        Error = "Active Directory module not available"
        Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    }
    $errorJson = $errorResult | ConvertTo-Json -Compress
    $errorJson | Out-File -FilePath $jsonOutputFile -Encoding UTF8 -Force
    Write-Output $errorJson
    exit 1
}

function Get-UserInfo {
    param([string]$UserInput, [string]$Type)
    
    Write-ScriptLog "Looking up user: $UserInput (Type: $Type)"
    Write-Host "Searching for user in Active Directory..." -ForegroundColor Yellow
    
    try {
        if ($Type -eq "upn") {
            Write-ScriptLog "Querying AD with UPN filter"
            $user = Get-ADUser -Filter "UserPrincipalName -eq '$UserInput'" -Properties *
        } else {
            Write-ScriptLog "Querying AD with Identity"
            $user = Get-ADUser -Identity $UserInput -Properties *
        }
        
        Write-ScriptLog "AD query completed"
        
        if (-not $user) {
            throw "User not found"
        }
        
        $domain = $env:USERDOMAIN.ToLower()
        $searchString = "$domain\$($user.SamAccountName)"
        Write-ScriptLog "User found: $searchString"
        Write-Host "Found user: $($user.Name) ($($user.SamAccountName))" -ForegroundColor Green
        
        return @{
            SearchString = $searchString
            Name = $user.Name
            SamAccountName = $user.SamAccountName
            UserPrincipalName = $user.UserPrincipalName
            Enabled = $user.Enabled
            LockedOut = $user.LockedOut
            BadLogonCount = $user.BadLogonCount
        }
    } catch {
        Write-ScriptLog "ERROR: User lookup failed - $($_.Exception.Message)"
        Write-Host "ERROR: Failed to find user - $($_.Exception.Message)" -ForegroundColor Red
        throw
    }
}

function Search-NetlogonLogs {
    param([string]$SearchUser, [string]$SamAccountName)
    
    Write-ScriptLog "Searching netlogon logs for: $SearchUser and (null)\$SamAccountName"
    Write-Host "Getting list of domain controllers..." -ForegroundColor Yellow
    
    # Escape backslashes in the search string for regex
    $escapedSearchUser = $SearchUser -replace '\\', '\\'
    Write-ScriptLog "Escaped search string: $escapedSearchUser"
    Write-ScriptLog "Also searching for: (null)\\$SamAccountName"
    
    # Get domain controllers
    try {
        Write-ScriptLog "Querying domain controllers..."
        $DCs = Get-ADDomainController -Filter * | 
               Select-Object -ExpandProperty HostName | 
               Where-Object { $_ -notin $ExcludedDCs }
        Write-ScriptLog "Found $($DCs.Count) domain controllers: $($DCs -join ', ')"
        Write-Host "Found $($DCs.Count) domain controllers: $($DCs -join ', ')" -ForegroundColor Green
    } catch {
        Write-ScriptLog "ERROR: Failed to get domain controllers - $($_.Exception.Message)"
        Write-Host "ERROR: Failed to get domain controllers - $($_.Exception.Message)" -ForegroundColor Red
        throw "Cannot get domain controllers"
    }
    
    # Initialize arrays properly
    $allEvents = @()
    $dcResults = @{}
    $dcCounter = 0
    
    Write-ScriptLog "Initialized empty event arrays"
    
    foreach ($DC in $DCs) {
        $dcCounter++
        Write-ScriptLog "[$dcCounter/$($DCs.Count)] Searching DC: $DC"
        Write-Host "[$dcCounter/$($DCs.Count)] Checking $DC..." -ForegroundColor Cyan
        
        try {
            $dcEvents = @()
            
            # Test DC connectivity
            Write-ScriptLog "  Testing connectivity to $DC"
            Write-Host "  Testing connectivity..." -NoNewline
            
            try {
                $pingResult = Test-Connection -ComputerName $DC -Count 1 -Quiet
                if (-not $pingResult) {
                    Write-Host " FAILED" -ForegroundColor Red
                    Write-ScriptLog "  WARNING: Cannot ping $DC"
                    $dcResults[$DC] = @{
                        EventCount = 0
                        Events = @()
                        Error = "Cannot ping DC"
                    }
                    continue
                }
                Write-Host " OK" -ForegroundColor Green
            } catch {
                Write-Host " FAILED ($($_.Exception.Message))" -ForegroundColor Red
                Write-ScriptLog "  WARNING: Ping failed for $DC - $($_.Exception.Message)"
                $dcResults[$DC] = @{
                    EventCount = 0
                    Events = @()
                    Error = "Ping failed: $($_.Exception.Message)"
                }
                continue
            }
            
            # Search both netlogon files
            $logFiles = @(
                "\\$DC\C$\Windows\debug\netlogon.log",
                "\\$DC\C$\Windows\debug\netlogon.bak"
            )
            
            foreach ($logPath in $logFiles) {
                $logName = Split-Path $logPath -Leaf
                Write-ScriptLog "  Checking: $logPath"
                Write-Host "    Checking $logName..." -NoNewline
                
                if (Test-Path $logPath) {
                    Write-Host " Found" -ForegroundColor Green
                    Write-ScriptLog "    Reading: $logPath"
                    Write-Host "      Reading file..." -NoNewline
                    
                    try {
                        $content = Get-Content -Path $logPath -ErrorAction Stop
                        Write-Host " $($content.Count) lines" -ForegroundColor Green
                        Write-ScriptLog "      File read successfully: $($content.Count) lines"
                        
                        Write-Host "      Searching for authentication failures..." -NoNewline
                        
                        # Search for both domain\username and (null)\username patterns
                        $matches = $content | Where-Object {
                            ($_ -match "\[LOGON\].*$escapedSearchUser.*Returns 0xC000006A") -or
                            ($_ -match "\[LOGON\].*$escapedSearchUser.*Returns 0xC0000234") -or
                            ($_ -match "\[LOGON\].*\(null\)\\$SamAccountName.*Returns 0xC000006A") -or
                            ($_ -match "\[LOGON\].*\(null\)\\$SamAccountName.*Returns 0xC0000234")
                        }
                        
                        Write-Host " $($matches.Count) failures" -ForegroundColor $(if ($matches.Count -gt 0) { "Red" } else { "Gray" })
                        
                        foreach ($match in $matches) {
                            $eventType = "unknown"
                            if ($match -match "Returns 0xC000006A") { $eventType = "bad-password" }  
                            elseif ($match -match "Returns 0xC0000234") { $eventType = "lockout" }
                            
                            $event = @{
                                Type = $eventType
                                DC = $DC
                                Details = $match.Trim()
                                Source = Split-Path $logPath -Leaf
                            }
                            
                            $dcEvents += $event
                            $allEvents += $event
                        }
                        
                        Write-ScriptLog "      Found $($matches.Count) authentication failures in $logName"
                    } catch {
                        Write-Host " ERROR: $($_.Exception.Message)" -ForegroundColor Red
                        Write-ScriptLog "      ERROR reading file: $($_.Exception.Message)"
                    }
                } else {
                    Write-Host " Not accessible" -ForegroundColor Gray
                    Write-ScriptLog "    File not accessible: $logPath"
                }
            }
            
            $dcResults[$DC] = @{
                EventCount = $dcEvents.Count
                Events = $dcEvents
            }
            
            Write-Host "  DC $DC complete: $($dcEvents.Count) authentication failures" -ForegroundColor $(if ($dcEvents.Count -gt 0) { "Red" } else { "Gray" })
            
        } catch {
            Write-ScriptLog "  ERROR accessing $DC - $($_.Exception.Message)"
            Write-Host "  ERROR: $($_.Exception.Message)" -ForegroundColor Red
            $dcResults[$DC] = @{
                EventCount = 0
                Events = @()
                Error = $_.Exception.Message
            }
        }
    }
    
    # Calculate statistics
    $lockoutCount = ($allEvents | Where-Object { $_.Type -eq "lockout" }).Count
    $badPasswordCount = ($allEvents | Where-Object { $_.Type -eq "bad-password" }).Count
    
    Write-ScriptLog "Search complete: $($allEvents.Count) total authentication failures ($lockoutCount lockouts, $badPasswordCount bad passwords)"
    
    Write-Host "`nSearch Summary:" -ForegroundColor White
    Write-Host "  Total Authentication Failures: $($allEvents.Count)" -ForegroundColor White
    Write-Host "  Lockout Events: $lockoutCount" -ForegroundColor $(if ($lockoutCount -gt 0) { "Red" } else { "Gray" })
    Write-Host "  Bad Password Events: $badPasswordCount" -ForegroundColor $(if ($badPasswordCount -gt 0) { "Yellow" } else { "Gray" })
    
    return @{
        TotalEvents = $allEvents.Count
        LockoutEvents = $lockoutCount
        BadPasswordEvents = $badPasswordCount
        LoginAttemptEvents = 0
        DomainControllers = $DCs.Count
        Events = $allEvents
        DCResults = $dcResults
    }
}

# Main execution
try {
    $userInfo = Get-UserInfo -UserInput $Username -Type $InputType
    $searchResults = Search-NetlogonLogs -SearchUser $userInfo.SearchString -SamAccountName $userInfo.SamAccountName
    
    # Create structured result object
    $result = @{
        Success = $true
        UserInfo = @{
            SearchString = $userInfo.SearchString
            Name = $userInfo.Name
            SamAccountName = $userInfo.SamAccountName
            UserPrincipalName = $userInfo.UserPrincipalName
            Enabled = $userInfo.Enabled
            LockedOut = $userInfo.LockedOut
            BadLogonCount = $userInfo.BadLogonCount
        }
        Analysis = @{
            TotalEvents = $searchResults.TotalEvents
            LockoutEvents = $searchResults.LockoutEvents
            BadPasswordEvents = $searchResults.BadPasswordEvents
            LoginAttemptEvents = $searchResults.LoginAttemptEvents
            DomainControllers = $searchResults.DomainControllers
            Events = @($searchResults.Events)
            DCResults = $searchResults.DCResults
        }
        Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    }
    
    Write-ScriptLog "Analysis completed successfully"
    
    # Write JSON output file
    $jsonContent = $result | ConvertTo-Json -Depth 10
    $jsonContent | Out-File -FilePath $jsonOutputFile -Encoding UTF8 -Force
    Write-ScriptLog "JSON results written to: $jsonOutputFile"
    
    # Console output for direct script execution
    Write-Output ($result | ConvertTo-Json -Depth 10 -Compress)
    
} catch {
    Write-ScriptLog "ERROR: Script failed - $($_.Exception.Message)"
    $errorResult = @{
        Success = $false
        Error = $_.Exception.Message
        Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    }
    
    # Write error to JSON file
    $errorJson = $errorResult | ConvertTo-Json -Compress
    $errorJson | Out-File -FilePath $jsonOutputFile -Encoding UTF8 -Force
    Write-ScriptLog "Error JSON written to: $jsonOutputFile"
    
    Write-Output $errorJson
}

Write-ScriptLog "=== Script Completed ==="
Previous Post Next Post

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