Windows OS Hub
  • Windows
    • Windows 11
    • Windows Server 2022
    • Windows 10
    • Windows Server 2019
    • Windows Server 2016
  • Microsoft
    • Active Directory (AD DS)
    • Group Policies (GPOs)
    • Exchange Server
    • Azure and Microsoft 365
    • Microsoft Office
  • Virtualization
    • VMware
    • Hyper-V
  • PowerShell
  • Linux
  • Home
  • About

Windows OS Hub

  • Windows
    • Windows 11
    • Windows Server 2022
    • Windows 10
    • Windows Server 2019
    • Windows Server 2016
  • Microsoft
    • Active Directory (AD DS)
    • Group Policies (GPOs)
    • Exchange Server
    • Azure and Microsoft 365
    • Microsoft Office
  • Virtualization
    • VMware
    • Hyper-V
  • PowerShell
  • Linux

 Windows OS Hub / PowerShell / Protecting Remote Desktop (RDP) Host from Brute Force Attacks

February 5, 2024 PowerShellWindows 10Windows Server 2019

Protecting Remote Desktop (RDP) Host from Brute Force Attacks

Any Windows host directly connected to the Internet with an open RDP port is periodically logged for remote brute-force password attempts. To effectively protect the default Remote Desktop protocol port (3389) from password brute-force attacks and vulnerability exploitations, it is recommended that the RDP host be placed behind a VPN or Remote Desktop Gateway. If it is not possible to implement such a scheme, you will need to configure additional means of RDP protection:

  • Don’t use the default user account names in Windows. If this is not done, users with popular account names (admin, administrator, user1) will be locked regularly according to the account lockup policy.
  • Enable users to use complex passwords through the Windows password policy;
  • Change the default RDP port number 3389 to a different one;
  • Configure 2FA for RDP logins (learn how to use the open-source MultiOTP service to enable RDP two-factor authentication).

In this article, we will configure a PowerShell script that mitigates brute-force Remote Desktop (RDP) logins and automatically blocks the IP addresses from which failed RDP authentication attempts are logged.

Detect and Block RDP Brute Force Attacks with PowerShell

If an attempt to authenticate in Windows over RDP fails, event ID 4625 will appear in the Security event log ( An account failed to log on with LogonType = 3 , See: Analyzing RDP connection event logs ). The event description includes the username under which the connection was attempted and the source IP address.

You can use the Get-WinEvent PowerShell cmdlet to list events with failed RDP logon attempts in the past 12 hour:

$result = Get-WinEvent -FilterHashtable @{LogName='Security';ID=4625; StartTime=(get-date).AddHours(-12)} | 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
    }
}
$result

List failed Remote Desktop (RDP) login attempts (4625 Event ID)

You can use a PowerShell script to parse the Event Viewer log. If there are more than 5 failed RDP login attempts from a specific IP address in the last 3 hours, the script will add that IP to the Windows Firewall blocking rule.

# The number of failed login attempts from an IP address, after which the IP is blocked.
$badAttempts = 5
# Check the number of failed RDP logon events for the last N hours.
$intervalHours = 2
# Create a new Windows Firewall rule if the previous blocking rule contains more than N  unique IP addresses
$ruleMaxEntries = 2000
# Port number on which the RDP service is listening
$RdpLocalPort=3389
# Log file
$log = "c:\ps\rdp_block.log"
# A list of trusted IP addresses from which RDP connections should never be blocked
$trustedIPs = @("192.168.1.100", "192.168.1.101","8.8.8.8")  
 $startTime = [DateTime]::Now.AddHours(-$intervalHours)
$badRDPlogons = Get-EventLog -LogName 'Security' -After $startTime -InstanceId 4625 |
    Where-Object { $_.Message -match 'logon type:\s+(3)\s' } |
    Select-Object @{n='IpAddress';e={$_.ReplacementStrings[-2]}}
$ipsArray = $badRDPlogons |
    Group-Object -Property IpAddress |
    Where-Object { $_.Count -ge $badAttempts } |
    ForEach-Object { $_.Name }
# Remove trusted IP addresses from the list
$ipsArray = $ipsArray | Where-Object { $_ -notin $trustedIPs }
if ($ipsArray.Count -eq 0) {
    return
}
[System.Collections.ArrayList]$ips = @()
[System.Collections.ArrayList]$current_ip_lists = @()
$ips.AddRange([string[]]$ipsArray)
$ruleCount = 1
$ruleName = "BlockRDPBruteForce" + $ruleCount
$foundRuleWithSpace = 0
while ($foundRuleWithSpace -eq 0) {
    $firewallRule = Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue
    if ($null -eq $firewallRule) {
  New-NetFirewallRule -DisplayName $ruleName –RemoteAddress 1.1.1.1 -Direction Inbound -Protocol TCP –LocalPort $RdpLocalPort -Action Block
        $firewallRule = Get-NetFirewallRule -DisplayName $ruleName
        $current_ip_lists.Add(@(($firewallRule | Get-NetFirewallAddressFilter).RemoteAddress))
        $foundRuleWithSpace = 1
    } else {
        $current_ip_lists.Add(@(($firewallRule | Get-NetFirewallAddressFilter).RemoteAddress))        
        if ($current_ip_lists[$current_ip_lists.Count – 1].Count -le ($ruleMaxEntries – $ips.Count)) {
            $foundRuleWithSpace = 1
        } else {
            $ruleCount++
            $ruleName = "BlockRDPBruteForce" + $ruleCount
        }
    }
}
# Remove IP addresses already in the blocking firewall rule 
for ($i = $ips.Count – 1; $i -ge 0; $i--) {
    foreach ($current_ip_list in $current_ip_lists) {
        if ($current_ip_list -contains $ips[$i]) {
            $ips.RemoveAt($i)
            break
        }
    }
}
if ($ips.Count -eq 0) {
    exit
}
# Block the IP address in Windows Firewall and log the action.
$current_ip_list = $current_ip_lists[$current_ip_lists.Count – 1]
foreach ($ip in $ips) {
    $current_ip_list += $ip
    (Get-Date).ToString().PadRight(22) + ' | ' + $ip.PadRight(15) + ' | The IP address has been blocked due to ' + ($badRDPlogons | Where-Object { $_.IpAddress -eq $ip }).Count + ' failed login attempts over ' + $intervalHours + ' hours' >> $log
}
Set-NetFirewallRule -DisplayName $ruleName -RemoteAddress $current_ip_list

The PowerShell script creates a Windows Defender Firewall rule that blocks connections to the RDP port for the resulting list of IP addresses.

Windows Firewall rule to block RDP attacks by IP addresses

The PowerShell script also writes to the log file the list of blocked IP addresses.

IP address blocking log

The block-rdp-brute-force.ps1 script is available for download from our GitHub repository: https://github.com/maxbakhub/winposh/blob/main/RDS/block-rdp-brute-force.ps1

Download the PS1 script from GitHub to your local drive:

$path = "C:\PS\"
If(!(test-path -PathType container $path)){
New-Item -ItemType Directory -Path $path}
$url = "https://raw.githubusercontent.com/maxbakhub/winposh/main/RDS/block-rdp-brute-force.ps1"
$output = "$path\block-rdp-brute-force.ps1"
Invoke-WebRequest -Uri $url -OutFile $output

You can run the script automatically when event 4625 appears in the Event Viewer. You can do this by creating a task trigger by assigning a Task Scheduler job to an Event ID. Such a scheduled task can be created using a PowerShell script:

$taskname="Block_RDP_Brute_Force_Attacks_PS"
$scriptPath="C:\PS\block-rdp-brute-force.ps1"
$triggers = @()
$triggers += New-ScheduledTaskTrigger -AtLogOn
$CIMTriggerClass = Get-CimClass -ClassName MSFT_TaskEventTrigger -Namespace Root/Microsoft/Windows/TaskScheduler:MSFT_TaskEventTrigger
$trigger = New-CimInstance -CimClass $CIMTriggerClass -ClientOnly
$trigger.Subscription =
@"
<QueryList><Query Id="0" Path="Security"><Select Path="Security">*[System[(EventID=4625)]]</Select></Query></QueryList>
"@
$trigger.Enabled = $True
$triggers += $trigger
$User='Nt Authority\System'
$Action=New-ScheduledTaskAction -Execute "Powershell.exe" -Argument "-NoProfile -NoLogo -NonInteractive -ExecutionPolicy Bypass -File $scriptPath"
Register-ScheduledTask -TaskName $taskname -Trigger $triggers -User $User -Action $Action -RunLevel Highest -Force

Scheduled task to block RDP attacks by IP address

The triggered scheduled task will run on every failed RDP logon attempt. It will analyze the Event Viewer logs and block IP addresses in the firewall rule. The task runs under a LocalSystem account and is independent of whether the user is logged on or not.

16 comments
16
Facebook Twitter Google + Pinterest
previous post
Software RAID1 (Mirror) for Boot Drive on Windows
next post
How to Install & Configure Repositories in CentOS/RHEL

