This is Part 2 of our Blog series on how to collect events using DCRs for advanced use cases. For Part 1, please check The power of Data Collection Rules: Collecting events for advanced use cases in Microsoft USOP - Microsoft Community Hub.
PowerShell is a great tool for administrators to manage devices and servers in their environment. When using it to administer remote systems with PSRemoting, you don’t leave credentials behind on the target systems – as opposed to RDP with which your credentials would be stored in the Local Security Authority (LSA). This provides many security benefits and helps prevent Pass-The-Hash attacks and other credential theft scenarios
Since it is a preinstalled tool, adversaries have been known to use PowerShell to attack organizations. Companies that have set up a robust PowerShell configuration and monitoring have a clear advantage against those adversaries! Thanks to PowerShell’s numerous built-in security and monitoring features, it is easy to detect and disrupt adversaries.
In this article we will look how you can set up your own monitoring mechanism to spot executed PowerShell code in your environment using Microsoft Sentinel and the Unified SecOps Platform. We will not discuss the various security features that can be configured for a robust PowerShell environment, there are other resources for it, as mentioned in our first article.
Step 1: Configure ScriptBlockLogging
In PowerShell, you can imagine a script block as a collection of commands and expressions that are executed together as one command: the “script block”.
Many companies become aware to the importance of logging only after an incident has occurred. At that point, it is not possible to detect what happened. Therefore, the PowerShell team decided to implement basic script block logging, starting with PowerShell 5, which can be useful to trace malicious activities performed prior to the incident.
The basic script block logging feature only captures some basic security-relevant script blocks as the default if not configured otherwise. This way, in case of a security incident, you have insights of what basic malicious activities were executed on your machine, however this also means that with this default configuration not every activity is captured. Therefore, to keep track of all the activities on business-critical servers and high value assets, we need to configure script block logging before we can start collecting and reviewing all relevant events.
This can be done by using Group Policy. Depending on the PowerShell version for which you want to configure script block logging, navigate to the following GPO path(s):
- Windows PowerShell: Computer Configuration > Policies > Administrative Templates > Windows Components > Windows PowerShell > Turn on PowerShell Script Block Logging
- PowerShell Core: Computer Configuration > Administrative Templates > PowerShell Core > Turn on PowerShell Script Block Logging
Configure this Setting as “Enabled” and confirm with “OK”. Do not check the box to “Log script block invocation start / stop events” as this setting is verbose and would generate a lot of noise.
If you have both PowerShell versions in use in your environment, we recommend configuring and monitoring both.
In the PowerShell Core policy, you can find an option to “Use Windows PowerShell Policy Setting”. Unless you have a use case to have different configurations for both versions, you can simply enable this option to sync your Windows PowerShell script block settings.
Please note that the PowerShell Core administrative template files (*.admx)might need to be imported first before you can use them. This article points out how you can locate and install them: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_group_policy_settings
Step 2: Detect and review executed PowerShell code
Now that you have configured everything, it is time to collect PowerShell script block logging events and review the executed code. The following script retrieves and filters event ID 4104 from both Windows PowerShell and PowerShell Core (excluding certain paths and user IDs) and then formats and displays the filtered event details :
$PSWinEventLog = @{ ProviderName = "Microsoft-Windows-PowerShell"; Id = 4104 }
$PSCoreEventLog = @{ ProviderName = "PowerShellCore"; Id = 4104 }
$PSWinEventLog, $PSCoreEventLog | ForEach-Object {
try {
Get-WinEvent -FilterHashtable $_ | Where-Object {
!($_.Properties[4].Value -Match "C:\\ProgramData\\Microsoft\\Windows Defender Advanced Threat Protection\\DataCollection\\") `
-and ($_.Properties[2].Value -ne "prompt") `
-and ($_.UserId -ne "S-1-5-18") `
-and ($_.UserId -ne "S-1-5-19") `
-and !( $_.Properties[4].Value -Match ".vscode\\extensions\\") `
-and !($_.Properties[4].Value -Match "C:\\Windows\\TEMP\\SDIAG_([A-Za-z0-9]+(-[A-Za-z0-9]+)+)\\CL_Utility\.ps1")
} | Select-Object TimeCreated, `
@{Name='ExecutedCode';Expression={ $_.Properties[2].Value }},`
UserId, `
LevelDisplayName, `
@{Name='Path';Expression={$_.Properties[4].Value}}, `
ProviderName, `
@{Name='ScriptblockId';Expression={$_.Properties[3].Value}},`
@{Name='CurrentPart';Expression={$_.Properties[0].Value}},`
@{Name='TotalParts';Expression={$_.Properties[1].Value}}`
| fl
}
catch {}
PowerShell script block events can be split into multiple events if the executed script block was too large. In that case, the “CurrentPart” field would indicate this by containing a value higher than 1. The “CurrentPart" field indicates the order of the script block pieces.
It is recommended to adjust this query to your environment; are there script blocks that are being run by certain programs in your environment that you can safely exclude from your review. You can use the following documentation as a reference for filtering: Creating Get-WinEvent queries with FilterHashtable - PowerShell | Microsoft Learn.
PowerShell is a great tool for administrators to manage devices and servers in their environment. When using it to administer remote
PowerShell script block events can be split into multiple events if the executed script block was too large. In that case, the “CurrentPart” field would indicate this by containing a value higher than 1. The “CurrentPart" field indicates the order of the script block pieces.
It is recommended to adjust this query to your environment; are there script blocks that are being run by certain programs in your environment that you can safely exclude from your review. You can use the following documentation as a reference for filtering: Creating Get-WinEvent queries with FilterHashtable - PowerShell | Microsoft Learn.
Step 3: Configure the data collection Rule (DCR) to collect the required events
Using the Azure Monitor agent (AMA), you can select the events you would like to collect from your servers using Xpath queries (please see Filter Windows events using Xpath queries for reference). If you are not familiar with the agent or you have not installed AMA yet in the servers you would like to monitor, please check the first article of this series: include link.
To create your DCR, as this time we are collecting non-Security events, go to Connectors and select Windows Forwarded Events. From here, select create DCR, add your servers, and under Collect select Custom. Paste the following xPath queries:
For Windows PowerShell:
Microsoft-Windows-PowerShell/Operational!*[System[(EventID=4104)]]
For PowerShell Core:
PowerShellCore/Operational!*[System[(EventID=4104)]]
To prevent getting events that we don’t need (e.g. events from background processes or system accounts), based on the query we constructed in Step 2, we can create a transformation in our DCR that will prevent those events from being ingested into Microsoft Sentinel, as they could be too verbose and we may not need them. For this purpose, you can add this transformation in your DCR:
"transformKql": "source | where SystemUserId !in ('S-1-5-18', 'S-1-5-19') | extend ScriptBlockText = parse_json(EventData).ScriptBlockText, ScriptBlockId = tostring(EventData.ScriptBlockId), MessageNumber = tostring(EventData.MessageNumber), MessageTotal = tostring(EventData.MessageTotal), Path = tostring(EventData.Path) | where tostring(ScriptBlockText) != 'prompt' | where Path != '.vscode\\\\extensions\\\\' and Path != 'C:\\\\Windows\\\\TEMP\\\\SDIAG_([A-Za-z0-9]+(-[A-Za-z0-9]+)+)\\\\CL_Utility.ps1' and Path != 'C:\\\\ProgramData\\\\Microsoft\\\\Windows Defender Advanced Threat Protection\\\\DataCollection\\\\'"
Alternatively, you can deploy this template from GitHub, which already includes the xPath queries described above, plus the transformation: Azure-Sentinel/DataConnectors/WindowsEvents/DataCollectionRulePowerShellEvents at master · Azure/Azure-Sentinel (github.com)
Step 4: Creating your detections
Now, we can go to Microsoft Sentinel or to the Unified security operations platform, which brings Microsoft Sentinel and Microsoft Defender XDR into a single unified portal (see how to Connect Microsoft Sentinel to Microsoft Defender XDR) and start querying your logs and generate detections.
It also makes sense to monitor for devices that never ran PowerShell code before, from which PowerShell code is executed unexpectedly. Are there devices that should never run PowerShell code (e.g. devices from Accounting or Marketing)? For this purpose, we could use a watchlist. Watchlists allow you to create a list of items you would like to use for correlation (e.g. high-value assets, terminated employees, service accounts, etc.). In our scenario, we have created a watchlist that determines to which team the machine belongs (Operations, Security, Marketing).
First, let’s have a look at our logs. On Microsoft Defender XDR, we can find them under Advanced Hunting. We are using this query to detect machines that are not part of the groups (Security and Operations) that we would expect to run PowerShell code using this query:
let AllowedGroups = dynamic(["Security","Operations"]);
WindowsEvent
| where EventID == 4104
| extend ScriptBlockText = parse_json(EventData).ScriptBlockText, ScriptBlockId = tostring(EventData.ScriptBlockId), MessageNumber = tostring(EventData.MessageNumber), MessageTotal = tostring(EventData.MessageTotal), Path = tostring(EventData.Path)
| lookup kind=inner _GetWatchlist('devicegroups') on $left.Computer == $right.SearchKey
| where Group !in (AllowedGroups)
| project TimeGenerated, ScriptBlockText, SystemUserId, DeviceName, Group, EventLevelName, Path, MessageNumber, MessageTotal, ScriptBlockId, Channel
| sort by TimeGenerated, ScriptBlockId, MessageNumber
Above, we can see what machines that were not from the expected departments have ran PowerShell code.
After polishing our watchlist and query, we are ready to create a detection under Analytics:
Please remember to match the Host entity under “Entity mapping” when you create your analytic rule. This is critical for correlation across data sources and alerts.
This concludes Part 2 of our 3 Part blog series on how to collect events using DCRs for advanced use cases. For Part 1, please check The power of Data Collection Rules: Collecting events for advanced use cases in Microsoft USOP - Microsoft Community Hub, and stay tuned for Part 3, in which we show how to monitor for indications that Defender for Endpoint (MDE) was shut down.
We welcome your feedback and questions on this or any of the other parts of this blog article series and look forward to hearing from you whether you would like to see more articles like this.
Special thanks to our reviewers Ashwin Patil and Yaniv Carmel from the Security Research team.
Authors: Miriam Wiesner (@miriamxyra) – Senior Security Research PM for Microsoft Defender XDR | Maria de Sousa-Valadas Castaño MariaSousaValadas – Senior Product Manager Unified SocOps Platform | Shirley Kochavi skochavi – Senior Product Manager Unified SocOps Platform