Skip to main content

How to Track Failed Login Attempts in Batch Script

Monitoring failed login attempts is a critical security task for any Windows machine exposed to a network. If an attacker is attempting a brute-force attack by guessing passwords, they will leave a trail of audit failure events in the Security log. A Batch script can scan these events programmatically, allowing you to detect a surge in failed attempts and trigger an alert before the attacker succeeds.

This guide will explain how to identify failed logons (Event ID 4625) and related security events using PowerShell from a Batch script.

Prerequisites: Audit Policy

Before any of these methods will work, Windows must be configured to record logon events. By default, some Windows editions do not audit failed logons.

To verify and enable auditing:

  1. Open secpol.msc (Local Security Policy)
  2. Navigate to Local Policies > Audit Policy
  3. Set Audit logon events to Failure (or Success, Failure for complete tracking)

Alternatively, from an elevated command prompt:

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

If auditing is not enabled, all methods in this guide will return zero results even when failed logons occur.

Method 1: Recent Failed Logon Summary

This method queries the Security log for Event ID 4625 (failed logon) within a configurable time window and displays a summary including the target username, source IP address, and failure reason.

@echo off
setlocal

set "MinutesBack=60"
set "MaxResults=20"

:: Verify admin privileges (required for Security log access)
net session >nul 2>&1
if errorlevel 1 (
echo [ERROR] Reading the Security log requires administrator privileges. >&2
echo Right-click and select "Run as administrator." >&2
endlocal
exit /b 1
)

echo [INFO] Searching for failed logons in the last %MinutesBack% minutes...
echo --------------------------------------------------

powershell -NoProfile -Command ^
"$events = Get-WinEvent -FilterHashtable @{" ^
" LogName='Security';" ^
" Id=4625;" ^
" StartTime=(Get-Date).AddMinutes(-%MinutesBack%)" ^
"} -MaxEvents %MaxResults% -ErrorAction SilentlyContinue;" ^
"if (-not $events) {" ^
" Write-Host 'No failed logon attempts found in the last %MinutesBack% minutes.';" ^
" Write-Host 'This is normal if no invalid logons have occurred.';" ^
" exit 0" ^
"};" ^
"Write-Host \"Found $($events.Count) failed logon attempt(s):`n\";" ^
"$events | ForEach-Object {" ^
" $xml = [xml]$_.ToXml();" ^
" $data = $xml.Event.EventData.Data;" ^
" $user = ($data | Where-Object Name -eq 'TargetUserName').'#text';" ^
" $domain = ($data | Where-Object Name -eq 'TargetDomainName').'#text';" ^
" $ip = ($data | Where-Object Name -eq 'IpAddress').'#text';" ^
" $reason = ($data | Where-Object Name -eq 'FailureReason').'#text';" ^
" $logonType = ($data | Where-Object Name -eq 'LogonType').'#text';" ^
" [PSCustomObject]@{" ^
" Time = $_.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss');" ^
" User = if ($domain) { \"$domain\\$user\" } else { $user };" ^
" SourceIP = if ($ip -and $ip -ne '-') { $ip } else { 'Local' };" ^
" LogonType = switch ($logonType) {" ^
" '2' { 'Interactive' }" ^
" '3' { 'Network' }" ^
" '10' { 'RemoteDesktop' }" ^
" default { $logonType }" ^
" }" ^
" }" ^
"} | Format-Table -AutoSize -Wrap"

echo --------------------------------------------------

endlocal
exit /b 0

Example of output:

Found 5 failed logon attempt(s):

Time User SourceIP LogonType
---- ---- -------- ---------
2024-05-10 14:28:33 WORKSTATION\admin 192.168.1.105 RemoteDesktop
2024-05-10 14:28:30 WORKSTATION\admin 192.168.1.105 RemoteDesktop
2024-05-10 14:15:12 WORKSTATION\admin 192.168.1.105 RemoteDesktop
2024-05-10 13:42:08 .\administrator 10.0.0.50 Network
2024-05-10 13:42:05 .\administrator 10.0.0.50 Network

Understanding Logon Types:

TypeNameWhat It Means
2InteractivePhysical keyboard/console logon
3NetworkFile share access, network resource
10RemoteInteractiveRemote Desktop (RDP)
8NetworkCleartextIIS basic authentication

Type 3 and Type 10 failures from external IP addresses are the most indicative of brute-force attacks.

Method 2: Brute-Force Detection (Count-Based Alert)

This method counts failed logon attempts within a time window and triggers an alert if the count exceeds a threshold: this is the primary indicator of a brute-force attack.

@echo off
setlocal

set "MinutesBack=10"
set "Threshold=10"
set "LogFile=%~dp0security_alerts.log"

:: Verify admin privileges
net session >nul 2>&1
if errorlevel 1 (
echo [ERROR] Administrator privileges required. >&2
endlocal
exit /b 1
)

echo [INFO] Checking for brute-force activity in the last %MinutesBack% minutes...

:: Initialize variables to ensure they are empty if the PowerShell command fails
set "FailCount="
set "TopSource="

