Research and Development

When attempting to gain a foothold into a Windows Domain, an attacker will often attempt one or two likely passwords against every user in the Active Directory, a so-called horizontal password guessing attack. A small number of failed logons per user will usually not trigger a user account lockout policy and can be very effective. This post will provide an example solution to detecting such attacks in near real time, using only native Windows tools.

Even with password complexity requirements and custom filters there is no built-in way to stop users choosing poor passwords. It is scary how may user accounts are identified with the password Password1 for example. We need a method of detecting password guessing attacks, preferably before someone takes control of the Domain.

By following the instructions below you can get hourly (can be trivially customised) notifications of such horizontal password guessing attacks.

Note: The following method has been developed using Windows 2012.

Configuring the Domain Controller

First we need to configure the Active Directory Domain Controller to log failed logon attempts:

  • From the Server Manager tool click Tools and select Group Policy Management, as shown in the screenshot below:
    Server Manager..Tools..Group Policy Management
    image-3915

    Figure 1. Opening the Group Policy Management tool

  • Expand the nodes in the left hand pane so you can see the policy Default Domain Controllers Policy for Domain Controllers within the Domain. Right-click it and select Edit, as shown below:
    Expand ' class=
    image-3916

    Figure 2. Edit the ‘Default Domain Controllers Policy’

  • In the Group Policy Management Editor expand “Computer Configuration > Policies > Windows Settings > Security Settings > Local Policies” and then click “Audit Policy”
  • Right-click “Audit account logon events” and select “Properties”, as shown below:
    Expand ' class=
    image-3917

    Figure 3. Editing the policy setting for ‘Audit account logon events’

  • Ensure that both “Define these policy settings” and “Failure” are enabled then click “OK”. The following screenshot shows both “Success” and “Failure” are selected:
    Tick ' class=
    image-3918

    Figure 4. Enabling failure events for the ‘Audit account logon events’ properties

  • When you click “OK” the updated policy settings will be visible. Next we force the server to recognise the updated policy settings by running the command gpupdate /force by pressing Windows key + r, as shown below:
    Run ' class=
    image-3919

    Figure 5. Forcing the updated policy to take effect

  • To test that the policy has taken affect we make a failed logon attempts (from another system). Note the IP address of the machine used in the screenshot below:
    Run ' class=
    image-3920

    Figure 6. Triggering a failed logon event with a “net use” command

  • By viewing the Event Viewer on the Domain Controller we can see in the following screenshot that failed logon attempts now generate Audit Failure events (in this case EventID 4771) and that the IPAddress shown matches the host from which the logon attempt was made:
    View the Domain event log to verify an ' class=
    image-3921

    Figure 7. Confirming a failed logon generates an event

Ok. That’s the Domain Controller logging configured. Now we need a method of parsing the event log to detect failed logon attempts…

Parsing the event logs

PowerShell has a cmdlet called get-WinEvent that allows us to filter out all events with a specific EventId within a given timespan.

$events = get-winevent -EA silentlycontinue -filterhashtable @{LogName='Security';id=4771; `
starttime=(Get-Date).AddHours(-1);endtime=(Get-Date) }

Note: The backtick at the end of the first line is PowerShell’s multiline indicator and is required.

By running the above PowerShell command we get all events from the Security log with an ID value of 4771 from the past hour. If we wanted to change the timespan we could replace AddHours(-1) with AddMinutes(-30) for the last 30 minutes, or AddDays(-1) for the last 24 hours. Those events will be accessed via the $events variable.

If we want to check additional EventIds we simply add extra calls to get-WinEvent like so:

$events += get-winevent -EA silentlycontinue -filterhashtable @{LogName='Security';id=1234; `
starttime=(Get-Date).AddHours(-1);endtime=(Get-Date) }

Note the use of += to append the extra events.

We specify the parameter -EA silentlycontinue to avoid error messages if there are no events returned.

Of course some of those events might well be innocent users who mis-typed their password. Someone performing a horizontal password guessing attack against Active Directory users will be running that attack from a single host on the network (E.g. an IP address). Or several hosts might be being used, each testing a sub-set of user accounts and/or passwords. We want to identify any IP address that failed to logon more than a specified number of times within our timespan.

In order to obtain information from the event entry message we need to convert the event to XML so that we can parse it. We will make a note of each IP address that generated the failed logon event by looping through each event (remember we filtered only those events we are interested in) and increment a counter specific for each unique IP address we encounter.

Once we have counted each failed logon attempt originating from all the source IPs referenced in the log event we simply report on any IP where the counted value exceeds our specified threshold value by sending an email alert.

The complete script

The following PowerShell script implements the complete process:

