Many organizations rely on Microsoft technologies to get work done. At the same time, threat actors can exploit operating systems like Windows. Luckily, Windows logs OS security events to help you track down this behavior.
Security events produced by Windows serve as a critical resource in the incident response process. Tools such as Microsoft’s Windows Event Viewer provide you with the access necessary to review captured events, but detecting abnormalities by manually scrolling through a crowded log is unrealistic.
In this post, you will learn how to track down potential security breaches in Windows by learning about audit policies, Windows event logs, and analyzing security events with PowerShell.
Prerequisites
This article is meant to convey information that teaches you how to analyze Windows security events with PowerShell. If you’d like to follow along with any of the demonstrations, you will need:
- A Windows 10+ PC – This PC will be used to generate and track down potential security events in the event log. This tutorial will be using Windows PowerShell 5.1.
- Administrator rights on the Windows PC
- A PowerShell code editor such PowerShell ISE or Visual Studio (VS) Code.
Where Windows Stores Security Events
When an action is taken on a Windows operating system, Windows logs the action as an event in one or more event logs. Windows event logs are stored on the file system, by default, in the %SystemRoot%\system32\winevt\logs directory. This location can be changed by modifying the respective event log’s EventLog registry subkey.
If you’d like to see where the most prominent event logs are stored (Application, Security, and System) on your system, copy and paste the below code into a PowerShell console or save it as a script.
To access the storage location of the Security log file, you need to run the code as an Administrator.
#Present application, security, and system logs in an array.
$arrLogs = @(
"Application"
"Security"
"System"
)
#Use the ForEach-Object cmdlet to target each respective log with the Get-ItemProperty cmdlet.
$arrLogs | ForEach-Object {
#Use Get-ItemProperty cmdlet to list the configured file path for the application, security, and system log.
Get-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\EventLog\$_ -Name File | Select-Object PSChildName,File
}
The following screenshot shows the code’s expected output, displaying the log name and storage location for the Application, Security, and System log files.
Audit Policies: Defining Events to Record
By default, Windows doesn’t capture all of the security events that might be needed to detect or investigate a breach. To control what Windows does and does not record, you must define and apply audit policies. An audit policy is a set of instructions passed to Windows that tells it what events to record.
There are a few different ways to assign and work with audit policies, such as Group Policy. Group Policy works well if you must implement audit policies across many machines. But in this article, you’re going to stick to a single device, so you’ll use the auditpol tool. The auditpol tool comes installed with Windows and allows you to find and set audit policies on a Windows system.
Finding Audit Policies
For example, to find the status of all audit policies on your Windows system, use the /get
parameter as shown below. Using the /category
parameter followed by a wildcard tells auditpol to find the status of all audit policies; not just one matching a specific category or subcategory.
#Obtain the system's audit policy configuration.
auditpol /get /category:*
The following screenshot shows a truncated version of the code’s expected output, displaying the Account Management audit policy category, subcategories, and status (Setting).
A Setting that is configured as No Auditing means that all events associated with that audit policy subcategory will not be logged.
Setting Audit Policies
The auditpol tool can do more than view audit policy settings. It can also modify them using the auditpol /set
command. To demonstrate future sections in this tutorial, open a PowerShell console as administrator and run the below command. This command begins logging all events (success and failure) that are a part of the Logon subcategory.
Configuring the Logon subcategory forces your system to record events:
- 4624: An account was successfully logged on
- 4625: An account failed to log on
- 4626: User/Device claims information
- 4648: A logon was attempted using explicit credentials
- 4675: SIDs were filtered
#Set Logon Events to capture Success/Failure activity.
auditpol /set /subcategory:"Logon" /success:enable /failure:enable
There are numerous resources available to assist you with best-practice audit policy configuration, including the Center for Internet Security (CIS) Benchmarks, and Defense Information Systems Agency (DISA) Security Technical Implementation Guides (STIG), and guidance published by Microsoft.
Generating Logon Failure Logs for Analysis
This article will be a tutorial, and will expect you to follow along. If you’ve configured Windows to audit Logon events above, let’s now generate some security events for analysis later. More specifically, let’s generate 35 failed logon attempts which will be recorded in your system’s security log to mimic brute force activity.
1. Open your favorite code editor.
2. Copy the following code and paste it into the code editor. This code snippet attempts to open up the PowerShell.exe process using the Start-Process
cmdlet using bogus usernames and passwords.
#Define 5 usernames to record as logon failures.
$arrUsers = @(
"AtaBlogUser1"
"AtaBlogUser2"
"AtaBlogUser3"
"AtaBlogUser4"
"AtaBlogUser5"
)
#Loop through usernames using ForEach-Object to generate a logon failure for each one.
$arrUsers | ForEach-Object {
$securePassword = ConvertTo-SecureString "AtA810GRu13Z%%" -AsPlainText -Force
$storedCredential = New-Object System.Management.Automation.PSCredential($_, $securePassword)
Start-Process -FilePath PowerShell -Credential $storedCredential
}
#Generate 30 logon failures for user AtaBlogUser.
$i = 0
Do {
$securePassword = ConvertTo-SecureString "AtA810GRu13Z%%" -AsPlainText -Force
$storedCredential = New-Object System.Management.Automation.PSCredential("AtaBlogUser", $securePassword)
Start-Process -FilePath PowerShell -Credential $storedCredential
$i++
} Until ($i -eq 30)
3. Save the PowerShell script as Invoke-BogusEvents.ps1 or whatever name you’d like and execute the script.
When executed, you’ll notice an expected error repeated 35 times indicating The user name or password is incorrect.
If you are not receiving the expected output, ensure that the Secondary Logon service is in a Running state.
Accessing Windows Events with PowerShell
Now that you’re sure to have at least 35 Windows security events, let’s dig into how to find them with PowerShell’s Get-WinEvent
cmdlet.
You may be familiar with PowerShell’s
Get-EventLog
cmdlet, which is also used to access the event log programmatically.Get-EventLog
uses a Win32 Application Programming Interface (API) that is deprecated and will not be discussed in this post.
Open a PowerShell console as an administrator and invoke the Get-WinEvent
cmdlet passing it the FilterHashtable
and MaxEvents
parameter as shown below.
The command below queries your system’s security log (LogName='Security'
) for event ID 4625 (ID=4625
) and returns the first 10 newest instances (MaxEvents 10
).
#Filter the security log for the first 10 instances of Event ID 4625
Get-WinEvent -FilterHashtable @{LogName='Security';ID=4625} -MaxEvents 10
If successful, you should see an output similar to the following:
Accessing Event Properties with Get-WinEvent
In the above section, you used Get-WinEvent
to see Windows security events at a high level, but a Windows event contains so much more information. Each Windows event has valuable properties that you can use for deeper analysis.
Windows Events as XML
When Windows records an event, it is stored in XML format. If that’s the case, then why did your Get-WinEvent
command return typical PowerShell objects? The Get-WinEvent
cmdlet reads the native Windows API and translates the events into PowerShell objects for increased functionality.
Each Windows event has various attributes that follow a specific XML schema or structure.
You’ll see below that each event follows a specific structure with three attributes:
name
– The name of the propertyinType
– The input type definition or how the event accepts a valueoutputType
– The output type definition or how the event is recorded
Finding Event XML Templates with PowerShell
As mentioned above, every Windows security event is stored in XML and has a specific schema, but what does that schema look like? Let’s find out.
In one of the previous sections, you generated a few events with ID 4625 in the security event log. This type of event has specific attributes that only apply to it. To find those attributes and what the template looks like:
1. Open a PowerShell console as an administrator if you don’t already have it open.
2. Run Get-WinEvent
again, but this time use the ListProvider
parameter specifying the provider Windows uses to record events to the security event log and only return the Events
property.
The Events
property contains all events that the list provider has recorded and exposes the XML template for each of those events.
(Get-WinEvent -ListProvider 'Microsoft-Windows-Security-Auditing').Events
3. Now that you have the code to find templates for all of the event types, narrow that down by only returning the event associated with ID 4625.
(Get-WinEvent -ListProvider 'Microsoft-Windows-Security-Auditing').Events | Where-Object -Property ID -eq 4625
4. Once you’re returning only the Logon event type with event ID 4625, limit that to only show the Template
property like below.
#Obtain event XML template for event properties of Event ID 4625.
((Get-WinEvent -ListProvider 'Microsoft-Windows-Security-Auditing').Events | Where-Object -Property ID -eq 4625).Template
The following screenshot shows a truncated version of the code’s output, identifying the event property name, input type, and output type. You can see that event ID 4625 has event properties with various input and output definitions.
The screenshot below highlights the SubjectUserSid
property of Event ID 4625. This particular event accepts an input type (inType
) of win:SID
and renders the output (outType
) as a string
which is how it is stored within the security log.
How PowerShell Translates XML to Objects
Now that you’ve seen how Windows stores events in XML and how to see those templates in PowerShell, let’s turn to how PowerShell translates that XML into objects.
1. Run the Get-WinEvent
command again to return our event ID 4625. Up until now, this is nothing new. Notice that PowerShell only shows four properties, TimeCreated
, Id
, LevelDisplayName
, and Message
.
Get-WinEvent -FilterHashtable @{LogName='Security';ID=4625} -MaxEvents 1
By default, the Get-WinEvent
cmdlet doesn’t return all attributes from the event’s XML data source as a PowerShell object.
2. Now, pipe the output of the above command to the Select-Object
cmdlet and specify the Property
parameter passing a value of to show all properties.
Get-WinEvent -FilterHashtable @{LogName='Security';ID=4625} -MaxEvents 1 | Select-Object -Property *
Notice below that PowerShell was hiding many different properties. More specifically, a property called Properties
. The Properties
property contains the value of each event attribute that you saw earlier in the XML template.
3. Limit the output of the Get-WinEvent
command above to expose the Properties
property. This property stores all event properties, not PowerShell object properties, in an array.
#Output event properties array for the first instance of Event ID 4625
$eventProperties = (Get-WinEvent -FilterHashtable @{LogName='Security';ID=4625} -MaxEvents 1).properties
$eventProperties
On the left of the screenshot below is the output from the above command. The array contains the values for each of the XML attributes in the XML template on the right side of the screenshot.
The code’s output shown in the screenshot communicates that an authentication failure occurred for user AtaBlogUser
(TargetUserName
) from system Desktop-XXXXX
(WorkstationName
) using an IP Address of ::1
(IpAddress
).
Perhaps you’d like only to return the value for the TargetUserName
event property. Since you’ve already stored all event properties in a variable called $eventProperties
, reference the fifth index, which holds the value for TargetUserName
.
You must reference the value
property on the individual event property object to only return the value (AtaBlogUser
). $eventProperties[5].value
$eventProperties[5].value
The practices described throughout this section will be used in subsequent sections to track down the brute force attempt you simulated earlier in this post.
Detecting a Brute Force Attack
You are now prepared to use your PowerShell skills to track down the brute force attack you replicated earlier in this post! Let’s put your skills to the test by simulating what it may look like to track down a brute force attack based on a specific timeframe.
Let’s say you were alerted to an incident where your organization believes someone is trying to use an administrative account to log onto an important Windows Server. This activity started yesterday. You must discover the number of event ID 4625: An account failed to log on that occurred over the last 24 hours and determine each event’s logon type.
1. Find all events with ID 4625 (ID=4625
) in the Windows security log (LogName="Security"
) for the last 24 hours (StartTime=((Get-Date).AddDays(-1).Date
), ending at the current time (Get-Date
).
$events = Get-WinEvent -FilterHashTable @{LogName="Security";ID=4625;StartTime=((Get-Date).AddDays(-1));EndTime=(Get-Date)}
2. Now, count all events stored in the variable to determine if there are more failed log on events than expected.
$events.Count
You should now see a numerical value indicating the number of times event ID 4625 was found in the security event log for the last 24 hours.
3. So you’ve determined a brute force attack has occurred, now track down more information about these Windows security events. To do so, only return the attributes from each of the events you’re interested in.
As mentioned earlier, each value for a particular event is stored in an array with a specific index. The interesting event properties for this demo are below.
- TargetUserName Index:
[5]
- LogonType Index:
[10]
- WorkstationName Index:
[13]
- IpAddress Index:
[19]
The below code sample reads each object in the $events variable, gathers only the interesting properties, and concatenates them into a single line.
#Extract TargetUserName, LogonType, WorkstationName, and IpAddress event properties from all instances of Event ID 4625 in the last 24 hours.
$events | ForEach-Object {
## Reference the properties object property
## Only return the value of indexes 5,10,13 and 19 from the properties array
## Concatenate all values together by joining them with a comma
$_.properties[5,10,13,19].value -join ", "
}
The following screenshot shows a truncated version of the code’s expected output, detailing a comma-separated list of TargetUserName, LogonType, WorkstationName, and IpAddress.
4. As you saw from the XML template earlier, event ID 4625’s template has a LogonType
attribute. This attribute indicates the method in which the account attempted to authenticate. Through some further investigation, you noticed that the LogonType
was different on occasion.
The LogonType
value is a numerical value from 2-11, but what does that mean? You perform some research and discover what each value means.
2 – Interactive – A user logged on to this computer.
3 – Network – A user or computer logged on to this computer from the network.
4 – Batch – Batch logon type is used by batch servers, where processes may be executing on behalf of a user without their direct intervention.
5 – Service – A service was started by the Service Control Manager.
7 – Unlock – This workstation was unlocked.
8 – NetworkCleartext – A user logged on to this computer from the network. The user’s password was passed to the authentication package in its unhashed form. The built-in authentication packages all hash credentials before sending them across the network. The credentials do not traverse the network in plaintext (also called cleartext).
9 – NewCredentials – A caller cloned its current token and specified new credentials for outbound connections. The new logon session has the same local identity, but uses different credentials for other network connections.
10 – RemoteInteractive – A caller cloned its current token and specified new credentials for outbound connections. The new logon session has the same local identity, but uses different credentials for other network connections.
11 – CachedInteractive – A user logged on to this computer with network credentials that were stored locally on the computer. The domain controller was not contacted to verify the credentials.
Now that you have a good understanding of each LogonType
, rather than seeing a numerical value in the output, you want a more descriptive string. To create “maps” in PowerShell, use a hashtable.
$logonTypes = @{
[uint32]2 = "Interactive"
[uint32]3 = "Network"
[uint32]4 = "Batch"
[uint32]5 = "Service"
[uint32]7 = "Unlock"
[uint32]8 = "NetworkCleartext"
[uint32]9 = "NewCredentials"
[uint32]10 = "RemoteInteractive"
[uint32]11 = "CachedInteractive"
}
5. Combine Get-WinEvent
and the LogonType
hashtable with ForEach-Object
to create a script that will only return the properties you desire with a user-friendly LogonType
value, as shown below. The Format-Table
cmdlet adds to the user-friendly output by formatting PowerShell’s response as a table.
#Use Get-WinEvent to access the properties of each logged instance of Event ID 4625
$events = Get-WinEvent -FilterHashTable @{LogName="Security";ID=4625;StartTime=((Get-Date).AddDays(-1).Date);EndTime=(Get-Date)}
## Create the numerical value to string "map"
$logonTypes = @{
[uint32]2 = "Interactive"
[uint32]3 = "Network"
[uint32]4 = "Batch"
[uint32]5 = "Service"
[uint32]7 = "Unlock"
[uint32]8 = "NetworkCleartext"
[uint32]9 = "NewCredentials"
[uint32]10 = "RemoteInteractive"
[uint32]11 = "CachedInteractive"
}
## Begin processing each object in the $events array
$events | ForEach-Object {
## Look up the numerical value in the hashtable
$logonType = $logonTypes[$_.properties[10].value]
#Create custom PowerShell object to output relevant event properties
[PSCustomObject]@{
TimeCreated = $_.TimeCreated
TargetUserName = $_.properties[5].value
LogonType = $logonType
WorkstationName = $_.properties[13].value
IpAddress = $_.properties[19].value
}
} | Format-Table -Wrap
At this point, you now have a script that returns PSCustomObject type objects allowing you to perform many different types of analysis! To finalize this tutorial’s analysis, prioritize the authentication failure attempts by TargetUserName
. To prioritize the failures by the TargetUserName
property, combine the above code with the Group-Object
cmdlet. Use Sort-Object
and its Descending
switch to identify the highest offending user.
#Use Get-WinEvent to access the properties of each logged instance of Event ID 4625
$events = Get-WinEvent -FilterHashTable @{LogName="Security";ID=4625;StartTime=((Get-Date).AddDays(-1).Date);EndTime=(Get-Date)}
## Create the numerical value to string "map"
$logonTypes = @{
[uint32]2 = "Interactive"
[uint32]3 = "Network"
[uint32]4 = "Batch"
[uint32]5 = "Service"
[uint32]7 = "Unlock"
[uint32]8 = "NetworkCleartext"
[uint32]9 = "NewCredentials"
[uint32]10 = "RemoteInteractive"
[uint32]11 = "CachedInteractive"
}
## Begin processing each object in the $events array
$events | ForEach-Object {
## Look up the numerical value in the hashtable
$logonType = $logonTypes[$_.properties[10].value]
#Create custom PowerShell object to output relevant event properties
[PSCustomObject]@{
TimeCreated = $_.TimeCreated
TargetUserName = $_.properties[5].value
LogonType = $logonType
WorkstationName = $_.properties[13].value
IpAddress = $_.properties[19].value
}
} | Group-Object -Property TargetUserName | Sort-Object -Property Count -Descending
Great work! You just used PowerShell to detect the brute force attempt that you simulated earlier in this post. According to the output, AtaBlogUser failed to authenticate 30 times in the last 24 hours!
Next Steps
In this tutorial, you learned how Windows logs events, how to enable event logging for certain event types, and how to build a PowerShell tool to query these events.
With the PowerShell script you now have, how can you make it better? How will you take the code you’ve learned about today and build a better tool?