How to Track Important Windows Security Events with PowerShell

Published:21 April 2021 - 14 min. read

Justin Sylvester Image

Justin Sylvester

Read more tutorials by Justin Sylvester!

Azure Cloud Labs: these FREE, on‑demand Azure Cloud Labs will get you into a real‑world environment and account, walking you through step‑by‑step how to best protect, secure, and recover Azure data.


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.

Application, Security, and System audit log location
Application, Security, and System audit log location

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).

Audit policy category, subcategory, and configuration
Audit policy category, subcategory, and configuration

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:

#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.

Authentication failure due to incorrect user name or password.
Authentication failure due to incorrect user name or password.

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:

10 instances of Event ID 4625
10 instances of Event ID 4625

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 property
  • inType – The input type definition or how the event accepts a value
  • outputType – 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
Get Win Event List Provider
Get Win Event List Provider

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.

XML template example
XML template example

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
Get-WinEvent FilterHashtable
Get-WinEvent FilterHashtable

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.

Powershell hiding Properties
Powershell hiding Properties

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).

Expected output correlating property values to Event ID 4625's XML template.
Expected output correlating property values to Event ID 4625’s XML template.

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
Event attribute property positions
Event attribute property positions

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.

A truncated version of the code's output, detailing TargetUserName, LogonType, WorkstationName, and IpAddress property values.
A truncated version of the code’s output, detailing TargetUserName, LogonType, WorkstationName, and IpAddress property values.

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.

LogonType attribute
LogonType attribute

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-Objectcmdlet. 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!

AtaBlogUser Logon Failures
AtaBlogUser Logon Failures

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?

Hate ads? Want to support the writer? Get many of our tutorials packaged as an ATA Guidebook.

Explore ATA Guidebooks

Looks like you're offline!