:: Use a FOR loop to capture multi-line output from PowerShell.
:: The first line is the count, the second is the top IP.
for /f "delims=" %%i in (
'powershell -NoProfile -Command ^
"$events = Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625; StartTime=(Get-Date).AddMinutes(-%MinutesBack%)} -ErrorAction SilentlyContinue;" ^
"if ($events) {" ^
" $count = $events.Count;" ^
" $topIP = $events | ForEach-Object { ([xml]$_.ToXml()).Event.EventData.Data | Where-Object {$_.Name -eq 'IpAddress'} | Select-Object -ExpandProperty '#text' } |" ^
" Where-Object { $_ -and $_ -ne '-' } |" ^
" Group-Object | Sort-Object Count -Descending | Select-Object -First 1 -ExpandProperty Name;" ^
" if (-not $topIP) { $topIP = 'N/A' };" ^
" Write-Output $count;" ^
" Write-Output $topIP;" ^
"} else {" ^
" Write-Output 0;" ^
" Write-Output 'N/A';" ^
"}"'
) do (
:: The first line of output goes into FailCount
if not defined FailCount (
set "FailCount=%%i"
) else (
:: The second line of output goes into TopSource
set "TopSource=%%i"
)
)

:: Check if we got a valid count from PowerShell
if not defined FailCount (
echo [ERROR] Failed to retrieve event count from PowerShell. >&2
endlocal
exit /b 1
)

echo [INFO] Failed logons in last %MinutesBack% minutes: %FailCount%

if %FailCount% gtr %Threshold% (
echo [CRITICAL] Possible brute-force attack detected! >&2
echo [CRITICAL] %FailCount% failures in %MinutesBack% minutes (threshold: %Threshold%^) >&2
echo [CRITICAL] Top source IP: %TopSource% >&2

:: Log the alert
echo [%date% %time%] ALERT: %FailCount% failed logons in %MinutesBack%min. Top source: %TopSource% >> "%LogFile%"

:: Write to Event Log for monitoring tool visibility
eventcreate /T WARNING /ID 800 /L APPLICATION /SO "SecurityMonitor" ^
/D "Brute-force alert: %FailCount% failed logons in %MinutesBack% minutes from %TopSource%" >nul 2>&1

endlocal
exit /b 1
) else (
echo [OK] Failed logon count is within normal range.
)

endlocal
exit /b 0

Why the "top source IP" is extracted:

Knowing the count alone is not enough for response. The IP address tells you where the attack is coming from, enabling targeted action - blocking that IP in the firewall, investigating whether it belongs to an internal system (possible compromised machine) or an external attacker.

Scheduling for continuous monitoring:

Run this script as a scheduled task every 5–10 minutes. Use the exit code (0 = normal, 1 = alert) to trigger Task Scheduler's "on failure" actions, or have a parent monitoring script react to the alert.

Method 3: Failed Logon Report by Source IP

For incident investigation, you need to see which IP addresses are generating the most failures, not just the most recent events.

@echo off
setlocal

set "HoursBack=24"
set "ReportFile=%~dp0failed_logons_%date:~-4%%date:~4,2%%date:~7,2%.csv"

net session >nul 2>&1
if errorlevel 1 (
echo [ERROR] Administrator privileges required. >&2
endlocal
exit /b 1
)

echo [INFO] Generating failed logon report for the last %HoursBack% hours...

powershell -NoProfile -Command ^
"$events = Get-WinEvent -FilterHashtable @{" ^
" LogName='Security';" ^
" Id=4625;" ^
" StartTime=(Get-Date).AddHours(-%HoursBack%)" ^
"} -ErrorAction SilentlyContinue;" ^
"if (-not $events) {" ^
" Write-Host 'No failed logons in the last %HoursBack% hours.';" ^
" exit 0" ^
"};" ^
"Write-Host \"Total failed logons: $($events.Count)`n\";" ^
"$parsed = $events | ForEach-Object {" ^
" $xml = [xml]$_.ToXml();" ^
" $data = $xml.Event.EventData.Data;" ^
" [PSCustomObject]@{" ^
" Time = $_.TimeCreated;" ^
" User = ($data | Where-Object Name -eq 'TargetUserName').'#text';" ^
" IP = ($data | Where-Object Name -eq 'IpAddress').'#text';" ^
" LogonType = ($data | Where-Object Name -eq 'LogonType').'#text'" ^
" }" ^
"};" ^
"Write-Host '--- Top Source IPs ---';" ^
"$parsed | Where-Object { $_.IP -and $_.IP -ne '-' } |" ^
" Group-Object IP |" ^
" Sort-Object Count -Descending |" ^
" Select-Object @{N='SourceIP';E={$_.Name}}, Count, @{N='LastSeen';E={($_.Group | Sort-Object Time -Descending | Select-Object -First 1).Time.ToString('yyyy-MM-dd HH:mm:ss')}} |" ^
" Format-Table -AutoSize;" ^
"Write-Host '--- Top Targeted Users ---';" ^
"$parsed | Group-Object User |" ^
" Sort-Object Count -Descending |" ^
" Select-Object @{N='Username';E={$_.Name}}, Count |" ^
" Select-Object -First 10 |" ^
" Format-Table -AutoSize;" ^
"$parsed | Select-Object" ^
" @{N='Timestamp';E={$_.Time.ToString('yyyy-MM-dd HH:mm:ss')}}," ^
" User, IP, LogonType |" ^
" Export-Csv -Path '%ReportFile%' -NoTypeInformation;" ^
"Write-Host \"Full report saved: %ReportFile%\""

