Windows Server 2008's native account lockout policy is a good first line of defense, but sophisticated attackers often rotate through thousands of username/password combinations from a single IP address without triggering account lockouts. I recently encountered a case where an attacker was making exactly 1 login attempt per second - carefully staying below the threshold that would lock any particular account.
Account lockout policies protect against password spraying (trying one common password against many accounts). IP-based blocking handles credential stuffing (trying many passwords against a few accounts). The ideal solution combines both approaches.
We'll create a PowerShell script that:
- Monitors Windows Security Event Log for failed login attempts (Event ID 4625)
- Tracks IPs that exceed a threshold of failed attempts
- Automatically adds firewall rules to block malicious IPs
- Includes an expiration mechanism (30 minute blocks)
Here's the complete script that handles both detection and blocking:
# Configure these variables
$maxAttempts = 5 # Max failed attempts before blocking
$blockDuration = 30 # Minutes to block the IP
$eventID = 4625 # Failed logon event ID
# Main monitoring function
function Monitor-FailedLogons {
$blockedIPs = @{}
# Query events in a loop
while ($true) {
$events = Get-WinEvent -FilterHashtable @{
LogName='Security'
ID=$eventID
StartTime=(Get-Date).AddMinutes(-1)
} -ErrorAction SilentlyContinue
# Process each failed logon event
foreach ($event in $events) {
$ip = ($event.Properties[19].Value -split ":")[0]
if ($ip -and $ip -ne "-") {
if ($blockedIPs.ContainsKey($ip)) {
# Existing IP - increment count
$blockedIPs[$ip].Count++
# Block if threshold reached
if ($blockedIPs[$ip].Count -ge $maxAttempts -and
-not $blockedIPs[$ip].Blocked) {
Block-IP $ip
$blockedIPs[$ip].Blocked = $true
$blockedIPs[$ip].BlockTime = Get-Date
}
} else {
# New IP - initialize tracking
$blockedIPs[$ip] = @{
Count = 1
Blocked = $false
BlockTime = $null
}
}
}
}
# Remove expired blocks
$now = Get-Date
foreach ($ip in $blockedIPs.Keys) {
if ($blockedIPs[$ip].Blocked -and
($now - $blockedIPs[$ip].BlockTime).TotalMinutes -ge $blockDuration) {
Remove-IPBlock $ip
$blockedIPs.Remove($ip)
}
}
Start-Sleep -Seconds 30
}
}
# Helper function to add firewall rule
function Block-IP {
param($ip)
$ruleName = "AutoBlock_$ip"
if (-not (Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue)) {
New-NetFirewallRule -DisplayName $ruleName -Direction Inbound -Action Block
-RemoteAddress $ip -Protocol Any -Enabled True | Out-Null
Write-Host "[$(Get-Date)] Blocked IP: $ip"
}
}
# Helper function to remove firewall rule
function Remove-IPBlock {
param($ip)
$ruleName = "AutoBlock_$ip"
if (Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue) {
Remove-NetFirewallRule -DisplayName $ruleName | Out-Null
Write-Host "[$(Get-Date)] Unblocked IP: $ip"
}
}
# Start monitoring
Monitor-FailedLogons
For production use:
- Run the script as a Windows Scheduled Task with highest privileges
- Consider increasing the sleep interval to 2-5 minutes for reduced overhead
- Log blocked IPs to a file for forensic analysis
- Combine with RDP port obfuscation or VPN access for maximum security
For environments with multiple servers, you can centralize monitoring by:
- Configuring Windows Event Forwarding to send Security logs to a central server
- Modifying the script to process forwarded events
- Implementing distributed blocking by pushing firewall rules to target servers
When monitoring Windows Server 2008 security logs, you'll typically see Event ID 4625 for failed logins. A common pattern looks like:
Event ID: 4625 Logon Type: 3 (Network) Source Network Address: 192.168.1.100 Account Name: Administrator
Here's a complete PowerShell solution that automatically adds firewall rules for repeated offenders:
# Failed Login Threshold
$threshold = 5
# Block Duration in minutes
$blockDuration = 30
# Parse Security Log for Failed Attempts
$failedLogins = Get-WinEvent -FilterHashtable @{
LogName='Security'
ID=4625
} -MaxEvents 1000 | Where-Object {
$_.Properties[5].Value -eq 3 -and # Network logon
$_.Properties[19].Value -ne '-' # Has IP address
}
# Group by IP and filter
$offenders = $failedLogins | Group-Object { $_.Properties[19].Value } |
Where-Object { $_.Count -ge $threshold }
# Block each offender in Windows Firewall
foreach ($offender in $offenders) {
$ip = $offender.Name
$ruleName = "Block_BruteForce_$ip"
# Check if rule already exists
if (-not (Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue)) {
New-NetFirewallRule -DisplayName $ruleName -Direction Inbound -Action Block
-RemoteAddress $ip -Protocol Any -Enabled True
# Schedule unblock after duration
$unblockTime = (Get-Date).AddMinutes($blockDuration)
$action = {
Remove-NetFirewallRule -DisplayName $ruleName
}
Register-ScheduledTask -TaskName "Unblock_$ip" -Trigger (New-ScheduledTaskTrigger -Once -At $unblockTime)
-Action (New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "-Command "$action"") -Force
}
}
To run this script automatically:
- Save it as
BlockBruteForce.ps1 - Create a scheduled task that triggers on Event ID 4625
- Set the action to run the PowerShell script
For a more robust solution, consider using Fail2Ban with Windows:
# Sample fail2ban jail configuration [windows-rdp] enabled = true port = 3389 filter = windows-rdp logpath = %windir%\System32\winevt\Logs\Security.evtx maxretry = 5 bantime = 1800 action = iptables-allports[name=WindowsRDP,protocol=all]
When implementing this solution, monitor these additional Event IDs:
- 4768: Kerberos authentication (TGT request)
- 4771: Kerberos pre-authentication failed
- 4624: Successful logon (to detect successful breaches)