Защита Windows Server от брутфорса (RDP и SMB): скоростной и безопасный аналог Fail2ban на PowerShell


Защита Windows Server от RDP и SMB брутфорса: скоростной аналог Fail2ban на PowerShell и Windows Firewall с защитой от самоблокировки администратора.

Если сервер Windows смотрит сетевым интерфейсом в открытый интернет, журнал безопасности (Security Log) за считанные минуты переполняется событиями с кодом 4625 (неудачная попытка входа). Автоматизированные ботнеты непрерывно подбирают пароли к учетным записям admin, administrator, root, админ.

Стандартный подход — автоматизировать блокировку атакующих через PowerShell и Windows Firewall. Однако написание такого скрипта «в лоб» таит в себе неочевидные ловушки: от полной изоляции удаленного сервера до глухого зависания PowerShell при разборе журналов.

Боевой опыт: скрипт безопасности обязан быть отказоустойчивым. Любая ошибка в логике правил брандмауэра может заблокировать доступ к серверу без возможности удаленного восстановления.

3 критические ловушки при создании защиты

  1. Ловушка дефолтов Windows Firewall:
    Команда New-NetFirewallRule -Action Block без немедленно переданного списка IP по умолчанию блокирует ВЕСЬ входящий трафик со всего мира на все порты. Сервер моментально становится «кирпичом».
  2. Ловушка протоколов (RDP vs SMB):
    Боты часто перебирают пароли не только через RDP (порт 3389), но и через SMB (порт 445 / NetBIOS). Блокировка только RDP-порта оставляет сервер уязвимым для атак через общий доступ к файлам. Злоумышленника необходимо блокировать по всем портам целиком.
  3. Ловушка производительности PowerShell (тяжелый XML):
    Использование [xml]$Event.ToXml() в цикле при атаке из сотен или тысяч попыток намертво вешает скрипт на парсинге XML-дерева. Чтение заранее скомпилированных свойств события напрямую из памяти через $Event.Properties ускоряет работу в десятки раз: 600+ событий обрабатываются за 1 секунду.

Архитектура безопасного скрипта

  • Автоматическая защита сессии админа: скрипт перед каждой операцией опрашивает активные TCP-соединения (Get-NetTCPConnection) и принудительно заносит IP вашей текущей сессии в белый список. Вы никогда не заблокируете сами себя.
  • Строгое точное совпадение (Exact Match): проверка через -contains исключает ложные срабатывания на легитимных пользователей (например, admin_corp или user_backup). Доменные префиксы вида DOMAIN\user отсекаются автоматически.
  • Блокировка по всем портам и протоколам: для обнаруженного IP создается правило Protocol: Any, LocalPort: Any.

Финальный турбо-скрипт: Block-RDP-BruteForce.ps1

Сохраните следующий код в файл C:\Scripts\Block-RDP-BruteForce.ps1:

# =====================================================================
# Турбо-скрипт защиты от брутфорса RDP/SMB (блокировка по ВСЕМ портам)
# Совместимость: Windows Server 2016+ / PowerShell 5.1
# =====================================================================

$RuleName     = "RDP-BruteForce-BlockList"
$MinutesBack  = 10   # За сколько последних минут анализировать журнал
$Threshold    = 2    # Порог попыток для бана
$LogPath      = "C:\Scripts\rdp_block.log"

# 1. Белый список постоянных доверенных адресов
$StaticWhitelist = @("127.0.0.1", "::1")

# 2. Имена-ловушки (строгое совпадение, регистр не имеет значения)
$HoneypotNames = @(
    "admin", "administrator", "админ", "администратор",
    "root", "guest", "гость", "test", "support"
)

function Write-Log {
    param([string]$Message)
    $Timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
    $LogEntry = "[$Timestamp] $Message"
    Write-Host $LogEntry
    try { Add-Content -Path $LogPath -Value $LogEntry -ErrorAction SilentlyContinue } catch {}
}

# 3. Автоопределение RDP-порта и защита активной сессии администратора
$RdpPort = (Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name PortNumber -ErrorAction SilentlyContinue).PortNumber
if (-not $RdpPort) { $RdpPort = 3389 }

$ActiveWhitelist = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
foreach ($ip in $StaticWhitelist) { if ($ip) { [void]$ActiveWhitelist.Add($ip.Trim()) } }

try {
    $Established = Get-NetTCPConnection -LocalPort $RdpPort -State Established -ErrorAction SilentlyContinue
    if ($Established) {
        foreach ($conn in $Established) {
            if ($conn.RemoteAddress -and $conn.RemoteAddress -ne "0.0.0.0" -and $conn.RemoteAddress -ne "::") {
                [void]$ActiveWhitelist.Add($conn.RemoteAddress)
            }
        }
    }
} catch {}

# 4. Чтение журнала безопасности (событие 4625)
$StartTime = (Get-Date).AddMinutes(-$MinutesBack)

try {
    # Ограничение 1000 событий защищает память от переполнения при DDoS
    $Events = Get-WinEvent -FilterHashtable @{ LogName='Security'; ID=4625; StartTime=$StartTime } -MaxEvents 1000 -ErrorAction Stop
} catch {
    exit 0 # Событий нет — штатный выход
}

# 5. Мгновенный разбор свойств события из оперативной памяти
$CandidateIPs = [System.Collections.Generic.List[string]]::new()