Related Reading

View Windows Update History with PowerShell (CMD)

April 30, 2025

Change BIOS from Legacy to UEFI without Reinstalling...

April 21, 2025

Uninstalling Windows Updates via CMD/PowerShell

April 18, 2025

Allowing Ping (ICMP Echo) Responses in Windows Firewall

April 15, 2025

How to Pause (Delay) Update Installation on Windows...

April 11, 2025

16 comments

WS June 13, 2020 - 10:25 pm

Excellent script !
A very minor point is that the script looks for the attempts of the last 3 hours ($Last_n_Hours = [DateTime]::Now.AddHours(-3)) but logs ‘ attempts for 2 hours’. I used a variable instead in order to customise easily and print the corresponding value.
Thank you so much.

Reply
R0man June 21, 2020 - 1:24 pm

Very useful THANK YOU
Only thing I inserted due of error for Set-NetFirewallRule saying “The address is invalid”:

$current_ips = @()
$currentIps = (Get-NetFirewallRule -DisplayName “BlockRDPBruteForce” | Get-NetFirewallAddressFilter ).RemoteAddress
foreach ($cip in $currentIps)
{
$current_ips += $cip
}

Reply
Federico October 27, 2020 - 4:16 pm

Hello, I’m having trouble with this scrip
I get “The address is invalid” when I run it and when add what R0MAN suggest I get no error but nothing is added to the firewall rule… any idea what I’m doing wrong?
Thanks in advance.

Reply
Dean February 11, 2021 - 10:08 am

Hi Federico

Did you ever find the solution. Battling with this aswell

Reply
federico February 11, 2021 - 11:28 am

Sadly no, I never find a solution to this.

Reply
Brett March 10, 2021 - 6:01 am

Replace
$currentIps = (Get-NetFirewallRule -DisplayName “BlockRDPBruteForce” | Get-NetFirewallAddressFilter ).RemoteAddress
With
$current_ips = @((Get-NetFirewallRule -DisplayName “BlockRDPBruteForce” | Get-NetFirewallAddressFilter ).RemoteAddress)

And it should work.

Reply
Sarge February 23, 2021 - 4:49 pm

The script to create the firewall rule works but then the next script to select the offending IPs gives me this error… What is wrong here?

At line:1 char:45
+ $Last_n_Hours = [DateTime]::Now.AddHours(-3)$badRDPlogons = Get-EventLog -LogNam …
+ ~~~~~~~~~~~~~
Unexpected token ‘$badRDPlogons’ in expression or statement.
+ CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : UnexpectedToken

Reply
Rahil Sarwar March 17, 2021 - 12:24 pm

$Last_n_Hours = [DateTime]::Now.AddHours(-3)
$badRDPlogons = Get-EventLog -LogName ‘Security’ -after $Last_n_Hours -InstanceId 4625 | ?{$_.Message -match ‘logon type:\s+(3)\s’} | Select-Object @{n=’IpAddress’;e={$_.ReplacementStrings[-2]} }
$getip = $badRDPlogons | group-object -property IpAddress | where {$_.Count -gt 5} | Select -property Name
$log = “C:\ps\rdp_blocked_ip.txt”
$current_ips = @((Get-NetFirewallRule -DisplayName “BlockRDPBruteForce” | Get-NetFirewallAddressFilter ).RemoteAddress)
foreach ($ip in $getip)
{
$current_ips += $ip.name
(Get-Date).ToString() + ‘ ‘ + $ip.name + ‘ The IP address has been blocked due to ‘ + ($badRDPlogons | where {$_.IpAddress -eq $ip.name}).count + ‘ attempts for 2 hours’>> $log # writing the IP blocking event to the log file
}
$current_ips = $current_ips | select -Unique
Set-NetFirewallRule -DisplayName “BlockRDPBruteForce” -RemoteAddress $current_ips

Reply
Joshua September 2, 2021 - 12:36 pm

Can anyone post the whole script in its entirety once its working?

Reply
xcx June 6, 2022 - 8:49 pm

