In part 1, we looked at PowerShell get winevent to work with the event log: Get-WinEvent. In part 2 we looked at 10 practical examples of using Get-WinEvent to perform threat hunting using event log data, using -FilterHashTable, the PowerShell pipeline, and -FilterXPath.
In this article we'll look at using a third-party script to make the data in the Get-WinEventMessage 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 Get-WinEventMessage property data much easier: Convert-EventLogRecords.
Get-WinEvent 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 Message 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 Message 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 Get-WinEvent properties.
Let's dig into the Message property for the event ID 4624 event, declaring a variable $logonEvent:
PS C:\Windows\System32> $logonEvent = Get-WinEvent -FilterHashtable @{ LogName="Security"; ID=4624 } -MaxEvents 1 PS C:\Windows\System32>
Next, let's convert the $logonEvent 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 $xml. The value stored in $xml is the result of calling the ToXml() method on the $logonEvent variable for the 4624 event ID.
To see a list of available methods on a variable, use Get-Member (e.g., $logonEvent | Get-Member -MemberType Method).
With the $xml 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 Event. 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 $xml.Event has three member objects: xmlns, System, and EventData. We can continue to interrogate the data by using dot notation to access the EventData object:
PS C:\Windows\System32> $xml.Event.EventData Data ---- {SubjectUserSid, SubjectUserName, SubjectDomainName, SubjectLogonId...}
Which reveals a Data 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 $xml.Event.EventData.Data 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., *[EventData[Data[@Name='ProcessName']). But, as Paul pointed out to me, there is an easier way.
Convert-EventLogRecord
Convert-EventLogRecord is a PowerShell function written by @JeffHicks, available as part of his PSScriptTools project. Using Convert-EventLogRecord allows us to easily use Get-WinEvent, taking the individual XML data elements in the Message property and make them individually accessible on the pipeline.
TL/DR: It makes the event log data much more easily accessible.
To use this function, first make a location where you can store PowerShell scripts. I'll make a directory called C:\Tools. Then, change to the directory and download the Convert-EventLogRecord.ps1 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>
You can use the mkdir, cd, and iwr aliases instead of typing out these commands..I'm trying to get in the habit of typing full command names for clarity in documentation.
Next, import the script for use. To use the script on Windows 10, you'll need to change the PowerShell ExecutionPolicy. On Twitter advice, change the policy to RemoteSigned to block browser-downloaded PowerShell scripts, but not local scripts like those downloaded with Invoke-WebRequest (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 Convert-EventLogRecord.ps1 into the PowerShell session namespace using Import-Module:
PS C:\Tools> Import-Module .\Convert-EventLogRecord.ps1 PS C:\Tools>
Perfect! Now you have access to Convert-EventLogRecord 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 Get-WinEvent 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 Convert-EventLogRecord:
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 Convert-EventLogRecord to the pipeline, it takes the elements in the Message parameter and makes them accessible as properties. This allows us to retrieve specific information elements such as TargetUserName:
PS C:\Tools> Get-WinEvent -FilterHashtable @{ LogName="Security"; ID=4624 } -MaxEvents 1 | Convert-EventLogRecord | Select-Object -Property TargetUserName TargetUserName -------------- SYSTEM
Since these added Message data elements are properties, we can reference them in the pipeline using Where-Object 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:
- Get-WinEvent: Get event log information
- -FilterHashtable @{ LogName="Security"; ID=4624 } |: Filter the results using a filter hash table where the logname is Security for event ID 4624; start a pipeline
- Convert-EventLogRecord |: Convert the results to merge in the Message elements as new properties
- Where-Object -Property TargetUserName -NE 'SYSTEM' |: Apply a filter on the TargetUserName property (added by Convert-EventLogRecord from the Message element), eliminating results where the target user name is SYSTEM using the not equals (-NE) operator
- Select-Object TargetUsername, TimeCreated, LogonType: Select the TargetUsername, TimeCreated, and LogonType fields for the output
Convert-EventLogRecord Performance Considerations
Convert-EventLogRecord makes accessing the data in the Message 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 Convert-EventLogRecord for filtering then you may be in for some waiting.
To avoid unnecessary performance detriment, remember this rule:
Filter hash table first; Convert-EventLogRecord if you must.
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 svchost.exe. You could use Convert-EventLogRecord 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 Convert-EventLogRecord and Where-Object 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:
Filter hash table first; Convert-EventLogRecord if you must.
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 Convert-EVentLogRecord 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.