Anavem
Languagefr
How to Detect and Block Brute Force Attacks on Windows Server with PowerShell

How to Detect and Block Brute Force Attacks on Windows Server with PowerShell

Build automated PowerShell scripts to detect RDP brute force attacks through Windows Event Log analysis and automatically block malicious IPs using Windows Defender Firewall rules.

April 24, 2026 18 min
hardpowershell 7 steps 18 min

Why Implement PowerShell-Based Brute Force Protection on Windows Server?

Windows servers running Remote Desktop Services face constant brute force attacks from automated bots scanning for weak credentials. Traditional solutions often require expensive third-party tools or complex configurations. PowerShell provides a native, cost-effective approach to detect and automatically block these attacks using built-in Windows features.

How Does Event Log Analysis Detect RDP Brute Force Attacks?

Windows Security Event Log captures every failed authentication attempt as Event ID 4625. By analyzing these events with PowerShell, you can identify patterns indicating brute force attacks - typically multiple failed attempts from the same IP address within a short timeframe. The key advantage is leveraging existing Windows logging infrastructure without additional software.

What Makes Automated Firewall Rule Management Effective?

Modern attackers often use distributed networks, making individual IP blocking less effective. This tutorial implements subnet-level blocking by converting attacking IPs to /16 subnets, providing broader protection while maintaining performance. The automated 24-hour clearing cycle prevents permanent blocking of dynamic IP addresses while ensuring persistent attackers are quickly re-blocked.

You'll build a complete protection system using PowerShell scripts that monitor Event ID 4625, identify attack patterns, automatically update Windows Defender Firewall rules, and maintain the system through scheduled tasks. This approach provides enterprise-level protection using only native Windows capabilities.

Implementation Guide

Full Procedure

01

Enable Security Event Logging for Failed Logon Attempts

First, ensure Windows is logging failed authentication attempts. Open Group Policy Management Console and navigate to the audit policy settings.

gpedit.msc

Navigate to Computer Configuration > Windows Settings > Security Settings > Local Policies > Audit Policy. Enable Audit logon events for both Success and Failure.

Alternatively, configure this via PowerShell:

auditpol /set /category:"Logon/Logoff" /success:enable /failure:enable

Apply the changes immediately:

gpupdate /force

Verification: Check that Event ID 4625 appears in the Security Event Log after a failed RDP attempt:

Get-WinEvent -FilterHashtable @{LogName='Security'; ID=4625} -MaxEvents 5
Pro tip: Enable firewall logging to get additional attack data. Run netsh advfirewall set allprofiles logging filename C:\Windows\System32\LogFiles\Firewall\pfirewall.log
02

Create the Base Firewall Rule for Blocking Attackers

Create a disabled firewall rule that your PowerShell script will populate with malicious IP addresses. This approach prevents accidentally blocking all traffic.

Open Windows Defender Firewall with Advanced Security as Administrator:

wf.msc

Create a new inbound rule via PowerShell:

New-NetFirewallRule -DisplayName "BlockAttackers" -Direction Inbound -Protocol TCP -Action Block -Enabled False -RemoteAddress "127.0.0.1"

The rule starts disabled with a placeholder IP address. Your script will update the RemoteAddress field with actual attacker IPs.

Verification: Confirm the rule exists but is disabled:

Get-NetFirewallRule -DisplayName "BlockAttackers" | Select-Object DisplayName, Enabled, Direction
Warning: Never enable a firewall rule with an empty RemoteAddress field or wildcard (*) - this will block ALL incoming traffic and lock you out of the server.
03

Build the Brute Force Detection Script

Create the main PowerShell script that analyzes Event ID 4625 entries and identifies brute force patterns. Save this as C:\Scripts\RDP_BruteForce_Detection.ps1:

# RDP Brute Force Detection and Blocking Script
param(
    [int]$Hours = 1,
    [int]$AttackThreshold = 5
)

# Get failed RDP logon attempts from the last specified hours
$FailedLogons = Get-WinEvent -FilterHashtable @{
    LogName='Security'
    ID=4625
    StartTime=(Get-Date).AddHours(-$Hours)
} -ErrorAction SilentlyContinue | ForEach-Object {
    $EventXml = ([xml]$_.ToXml()).Event
    [PSCustomObject]@{
        UserName = ($EventXml.EventData.Data | Where-Object { $_.Name -eq 'TargetUserName' }).'#text'
        IpAddress = ($EventXml.EventData.Data | Where-Object { $_.Name -eq 'IpAddress' }).'#text'
        EventDate = [DateTime]$EventXml.System.TimeCreated.SystemTime
        LogonType = ($EventXml.EventData.Data | Where-Object { $_.Name -eq 'LogonType' }).'#text'
    }
}

# Filter for RDP attempts (LogonType 10) and group by IP
$AttackerIPs = $FailedLogons | Where-Object { 
    $_.LogonType -eq '10' -and 
    $_.IpAddress -ne '-' -and 
    $_.IpAddress -ne '127.0.0.1' 
} | Group-Object IpAddress | Where-Object { $_.Count -ge $AttackThreshold }

if ($AttackerIPs) {
    Write-Host "Found $($AttackerIPs.Count) attacking IP addresses:" -ForegroundColor Red
    $AttackerIPs | ForEach-Object {
        Write-Host "  $($_.Name): $($_.Count) failed attempts" -ForegroundColor Yellow
    }
    return $AttackerIPs.Name
} else {
    Write-Host "No brute force attacks detected in the last $Hours hour(s)" -ForegroundColor Green
    return @()
}

Create the Scripts directory first:

New-Item -Path "C:\Scripts" -ItemType Directory -Force

Verification: Test the script manually:

C:\Scripts\RDP_BruteForce_Detection.ps1 -Hours 24 -AttackThreshold 3
04

Create the IP Blocking and Subnet Conversion Logic

Build the script that converts individual attacking IPs to /16 subnets and updates the firewall rule. Save as C:\Scripts\Block_Attackers.ps1:

# Block Attackers Script with Subnet Conversion
param(
    [string[]]$AttackerIPs,
    [string]$RuleName = "BlockAttackers"
)

if (-not $AttackerIPs -or $AttackerIPs.Count -eq 0) {
    Write-Host "No IPs to block" -ForegroundColor Green
    return
}

# Convert IPs to /16 subnets for broader protection
$Subnets = @()
foreach ($IP in $AttackerIPs) {
    if ($IP -match '^(\d+\.\d+)\.(\d+)\.(\d+)$') {
        $Subnet = "$($matches[1]).0.0/16"
        if ($Subnets -notcontains $Subnet) {
            $Subnets += $Subnet
            Write-Host "Converting $IP to subnet $Subnet" -ForegroundColor Cyan
        }
    }
}

# Get existing blocked addresses from the firewall rule
$ExistingRule = Get-NetFirewallRule -DisplayName $RuleName -ErrorAction SilentlyContinue
if ($ExistingRule) {
    $ExistingAddresses = (Get-NetFirewallAddressFilter -AssociatedNetFirewallRule $ExistingRule).RemoteAddress
    
    # Combine existing and new subnets, remove duplicates
    $AllSubnets = ($ExistingAddresses + $Subnets) | Where-Object { $_ -ne "127.0.0.1" } | Sort-Object -Unique
    
    # Update the firewall rule with all subnets
    Set-NetFirewallRule -DisplayName $RuleName -RemoteAddress $AllSubnets
    
    # Enable the rule now that it has valid IP addresses
    Set-NetFirewallRule -DisplayName $RuleName -Enabled True
    
    Write-Host "Updated firewall rule with $($AllSubnets.Count) blocked subnets" -ForegroundColor Green
    Write-Host "Blocked subnets: $($AllSubnets -join ', ')" -ForegroundColor Yellow
    
    # Log the action to Event Log
    Write-EventLog -LogName Application -Source "RDC Brute Force Prevention Script" -EventId 1001 -EntryType Information -Message "Blocked subnets: $($AllSubnets -join ', ')"
} else {
    Write-Host "Firewall rule '$RuleName' not found" -ForegroundColor Red
}

Register the custom event source for logging:

New-EventLog -LogName Application -Source "RDC Brute Force Prevention Script" -ErrorAction SilentlyContinue

