Contents

Alert changes to sensitive AD groups using MDI

Microsoft Defender for Identity is a very powerful tool when it comes to track changes to users and groups in your on-prem Active Directory. When used in combination of the advanced hunting capabilities available in the Microsoft 365 Defender portal and custom detection rules you can very easily automate the change tracking.

If you protect any on-prem Active Directory, you should be aware to changes to any privileged groups. Microsoft itself list a few of them in their documentation on Active Directory Domain Services and in the Defender for Identity documentation adds additional ones. In total there are 17 groups marked as sensitive.

  • Account Operators
  • Administrators
  • Backup Operators
  • Domain Admins
  • Domain Controllers
  • Enterprise Admins
  • Enterprise Read-only Domain Controllers
  • Group Policy Creator Owners
  • Incoming Forest Trust Builders
  • Microsoft Exchange Servers
  • Network Configuration Operators
  • Power Users
  • Print Operators
  • Read-only Domain Controllers
  • Replicators
  • Schema Admins
  • Server Operators

If you have an on-prem Exchange environment and it is not configured for split permissions you should also monitor any changes to the following groups.

  • Exchange Trusted Subsystem
  • Exchange Windows Permission
  • Organization Management

All this is very hard if you do not have any central logging. This is where Defender for Identity comes into play. All changes to groups are monitored and reported back to the cloud. Make sure you have setup the Windows Event collection correctly and all Event Ids mentioned in this article are audited.

Advanced Hunting

Now let’s go hunting. In the Advanced hunting schema section, you will find a table named IdentityDirectoryEvents which contains all this data, neatly organized for you to query it.

Which event to filter?

The first step is to check which actions are logged and how you can filter on a specific ActionType. The following query will present you with every action and the name for it in MDI that happened in your AD in the timeframe you queried.

IdentityDirectoryEvents 
| distinct ActionType
| sort by ActionType asc 

To check the full list of ActionTypes you will have to use the in-portal schema reference because even Microsoft does not list those in the public documentation.

The results should look something like this:

/en/alert-sensitive-ad-groups-mdi/images/ActionTypes.png

In this list you will most likely find one action type named “Group Membership changed”. This is the one you want to query.

List all group membership changes

To avoid to many results, I limited the results to 100 items.

IdentityDirectoryEvents
| where ActionType == "Group Membership changed"
| limit 100

The results are not quite what you might expect.

/en/alert-sensitive-ad-groups-mdi/images/RawQueryResults.png

All the information there, but not in a very easy to consume format and most important data is stored as JSON in the AdditionalFields column.

/en/alert-sensitive-ad-groups-mdi/images/AdditionalFields.png

Untangle the data

Kusto has a powerful function to extract JSON data from this AdditionalFields column: parse_json

Let’s break down this massive query line by line.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
IdentityDirectoryEvents
| where Timestamp >= now(-2d)
| where ActionType == "Group Membership changed"
| extend AdditionalFields = parse_json(AdditionalFields)
| extend FromGroup = AdditionalFields.["FROM.GROUP"]
| extend ToGroup = AdditionalFields.["TO.GROUP"]
// Extract target user or device name
| extend TargetObject =  iff( isnull(AdditionalFields.["TARGET_OBJECT.USER"]), AdditionalFields.["TARGET_OBJECT.GROUP"], AdditionalFields.["TARGET_OBJECT.USER"])
// Special case group managed service accounts and devices
| extend TargetObject =  iff( isnull(TargetObject), AdditionalFields.["TARGET_OBJECT.DEVICE"], TargetObject)
| project-away AdditionalFields
| project-reorder Timestamp, ActionType,Application, FromGroup, ToGroup, TargetObject

At first the query only checks for data that has a timestamp in the last two days and only if the ActionType equals “Group Membership changed”.

1
2
3
IdentityDirectoryEvents
| where Timestamp >= now(-2d)
| where ActionType == "Group Membership changed"

In the next line the function parse_json extracts all fields from AdditionalFields and puts those as dynamic columns in the column AdditionalFields. So, it actually overwrites the column it just extracted the data from.

4
| extend AdditionalFields = parse_json(AdditionalFields)

Now you can address each field the column directly and the next two lines extract the information about which group an object was added to or removed from. Those are then saved in new columns named FromGroup and ToGroup.

5
6
| extend FromGroup = AdditionalFields.["FROM.GROUP"]
| extend ToGroup = AdditionalFields.["TO.GROUP"]

The next two lines are used to check the target object. This might be a user, a group or a computer object. At the end there should only be one column to evaluate, so a bit of AI if/then evaluation is needed.

The first iff checks if the additional field TARGET_OBJECT.USER is empty and if this is true sets TARGET_OBJECT.GROUP as the value. If it’s not empty TARGET_OBJECT.USER will be the value.

Should neither of the columns have had any data then the second iff uses TARGET_OBJECT.DEVICE as a last resort to fill the information.