foreach ($Event in $Events) {
    try {
        # Свойства Event 4625 в Windows Server: [5]=TargetUserName, [10]=LogonType, [19]=IpAddress
        $RawUser   = [string]$Event.Properties[5].Value
        $LogonType = [string]$Event.Properties[10].Value
        $IpStr     = [string]$Event.Properties[19].Value

        # Только сетевые входы (3 = Network/SMB/NLA, 10 = RDP)
        if ($LogonType -ne '3' -and $LogonType -ne '10') { continue }
        if ([string]::IsNullOrWhiteSpace($IpStr) -or $IpStr -eq '-' -or $IpStr -eq '127.0.0.1' -or $IpStr -eq '::1') { continue }

        $parsedIP = $null
        if (-not [System.Net.IPAddress]::TryParse($IpStr, [ref]$parsedIP)) { continue }

        # Очистка имени от домена (DOMAIN\admin -> admin, admin@domain.local -> admin)
        $CleanUser = $RawUser.Trim() -replace '^[^\\]+\\', '' -replace '@.+$', ''

        if ($HoneypotNames -contains $CleanUser) {
            $CandidateIPs.Add($IpStr)
        }
    } catch {}
}

if ($CandidateIPs.Count -eq 0) { exit 0 }

# 6. Группировка и проверка порога
$Violators = $CandidateIPs | Group-Object | Where-Object { $_.Count -ge $Threshold } | Select-Object -ExpandProperty Name
if (-not $Violators) { exit 0 }

# 7. Получение текущих правил брандмауэра
$ExistingRule = Get-NetFirewallRule -DisplayName $RuleName -ErrorAction SilentlyContinue
$CurrentBlocked = @()

if ($ExistingRule) {
    $FilterObj = Get-NetFirewallAddressFilter -AssociatedNetFirewallRule $ExistingRule
    if ($FilterObj.RemoteAddress) {
        $CurrentBlocked = @($FilterObj.RemoteAddress) | Where-Object { $_ -ne "Any" -and $_ -ne "" }
    }
}

# 8. Фильтрация белого списка
$IPsToAdd = @()
foreach ($vIP in $Violators) {
    if ($ActiveWhitelist.Contains($vIP)) { continue }
    if ($CurrentBlocked -notcontains $vIP) { $IPsToAdd += $vIP }
}

if ($IPsToAdd.Count -eq 0) { exit 0 }

# 9. Блокировка нарушителей по ВСЕМ портам и протоколам
$AllBlocked = @($CurrentBlocked + $IPsToAdd) | Select-Object -Unique

if ($AllBlocked.Count -gt 0) {
    if ($ExistingRule) {
        Set-NetFirewallRule -DisplayName $RuleName -RemoteAddress $AllBlocked
    } else {
        New-NetFirewallRule -DisplayName $RuleName `
                            -Name $RuleName `
                            -Description "Auto-block brute-force attackers across ALL ports" `
                            -Direction Inbound `
                            -Action Block `
                            -RemoteAddress $AllBlocked `
                            -Profile Any | Out-Null
    }

    foreach ($blockedIP in $IPsToAdd) {
        Write-Log "НАГЛУХО ЗАБЛОКИРОВАН IP: $blockedIP (по ВСЕМ портам)"
    }
}

Развертывание и автоматизация

Шаг 1. Включение аудита безопасности Windows

Убедитесь, что система протоколирует неудачные попытки входа:

auditpol /set /subcategory:"Вход в систему" /failure:enable

Шаг 2. Запуск по расписанию (каждые 5 минут)

Регистрируем задачу в Планировщике заданий Windows от имени учетной записи SYSTEM:

$Action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-ExecutionPolicy Bypass -WindowStyle Hidden -File `"C:\Scripts\Block-RDP-BruteForce.ps1`""
$Trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 5)
$Principal = New-ScheduledTaskPrincipal -UserId "NT AUTHORITY\SYSTEM" -LogonType ServiceAccount -RunLevel Highest

Register-ScheduledTask -TaskName "Block-RDP-BruteForce" -Action $Action -Trigger $Trigger -Principal $Principal -Force

Шпаргалка по управлению блокировками

  • Проверить параметры блокировки портов:
    Get-NetFirewallPortFilter -AssociatedNetFirewallRule (Get-NetFirewallRule -DisplayName "RDP-BruteForce-BlockList")
    # В полях Protocol и LocalPort должно стоять 'Any'
  • Посмотреть список заблокированных IP:
    $Rule = Get-NetFirewallRule -DisplayName "RDP-BruteForce-BlockList" -ErrorAction SilentlyContinue
    if ($Rule) {
        (Get-NetFirewallAddressFilter -AssociatedNetFirewallRule $Rule).RemoteAddress
    } else { "Список пуст" }
  • Посмотреть историю блокировок в логе:
    Get-Content "C:\Scripts\rdp_block.log" -Tail 20
  • Разблокировать конкретный IP вручную (например, 1.2.3.4):
    $Rule = Get-NetFirewallRule -DisplayName "RDP-BruteForce-BlockList"
    $IPs = @((Get-NetFirewallAddressFilter -AssociatedNetFirewallRule $Rule).RemoteAddress) | Where-Object { $_ -ne "1.2.3.4" }
    if ($IPs.Count -gt 0) {
        Set-NetFirewallRule -DisplayName "RDP-BruteForce-BlockList" -RemoteAddress $IPs
    } else {
        Remove-NetFirewallRule -DisplayName "RDP-BruteForce-BlockList"
    }
Итог: Готовое решение работает автономно, обрабатывает сотни атак за доли секунды, наглухо перекрывает доступ нарушителям по всем протоколам и полностью защищает администратора от случайной самоблокировки.

Оставить комментарий

Рекомендуем: