Training
Get a free hour of SANS training

Experience SANS training through course previews.

Learn More
Learning Paths
Can't find what you are looking for?

Let us help.

Contact us
Resources
Join the SANS Community

Become a member for instant access to our free resources.

Sign Up
For Organizations
Interested in developing a training plan to fit your organization’s needs?

We're here to help.

Contact Us
Talk with an expert

Month of PowerShell - Working with the Event Log, Part 3 - Accessing Message Elements

In part 3 of Working with the Event Log we look at using a third-party function to make accessing event log data much easier.

Authored byJoshua Wright
Joshua Wright

#monthofpowershell

In part 1, we looked at PowerShell get winevent to work with the event log: [code]Get-WinEvent[/code]. In part 2 we looked at 10 practical examples of using [code]Get-WinEvent[/code] to perform threat hunting using event log data, using [code]-FilterHashTable[/code], the PowerShell pipeline, and [code]-FilterXPath[/code].

In this article we'll look at using a third-party script to make the data in the [code]Get-WinEvent[/code][code]Message[/code] property much more easily accessible.

My DMs Are Open

Paul Masek (SANS Advisor Board member and threat hunting fan) reached out after my part 1 article to share a third-party script that makes working with the [code]Get-WinEvent[/code][code]Message[/code] property data much easier: Convert-EventLogRecords.

[code]Get-WinEvent[/code] is a powerful cmdlet, and once you get the hang of building filter hash tables (see part 2 of this series) you can quickly interrogate event logs for a lot of data. However, the data in the event log [code]Message[/code] property is not nearly as easily accessible.

For example, let's look at event ID 4624 in the Security event log, An account was successfully logged on:

PS C:\Windows\System32> Get-WinEvent -FilterHashtable @{ LogName="Security"; ID=4624 } -MaxEvents 1


   ProviderName: Microsoft-Windows-Security-Auditing

TimeCreated                      Id LevelDisplayName Message
-----------                      -- ---------------- -------
7/14/2022 10:58:00 AM          4624 Information      An account was successfully logged ...


PS C:\Windows\System32> Get-WinEvent -FilterHashtable @{ LogName="Security"; ID=4624 } -MaxEvents 1 | Select-Object -Property Message | Format-List


Message : An account was successfully logged on.

          Subject:
                Security ID:            S-1-5-18
                Account Name:           SEC504STUDENT$
                Account Domain:         SEC504
                Logon ID:               0x3E7

          Logon Information:
                Logon Type:             5
                Restricted Admin Mode:  -
                Virtual Account:                No
                Elevated Token:         Yes

          Impersonation Level:          Impersonation

          New Logon:
                Security ID:            S-1-5-18
                Account Name:           SYSTEM
                Account Domain:         NT AUTHORITY
                Logon ID:               0x3E7
                Linked Logon ID:                0x0
                Network Account Name:   -
                Network Account Domain: -
                Logon GUID:             {00000000-0000-0000-0000-000000000000}

          Process Information:
                Process ID:             0x290
                Process Name:           C:\Windows\System32\services.exe

          Network Information:
                Workstation Name:       -
                Source Network Address: -
                Source Port:            -

          Detailed Authentication Information:
                Logon Process:          Advapi
                Authentication Package: Negotiate
                Transited Services:     -
                Package Name (NTLM only):       -
                Key Length:             0
...

When we access the [code]Message[/code] property in the pipeline, it appears as if it were unstructured data: just a big string blob with indentations and newlines. However, this data is actually an XML structure with lots of elements that are individually accessible, just not as [code]Get-WinEvent[/code] properties.

Let's dig into the [code]Message[/code] property for the event ID 4624 event, declaring a variable [code]$logonEvent[/code]:

PS C:\Windows\System32> $logonEvent = Get-WinEvent -FilterHashtable @{ LogName="Security"; ID=4624 } -MaxEvents 1
PS C:\Windows\System32>

Next, let's convert the [code]$logonEvent[/code] variable into XML format:

PS C:\Windows\System32> $xml = $logonEvent.ToXml()
PS C:\Windows\System32>

Here we declare a new variable for ease-of-access called [code]$xml[/code]. The value stored in [code]$xml[/code] is the result of calling the [code]ToXml()[/code] method on the [code]$logonEvent[/code] variable for the 4624 event ID.

With the [code]$xml[/code] variable declared, we can start to interrogate the data and look at the available properties. You can start just by entering the variable name:

PS C:\Windows\System32> $xml

Event
-----
Event

This output indicates there is a member object called [code]Event[/code]. We can access this object using dot notation:

PS C:\Windows\System32> $xml.Event

xmlns                                                 System EventData
-----                                                 ------ ---------
http://schemas.microsoft.com/win/2004/08/events/event System EventData

This reveals that [code]$xml.Event[/code] has three member objects: [code]xmlns[/code], [code]System[/code], and [code]EventData[/code]. We can continue to interrogate the data by using dot notation to access the [code]EventData[/code] object:

PS C:\Windows\System32> $xml.Event.EventData

Data
----
{SubjectUserSid, SubjectUserName, SubjectDomainName, SubjectLogonId...}

Which reveals a [code]Data[/code] object:

PS C:\Windows\System32> $xml.Event.EventData.Data

Name                      #text
----                      -----
SubjectUserSid            S-1-5-18
SubjectUserName           SEC504STUDENT$
SubjectDomainName         SEC504
SubjectLogonId            0x3e7
TargetUserSid             S-1-5-18
TargetUserName            SYSTEM
TargetDomainName          NT AUTHORITY
TargetLogonId             0x3e7
LogonType                 5
LogonProcessName          Advapi
AuthenticationPackageName Negotiate
WorkstationName           -
LogonGuid                 {00000000-0000-0000-0000-000000000000}
TransmittedServices       -
LmPackageName             -
KeyLength                 0
ProcessId                 0x290
ProcessName               C:\Windows\System32\services.exe
IpAddress                 -
IpPort                    -
ImpersonationLevel        %%1833
RestrictedAdminMode       -
TargetOutboundUserName    -
TargetOutboundDomainName  -
VirtualAccount            %%1843
TargetLinkedLogonId       0x0
ElevatedToken             %%1842

