In the previous post I explained how to hunt for exact same scenario using CrowdStrike. I thought it’d be fun to see how would it look like with Microsoft Defender for Endpoint using its Advanced Hunting module and the Kusto query language.
Techniques of interest:
|_ Remote Services: Remote Desktop Protocol
|_ Remote Services: SMB/Windows Admin Shares
If a Threat Actor (TA) would successfully employ the above-mentioned sub-techniques of T1021 then in Windows Active Directory environment it should demonstrate itself by Windows logon events with types 3 and 10 being generated on target machines.
If we were able to identify any single user account logging into multiple hosts in the domain, it could be an indicator of the above described activity. Deeper investigation of actions performed by such an account on target hosts as well as historical baseline comparison of its logon activities, could lead to uncovering of TA operations in an environment and hence prove the hypothesis true.
I’ll start by navigating to the Advanced hunting module in the Microsoft Defender Security Center. The console itself provides very good reference documentation and couple of query examples to get you going.
More complete documentation on the Kusto query language can be found here and nice set of links to other resources around the Advanced hunting here. Additionally, once in there, in the pane to the left you can find extensive set of saved Community queries that show the power of this module and serve as a great kickstarter to writing your own queries.
As for our scenario I need to be looking at logon activities then I’ll be using the DeviceLogonEvent table.
Let’s have a look at the below query.
DeviceLogonEvents | where LogonType in ("Network", "RemoteInteractive") and isnotempty(AccountName) | summarize logonsets = makeset(DeviceName, 2000) by AccountName | project AccountName, HostsAccessed = array_length(logonsets), logonsets | where HostsAccessed > 3 | sort by HostsAccessed
When I was doing this with CrowdStrike I generated statistics about the number of logons for distinct user accounts and the number of hosts these logons where performed on. While with Kusto something similar would be possible then I decided to go with the makeset() aggregation function within the summarize operator. In the example above I’m using it to aggregate DeviceNames related to users’ logon activities in a JSON array. In other words I’ll be getting results table with user names in one column, number of hosts they logged into (filtered for RemoteInteractive and Network types only) in another and a list of hostnames they accessed or tried to access in the last one. This is great to work with and to be exported to csv/xlsx for further processing. I’m also filtering the results for the desired logon types (3 and 10) and users who logged at minimum to 4 different hosts.
It’s worth to mention that the makeset() function has a default size of the output array set to 128 and in my scenario I extended it to 2000 (hostnames) as I’m running it on quite a large environment.
As in the previous post I’d also like to have additional filtering capabilities for types of systems the logons occur on for example to limit the results to client workstations where remote logons are less common than on servers. While I was able to find the OSPlatform column in the DeviceInfo table that should contain OS version:
Then for some weird reason it often did not contain any values. So I ended up using the MachineGroup column that in my case provided the necessary distinction. This new query was however taking much longer to execute likely due to the join statement (needed as MachineGroup column comes from different table than the logon events) hence the limit on the number of results.
DeviceLogonEvents | where LogonType in ("Network", "RemoteInteractive") | join kind=inner ( DeviceInfo | where MachineGroup == "Windows10" ) on DeviceId, DeviceName | summarize logonsets = makeset(DeviceName) by AccountName | project AccountName, HostsAccessed = array_length(logonsets), logonsets | where HostsAccessed > 3 | sort by HostsAccessed | limit 100
So now we have the list of users who were remotely accessing (or trying to access) large number of assets. Thanks to the use of makeset() function we can conveniently start reviewing particular users directly in the dashboard (results are redacted).
Now we need to choose accounts that we want to investigate deeper. Next step would be to understand if such an activity is common for the selected account or not. For this we’ll need to visualize selected user’s logon activities over time. For that I’ll use the summarize operator and project results on a simple line chart. At this point I need to admit that the speed Defender’s console comes back with results for 30 days is really staggering (in a positive way).
It’s clear that for this account large number of logon activities is nothing new but there was an unusual spike of activity on 10 of November that we should drill down into. This can be done by reviewing events in that timeframe for that user in the tables DeviceLogonEvents (to see the details behind the large number of logon events that we found) and DeviceProcessEvents (to find processes executed by this user). In my case it turned out the logon events were representing failed attempts. This could mean expired password for an account used for scripted tasks or something malicious.
Commands executed by investigated user in a broader timeframe revealed that indeed this account was used for automation. However, at the same time, as this hunt results were handed over to IR team to check the reasons for failures with the account owner, they revealed poor security practice in the tasks performed with use of this account. Maybe not something extremely exiting but a security threat in the environment nevertheless 🙂