ThreatHunting/Find-LateralMovement.ps1
|
function Find-LateralMovement { <# .SYNOPSIS Detects lateral movement indicators in the environment .DESCRIPTION Identifies signs of lateral movement including remote connections, pass-the-hash, PSRemoting, RDP, SMB shares, and credential access .PARAMETER CheckNetworkConnections Check for suspicious network connections .PARAMETER CheckRemoteExecution Check for remote execution indicators .PARAMETER CheckAuthentication Check authentication logs for lateral movement .PARAMETER CheckSMBShares Check SMB share access .PARAMETER Days Number of days to check in event logs (default: 7) .EXAMPLE Find-LateralMovement -CheckNetworkConnections -CheckAuthentication Find-LateralMovement -CheckNetworkConnections -CheckRemoteExecution -CheckAuthentication -Days 1 .OUTPUTS PSCustomObject with lateral movement indicators #> [CmdletBinding()] param( [Parameter()] [switch]$CheckNetworkConnections, [Parameter()] [switch]$CheckRemoteExecution, [Parameter()] [switch]$CheckAuthentication, [Parameter()] [switch]$CheckSMBShares, [Parameter()] [int]$Days = 7 ) begin { Write-Host "=== LATERAL MOVEMENT DETECTION ===" -ForegroundColor Cyan Write-Host "Start Time: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor Yellow Write-Host "Analysis Period: Last $Days days" -ForegroundColor Yellow if (-not ($CheckNetworkConnections -or $CheckRemoteExecution -or $CheckAuthentication -or $CheckSMBShares)) { $CheckNetworkConnections = $true $CheckAuthentication = $true Write-Host "No specific check selected - enabling default checks" -ForegroundColor Yellow } $results = [PSCustomObject]@{ ScanDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss" ComputerName = $env:COMPUTERNAME AnalysisPeriod = $Days Indicators = @{ NetworkConnections = @() RemoteExecution = @() SuspiciousAuthentication = @() SMBAccess = @() } # Per-section truncation flags: set when a Get-WinEvent query returns # exactly its -MaxEvents cap (meaning more matching events likely exist) Truncated = @{ NetworkConnections = $false RemoteExecution = $false SuspiciousAuthentication = $false SMBAccess = $false } Summary = @{ TotalIndicators = 0 CriticalFindings = 0 HighFindings = 0 MediumFindings = 0 LowFindings = 0 } Recommendations = @() } $startDate = (Get-Date).AddDays(-$Days) } process { try { # 1. Check Network Connections for Lateral Movement if ($CheckNetworkConnections) { Write-Host "`n[1/4] Checking network connections..." -ForegroundColor Cyan try { # Check for established connections to internal IPs on suspicious ports $connections = Get-NetTCPConnection -State Established -ErrorAction SilentlyContinue $suspiciousPorts = @{ 445 = 'SMB' 135 = 'RPC' 139 = 'NetBIOS' 5985 = 'WinRM HTTP' 5986 = 'WinRM HTTPS' 3389 = 'RDP' } $foundCount = 0 foreach ($conn in $connections) { if ($conn.RemotePort -in $suspiciousPorts.Keys) { $process = Get-Process -Id $conn.OwningProcess -ErrorAction SilentlyContinue if (-not $process) { continue } # Skip if it's a standard system process in expected scenarios if ($process.Name -in @('svchost', 'System')) { continue } $indicator = [PSCustomObject]@{ Type = 'NetworkConnection' Process = $process.Name ProcessId = $conn.OwningProcess LocalAddress = "$($conn.LocalAddress):$($conn.LocalPort)" RemoteAddress = "$($conn.RemoteAddress):$($conn.RemotePort)" Protocol = $suspiciousPorts[$conn.RemotePort] Severity = 'Medium' Description = "Connection to $($suspiciousPorts[$conn.RemotePort]) port from $($process.Name)" Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" } $results.Indicators.NetworkConnections += $indicator $results.Summary.MediumFindings++ $foundCount++ if ($foundCount -le 5) { Write-Host " [!] $($process.Name) -> $($conn.RemoteAddress):$($conn.RemotePort) [$($suspiciousPorts[$conn.RemotePort])]" -ForegroundColor Yellow } } } if ($foundCount -gt 5) { Write-Host " ... and $($foundCount - 5) more connections" -ForegroundColor Gray } if ($foundCount -eq 0) { Write-Host " No suspicious network connections detected" -ForegroundColor Green } } catch { Write-Warning "Error checking network connections: $_" } } # 2. Check Remote Execution Indicators if ($CheckRemoteExecution) { Write-Host "`n[2/4] Checking remote execution indicators..." -ForegroundColor Cyan try { # Check for PSRemoting activity (Event ID 4648 - Explicit credential logon) $maxRemoteEvents = 100 $remoteEvents = @(Get-WinEvent -FilterHashtable @{ LogName = 'Security' ID = 4648 StartTime = $startDate } -MaxEvents $maxRemoteEvents -ErrorAction SilentlyContinue) # Get-WinEvent returns exactly MaxEvents when more matching # events exist - flag the cap instead of silently truncating if ($remoteEvents.Count -ge $maxRemoteEvents) { $results.Truncated.RemoteExecution = $true Write-Warning "Remote execution check (Event ID 4648) hit the $maxRemoteEvents-event cap - results are likely TRUNCATED. Narrow -Days for complete coverage." } if ($remoteEvents) { $groupedEvents = $remoteEvents | Group-Object -Property { $_.Properties[8].Value } | Sort-Object Count -Descending foreach ($group in $groupedEvents | Select-Object -First 5) { $event = $group.Group[0] $indicator = [PSCustomObject]@{ Type = 'RemoteExecution' Process = 'N/A' ProcessId = 'N/A' TargetServer = $event.Properties[8].Value Account = $event.Properties[1].Value Count = $group.Count Severity = if ($group.Count -gt 10) { 'High' } else { 'Medium' } Description = "Explicit credential logon to $($event.Properties[8].Value) - $($group.Count) times" Timestamp = $event.TimeCreated.ToString("yyyy-MM-dd HH:mm:ss") } $results.Indicators.RemoteExecution += $indicator if ($group.Count -gt 10) { $results.Summary.HighFindings++ } else { $results.Summary.MediumFindings++ } Write-Host " [!] Explicit credential logon: $($event.Properties[8].Value) ($($group.Count) times)" -ForegroundColor Yellow } } else { Write-Host " No remote execution indicators detected" -ForegroundColor Green } } catch { Write-Warning "Error checking remote execution: $_" } } # 3. Check Authentication Logs for Lateral Movement if ($CheckAuthentication) { Write-Host "`n[3/4] Checking authentication logs..." -ForegroundColor Cyan try { # Event ID 4624 - Successful logon (Type 3 = Network, Type 10 = RemoteInteractive/RDP) $maxLogonEvents = 500 $logonEvents = @(Get-WinEvent -FilterHashtable @{ LogName = 'Security' ID = 4624 StartTime = $startDate } -MaxEvents $maxLogonEvents -ErrorAction SilentlyContinue) # Get-WinEvent returns exactly MaxEvents when more matching # events exist - flag the cap instead of silently truncating if ($logonEvents.Count -ge $maxLogonEvents) { $results.Truncated.SuspiciousAuthentication = $true Write-Warning "Authentication check (Event ID 4624) hit the $maxLogonEvents-event cap - results are likely TRUNCATED. Narrow -Days for complete coverage." } if ($logonEvents) { # Filter for Type 3 (Network) and Type 10 (RemoteInteractive) $suspiciousLogons = $logonEvents | Where-Object { $logonType = $_.Properties[8].Value $logonType -in @(3, 10) } if ($suspiciousLogons) { # Group by source IP $groupedByIP = $suspiciousLogons | Group-Object -Property { $_.Properties[18].Value } | Where-Object { $_.Name -ne '-' -and $_.Name -ne '127.0.0.1' } | Sort-Object Count -Descending foreach ($group in $groupedByIP | Select-Object -First 5) { $event = $group.Group[0] $logonType = $event.Properties[8].Value $logonTypeName = switch ($logonType) { 3 { 'Network' } 10 { 'RemoteInteractive/RDP' } default { "Type $logonType" } } $indicator = [PSCustomObject]@{ Type = 'SuspiciousAuthentication' Process = 'N/A' ProcessId = 'N/A' SourceIP = $group.Name Account = $event.Properties[5].Value LogonType = $logonTypeName Count = $group.Count Severity = if ($group.Count -gt 20) { 'High' } else { 'Medium' } Description = "$logonTypeName logon from $($group.Name) - $($group.Count) times" Timestamp = $event.TimeCreated.ToString("yyyy-MM-dd HH:mm:ss") } $results.Indicators.SuspiciousAuthentication += $indicator if ($group.Count -gt 20) { $results.Summary.HighFindings++ } else { $results.Summary.MediumFindings++ } Write-Host " [!] $logonTypeName from $($group.Name): $($group.Count) times" -ForegroundColor Yellow } } else { Write-Host " No suspicious authentication events detected" -ForegroundColor Green } } else { Write-Host " No authentication events found in specified period" -ForegroundColor Gray } } catch { Write-Warning "Error checking authentication logs: $_" } } # 4. Check SMB Share Access if ($CheckSMBShares) { Write-Host "`n[4/4] Checking SMB share access..." -ForegroundColor Cyan try { # Event ID 5140 - Network share object accessed $maxShareEvents = 200 $shareEvents = @(Get-WinEvent -FilterHashtable @{ LogName = 'Security' ID = 5140 StartTime = $startDate } -MaxEvents $maxShareEvents -ErrorAction SilentlyContinue) # Get-WinEvent returns exactly MaxEvents when more matching # events exist - flag the cap instead of silently truncating if ($shareEvents.Count -ge $maxShareEvents) { $results.Truncated.SMBAccess = $true Write-Warning "SMB share access check (Event ID 5140) hit the $maxShareEvents-event cap - results are likely TRUNCATED. Narrow -Days for complete coverage." } if ($shareEvents) { # Split share access: admin/hidden shares (IPC$, ADMIN$, C$, ...) # are the PRIMARY lateral movement signal; other shares are noise $adminShareEvents = @($shareEvents | Where-Object { $shareName = $_.Properties[7].Value $shareName -match 'IPC\$|ADMIN\$|[A-Za-z]\$' }) $otherShareEvents = @($shareEvents | Where-Object { $shareName = $_.Properties[7].Value $shareName -notmatch 'IPC\$|ADMIN\$|[A-Za-z]\$' }) $shareSets = @( @{ Events = $adminShareEvents; Severity = 'High'; Label = 'Admin share' } @{ Events = $otherShareEvents; Severity = 'Low'; Label = 'Share' } ) $foundShares = $false foreach ($shareSet in $shareSets) { if ($shareSet.Events.Count -eq 0) { continue } $foundShares = $true $groupedShares = $shareSet.Events | Group-Object -Property { "$($_.Properties[1].Value)|$($_.Properties[7].Value)" } | Sort-Object Count -Descending foreach ($group in $groupedShares | Select-Object -First 5) { $parts = $group.Name -split '\|' $event = $group.Group[0] $indicator = [PSCustomObject]@{ Type = 'SMBAccess' Process = 'N/A' ProcessId = 'N/A' Account = $parts[0] ShareName = $parts[1] Count = $group.Count Severity = $shareSet.Severity Description = "$($shareSet.Label) access: $($parts[1]) by $($parts[0]) - $($group.Count) times" Timestamp = $event.TimeCreated.ToString("yyyy-MM-dd HH:mm:ss") } $results.Indicators.SMBAccess += $indicator if ($shareSet.Severity -eq 'High') { $results.Summary.HighFindings++ Write-Host " [!] ADMIN SHARE: $($parts[1]) by $($parts[0]) ($($group.Count) times)" -ForegroundColor Red } else { $results.Summary.LowFindings++ Write-Host " [!] Share: $($parts[1]) by $($parts[0]) ($($group.Count) times)" -ForegroundColor Yellow } } } if (-not $foundShares) { Write-Host " No suspicious SMB share access detected" -ForegroundColor Green } } else { Write-Host " No SMB share access events found" -ForegroundColor Gray } } catch { Write-Warning "Error checking SMB share access: $_" } } # Calculate summary $results.Summary.TotalIndicators = $results.Indicators.NetworkConnections.Count + $results.Indicators.RemoteExecution.Count + $results.Indicators.SuspiciousAuthentication.Count + $results.Indicators.SMBAccess.Count # Generate recommendations if ($results.Summary.TotalIndicators -gt 0) { if ($results.Summary.HighFindings -gt 0) { $results.Recommendations += "HIGH: Investigate high-severity lateral movement indicators immediately" } $results.Recommendations += "Review source IPs and accounts for suspicious activity" $results.Recommendations += "Check for compromised credentials" $results.Recommendations += "Enable enhanced logging (Sysmon, PowerShell logging)" $results.Recommendations += "Implement network segmentation" $results.Recommendations += "Deploy EDR solution for behavioral detection" if ($results.Indicators.RemoteExecution.Count -gt 0) { $results.Recommendations += "Review PSRemoting and WinRM usage policies" } if ($results.Indicators.SuspiciousAuthentication.Count -gt 0) { $results.Recommendations += "Investigate authentication patterns for pass-the-hash indicators" } } } catch { Write-Error "Error during lateral movement detection: $_" throw } } end { Write-Host "`n=== SCAN COMPLETE ===" -ForegroundColor $( if ($results.Summary.HighFindings -gt 0) { 'Red' } elseif ($results.Summary.TotalIndicators -gt 0) { 'Yellow' } else { 'Green' } ) Write-Host "`nDetection Summary:" -ForegroundColor Cyan Write-Host " Total Indicators: $($results.Summary.TotalIndicators)" -ForegroundColor White Write-Host " Critical: $($results.Summary.CriticalFindings)" -ForegroundColor Red Write-Host " High: $($results.Summary.HighFindings)" -ForegroundColor Yellow Write-Host " Medium: $($results.Summary.MediumFindings)" -ForegroundColor Yellow Write-Host " Low: $($results.Summary.LowFindings)" -ForegroundColor White $truncatedSections = @($results.Truncated.GetEnumerator() | Where-Object { $_.Value } | ForEach-Object { $_.Key }) if ($truncatedSections.Count -gt 0) { Write-Host " TRUNCATED (event cap hit, partial data): $($truncatedSections -join ', ')" -ForegroundColor Red $results.Recommendations += "Event log queries hit their -MaxEvents cap in: $($truncatedSections -join ', ') - narrow -Days and re-run for complete coverage" } if ($results.Summary.TotalIndicators -gt 0) { Write-Host "`nIndicator Breakdown:" -ForegroundColor Cyan if ($results.Indicators.NetworkConnections.Count -gt 0) { Write-Host " Network Connections: $($results.Indicators.NetworkConnections.Count)" -ForegroundColor White } if ($results.Indicators.RemoteExecution.Count -gt 0) { Write-Host " Remote Execution: $($results.Indicators.RemoteExecution.Count)" -ForegroundColor White } if ($results.Indicators.SuspiciousAuthentication.Count -gt 0) { Write-Host " Suspicious Authentication: $($results.Indicators.SuspiciousAuthentication.Count)" -ForegroundColor White } if ($results.Indicators.SMBAccess.Count -gt 0) { Write-Host " SMB Access: $($results.Indicators.SMBAccess.Count)" -ForegroundColor White } } else { Write-Host "`nNo lateral movement indicators detected" -ForegroundColor Green } if ($results.Recommendations.Count -gt 0) { Write-Host "`nRecommendations:" -ForegroundColor Cyan $results.Recommendations | Select-Object -Unique | ForEach-Object { Write-Host " ! $_" -ForegroundColor Yellow } } return $results } } |