Accessing [code]$xml.Event.EventData.Data[/code] displays lots of new parameters accessible by name instead of just one large string. This is the data that can be accessed using XmlPath notation (e.g., [code]*[EventData[Data[@Name='ProcessName'][/code]). But, as Paul pointed out to me, there is an easier way.

Convert-EventLogRecord

[code]Convert-EventLogRecord[/code] is a PowerShell function written by @JeffHicks, available as part of his PSScriptTools project. Using [code]Convert-EventLogRecord[/code] allows us to easily use [code]Get-WinEvent[/code], taking the individual XML data elements in the [code]Message[/code] property and make them individually accessible on the pipeline.

To use this function, first make a location where you can store PowerShell scripts. I'll make a directory called [code]C:\Tools[/code]. Then, change to the directory and download the [code]Convert-EventLogRecord.ps1[/code] script, as shown here:

PS C:\Windows\System32> New-Item -ItemType Directory -Path C:\Tools -ErrorAction SilentlyContinue
PS C:\Windows\System32> Set-Location C:\Tools
PS C:\Tools> Invoke-WebRequest -Uri https://raw.githubusercontent.com/jdhitsolutions/PSScriptTools/master/functions/Convert-EventLogRecord.ps1 -OutFile Convert-EventLogRecord.ps1
PS C:\Tools>

Next, import the script for use. To use the script on Windows 10, you'll need to change the PowerShell [code]ExecutionPolicy[/code]. On Twitter advice, change the policy to [code]RemoteSigned[/code] to block browser-downloaded PowerShell scripts, but not local scripts like those downloaded with [code]Invoke-WebRequest[/code] (again, I think PowerShell execution policies are silly; more on that another time):

PS C:\Tools> Set-ExecutionPolicy RemoteSigned -Force
PS C:\Tools>

Next, import the [code]Convert-EventLogRecord.ps1[/code] into the PowerShell session namespace using [code]Import-Module[/code]:

PS C:\Tools> Import-Module .\Convert-EventLogRecord.ps1
PS C:\Tools>

Perfect! Now you have access to [code]Convert-EventLogRecord[/code] as a function in PowerShell. Let's take a look at the properties available for the 4624 event with and without it. First, here are the properties available using [code]Get-WinEvent[/code] with a filter for event ID 4624:

PS C:\Tools> Get-WinEvent -FilterHashtable @{ LogName="Security"; ID=4624 } -MaxEvents 1 | Get-Member -MemberType Property


   TypeName: System.Diagnostics.Eventing.Reader.EventLogRecord

Name                 MemberType Definition
----                 ---------- ----------
ActivityId           Property   System.Nullable[guid] ActivityId {get;}
Bookmark             Property   System.Diagnostics.Eventing.Reader.EventBookmark Bookmark {get;}
ContainerLog         Property   string ContainerLog {get;}
Id                   Property   int Id {get;}
Keywords             Property   System.Nullable[long] Keywords {get;}
KeywordsDisplayNames Property   System.Collections.Generic.IEnumerable[string] KeywordsDisplayNames {get;}
Level                Property   System.Nullable[byte] Level {get;}
LevelDisplayName     Property   string LevelDisplayName {get;}
LogName              Property   string LogName {get;}
MachineName          Property   string MachineName {get;}
MatchedQueryIds      Property   System.Collections.Generic.IEnumerable[int] MatchedQueryIds {get;}
Opcode               Property   System.Nullable[int16] Opcode {get;}
OpcodeDisplayName    Property   string OpcodeDisplayName {get;}
ProcessId            Property   System.Nullable[int] ProcessId {get;}
Properties           Property   System.Collections.Generic.IList[System.Diagnostics.Eventing.Reader.EventProperty] Prope...
ProviderId           Property   System.Nullable[guid] ProviderId {get;}
ProviderName         Property   string ProviderName {get;}
Qualifiers           Property   System.Nullable[int] Qualifiers {get;}
RecordId             Property   System.Nullable[long] RecordId {get;}
RelatedActivityId    Property   System.Nullable[guid] RelatedActivityId {get;}
Task                 Property   System.Nullable[int] Task {get;}
TaskDisplayName      Property   string TaskDisplayName {get;}
ThreadId             Property   System.Nullable[int] ThreadId {get;}
TimeCreated          Property   System.Nullable[datetime] TimeCreated {get;}
UserId               Property   System.Security.Principal.SecurityIdentifier UserId {get;}
Version              Property   System.Nullable[byte] Version {get;}

Here is the same event, this time using [code]Convert-EventLogRecord[/code]:

PS C:\Tools> Get-WinEvent -FilterHashtable @{ LogName="Security"; ID=4624 } -MaxEvents 1 | Convert-EventLogRecord |Get-Member


   TypeName: System.Management.Automation.PSCustomObject

Name                      MemberType   Definition
----                      ----------   ----------
Equals                    Method       bool Equals(System.Object obj)
GetHashCode               Method       int GetHashCode()
GetType                   Method       type GetType()
ToString                  Method       string ToString()
AuthenticationPackageName NoteProperty string AuthenticationPackageName=Negotiate
Computername              NoteProperty string Computername=Sec504Student
ElevatedToken             NoteProperty string ElevatedToken=%%1842
ID                        NoteProperty int ID=4624
ImpersonationLevel        NoteProperty string ImpersonationLevel=%%1833
IpAddress                 NoteProperty string IpAddress=-
IpPort                    NoteProperty string IpPort=-
KeyLength                 NoteProperty string KeyLength=0
Keywords                  NoteProperty ReadOnlyCollection[string] Keywords=System.Collections.ObjectModel.ReadOnlyCollec...
LmPackageName             NoteProperty string LmPackageName=-
LogName                   NoteProperty string LogName=Security
LogonGuid                 NoteProperty string LogonGuid={00000000-0000-0000-0000-000000000000}
LogonProcessName          NoteProperty string LogonProcessName=Advapi
LogonType                 NoteProperty string LogonType=5
Message                   NoteProperty string Message=An account was successfully logged on....
ProcessId                 NoteProperty string ProcessId=0x290
ProcessName               NoteProperty string ProcessName=C:\Windows\System32\services.exe
RecordType                NoteProperty string RecordType=Information
RestrictedAdminMode       NoteProperty string RestrictedAdminMode=-
Source                    NoteProperty string Source=Microsoft-Windows-Security-Auditing
SubjectDomainName         NoteProperty string SubjectDomainName=SEC504
SubjectLogonId            NoteProperty string SubjectLogonId=0x3e7
SubjectUserName           NoteProperty string SubjectUserName=SEC504STUDENT$
SubjectUserSid            NoteProperty string SubjectUserSid=S-1-5-18
TargetDomainName          NoteProperty string TargetDomainName=NT AUTHORITY
TargetLinkedLogonId       NoteProperty string TargetLinkedLogonId=0x0
TargetLogonId             NoteProperty string TargetLogonId=0x3e7
TargetOutboundDomainName  NoteProperty string TargetOutboundDomainName=-
TargetOutboundUserName    NoteProperty string TargetOutboundUserName=-
TargetUserName            NoteProperty string TargetUserName=SYSTEM
TargetUserSid             NoteProperty string TargetUserSid=S-1-5-18
TimeCreated               NoteProperty datetime TimeCreated=7/14/2022 3:31:27 PM
TransmittedServices       NoteProperty string TransmittedServices=-
VirtualAccount            NoteProperty string VirtualAccount=%%1843
WorkstationName           NoteProperty string WorkstationName=-

When we add [code]Convert-EventLogRecord[/code] to the pipeline, it takes the elements in the [code]Message[/code] parameter and makes them accessible as properties. This allows us to retrieve specific information elements such as [code]TargetUserName[/code]:

PS C:\Tools> Get-WinEvent -FilterHashtable @{ LogName="Security"; ID=4624 } -MaxEvents 1 | Convert-EventLogRecord | Select-Object -Property TargetUserName

TargetUserName
--------------
SYSTEM

Since these added [code]Message[/code] data elements are properties, we can reference them in the pipeline using [code]Where-Object[/code] too:

PS C:\Tools> Get-WinEvent -FilterHashtable @{ LogName="Security"; ID=4624 } | Convert-EventLogRecord | Where-Object -Property TargetUserName -NE 'SYSTEM' | Select-Object TargetUsername, TimeCreated, LogonType

TargetUserName TimeCreated           LogonType
-------------- -----------           ---------
assetmgr       7/13/2022 11:10:23 AM 3
assetmgr       7/13/2022 11:09:42 AM 3
Sec504         7/13/2022 12:52:54 AM 3
Sec504         7/13/2022 12:12:18 AM 2
Sec504         7/13/2022 12:12:18 AM 2
ANONYMOUS L... 7/13/2022 12:11:47 AM 3
LOCAL SERVICE  7/13/2022 12:11:43 AM 5
DWM-1          7/13/2022 12:11:43 AM 2
DWM-1          7/13/2022 12:11:43 AM 2
NETWORK SER... 7/13/2022 12:11:43 AM 5
UMFD-1         7/13/2022 12:11:43 AM 2
UMFD-0         7/13/2022 12:11:43 AM 2
Sec504         7/13/2022 12:10:19 AM 2
Sec504         7/13/2022 12:10:19 AM 2
...

Let's break down this command step-by-step:

  • [code]Get-WinEvent[/code]: Get event log information
  • [code]-FilterHashtable @{ LogName="Security"; ID=4624 } |[/code]: Filter the results using a filter hash table where the logname is Security for event ID 4624; start a pipeline
  • [code]Convert-EventLogRecord |[/code]: Convert the results to merge in the [code]Message[/code] elements as new properties
  • [code]Where-Object -Property TargetUserName -NE 'SYSTEM' |[/code]: Apply a filter on the [code]TargetUserName[/code] property (added by [code]Convert-EventLogRecord[/code] from the [code]Message[/code] element), eliminating results where the target user name is SYSTEM using the not equals ([code]-NE[/code]) operator
  • [code]Select-Object TargetUsername, TimeCreated, LogonType[/code]: Select the [code]TargetUsername[/code], [code]TimeCreated[/code], and [code]LogonType[/code] fields for the output

Convert-EventLogRecord Performance Considerations

[code]Convert-EventLogRecord[/code] makes accessing the data in the [code]Message[/code] property a lot easier, but it can be detrimental to performance if used with lots of event data. Each event returned in the pipeline has the XML data extracted and added as custom properties to the object. This isn't a lot of overhead, but if you send all events to [code]Convert-EventLogRecord[/code] for filtering then you may be in for some waiting.

To avoid unnecessary performance detriment, remember this rule:

For example, let's say you want to see the Security event logs with event ID 4799 (A security-enabled local group membership was enumerated.) where the process name enumerating the group is not [code]svchost.exe[/code]. You could use [code]Convert-EventLogRecord[/code] to query both the event ID and the process name in the pipeline:

PS C:\Tools> Measure-Command { Get-WinEvent -LogName 'Security' | Convert-EventLogRecord | Where-Object -Property Id -EQ 4799 | Where-Object -Property CallerProcessName -NotLike '*esentutl*' | Select-Object -Property TimeCreated, SubjectAccountName, CallerProcessName }


Days              : 0
Hours             : 0
Minutes           : 7
Seconds           : 56
Milliseconds      : 708
Ticks             : 4767084570
TotalDays         : 0.00551745899305556
TotalHours        : 0.132419015833333
TotalMinutes      : 7.94514095
TotalSeconds      : 476.708457
TotalMilliseconds : 476708.457

This took NNN seconds. I took a nap while it was running and came back. This example violates the rule: filter hash table first. We can rewrite this query using a filter hash table for the event ID, then use [code]Convert-EventLogRecord[/code] and [code]Where-Object[/code] for the properties we can't access in the filter hash table:

PS C:\Tools> Measure-Command { Get-WinEvent -FilterHashtable @{LogName='Security'; ID=4799} | Convert-EventLogRecord | Where-Object -Property CallerProcessName -NotLike '*esentutl*' | Select-Object -Property TimeCreated, SubjectAccountName, CallerProcessName }


Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 18
Milliseconds      : 373
Ticks             : 183738383
TotalDays         : 0.000212660165509259
TotalHours        : 0.00510384397222222
TotalMinutes      : 0.306230638333333
TotalSeconds      : 18.3738383
TotalMilliseconds : 18373.8383

Only 18 seconds on my system, a huge improvement. Remember:

Conclusion

In this article we looked we looked at some of the challenges in accessing the data elements embedded within the event log Message property, and how the [code]Convert-EVentLogRecord[/code] function makes accessing that data much easier, giving you a lot more flexibility in building a pipeline to query the event log.

We have one last article in this short series: optimizing the Windows event log configuration. Stay tuned!

-Joshua Wright

Return to Getting Started With PowerShell


Joshua Wright is the author of SANS SEC504: Hacker Tools, Techniques, and Incident Handling, a faculty fellow for the SANS Institute, and a senior technical director at Counter Hack.

Month of PowerShell - Working with the Event Log, Part 3 - Accessing Message Elements | SANS Institute