Verification: Test the blocking script with a test IP:

C:\Scripts\Block_Attackers.ps1 -AttackerIPs @("192.168.1.100")
05

Implement Automatic Rule Clearing for 24-Hour Cycles

Create a cleanup script that clears blocked IPs every 24 hours to prevent permanent blocking of dynamic IP addresses. Save as C:\Scripts\Clear_Blocked_IPs.ps1:

# Clear Blocked IPs Script - Runs daily at midnight
param(
    [string]$RuleName = "BlockAttackers"
)

# Reset the firewall rule to placeholder IP and disable it
Set-NetFirewallRule -DisplayName $RuleName -RemoteAddress "127.0.0.1" -Enabled False

# Log the clearing action
Write-EventLog -LogName Application -Source "RDC Brute Force Prevention Script" -EventId 1002 -EntryType Information -Message "Cleared all blocked IPs from firewall rule at $(Get-Date)"

Write-Host "Cleared all blocked IPs from firewall rule '$RuleName'" -ForegroundColor Green

Create the master script that combines detection and blocking. Save as C:\Scripts\RDP_Protection_Master.ps1:

# Master RDP Protection Script
$AttackerIPs = & "C:\Scripts\RDP_BruteForce_Detection.ps1" -Hours 1 -AttackThreshold 5

if ($AttackerIPs -and $AttackerIPs.Count -gt 0) {
    & "C:\Scripts\Block_Attackers.ps1" -AttackerIPs $AttackerIPs
}

# Check if it's midnight (within 5 minutes) and clear rules
$CurrentTime = Get-Date
if ($CurrentTime.Hour -eq 0 -and $CurrentTime.Minute -lt 5) {
    & "C:\Scripts\Clear_Blocked_IPs.ps1"
}

Verification: Test the complete workflow:

C:\Scripts\RDP_Protection_Master.ps1
Pro tip: Use 24-hour clearing cycles to handle dynamic IP addresses from legitimate users who might get temporarily blocked. Persistent attackers will be re-blocked quickly.
06

Configure Scheduled Task for Automated Execution

Set up a scheduled task to run the protection script every 5 minutes. Use PowerShell to create the task programmatically:

# Create scheduled task for RDP protection
$TaskName = "RDP_BruteForce_Protection"
$ScriptPath = "C:\Scripts\RDP_Protection_Master.ps1"

