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:
@@ -409,6 +409,10 @@ function Get-RdpMonitorExchangeNoiseSettingDefinitions {
|
|||||||
@{
|
@{
|
||||||
Pattern = '(?m)^\s*(\#\s*)?\$WinRmIgnoreMachineAccounts\s*='
|
Pattern = '(?m)^\s*(\#\s*)?\$WinRmIgnoreMachineAccounts\s*='
|
||||||
Line = '$WinRmIgnoreMachineAccounts = 1'
|
Line = '$WinRmIgnoreMachineAccounts = 1'
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
Pattern = '(?m)^\s*(\#\s*)?\$WinRmExchangeStrictMode\s*='
|
||||||
|
Line = '$WinRmExchangeStrictMode = 1'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -514,7 +518,8 @@ function Repair-RdpMonitorSettingsWinRmLinesIfInvalid {
|
|||||||
'WinRmCorrelateSecurity4624',
|
'WinRmCorrelateSecurity4624',
|
||||||
'WinRm4624CorrelationWindowSeconds',
|
'WinRm4624CorrelationWindowSeconds',
|
||||||
'WinRmIgnoreLocalSource',
|
'WinRmIgnoreLocalSource',
|
||||||
'WinRmIgnoreMachineAccounts'
|
'WinRmIgnoreMachineAccounts',
|
||||||
|
'WinRmExchangeStrictMode'
|
||||||
)
|
)
|
||||||
$newContent = $c
|
$newContent = $c
|
||||||
$fixed = $false
|
$fixed = $false
|
||||||
|
|||||||
@@ -76,8 +76,9 @@ flowchart TD
|
|||||||
- `${Ignore4624-LT3-EmptyIP-Event} = $true`
|
- `${Ignore4624-LT3-EmptyIP-Event} = $true`
|
||||||
- `$WinRmIgnoreLocalSource = 1`
|
- `$WinRmIgnoreLocalSource = 1`
|
||||||
- `$WinRmIgnoreMachineAccounts = 1`
|
- `$WinRmIgnoreMachineAccounts = 1`
|
||||||
|
- `$WinRmExchangeStrictMode = 1`
|
||||||
|
|
||||||
Подавление шума WinRM (HealthMailbox, loopback) и лишних **4624 LT3**.
|
Подавление шума WinRM (HealthMailbox, loopback), ложных WinRM 91↔4624 (Outlook/LT3) и лишних **4624 LT3**.
|
||||||
|
|
||||||
**Мониторинг очередей транспорта и правил пересылки** — другой скрипт, **не** через GPO RDP-монитора:
|
**Мониторинг очередей транспорта и правил пересылки** — другой скрипт, **не** через GPO RDP-монитора:
|
||||||
|
|
||||||
|
|||||||
+105
-5
@@ -89,7 +89,7 @@ $script:SkipLogDetailLimit = 15
|
|||||||
# строки ниже, если правки «мелкие» и вы не хотите менять отображаемую версию в логах).
|
# строки ниже, если правки «мелкие» и вы не хотите менять отображаемую версию в логах).
|
||||||
# Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только
|
# Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только
|
||||||
# исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1).
|
# исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1).
|
||||||
$ScriptVersion = "2.0.22-SAC"
|
$ScriptVersion = "2.0.23-SAC"
|
||||||
|
|
||||||
# Логи (все под InstallRoot)
|
# Логи (все под InstallRoot)
|
||||||
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
|
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
|
||||||
@@ -149,6 +149,8 @@ $WinRm4624CorrelationWindowSeconds = 15
|
|||||||
# Исключить шум Exchange/локальный WinRM: HealthMailbox*, учётки *$, ::1, fe80:, 127.0.0.1
|
# Исключить шум Exchange/локальный WinRM: HealthMailbox*, учётки *$, ::1, fe80:, 127.0.0.1
|
||||||
$WinRmIgnoreLocalSource = 1
|
$WinRmIgnoreLocalSource = 1
|
||||||
$WinRmIgnoreMachineAccounts = 1
|
$WinRmIgnoreMachineAccounts = 1
|
||||||
|
# На сервере Exchange: не алертить WinRM 91 без user в EventData/Properties; 4624 только LogonProcess WinRM.
|
||||||
|
$WinRmExchangeStrictMode = 1
|
||||||
|
|
||||||
# Admin share (Security 5140): C$, ADMIN$ — рабочие станции и серверы; нужен audit File Share.
|
# Admin share (Security 5140): C$, ADMIN$ — рабочие станции и серверы; нужен audit File Share.
|
||||||
$EnableAdminShareMonitoring = 1
|
$EnableAdminShareMonitoring = 1
|
||||||
@@ -1631,11 +1633,85 @@ function Get-WinRm91EventInfo {
|
|||||||
return $eventData
|
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 {
|
function Find-CorrelatedNetworkLogon4624 {
|
||||||
param(
|
param(
|
||||||
[datetime]$AroundTime,
|
[datetime]$AroundTime,
|
||||||
[string]$UsernameHint = '',
|
[string]$UsernameHint = '',
|
||||||
[int]$WindowSeconds = 15
|
[int]$WindowSeconds = 15,
|
||||||
|
[switch]$RequireWinRmLogonProcess
|
||||||
)
|
)
|
||||||
$start = $AroundTime.AddSeconds(-1 * [Math]::Abs($WindowSeconds))
|
$start = $AroundTime.AddSeconds(-1 * [Math]::Abs($WindowSeconds))
|
||||||
$end = $AroundTime.AddSeconds([Math]::Abs($WindowSeconds))
|
$end = $AroundTime.AddSeconds([Math]::Abs($WindowSeconds))
|
||||||
@@ -1653,6 +1729,13 @@ function Find-CorrelatedNetworkLogon4624 {
|
|||||||
$info = Get-LoginEventInfo -Event $ev
|
$info = Get-LoginEventInfo -Event $ev
|
||||||
if ($info.LogonType -ne 3) { continue }
|
if ($info.LogonType -ne 3) { continue }
|
||||||
if ($info.SourceIP -eq '-' -or [string]::IsNullOrWhiteSpace($info.SourceIP)) { 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 [string]::IsNullOrWhiteSpace($UsernameHint) -and $UsernameHint -ne '-') {
|
||||||
if (-not (Test-RdpMonitorUsernameMatchesToken -Username $info.Username -Token $UsernameHint)) {
|
if (-not (Test-RdpMonitorUsernameMatchesToken -Username $info.Username -Token $UsernameHint)) {
|
||||||
continue
|
continue
|
||||||
@@ -3978,6 +4061,9 @@ function Start-LoginMonitor {
|
|||||||
if (Test-MonitorFeatureEnabled -Value $EnableWinRmInboundMonitoring) {
|
if (Test-MonitorFeatureEnabled -Value $EnableWinRmInboundMonitoring) {
|
||||||
if (Test-WinRmLogAvailable) {
|
if (Test-WinRmLogAvailable) {
|
||||||
Write-Log "WinRM inbound (Enter-PSSession): включён (Operational IDs: $($WinRmInboundShellEventIds -join ', '); correlate 4624=$WinRmCorrelateSecurity4624)."
|
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 {
|
} else {
|
||||||
Write-Log "WinRM inbound: включён в настройках, но журнал WinRM Operational недоступен."
|
Write-Log "WinRM inbound: включён в настройках, но журнал WinRM Operational недоступен."
|
||||||
}
|
}
|
||||||
@@ -4378,16 +4464,30 @@ function Start-LoginMonitor {
|
|||||||
if ($winRmEvents) {
|
if ($winRmEvents) {
|
||||||
foreach ($event in $winRmEvents) {
|
foreach ($event in $winRmEvents) {
|
||||||
if ($event.TimeCreated -le $lastWinRmCheckTime) { continue }
|
if ($event.TimeCreated -le $lastWinRmCheckTime) { continue }
|
||||||
|
$exchangeWinRmStrict = Test-WinRmExchangeStrictActive
|
||||||
|
$userInWinRmEvent = Test-WinRm91UserPresentInEventRecord -Event $event
|
||||||
$wr = Get-WinRm91EventInfo -Event $event
|
$wr = Get-WinRm91EventInfo -Event $event
|
||||||
if (Test-MonitorFeatureEnabled -Value $WinRmCorrelateSecurity4624) {
|
if (Test-MonitorFeatureEnabled -Value $WinRmCorrelateSecurity4624) {
|
||||||
$corr = Find-CorrelatedNetworkLogon4624 -AroundTime $wr.TimeCreated `
|
$corr = Find-CorrelatedNetworkLogon4624 -AroundTime $wr.TimeCreated `
|
||||||
-UsernameHint $wr.User -WindowSeconds $WinRm4624CorrelationWindowSeconds
|
-UsernameHint $wr.User -WindowSeconds $WinRm4624CorrelationWindowSeconds `
|
||||||
|
-RequireWinRmLogonProcess:$exchangeWinRmStrict
|
||||||
if ($null -ne $corr) {
|
if ($null -ne $corr) {
|
||||||
$wr.SourceIP = $corr.SourceIP
|
if ($wr.SourceIP -eq '-') { $wr.SourceIP = $corr.SourceIP }
|
||||||
if ($wr.User -eq '-' -and $corr.Username -ne '-') { $wr.User = $corr.Username }
|
if ($wr.User -eq '-' -and $corr.Username -ne '-' -and -not $exchangeWinRmStrict) {
|
||||||
|
$wr.User = $corr.Username
|
||||||
|
}
|
||||||
$wr.LogonType = $corr.LogonType
|
$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
|
$winRmIgnoreReason = Get-WinRmIgnoreReason -Username $wr.User -SourceIP $wr.SourceIP
|
||||||
if (-not [string]::IsNullOrWhiteSpace($winRmIgnoreReason)) {
|
if (-not [string]::IsNullOrWhiteSpace($winRmIgnoreReason)) {
|
||||||
Write-Log "Skip WinRM $($event.Id): User=$($wr.User) IP=$($wr.SourceIP) — reason=$winRmIgnoreReason"
|
Write-Log "Skip WinRM $($event.Id): User=$($wr.User) IP=$($wr.SourceIP) — reason=$winRmIgnoreReason"
|
||||||
|
|||||||
@@ -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`.
|
- **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`.
|
- **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.
|
- **Диагностика 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) Подготовка
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ $GetInventory = $true
|
|||||||
# $EnableAdminShareMonitoring = 1 # Security 5140 C$/ADMIN$ (audit File Share)
|
# $EnableAdminShareMonitoring = 1 # Security 5140 C$/ADMIN$ (audit File Share)
|
||||||
# $WinRmIgnoreLocalSource = 1 # ::1, 127.0.0.1, fe80 (шум Exchange/локальный WinRM)
|
# $WinRmIgnoreLocalSource = 1 # ::1, 127.0.0.1, fe80 (шум Exchange/локальный WinRM)
|
||||||
# $WinRmIgnoreMachineAccounts = 1 # учётки, оканчивающиеся на $
|
# $WinRmIgnoreMachineAccounts = 1 # учётки, оканчивающиеся на $
|
||||||
|
# $WinRmExchangeStrictMode = 1 # Exchange: user в Event 91 обязателен; 4624 только LogonProcess WinRM
|
||||||
# HealthMailbox* уже в ExcludedUserPatterns скрипта
|
# HealthMailbox* уже в ExcludedUserPatterns скрипта
|
||||||
# Проверка: powershell -File Login_Monitor.ps1 -CheckSac
|
# Проверка: powershell -File Login_Monitor.ps1 -CheckSac
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
2.0.22-SAC
|
2.0.23-SAC
|
||||||
|
|||||||
Reference in New Issue
Block a user