# Script: detect-horizontal-user-brute-froce-attack.ps1
# Author: Richard Hatch, 2014 - RGH@Portcullis-Security.com

# Number of failed logons within the defined time span
# Total events -gt this value will trick the response action
$threshold = 4

#The email server to use when sending alerts
$EmailServer = "127.0.0.1"

#The email address to send from
$mail_domainSrc = "alerts@mydomain.com"

#clean-up any data from a previous run
Remove-Variable events

#get the events from the local system. RUN ON DC (or central log server)
$events = get-winevent -EA silentlycontinue -filterhashtable @{LogName='Security';id=4771; `
starttime=(Get-Date).AddHours(-1);endtime=(Get-Date)  }

# Copy the following line for each required ID value (and update the ID param!)

# $events += get-winevent -EA silentlycontinue -filterhashtable @{LogName='Security'; `
#id=4771;starttime=(Get-Date).AddHours(-1);endtime=(Get-Date) }

#declare our data hash
$scares = @{}

#process each event
ForEach ($evt in $events)
{
	#convert the event data to xml so we can get items
	$evtxml =
1
$evt.ToXML() $curTargetUserName = "" $curServiceName = "" $curIPAddress = "" $curEventTime = $evtxml.Event.System.TimeCreated.SystemTime #need to loop through each 'data' properties for ($i=0; $i -lt $evtxml.Event.EventData.childNodes.count; $i++) { if ($evtxml.Event.EventData.Data[$i].name -eq "IpAddress") { $curIPAddress = $evtxml.Event.EventData.Data[$i].'#text' } } #end for ($i=0; $i -lt $evtxml.Event.EventData.count; $i++) #If we have never seen this IP failing to logon create a new entry # #Without this check the code attempts to repeatedly create the #same hash entry, causing an error. if (!($scares.ContainsKey($curIPAddress))) { $scares.Add($curIPAddress, 0); #init to 0 } #count this failed logon attempt $scares["$curIPAddress"] += 1 } #end ForEach ($evt in $events) $scares.GetEnumerator() | % { if ( $($_.value) -gt $threshold ) { $attackerIP = $($_.key) $attackerFailedTimes = $($_.value) Write-Host "[!] IP: $($_.key) failed to login $($_.value) times in the last hour" #$mail_domainSrc = [string]([adsi]'').distinguishedName $mail_subject = "ALERT ${mail_domainSrc}: Host $($_.key) may be attacking ActiveDirectory user accounts" $mail_body = " ALERT! A possible brute-force password guessing attack detected within the last hour from the host with the IP $attackerIP. Failed logon count was $attackerFailedTimes $mail_domainSrc " Send-MailMessage -To ITSecurity@mydomain.com -From $mail_domainSrc ` -Subject $mail_subject -Body $mail_body -SmtpServer $EmailServer ` -Priority High } #end if ( $($_.value) -gt $threshold ) } #end $scares.GetEnumerator() | % {

The PowerShell script will diplay information to the PowerShell Console (if visible) and send an email, in this case to ITSecurity@mydomain.com from alerts@mydomain.com, using the Send-MailMessage cmdlet.

We can test the script with the following command:

c:\> powershell -File Detect-horizontal-user-brute-force-attack.ps1
[!] IP: ::ffff:10.1.2.3 failed to login 6 times in the last hour

Note: You may need to first enable external scripting within PowerShell:

PS> Set-ExecutionPolicy unrestricted

For a more secure configuration of PowerShell you can specify Signed instead of Unrestricted. More details on this can be found on Microsoft’s web site.

Running the script automatically

Now we need a method of running the script on the Domain Controller each hour. We can use the task scheduler (as an Administrator):

c:\> schtasks /create /tn AD_PwdGuess_Alerter /tr "powershell -NoLogo -WindowStyle hidden -File detect-horizontal-user-brute-force-attack.ps1 /sc hour /mo 1 /ru "NT AUTHORITY\LOCALSERVICE"

Once we create the scheduled task we need to start it:

c:\> schtasks /run /tn AD_PwdGuess_Alerter

Note: For extra security you should create a service account with the minimum privileges required to access the event log and send Emails, and specify that account in the /ru parameter, in place of NT AUTHORITY\LOCALSERVICE.

And that’s it. You may want to tweak the time period settings and the $mail_domainSr value, and the email settings will need to be updated.

This solution can also be used to cover password attacks on local user accounts through the use of Centralised Event Logging. Also see the National Security Agency’s (NSA) detailed paper on configuring centralised event logging.


Request to be added to the Portcullis Labs newsletter

We will email you whenever a new tool, or post is added to the site.

Your Name (required)

Your Email (required)