7
8
| extend TargetObject = iff( isnull(AdditionalFields.["TARGET_OBJECT.USER"]), AdditionalFields.["TARGET_OBJECT.GROUP"], AdditionalFields.["TARGET_OBJECT.USER"])
| extend TargetObject = iff( isnull(TargetObject), AdditionalFields.["TARGET_OBJECT.DEVICE"], TargetObject)
Info
Since group managed service accounts are also computer objects they are also correctly extracted.

At last the column AdditionalFields is removed, because it is no longer needed and also the ordering of the columns is changed, so that the important information in presented at the front.

11
12
| project-away AdditionalFields
| project-reorder Timestamp, ActionType,Application, FromGroup, ToGroup, TargetObject

The result is a much cleaner and easy to read dataset.

/en/alert-sensitive-ad-groups-mdi/images/CleanedUpResults.png

Query on relevant groups

Since the last query report every group change made in your environment in the last two days, we need to strip it further down.

This is done by a ‘virtual table’ named GroupsToMonitor. This table contains only one column GroupName and for each group you want to monitor one row. As you can see it’s super easy to extend this to important custom groups you might have in your environment.

Note
Since this query does not recognize changes to nested groups, you MUST list every sensitive group manually.
let GroupsToMonitor = datatable(GroupName:string)
[
"Account Operators",
"Administrators",
"Backup Operators",
"Domain Admins",
"Domain Controllers",
"Enterprise Admins",
"Enterprise Read-only Domain Controllers",
"Exchange Trusted Subsystem",
"Exchange Windows Permission",
"Group Policy Creator Owners",
"Incoming Forest Trust Builders",
"Microsoft Exchange Servers",
"Network Configuration Operators",
"Organization Management",
"Power Users",
"Print Operators",
"Read-only Domain Controllers",
"Replicators",
"Schema Admins",
"Server Operators",
];
IdentityDirectoryEvents
| where Timestamp >= now(-2d)
| where ActionType == "Group Membership changed"
| extend AdditionalFields = parse_json(AdditionalFields)
| extend FromGroup = AdditionalFields.["FROM.GROUP"]
| extend ToGroup = AdditionalFields.["TO.GROUP"]
// Extract target user or device name
| extend TargetObject =  iff( isnull(AdditionalFields.["TARGET_OBJECT.USER"]), AdditionalFields.["TARGET_OBJECT.GROUP"], AdditionalFields.["TARGET_OBJECT.USER"])
// Special case group managed service accounts and devices
| extend TargetObject =  iff( isnull(TargetObject), AdditionalFields.["TARGET_OBJECT.DEVICE"], TargetObject)
| where FromGroup in (GroupsToMonitor) or ToGroup in (GroupsToMonitor)
| order by Timestamp
| project-away AdditionalFields
| project-reorder Timestamp, ActionType,Application, FromGroup, ToGroup, TargetObject

The result will, in most cases, be empty. This is a good thing if you have not added a domain admin lately.

/en/alert-sensitive-ad-groups-mdi/images/NoResultFound.png

Create a custom detection

Now all you need to do is to create a custom detection rule to receive a timely alert when one of those group memberships change. After executing your modified query click on “create detection rule”

/en/alert-sensitive-ad-groups-mdi/images/CreateDetectionRule.png

Name your alert and add information on how to handle this alert. Choose the severity and category in respect to your companies guidelines.

/en/alert-sensitive-ad-groups-mdi/images/AlertDetails.png

For impacted entities select TargetAccountUpn and skip the action part.

/en/alert-sensitive-ad-groups-mdi/images/ImpactedEntities.png

Create the alert and switch to Custom detection rules

/en/alert-sensitive-ad-groups-mdi/images/Success.png

/en/alert-sensitive-ad-groups-mdi/images/DetectionRule.png

If you want to check the results you can manually run the detection.

/en/alert-sensitive-ad-groups-mdi/images/RunManually.png

The Alert

Should time come and the alert is triggered you will have the newly added or removed user highlighted in the impacted user section in the incident.

/en/alert-sensitive-ad-groups-mdi/images/ImpactedUser.png

You can also dive deeper in the alert details, and you will find the query results including the affected group. Since FromGroup and ToGroup are different columns it’s easy to see if somebody elevated their privileges or was removed.

/en/alert-sensitive-ad-groups-mdi/images/AlertDetails_2.png

Note

With some minor changes to the query, you could create two different detections:

  • One is raised with high severity when somebody is added to a group
  • The other one with a severity if information when somebody is removed from a group

Conclusion

Defender for Identity and custom detections are a great addition to your toolset to protect your environment. The additional signals you can tap into are a treasure to detect changes to your on-Prem environment.

I only can encourage you to check out advanced hunting even more and try out new stuff. Since all the data is read only you cannot break anything just by querying the data and the language is smart enough to compensate for unperfect queries like mine.

If you, do try to limit your result with | limit 10 to speed it up even further.

And if you enjoyed this blog post check out my post on Automated response to C2 traffic on your devices.