Detection/Find-SuspiciousAuthentication.ps1
|
function Find-SuspiciousAuthentication { <# .SYNOPSIS Detects suspicious authentication events and patterns .DESCRIPTION Analyzes Windows Security event logs for suspicious authentication activities including brute force attempts, unusual logon patterns, NewCredentials (runas /netonly) logons, NTLM network logon usage, credential dumping, and privilege escalation .PARAMETER Hours Number of hours to look back in event logs (default: 24) .PARAMETER IncludeSuccessful Include successful authentications in analysis .PARAMETER ExportPath Path to export detailed results .EXAMPLE Find-SuspiciousAuthentication Find-SuspiciousAuthentication -Hours 48 -IncludeSuccessful -ExportPath "C:\Audits" .OUTPUTS PSCustomObject with authentication analysis results #> [CmdletBinding()] param( [Parameter()] [int]$Hours = 24, [Parameter()] [switch]$IncludeSuccessful, [Parameter()] [string]$ExportPath ) begin { Write-Host "Analyzing authentication events (last $Hours hours)..." -ForegroundColor Cyan $results = [PSCustomObject]@{ AnalysisDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss" TimeRange = "$Hours hours" FailedLogons = @() AccountLockouts = @() PrivilegeEscalations = @() UnusualLogons = @() ExplicitCredentials = @() NewCredentialLogons = @() NtlmNetworkLogonIndicators = @() BruteForceIndicators = @() Statistics = @{ TotalFailedLogons = 0 TotalLockouts = 0 TotalPrivilegeUse = 0 TotalExplicitCreds = 0 TotalNewCredentialLogons = 0 NtlmNetworkLogonAccounts = 0 UniqueFailedAccounts = 0 UniqueSourceIPs = 0 } } $startTime = (Get-Date).AddHours(-$Hours) $getEventValue = { param($EventData, [string]$Name) if ($null -eq $EventData) { return $null } $node = $EventData | Where-Object { $_.Name -eq $Name } if ($null -ne $node) { return $node.InnerText } return $null } } process { try { # Event IDs to analyze # 4625 - Failed logon # 4740 - Account lockout # 4672 - Special privileges assigned # 4648 - Logon with explicit credentials # 4624 - Successful logon # 4634 - Logoff # 1. Analyze Failed Logons (Event ID 4625) Write-Verbose "Analyzing failed logon attempts..." $failedLogons = @(Get-WinEvent -FilterHashtable @{ LogName = 'Security' ID = 4625 StartTime = $startTime } -ErrorAction SilentlyContinue) if ($failedLogons.Count -gt 0) { $results.Statistics.TotalFailedLogons = $failedLogons.Count foreach ($event in $failedLogons) { if ($null -eq $event) { continue } try { $xml = [xml]$event.ToXml() $eventData = $xml.Event.EventData.Data } catch { Write-Verbose "Skipping unreadable event: $_" continue } $targetAccount = & $getEventValue $eventData 'TargetUserName' $targetDomain = & $getEventValue $eventData 'TargetDomainName' $workstation = & $getEventValue $eventData 'WorkstationName' $sourceIP = & $getEventValue $eventData 'IpAddress' $logonType = & $getEventValue $eventData 'LogonType' $status = & $getEventValue $eventData 'Status' $subStatus = & $getEventValue $eventData 'SubStatus' $results.FailedLogons += [PSCustomObject]@{ TimeCreated = $event.TimeCreated Account = "$targetDomain\$targetAccount" Workstation = $workstation SourceIP = $sourceIP LogonType = $logonType FailureReason = switch ($subStatus) { '0xC0000064' { 'User name does not exist' } '0xC000006A' { 'Correct user name but wrong password' } '0xC000006D' { 'Bad user name or password' } '0xC000006E' { 'Account restriction' } '0xC000006F' { 'Logon outside allowed time' } '0xC0000070' { 'Workstation restriction' } '0xC0000071' { 'Password expired' } '0xC0000072' { 'Account disabled' } '0xC0000193' { 'Account expiration' } '0xC0000224' { 'Password must change' } '0xC0000234' { 'Account locked out' } default { "Unknown ($subStatus)" } } } } # Detect brute force patterns $failuresByAccount = $results.FailedLogons | Group-Object -Property Account foreach ($group in $failuresByAccount) { if ($group.Count -ge 5) { $timeSpan = ($group.Group | Measure-Object -Property TimeCreated -Maximum -Minimum) $duration = ($timeSpan.Maximum - $timeSpan.Minimum).TotalMinutes $results.BruteForceIndicators += [PSCustomObject]@{ Account = $group.Name FailureCount = $group.Count TimeSpan = [math]::Round($duration, 2) SourceIPs = ($group.Group.SourceIP | Select-Object -Unique) -join ', ' Severity = if ($group.Count -ge 10) { 'High' } elseif ($group.Count -ge 5) { 'Medium' } else { 'Low' } } } } # Calculate statistics safely try { $failedArray = @($results.FailedLogons) if ($failedArray.Count -gt 0) { $results.Statistics.UniqueFailedAccounts = (@($failedArray.Account | Select-Object -Unique)).Count $results.Statistics.UniqueSourceIPs = (@($failedArray | Where-Object { -not [string]::IsNullOrEmpty($_.SourceIP) -and $_.SourceIP -ne '-' } | Select-Object -ExpandProperty SourceIP -Unique)).Count } } catch { Write-Verbose "Could not calculate unique statistics" } } # 2. Account Lockouts (Event ID 4740) Write-Verbose "Checking account lockouts..." $lockouts = @(Get-WinEvent -FilterHashtable @{ LogName = 'Security' ID = 4740 StartTime = $startTime } -ErrorAction SilentlyContinue) if ($lockouts.Count -gt 0) { $results.Statistics.TotalLockouts = $lockouts.Count foreach ($event in $lockouts) { if ($null -eq $event) { continue } try { $xml = [xml]$event.ToXml() $eventData = $xml.Event.EventData.Data } catch { Write-Verbose "Skipping unreadable event: $_" continue } $targetAccount = & $getEventValue $eventData 'TargetUserName' $callerComputer = & $getEventValue $eventData 'TargetDomainName' $results.AccountLockouts += [PSCustomObject]@{ TimeCreated = $event.TimeCreated Account = $targetAccount CallerComputer = $callerComputer } } } # 3. Special Privileges Assigned (Event ID 4672) Write-Verbose "Analyzing privilege escalations..." $privEvents = @(Get-WinEvent -FilterHashtable @{ LogName = 'Security' ID = 4672 StartTime = $startTime } -ErrorAction SilentlyContinue) if ($privEvents.Count -gt 0) { $results.Statistics.TotalPrivilegeUse = $privEvents.Count # Look for unusual privilege assignments foreach ($event in $privEvents) { if ($null -eq $event) { continue } try { $xml = [xml]$event.ToXml() $eventData = $xml.Event.EventData.Data } catch { Write-Verbose "Skipping unreadable event: $_" continue } $subjectAccount = & $getEventValue $eventData 'SubjectUserName' $subjectDomain = & $getEventValue $eventData 'SubjectDomainName' $privileges = & $getEventValue $eventData 'PrivilegeList' # Flag if not a well-known admin account if ($subjectAccount -notmatch '(SYSTEM|LOCAL SERVICE|NETWORK SERVICE|Administrator|DWM-\d+|UMFD-\d+)') { $results.PrivilegeEscalations += [PSCustomObject]@{ TimeCreated = $event.TimeCreated Account = "$subjectDomain\$subjectAccount" Privileges = $privileges } } } } # 4. Explicit Credentials Usage (Event ID 4648) Write-Verbose "Checking explicit credential usage..." $explicitCreds = @(Get-WinEvent -FilterHashtable @{ LogName = 'Security' ID = 4648 StartTime = $startTime } -ErrorAction SilentlyContinue) if ($explicitCreds.Count -gt 0) { $results.Statistics.TotalExplicitCreds = $explicitCreds.Count foreach ($event in $explicitCreds) { if ($null -eq $event) { continue } try { $xml = [xml]$event.ToXml() $eventData = $xml.Event.EventData.Data } catch { Write-Verbose "Skipping unreadable event: $_" continue } $subjectAccount = & $getEventValue $eventData 'SubjectUserName' $targetAccount = & $getEventValue $eventData 'TargetUserName' $targetServer = & $getEventValue $eventData 'TargetServerName' $processName = & $getEventValue $eventData 'ProcessName' $results.ExplicitCredentials += [PSCustomObject]@{ TimeCreated = $event.TimeCreated SourceAccount = $subjectAccount TargetAccount = $targetAccount TargetServer = $targetServer Process = $processName } } } # 5. Successful Logons Analysis (Event ID 4624) Write-Verbose "Analyzing successful logons..." $successfulLogons = Get-WinEvent -FilterHashtable @{ LogName = 'Security' ID = 4624 StartTime = $startTime } -ErrorAction SilentlyContinue if ($successfulLogons) { $ntlmNetworkByAccount = @{} foreach ($event in $successfulLogons) { if ($null -eq $event) { continue } try { $xml = [xml]$event.ToXml() $eventData = $xml.Event.EventData.Data } catch { Write-Verbose "Skipping unreadable event: $_" continue } $targetAccount = & $getEventValue $eventData 'TargetUserName' $targetDomain = & $getEventValue $eventData 'TargetDomainName' $logonType = & $getEventValue $eventData 'LogonType' $sourceIP = & $getEventValue $eventData 'IpAddress' $workstation = & $getEventValue $eventData 'WorkstationName' $authPackage = & $getEventValue $eventData 'AuthenticationPackageName' $processName = & $getEventValue $eventData 'ProcessName' # LogonType 9 = NewCredentials (runas /netonly) - pass-the-hash / explicit credential reuse indicator if ($logonType -eq '9' -and $targetAccount -and $targetAccount -notmatch '\$$') { $results.NewCredentialLogons += [PSCustomObject]@{ TimeCreated = $event.TimeCreated Account = "$targetDomain\$targetAccount" Workstation = $workstation SourceIP = $sourceIP Process = $processName Severity = 'Medium' Indicator = 'NewCredentials logon (runas /netonly style) - possible pass-the-hash or explicit credential reuse' } } # NTLM network logons where Kerberos is normally expected - aggregated per account to keep noise down if ($logonType -eq '3' -and $authPackage -eq 'NTLM' -and $targetAccount -and $targetAccount -notmatch '\$$' -and $targetAccount -ne 'ANONYMOUS LOGON') { $accountKey = "$targetDomain\$targetAccount" if (-not $ntlmNetworkByAccount.ContainsKey($accountKey)) { $ntlmNetworkByAccount[$accountKey] = [PSCustomObject]@{ Account = $accountKey Count = 0 SourceIPs = @() LastSeen = $event.TimeCreated } } $ntlmNetworkByAccount[$accountKey].Count++ if ($sourceIP -and $sourceIP -ne '-' -and $ntlmNetworkByAccount[$accountKey].SourceIPs -notcontains $sourceIP) { $ntlmNetworkByAccount[$accountKey].SourceIPs += $sourceIP } if ($null -ne $event.TimeCreated -and $event.TimeCreated -gt $ntlmNetworkByAccount[$accountKey].LastSeen) { $ntlmNetworkByAccount[$accountKey].LastSeen = $event.TimeCreated } } if ($IncludeSuccessful) { # Flag unusual logon types or times $hour = $event.TimeCreated.Hour $isUnusual = $false $reason = "" # Check for after-hours logon (outside 7 AM - 7 PM) if ($hour -lt 7 -or $hour -ge 19) { $isUnusual = $true $reason = "After-hours logon" } # Type 10 = RemoteInteractive (RDP) if ($logonType -eq '10' -and $sourceIP -ne '127.0.0.1' -and $sourceIP -ne '-') { $isUnusual = $true $reason += "; Remote desktop logon from $sourceIP" } # Type 3 = Network logon to admin shares if ($logonType -eq '3' -and $targetAccount -match 'admin') { $isUnusual = $true $reason += "; Network logon with admin account" } if ($isUnusual) { $results.UnusualLogons += [PSCustomObject]@{ TimeCreated = $event.TimeCreated Account = $targetAccount LogonType = $logonType SourceIP = $sourceIP Workstation = $workstation Reason = $reason.TrimStart('; ') } } } } $results.Statistics.TotalNewCredentialLogons = @($results.NewCredentialLogons).Count foreach ($accountKey in $ntlmNetworkByAccount.Keys) { $ntlmInfo = $ntlmNetworkByAccount[$accountKey] $results.NtlmNetworkLogonIndicators += [PSCustomObject]@{ Account = $ntlmInfo.Account LogonCount = $ntlmInfo.Count SourceIPs = ($ntlmInfo.SourceIPs -join ', ') LastSeen = $ntlmInfo.LastSeen Severity = 'Low' Indicator = 'NTLM network logon where Kerberos is normally expected (informational)' } } $results.Statistics.NtlmNetworkLogonAccounts = @($results.NtlmNetworkLogonIndicators).Count } } catch { Write-Error "Error during authentication analysis: $_" throw } } end { # Display summary Write-Host "`nAuthentication Analysis Complete!" -ForegroundColor Green Write-Host "`n=== Statistics ===" -ForegroundColor Cyan Write-Host "Failed logon attempts: $($results.Statistics.TotalFailedLogons)" -ForegroundColor Yellow Write-Host "Account lockouts: $($results.Statistics.TotalLockouts)" -ForegroundColor $(if ($results.Statistics.TotalLockouts -gt 0) { 'Red' } else { 'Green' }) Write-Host "Privilege escalations: $($results.PrivilegeEscalations.Count)" -ForegroundColor Yellow Write-Host "Explicit credential usage: $($results.Statistics.TotalExplicitCreds)" -ForegroundColor Yellow Write-Host "NewCredentials (type 9) logons: $($results.Statistics.TotalNewCredentialLogons)" -ForegroundColor $(if ($results.Statistics.TotalNewCredentialLogons -gt 0) { 'Red' } else { 'Green' }) Write-Host "Accounts with NTLM network logons: $($results.Statistics.NtlmNetworkLogonAccounts)" -ForegroundColor Yellow Write-Host "Brute force indicators: $($results.BruteForceIndicators.Count)" -ForegroundColor $(if ($results.BruteForceIndicators.Count -gt 0) { 'Red' } else { 'Green' }) if ($IncludeSuccessful) { Write-Host "Unusual successful logons: $($results.UnusualLogons.Count)" -ForegroundColor Yellow } # Export if requested if ($ExportPath) { if (-not (Test-Path $ExportPath)) { New-Item -Path $ExportPath -ItemType Directory -Force | Out-Null } $exportFile = Join-Path -Path $ExportPath -ChildPath "AuthenticationAnalysis_$(Get-Date -Format 'yyyyMMdd_HHmmss').json" $results | ConvertTo-Json -Depth 5 | Out-File -FilePath $exportFile -Encoding UTF8 Write-Host "`nResults exported to: $exportFile" -ForegroundColor Cyan } return $results } } |