$Last_n_Hours = [DateTime]::Now.AddHours(-6)
$badRDPlogons = Get-EventLog -LogName ‘Security’ -after $Last_n_Hours -InstanceId 4625 | Where-Object { $_.Message -match ‘logon type:\s+(3)\s’ } | Select-Object @{n = ‘IpAddress’; e = { $_.ReplacementStrings[-2] } }
$getip = $badRDPlogons | group-object -property IpAddress | Where-Object { $_.Count -gt 10 } | Select-Object -property Name
$log = “C:\ps\rdp_blocked_ip.txt”
$current_ips = @(Get-NetFirewallRule -DisplayName “_BlockRDPBruteForce” | Get-NetFirewallAddressFilter ).RemoteAddress | Sort-Object -Unique
foreach ($ip in $getip) {
$current_ips += $ip.name
(Get-Date).ToString() + ‘ ‘ + $ip.name + ‘ The IP address has been blocked due to ‘ + ($badRDPlogons | Where-Object { $_.IpAddress -eq $ip.name }).count + ‘ attempts for 6 hours’>> $log # writing the IP blocking event to the log file
}
‘Script ran’ >> $log
Set-NetFirewallRule -DisplayName “_BlockRDPBruteForce” -RemoteAddress $current_ips

Reply
José October 19, 2022 - 5:17 pm

On the last part:
Set-NetFirewallRule -DisplayName “BlockRDPBruteForce” -RemoteAddress $current_ips
I’m getting the error: The address is invalid. Addresses may be specified as IP addresses, ranges, or subnets.
somehow it’s not parsing it correctly, anyone has run to this?

Reply
Prescott Chartier December 29, 2022 - 5:49 pm

$getip returns nothing. Looking at the log entries and none of the 4625 entries have an IP address in them, how is this possible?

Reply
Scott November 21, 2023 - 6:19 pm