# Define the action
$Action = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "-WindowStyle Hidden -ExecutionPolicy Bypass -File `"$ScriptPath`""

# Define the trigger (every 5 minutes)
$Trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 5) -RepetitionDuration (New-TimeSpan -Days 365)

# Define task settings
$Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -RunOnlyIfNetworkAvailable

# Create the task
Register-ScheduledTask -TaskName $TaskName -Action $Action -Trigger $Trigger -Settings $Settings -User "SYSTEM" -RunLevel Highest -Force

Verify the task was created and is running:

Get-ScheduledTask -TaskName "RDP_BruteForce_Protection" | Select-Object TaskName, State, LastRunTime

Start the task manually to test:

Start-ScheduledTask -TaskName "RDP_BruteForce_Protection"

Verification: Check the task history and event logs:

# Check task execution history
Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-TaskScheduler/Operational'; ID=200} -MaxEvents 5

# Check our custom event log entries
Get-WinEvent -FilterHashtable @{LogName='Application'; ProviderName='RDC Brute Force Prevention Script'} -MaxEvents 10
07

Test and Validate the Complete Protection System

Perform comprehensive testing to ensure the system works correctly without blocking legitimate traffic.

First, simulate a brute force attack by generating failed logon events (use a test account):

# Generate test failed logon events (run from another machine)
for ($i = 1; $i -le 10; $i++) {
    try {
        $cred = Get-Credential -UserName "testuser" -Message "Enter wrong password"
        Enter-PSSession -ComputerName "YourServerIP" -Credential $cred
    } catch {
        Write-Host "Failed attempt $i" -ForegroundColor Red
    }
    Start-Sleep -Seconds 2
}

Monitor the detection in real-time:

# Watch for new Event ID 4625 entries
Get-WinEvent -FilterHashtable @{LogName='Security'; ID=4625; StartTime=(Get-Date).AddMinutes(-10)} | 
    Select-Object TimeCreated, @{Name='SourceIP';Expression={([xml]$_.ToXml()).Event.EventData.Data | Where-Object {$_.Name -eq 'IpAddress'} | Select-Object -ExpandProperty '#text'}}

Verify the firewall rule is populated and enabled:

# Check firewall rule status
$Rule = Get-NetFirewallRule -DisplayName "BlockAttackers"
$Addresses = Get-NetFirewallAddressFilter -AssociatedNetFirewallRule $Rule

Write-Host "Rule Enabled: $($Rule.Enabled)" -ForegroundColor $(if($Rule.Enabled){'Green'}else{'Red'})
Write-Host "Blocked Addresses: $($Addresses.RemoteAddress -join ', ')" -ForegroundColor Yellow

Test connectivity from a blocked IP range (if possible):

# From the server, test if the rule is actually blocking
Test-NetConnection -ComputerName "localhost" -Port 3389 -InformationLevel Detailed

Verification: Check the complete system status:

# Complete system health check
Write-Host "=== RDP Brute Force Protection Status ===" -ForegroundColor Cyan
Write-Host "Scheduled Task: $(if((Get-ScheduledTask -TaskName 'RDP_BruteForce_Protection').State -eq 'Ready'){'Running'}else{'Not Running'})" -ForegroundColor Green
Write-Host "Firewall Rule: $(if((Get-NetFirewallRule -DisplayName 'BlockAttackers').Enabled){'Enabled'}else{'Disabled'})" -ForegroundColor Green
Write-Host "Event Logging: $(if((auditpol /get /category:'Logon/Logoff' | Select-String 'Failure.*Success').Count -gt 0){'Enabled'}else{'Disabled'})" -ForegroundColor Green
Write-Host "Recent Blocks: $((Get-WinEvent -FilterHashtable @{LogName='Application'; ProviderName='RDC Brute Force Prevention Script'} -MaxEvents 5 -ErrorAction SilentlyContinue).Count)" -ForegroundColor Yellow
Warning: Always test from a secondary connection method (console access, VPN, or different IP) before deploying to production. A misconfigured rule could lock you out of your own server.

Frequently Asked Questions

How many failed RDP attempts should trigger the brute force protection?+
The optimal threshold is 5-10 failed attempts within 3-5 minutes from the same IP address. This balance prevents false positives from legitimate users making typos while catching automated attacks. You can adjust the AttackThreshold parameter in the detection script based on your environment's needs and user behavior patterns.
Will this PowerShell solution block legitimate users who mistype passwords?+
The 24-hour automatic clearing cycle prevents permanent blocking of legitimate users. If a user gets blocked due to multiple failed attempts, they'll regain access within 24 hours. For immediate unblocking, administrators can manually remove IPs from the firewall rule or temporarily disable it. The subnet conversion (/16) means entire IP ranges get blocked, so consider your user base's IP distribution.
Can this script detect brute force attacks on services other than RDP?+
Yes, but requires modification. The script currently filters for LogonType 10 (RDP). You can adapt it for other services by changing the LogonType filter: LogonType 2 for interactive console logons, LogonType 3 for network logons (SMB/file shares), or LogonType 8 for network cleartext (IIS). Each service may use different event IDs and require specific filtering logic.
What happens if the PowerShell script blocks my own IP address?+
Always maintain alternative access methods before deploying: console access, VPN connection, or management network. If locked out, access the server through these alternatives and either disable the 'BlockAttackers' firewall rule or remove your IP from the RemoteAddress field. The script includes safeguards against blocking localhost (127.0.0.1) but cannot prevent blocking your external IP.
How much system performance impact does continuous Event Log monitoring have?+
Minimal impact when properly configured. The script runs every 5 minutes and only queries the last hour of Security Event Log entries with specific filtering (Event ID 4625). On busy servers, consider increasing the interval to 10-15 minutes or implementing more aggressive filtering. The Get-WinEvent cmdlet with FilterHashtable is optimized for performance compared to older Get-EventLog methods.

Discussion

Share your thoughts and insights

Sign in to join the discussion