endlocal
exit /b 0

Sample console output:

Total failed logons: 47

--- Top Source IPs ---
SourceIP Count LastSeen
-------- ----- --------
192.168.1.105 28 2024-05-10 14:28:33
10.0.0.50 15 2024-05-10 13:42:08
- 4 2024-05-10 09:15:22

--- Top Targeted Users ---
Username Count
-------- -----
administrator 31
admin 12
service_account 4

Full report saved: C:\Scripts\failed_logons_20240510.csv

What to look for:

  • One IP with many failures: Classic brute-force attack from a single source. Block the IP.
  • Many IPs with similar failure counts: Distributed brute-force (password spray). May require broader network controls.
  • "administrator" as the most targeted user: Default account names are always the first target. Consider renaming the built-in Administrator account.
  • Source IP = "-" or "Local": Failed logon at the physical console or via a local service. Investigate whether a service has incorrect credentials.

How to Avoid Common Errors

Problem: Access Denied When Querying the Security Log

The Security log requires administrator privileges for both reading and writing. A standard user will receive "Access Denied" or zero results.

Solution: All methods in this guide check for admin rights at startup:

net session >nul 2>&1
if errorlevel 1 (
echo [ERROR] Administrator privileges required. >&2
exit /b 1
)

Problem: Audit Policy Not Enabled

If the methods return zero results even after known failed logon attempts, the Windows audit policy may not be configured to record logon failures.

Solution: Verify and enable logon auditing:

:: Check current audit policy
auditpol /get /category:"Logon/Logoff"

:: Enable failure auditing
auditpol /set /category:"Logon/Logoff" /failure:enable

Wrong Way: Extracting Event Fields by Numeric Index

# FRAGILE: index positions vary by logon type and Windows version
$user = $event.Properties[5].Value
$ip = $event.Properties[19].Value

Event 4625 has different numbers of properties depending on the logon type. On some Windows versions, the IP address is at index 19; on others, it is at index 18 or 20.

Correct Way: Parse the event as XML and select fields by name:

$xml = [xml]$event.ToXml()
$data = $xml.Event.EventData.Data
$user = ($data | Where-Object Name -eq 'TargetUserName').'#text'
$ip = ($data | Where-Object Name -eq 'IpAddress').'#text'

Wrong Way: Searching the Application or System Log

Security events (logon success/failure, privilege changes, account modifications) are only recorded in the Security log. Querying Application or System will always return zero results for these event types.

Problem: Get-WinEvent Errors When No Events Match

When no events match the filter, Get-WinEvent throws a "No events were found" error rather than returning an empty result.

Solution: Always include -ErrorAction SilentlyContinue and check whether the result is null before processing.

Best Practices and Rules

1. Monitor Event ID 4625 AND 4740

Event IDMeaning
4625Failed logon attempt
4740Account locked out
4624Successful logon (monitor after brute-force to detect compromise)
4776Credential validation (NTLM authentication)

Monitoring 4625 alone misses the bigger picture. If an account gets locked out (4740), followed by a successful logon (4624) after the lockout period, the attacker may have guessed the password.

2. Schedule Regular Scans

Run Method 2 as a scheduled task every 5–10 minutes. Brute-force attacks can succeed within minutes on accounts with weak passwords. Hourly scans are too infrequent to provide meaningful protection.

3. Log Alerts Persistently

Console output disappears when the window closes. Always write alerts to both a text log file and the Windows Event Log (using eventcreate). Enterprise monitoring tools (SCOM, Splunk, Zabbix) watch the Event Log and can trigger automated responses.

4. Identify Internal vs. External Sources

Failed logons from internal IP addresses (192.168.x.x, 10.x.x.x) may indicate a compromised machine or a service with stale credentials, not necessarily an attack. Failed logons from external or unknown IPs are more concerning.

5. Consider Windows Built-In Protections

For production security, complement these scripts with:

  • Account lockout policies (secpol.msc > Account Policies > Account Lockout Policy)
  • Windows Firewall rules blocking repeated offenders
  • Network-level authentication (NLA) for RDP
  • Windows Defender Credential Guard on supported systems

Batch scripts provide visibility and alerting, but they are not a substitute for proper security controls.

Conclusions

Tracking failed login attempts is one of the most proactive security measures you can implement for your Windows servers. By parsing Event ID 4625 with XML-based field extraction, counting failures within time windows, and aggregating by source IP, you gain the ability to spot brute-force attacks in progress and react before a password is guessed. Combined with proper audit policies and scheduled monitoring, this automated vigilance is a foundational component of Windows security administration.