Here’s my version of the script that creates the rule for you if it doesn’t exist and checks for already blocked IPs. I set it up to trigger on failed login attempts (https://woshub.com/run-script-when-app-opens-closes/). I set it to run whether the user is logged in or not, but not to store password and to run with highest privileges (needed to modify firewall). This helps hide the window but no clue if it actually works while logged off. The action is to call powershell.exe with arguments:

-WindowStyle Hidden -File “”

The script is below. Make sure to set your log path and RDP port (you should probably use a random port in the range 49152 – 65535 rather than the default).

$hours = 3
$log = “”

$startTime = [DateTime]::Now.AddHours(-$hours)
$badRDPlogons = Get-EventLog -LogName ‘Security’ -after $startTime -InstanceId 4625 | ?{$_.Message -match ‘logon type:\s+(3)\s’} | Select-Object @{n=’IpAddress’;e={$_.ReplacementStrings[-2]} }
$getip = $badRDPlogons | group-object -property IpAddress | where {$_.Count -gt 5} | Select -property Name

$firewallRule = Get-NetFirewallRule -DisplayName “BlockRDPBruteForce” -ErrorAction SilentlyContinue
if ($null -eq $firewallRule) {
New-NetFirewallRule -DisplayName “BlockRDPBruteForce” –RemoteAddress 1.1.1.1 -Direction Inbound -Protocol TCP –LocalPort -Action Block
$firewallRule = Get-NetFirewallRule -DisplayName “BlockRDPBruteForce”
}

$current_ips = @(($firewallRule | Get-NetFirewallAddressFilter ).RemoteAddress)
foreach ($ip in $getip) {
if ($current_ips -notcontains $ip.name) {
$current_ips += $ip.name
(Get-Date).ToString() + ‘ ‘ + $ip.name + ‘ The IP address has been blocked due to ‘ + ($badRDPlogons | where {$_.IpAddress -eq $ip.name}).count + ‘ failed login attempts over ‘ + $hours + ‘ hours’>> $log
}
}
Set-NetFirewallRule -DisplayName “BlockRDPBruteForce” -RemoteAddress $current_ips

Reply
Scott November 22, 2023 - 1:53 pm

The formatting cut out a few bits in my comment. So the -File argument should point to your powershell script, you need to enter a log path on the “$log =” line, and make sure to put in your TCP port in the -LocalPort argument.

Also -WindowStyle Hidden isn’t really needed. If you set it to run whether the user is logged in or not, it hides the window. If you don’t set that, then the argument helps. But it still flickers in for a fraction of a second.

Reply
Scott January 25, 2024 - 11:47 pm

Updated version since my rule stopped adding entries after a certain point, not sure what the max is since mine was at 6400+ which it stopped working. This one creates new entries when it hits the max (1000 by default).

$badAttempts = 10
$intervalHours = 3
$ruleMaxEntries = 1000
$log = “”

$startTime = [DateTime]::Now.AddHours(-$intervalHours)
$badRDPlogons = Get-EventLog -LogName ‘Security’ -after $startTime -InstanceId 4625 | ?{$_.Message -match ‘logon type:\s+(3)\s’} | Select-Object @{n=’IpAddress’;e={$_.ReplacementStrings[-2]} }
$ipsArray = $badRDPlogons | Group-Object -property IpAddress | Where-Object {$_.Count -ge $badAttempts} | ForEach-Object {$_.Name}

if ($ipsArray.Count -eq 0) {
return
}
[System.Collections.ArrayList]$ips = @()
$ips.AddRange([string[]]$ipsArray)

$ruleCount = 1
$ruleName = “BlockRDPBruteForce” + $ruleCount
$foundRuleWithSpace = 0
[System.Collections.ArrayList]$current_ip_lists = @()

while ($foundRuleWithSpace -eq 0) {
$firewallRule = Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue
if ($null -eq $firewallRule) {
New-NetFirewallRule -DisplayName $ruleName –RemoteAddress 1.1.1.1 -Direction Inbound -Protocol TCP –LocalPort -Action Block
$firewallRule = Get-NetFirewallRule -DisplayName $ruleName
$current_ip_lists.Add(@(($firewallRule | Get-NetFirewallAddressFilter ).RemoteAddress))
$foundRuleWithSpace = 1
} else {
$current_ip_lists.Add(@(($firewallRule | Get-NetFirewallAddressFilter ).RemoteAddress))
if ($current_ip_lists[$current_ip_lists.Count – 1].Count -le ($ruleMaxEntries – $ips.Count)) {
$foundRuleWithSpace = 1
} else {
$ruleCount++
$ruleName = “BlockRDPBruteForce” + $ruleCount
}
}
}

for ($i = $ips.Count – 1; $i -ge 0; $i–) {
foreach ($current_ip_list in $current_ip_lists) {
if ($current_ip_list -contains $ips[$i]) {
$ips.RemoveAt($i)
break
}
}
}

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

$current_ip_list = $current_ip_lists[$current_ip_lists.Count – 1]
foreach ($ip in $ips) {
$current_ip_list += $ip
(Get-Date).ToString().PadRight(22) + ‘ | ‘ + $ip.PadRight(15) + ‘ | The IP address has been blocked due to ‘ + ($badRDPlogons | Where-Object {$_.IpAddress -eq $ip}).Count + ‘ failed login attempts over ‘ + $intervalHours + ‘ hours’>> $log
}
Set-NetFirewallRule -DisplayName $ruleName -RemoteAddress $current_ip_list

Reply
Dmitriy August 22, 2024 - 1:50 pm

Works like a charm! Thank you!

Reply

Leave a Comment Cancel Reply

join us telegram channel https://t.me/woshub
Join WindowsHub Telegram channel to get the latest updates!

Categories

  • Active Directory
  • Group Policies
  • Exchange Server
  • Microsoft 365
  • Azure
  • Windows 11
  • Windows 10
  • Windows Server 2022
  • Windows Server 2019
  • Windows Server 2016
  • PowerShell
  • VMware
  • Hyper-V
  • Linux
  • MS Office

Recent Posts

  • Cannot Install Network Adapter Drivers on Windows Server

    April 29, 2025
  • Change BIOS from Legacy to UEFI without Reinstalling Windows

    April 21, 2025
  • How to Prefer IPv4 over IPv6 in Windows Networks

    April 9, 2025
  • Load Drivers from WinPE or Recovery CMD

    March 26, 2025
  • How to Block Common (Weak) Passwords in Active Directory

    March 25, 2025
  • Fix: The referenced assembly could not be found error (0x80073701) on Windows

    March 17, 2025
  • Exclude a Specific User or Computer from Group Policy

    March 12, 2025
  • AD Domain Join: Computer Account Re-use Blocked

    March 11, 2025
  • How to Write Logs to the Windows Event Viewer from PowerShell/CMD

    March 3, 2025
  • How to Hide (Block) a Specific Windows Update

    February 25, 2025

Follow us

  • Facebook
  • Twitter
  • Telegram
Popular Posts
  • How to Download Offline Installer (APPX/MSIX) for Microsoft Store App
  • Get-ADUser: Find Active Directory User Info with PowerShell
  • How to Hide Installed Programs in Windows 10 and 11
  • Using Credential Manager on Windows: Ultimate Guide
  • Managing Printers and Drivers on Windows with PowerShell
  • PowerShell: Get Folder Size on Windows
  • Using Managed Service Accounts (MSA and gMSA) in Active Directory
Footer Logo

@2014 - 2024 - Windows OS Hub. All about operating systems for sysadmins


Back To Top