fix(winrm): Exchange strict mode to suppress false 91/4624 alerts

On Exchange role: require user in WinRM Event 91 EventData; correlate 4624 only when LogonProcess is WinRM. Prevents Outlook/LT3 false positives. Version 2.0.23-SAC.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
PTah
2026-06-05 11:07:21 +10:00
parent fe2e6d624f
commit 80b2337d61
6 changed files with 116 additions and 8 deletions
+6 -1
View File
@@ -409,6 +409,10 @@ function Get-RdpMonitorExchangeNoiseSettingDefinitions {
@{
Pattern = '(?m)^\s*(\#\s*)?\$WinRmIgnoreMachineAccounts\s*='
Line = '$WinRmIgnoreMachineAccounts = 1'
},
@{
Pattern = '(?m)^\s*(\#\s*)?\$WinRmExchangeStrictMode\s*='
Line = '$WinRmExchangeStrictMode = 1'
}
)
}
@@ -514,7 +518,8 @@ function Repair-RdpMonitorSettingsWinRmLinesIfInvalid {
'WinRmCorrelateSecurity4624',
'WinRm4624CorrelationWindowSeconds',
'WinRmIgnoreLocalSource',
'WinRmIgnoreMachineAccounts'
'WinRmIgnoreMachineAccounts',
'WinRmExchangeStrictMode'
)
$newContent = $c
$fixed = $false
+2 -1
View File
@@ -76,8 +76,9 @@ flowchart TD
- `${Ignore4624-LT3-EmptyIP-Event} = $true`
- `$WinRmIgnoreLocalSource = 1`
- `$WinRmIgnoreMachineAccounts = 1`
- `$WinRmExchangeStrictMode = 1`
Подавление шума WinRM (HealthMailbox, loopback) и лишних **4624 LT3**.
Подавление шума WinRM (HealthMailbox, loopback), ложных WinRM 91↔4624 (Outlook/LT3) и лишних **4624 LT3**.
**Мониторинг очередей транспорта и правил пересылки** — другой скрипт, **не** через GPO RDP-монитора:
+105 -5
View File
@@ -89,7 +89,7 @@ $script:SkipLogDetailLimit = 15
# строки ниже, если правки «мелкие» и вы не хотите менять отображаемую версию в логах).
# Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только
# исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1).
$ScriptVersion = "2.0.22-SAC"
$ScriptVersion = "2.0.23-SAC"
# Логи (все под InstallRoot)
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
@@ -149,6 +149,8 @@ $WinRm4624CorrelationWindowSeconds = 15
# Исключить шум Exchange/локальный WinRM: HealthMailbox*, учётки *$, ::1, fe80:, 127.0.0.1
$WinRmIgnoreLocalSource = 1
$WinRmIgnoreMachineAccounts = 1
# На сервере Exchange: не алертить WinRM 91 без user в EventData/Properties; 4624 только LogonProcess WinRM.
$WinRmExchangeStrictMode = 1
# Admin share (Security 5140): C$, ADMIN$ — рабочие станции и серверы; нужен audit File Share.
$EnableAdminShareMonitoring = 1
@@ -1631,11 +1633,85 @@ function Get-WinRm91EventInfo {
return $eventData
}
function Test-RdpMonitorExchangeServerRole {
if (-not [string]::IsNullOrWhiteSpace($env:ExchangeInstallPath)) {
if (Test-Path -LiteralPath $env:ExchangeInstallPath) { return $true }
}
foreach ($regPath in @(
'HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\Setup',
'HKLM:\SOFTWARE\Microsoft\ExchangeServer\v14\Setup'
)) {
try {
if ($null -ne (Get-ItemProperty -LiteralPath $regPath -ErrorAction Stop)) { return $true }
} catch { }
}
foreach ($root in @(
'C:\Program Files\Microsoft\Exchange Server\V15',
'C:\Program Files\Microsoft\Exchange Server\V14'
)) {
if (Test-Path -LiteralPath (Join-Path $root 'bin\RemoteExchange.ps1')) { return $true }
}
try {
$svc = Get-Service -Name 'MSExchangeIS', 'MSExchangeTransport' -ErrorAction SilentlyContinue |
Where-Object { $_.Status -eq 'Running' } |
Select-Object -First 1
if ($null -ne $svc) { return $true }
} catch { }
return $false
}
function Test-WinRmExchangeStrictActive {
if (-not (Test-RdpMonitorExchangeServerRole)) { return $false }
return Test-MonitorFeatureEnabled -Value $WinRmExchangeStrictMode
}
function Test-WinRm91UserPresentInEventRecord {
param($Event)
try {
$map = Get-EventDataMap -Event $Event
$user = Get-FirstNonEmptyMapValue -DataMap $map -Keys @(
'user', 'User', 'UserName', 'AccountName', 'SubjectUserName'
)
if (-not [string]::IsNullOrWhiteSpace($user) -and $user -ne '-') {
return $true
}
if ($Event.Properties.Count -gt 0) {
$propUser = [string]$Event.Properties[0].Value
if (-not [string]::IsNullOrWhiteSpace($propUser) -and
$propUser -notmatch '(?i)https?://|ResourceUri|clientIP:') {
return $true
}
}
} catch { }
return $false
}
function Get-WinRmExchangeStrictSkipReason {
param(
[bool]$UserPresentInWinRmEvent,
[string]$Username,
[string]$SourceIP,
[string]$ResourceUri
)
if (-not $UserPresentInWinRmEvent) {
return 'exchange-winrm-no-user-in-event'
}
$uriBad = ([string]::IsNullOrWhiteSpace($ResourceUri) -or $ResourceUri -eq '-' -or $ResourceUri -like '*%1*')
if ($uriBad -and ([string]::IsNullOrWhiteSpace($SourceIP) -or $SourceIP -eq '-')) {
return 'exchange-winrm-unresolved-uri-no-ip'
}
if (Test-RdpMonitorUsernameExcludedByPattern -Username $Username) {
return 'excluded-user-pattern'
}
return ''
}
function Find-CorrelatedNetworkLogon4624 {
param(
[datetime]$AroundTime,
[string]$UsernameHint = '',
[int]$WindowSeconds = 15
[int]$WindowSeconds = 15,
[switch]$RequireWinRmLogonProcess
)
$start = $AroundTime.AddSeconds(-1 * [Math]::Abs($WindowSeconds))
$end = $AroundTime.AddSeconds([Math]::Abs($WindowSeconds))
@@ -1653,6 +1729,13 @@ function Find-CorrelatedNetworkLogon4624 {
$info = Get-LoginEventInfo -Event $ev
if ($info.LogonType -ne 3) { continue }
if ($info.SourceIP -eq '-' -or [string]::IsNullOrWhiteSpace($info.SourceIP)) { continue }
if ($RequireWinRmLogonProcess) {
$logonProcess = [string]$info.ProcessName
if ([string]::IsNullOrWhiteSpace($logonProcess) -or $logonProcess -eq '-' -or
$logonProcess -notmatch '(?i)WinRM') {
continue
}
}
if (-not [string]::IsNullOrWhiteSpace($UsernameHint) -and $UsernameHint -ne '-') {
if (-not (Test-RdpMonitorUsernameMatchesToken -Username $info.Username -Token $UsernameHint)) {
continue
@@ -3978,6 +4061,9 @@ function Start-LoginMonitor {
if (Test-MonitorFeatureEnabled -Value $EnableWinRmInboundMonitoring) {
if (Test-WinRmLogAvailable) {
Write-Log "WinRM inbound (Enter-PSSession): включён (Operational IDs: $($WinRmInboundShellEventIds -join ', '); correlate 4624=$WinRmCorrelateSecurity4624)."
if (Test-WinRmExchangeStrictActive) {
Write-Log "WinRM Exchange strict: user обязателен в Event 91; корреляция 4624 только LogonProcess WinRM."
}
} else {
Write-Log "WinRM inbound: включён в настройках, но журнал WinRM Operational недоступен."
}
@@ -4378,16 +4464,30 @@ function Start-LoginMonitor {
if ($winRmEvents) {
foreach ($event in $winRmEvents) {
if ($event.TimeCreated -le $lastWinRmCheckTime) { continue }
$exchangeWinRmStrict = Test-WinRmExchangeStrictActive
$userInWinRmEvent = Test-WinRm91UserPresentInEventRecord -Event $event
$wr = Get-WinRm91EventInfo -Event $event
if (Test-MonitorFeatureEnabled -Value $WinRmCorrelateSecurity4624) {
$corr = Find-CorrelatedNetworkLogon4624 -AroundTime $wr.TimeCreated `
-UsernameHint $wr.User -WindowSeconds $WinRm4624CorrelationWindowSeconds
-UsernameHint $wr.User -WindowSeconds $WinRm4624CorrelationWindowSeconds `
-RequireWinRmLogonProcess:$exchangeWinRmStrict
if ($null -ne $corr) {
$wr.SourceIP = $corr.SourceIP
if ($wr.User -eq '-' -and $corr.Username -ne '-') { $wr.User = $corr.Username }
if ($wr.SourceIP -eq '-') { $wr.SourceIP = $corr.SourceIP }
if ($wr.User -eq '-' -and $corr.Username -ne '-' -and -not $exchangeWinRmStrict) {
$wr.User = $corr.Username
}
$wr.LogonType = $corr.LogonType
}
}
if ($exchangeWinRmStrict) {
$exchangeSkip = Get-WinRmExchangeStrictSkipReason `
-UserPresentInWinRmEvent:$userInWinRmEvent `
-Username $wr.User -SourceIP $wr.SourceIP -ResourceUri $wr.ResourceUri
if (-not [string]::IsNullOrWhiteSpace($exchangeSkip)) {
Write-Log "Skip WinRM $($event.Id): User=$($wr.User) IP=$($wr.SourceIP) — reason=$exchangeSkip"
continue
}
}
$winRmIgnoreReason = Get-WinRmIgnoreReason -Username $wr.User -SourceIP $wr.SourceIP
if (-not [string]::IsNullOrWhiteSpace($winRmIgnoreReason)) {
Write-Log "Skip WinRM $($event.Id): User=$($wr.User) IP=$($wr.SourceIP) — reason=$winRmIgnoreReason"
+1
View File
@@ -61,6 +61,7 @@ Get-Content "C:\ProgramData\RDP-login-monitor\Logs\login_monitor.log" -Tail 60
- **WinRM / Enter-PSSession (2.0.4+):** улучшен парсинг события `Microsoft-Windows-WinRM/Operational` (`ID=91`) — корректно извлекаются `user` и `clientIP` из EventData/Message fallback, событие отправляется в SAC как `winrm.session.started`.
- **Deploy WinRM self-heal (2.0.3+):** `Deploy-LoginMonitor.ps1` проверяет/дописывает обязательный блок WinRM inbound в `login_monitor.settings.ps1`, диагностирует доступность канала `Microsoft-Windows-WinRM/Operational` и пытается включить его через `wevtutil`.
- **Диагностика skip WinRM (2.0.5+):** в логах пишется точная причина `Skip WinRM 91` (`empty-user`, `excluded-user-pattern`, `machine-account`, `local-or-linklocal-ip`, `ignore-list-match`), что упрощает troubleshooting.
- **WinRM Exchange strict (2.0.23+):** на сервере с ролью Exchange — WinRM **91** без user в EventData/Properties не алертится; корреляция Security **4624** только при `LogonProcess WinRM` (отсекает ложные связки с Outlook/почтовым LT3). Причины skip: `exchange-winrm-no-user-in-event`, `exchange-winrm-unresolved-uri-no-ip`. Deploy дописывает `$WinRmExchangeStrictMode = 1`.
## 1) Подготовка
+1
View File
@@ -54,6 +54,7 @@ $GetInventory = $true
# $EnableAdminShareMonitoring = 1 # Security 5140 C$/ADMIN$ (audit File Share)
# $WinRmIgnoreLocalSource = 1 # ::1, 127.0.0.1, fe80 (шум Exchange/локальный WinRM)
# $WinRmIgnoreMachineAccounts = 1 # учётки, оканчивающиеся на $
# $WinRmExchangeStrictMode = 1 # Exchange: user в Event 91 обязателен; 4624 только LogonProcess WinRM
# HealthMailbox* уже в ExcludedUserPatterns скрипта
# Проверка: powershell -File Login_Monitor.ps1 -CheckSac
+1 -1
View File
@@ -1 +1 @@
2.0.22-SAC
2.0.23-SAC