Auditing Group Changes

At your company, you may have run into requests to find out who made a change to an Active Directory (AD) Group, including which users and groups were added and removed. I have generally been asked to investigate after a security breach occurred or things suddenly stopped working. In this post, I will show you how to enable auditing of group changes, what Windows EventIDs to look for,  and how Splunk can be used to more easily find the relevant information. Specifically, we will focus on Security-enabled AD and non-AD Groups.

Tip: For investigating Account-related events, see my Account Lockouts and Modifications post.

Active Directory Groups

Microsoft’s Active Directory (AD) is a service that governs how resources can be utilized by a collection of users, groups, and computers. Enterprises use AD to authenticate, authorize, secure, and audit access within a security boundary — a Domain — to file servers, computers, emails, and more. You are given a user account (often referred to as your “network login”) to access what has been made available to you. A Domain Controller (DC) is the server that contains a copy of the AD database and is responsible for the replication of said data between all other DCs within the Domain.

To secure access to a company’s resources, such as file shares, permissions can be granted to Active Directory accounts and groups. A group is a collection of members (such as users and other groups) to help make administration easier.

Group Scopes and Types

There are three Scopes of Active Directory groups:

  • Domain Local Group, sometimes referred to as “Local Group”
    • Do not confuse with “Builtin Local” groups
  • Global Group
  • Universal Group

Each scope is used for a specific purpose, can be applied to particular areas of the Domain or Forest, and generally, access should be granted using the “A-G-DL-P” method.

For each Scope, there are two Types:

  • Distribution – For email distribution lists
  • Security – For assigning access to resources

You can change groups to/from different Scopes and Types, but do so with extreme caution.

Tips from the Field

  • Scope Changes
    • Domain Local can only directly change to Universal
    • Domain Local can be changed to Global Group by first turning it to Universal
    • Global Group can only directly change to Universal
    • Universal Group can change directly to Domain Local or Global
  • Create a separate Distribution group for distribution lists, and a Security group for controlling permissions
    • Minimize the use of the same, mail-enabled Security group for managing permissions and as a distribution list

Builtin Local (Non-AD) Groups

The “Builtin Local” groups are NOT Active Directory Domain groups, their Scope and Type cannot be changed, and they exist locally on the Domain Controller, Member Server (Domain-joined), and Workstation.

  • Builtin Local group examples
    • Member Server/Workstation
      • Administrators, Power Users, Users, Remote Desktop Users, Hyper-V Administrators, Backup Operators, Cryptographic Operators, Network Configuration Operators, etc.
    • Domain Controller – Same as Member Server/Workstation plus:
      • Account Operators, Terminal Server License Servers, Print Operators, etc.

Enable Auditing

Before we can perform forensics on group changes, we have to turn on what to audit in Active Directory. This can and will increase the amount of data stored by the Domain Controllers’ Event Logs and why auditing for most events is disabled by default. Reference Microsoft’s Security Auditing documentation and determine what your organization wants and needs to log. You may want to start with just a few items to become familiar with policy configuration before opening the flood gates.

If your company has the ability to send the audit logs to a SIEM (Security Information and Event Management) instead, such as Splunk, you may want to utilize that to aid with better, faster, and deeper, investigative searches.

Event IDs

Active Directory changes and incidents are stored in Event Logs with a code: the Event ID. This allows one to more quickly search for just the data you need. Below are the Event IDs that relate to Active Directory Security Groups and what they are for. For additional details, go to Microsoft’s Audit Security Group Management documentation.

Group Changes: Type or Scope

  • 4764 – Group Type (or Scope) changed
    • According to Microsoft, this ID only occurs when a Group’s Type (regardless of its Scope) is changed
    • In my testing, however, this ID was also recorded when a Group’s Scope changed
    • Examples
      • Type change: Universal Security <> Universal Distribution
      • Scope change: Universal Security <> Global Security
      • Scope change: Universal Distribution <> Domain Local Distribution

Despite Microsoft’s Documentation indicating Event ID 4764 only applying to Group Type changes, my tests found it also occurring for Group Scope modifications.

SECURITY-Enabled Group Changes

Caution: During the course of an investigation, be aware that the Event IDs listed below ONLY apply to Security (not Distribution) Groups.

  • Example: Creation of a Universal Distribution Group does NOT log Event ID 4754 — but a Universal Security Group would

Security Group: Creation, Deletion, Change

  • 4731 Created, 4734 Deleted, 4735 Changed – Domain Local or Builtin Local group
  • 4727 Created, 4730 Deleted, 4737 Changed – Global Group
  • 4754 Created, 4758 Deleted, 4755 Changed – Universal Group

When a Group’s Scope is changed, the NEW Scope’s Event ID is recorded. Example: Universal to Global triggers ID 4737.
This Event may also occur with other changes, such as the discretionary access control list (DACL), but not all.

Security Group: Membership Change

  • 4732 Added, 4733 Removed – Domain Local or Builtin Local group member
  • 4728 Added, 4729 Removed – Global Group member
  • 4756 Added, 4757 Removed – Universal Group member

Other Security Group Events

  • 4799 – Domain Local or Builtin Local group enumerated by a process (“Active Directory Users and Computers” tool does not trigger this event)
    • Enumerating a group means its membership was read

Splunk Queries

Splunk is a VERY powerful, expensive tool that aggregates logs from multiple sources (such as systems, applications, network devices, and more) to allow you to search, monitor, and analyze a wealth of Big Data. It is a very useful SIEM (Security Information and Event Management) tool that can also be used to deconstruct a timeline of events, such as a breach in the network.

Splunk results can be downloaded as CSV, XML, and JSON files for further reporting and analysis. They can also be printed as PDF, although at the time of writing, the orientation is locked in Portrait mode

Oddball Event ID: 4756

I came across a possible bug with Event ID 4756. Instead of using “Group Name” and “Group Domain” under the “Group” header, it is the ONLY one that goes with “Account Name” and “Account Domain”:

To work around the uniqueness of ID 4756 using “Account” instead of “Group”, I added extra RegExes to the queries:

| rex "Group:\s+[^\n]+\s+Group Name:\s+(?<Group>.*)"
| rex "Group:\s+[^\n]+\s+[^\n]+\s+Group Domain:\s+(?<GroupDomain>.*)"
`comment("4756 uses 'Account', not 'Group'")` 
| rex "Group:\s+[^\n]+\s+Account Name:\s+(?<Group>.*)"
| rex "Group:\s+[^\n]+\s+[^\n]+\s+Account Domain:\s+(?<GroupDomain>.*)"

Search for Group Membership Changes

This query will locate any membership changes to Privileged AD groups within the last 30 days, including who made the change and who was added or removed.

Group Membership Changes

index="*" earliest=-30d latest=now() source=WinEventLog:Security
(Group_Name="Administrators" OR Group_Name="Enterprise Admins" OR Group_Name="Domain Admins" OR Group_Name="Schema Admins" OR Group_Name="Account Operators" OR Group_Name="Backup Operators" OR Group_Name="Server Operators" OR Group_Name="DHCP Administrators" OR Group_Name="DnsAdmins" OR Group_Name="Network Configuration Operators" OR Group_Name="Hyper-V Administrators" OR Group_Name="Domain Controllers" OR Group_Name="Read-only Domain Controllers" OR Group_Name="Cryptographic Operators" OR Group_Name="Cert Publishers")
(EventCode=4728 OR EventCode=4729 OR EventCode=4732 OR EventCode=4733 OR EventCode=4756 OR EventCode=4757)
| eval Time=strftime(_time, "%m/%d/%y %H:%M:%S") | sort -_time
| eval EventCode=case(EventCode==4728, "4728 [+] GG Sec", EventCode==4729, "4729 [-] GG Sec", EventCode==4732, "4732 [+] DL/BL Sec", EventCode==4733, "4733 [-] DL/BL Sec", EventCode==4756, "4756 [+] UG Sec", EventCode==4757, "4757 [-] UG Sec", 1=1, EventCode)
| rex "Subject:\s+[^\n]+\s+Account Name:\s+(?<ActionBy>.*)" | rex "Subject:\s+[^\n]+\s+[^\n]+\s+Account Domain:\s+(?<ActionByDomain>.*)"
| rex "Member:\s+\w+\s\w+:\s+(?<Member>.*)" | rex "Member:\s+Security ID:[^\n]+\s+Account Name:\s+(?<MemberDN>.*)"
| rex "Group:\s+[^\n]+\s+Group Name:\s+(?<Group>.*)" | rex "Group:\s+[^\n]+\s+[^\n]+\s+Group Domain:\s+(?<GroupDomain>.*)"
`comment("4756 uses 'Account', not 'Group'")` | rex "Group:\s+[^\n]+\s+Account Name:\s+(?<Group>.*)" | rex "Group:\s+[^\n]+\s+[^\n]+\s+Account Domain:\s+(?<GroupDomain>.*)"
| rename ComputerName as "Computer", name as "EventDescription"
| table Time, GroupDomain, Group, EventCode, Member, MemberDN, ActionBy, ActionByDomain, Computer, EventDescription

The “[+]” and “[-]” symbols signify a member as having been added or removed, respectively. “BL”, “DL”, “GG”, and “UG” refer to Builtin Local, Domain Local, Global, and Universal Group.

Tips:

  • For faster results, replace “index=*” with the appropriate index to search through
  • Commands are case-sensitive
  • “1=1, EventCode” means: Default = EventCode itself (to catch any value not matching any Case evaluations)

Modify the “Group_Name” line to look for events related to other AD groups:

  • Group_Name=”vpn_*” – Any AD group starting with “VPN_” (not case sensitive)
  • Group_Name=”Marketing Users” – Changes to the “Marketing Users” AD group

For changes made to Builtin Local or non-AD groups local to computers, add “Group_Domain” to the query:

  • “Users” group on “Computer01” computer: Group_Domain=”Computer01″ AND Group_Name=”Users”
  • “Administrators” of the Builtin Local group: Group_Domain=”Builtin” AND Group_Name=”Administrators”
    • Omitting Group_Domain will work too: Group_Name=”Administrators”

Membership Changes Made TO and BY an Account

There were two searches I wanted to create for finding membership changes:

  • What was done TO an account (a user)?
  • What was done BY an account (an administrator)?

However, creating the queries were not immediately obvious because there are multiple matches for both “Account Name” and “Security ID” in the Event message. Look closer at “Account Name” and you will see that one is in sAMAccountName (Subject) and the other in DistinguishedName (Member) format. With that in mind, we are now able to accomplish the goal I set out for. See the ‘Note on Multiple Matches‘ section below for an explanation.

Changes Made TO an Account

To search for any Group Membership changes made TO an account, you will need to use its DistinguishedName for “Account_Name”.

index="*" earliest=-30d latest=now() source=WinEventLog:Security
Account_Name="CN=Mike Yuen,OU=Test,DC=YuenX,DC=com"
(EventCode=4728 OR EventCode=4729 OR EventCode=4732 OR EventCode=4733 OR EventCode=4756 OR EventCode=4757)
| eval Time=strftime(_time, "%m/%d/%y %H:%M:%S") | sort -_time
| eval EventCode=case(EventCode==4728, "4728 [+] GG Sec", EventCode==4729, "4729 [-] GG Sec", EventCode==4732, "4732 [+] DL/BL Sec", EventCode==4733, "4733 [-] DL/BL Sec", EventCode==4756, "4756 [+] UG Sec", EventCode==4757, "4757 [-] UG Sec", 1=1, EventCode)
| rex "Subject:\s+[^\n]+\s+Account Name:\s+(?<ActionBy>.*)" | rex "Subject:\s+[^\n]+\s+[^\n]+\s+Account Domain:\s+(?<ActionByDomain>.*)"
| rex "Member:\s+\w+\s\w+:\s+(?<Member>.*)" | rex "Member:\s+Security ID:[^\n]+\s+Account Name:\s+(?<MemberDN>.*)"
| rex "Group:\s+[^\n]+\s+Group Name:\s+(?<Group>.*)" | rex "Group:\s+[^\n]+\s+[^\n]+\s+Group Domain:\s+(?<GroupDomain>.*)"
`comment("4756 uses 'Account', not 'Group'")` | rex "Group:\s+[^\n]+\s+Account Name:\s+(?<Group>.*)" | rex "Group:\s+[^\n]+\s+[^\n]+\s+Account Domain:\s+(?<GroupDomain>.*)"
| rename ComputerName as "Computer", name as "EventDescription"
| table Time, GroupDomain, Group, EventCode, Member, MemberDN, ActionBy, ActionByDomain, Computer, EventDescription
Changes Made BY an Account

To search for any Group Membership changes made BY an account, you will need its: sAMAccountName and DistinguishedName. If you use just the sAMAccountName, the result will be changes made TO and BY the account. See the ‘Note on Multiple Matches‘ section below for an explanation.

Caution: The ORDER is VERY important here. Account_Name must first be sAMAccountName, then DistinguishedName. If you reverse the order, the result will be entirely different because of Account_Name having multiple matches within the same Event message.

Use the same query as “Changes Made TO an Account” (above), except change the “Account_Name” line with:

Account_Name=Mike.Yuen NOT Account_Name="CN=Mike Yuen,OU=Test,DC=YuenX,DC=com"

Search for Group Changes

This query will locate AD group creations, deletions, changes, and AD group Type changes within the last 30 days.

Group Adds, Deletes, Changes

index="*" earliest=-30d latest=now() source=WinEventLog:Security
Group_Name="Domain *"
(EventCode=4764 OR EventCode=4731 OR EventCode=4734 OR EventCode=4735 OR EventCode=4727 OR EventCode=4730 OR EventCode=4737 OR EventCode=4754 OR EventCode=4758 OR EventCode=4755)
| eval Time=strftime(_time, "%m/%d/%y %H:%M:%S") | sort -_time
| eval EventCode=case(EventCode==4799, "4799 (e) DL/BL Sec ENUM", EventCode==4764, "4764 (x) Type/Scope CHANGE", EventCode==4731, "4731 (a) DL/BL Sec CREATE", EventCode==4734, "4734 (d) DL/BL Sec DELETE", EventCode==4735, "4735 (c) DL/BL Sec CHANGE", EventCode==4727, "4727 (a) GG Sec CREATE", EventCode==4730, "4730 (d) GG Sec DELETE", EventCode==4737, "4737 (c) GG Sec CHANGE", EventCode==4754, "4754 (a) UG Sec CREATE", EventCode==4758, "4758 (d) UG Sec DELETE", EventCode==4755, "4755 (c) UG Sec CHANGE", 1=1, EventCode)
| rex "Subject:\s+[^\n]+\s+Account Name:\s+(?<ActionBy>.*)" | rex "Subject:\s+[^\n]+\s+[^\n]+\s+Account Domain:\s+(?<ActionByDomain>.*)"
| rex "Group:\s+[^\n]+\s+Group Name:\s+(?<Group>.*)" | rex "Group:\s+[^\n]+\s+[^\n]+\s+Group Domain:\s+(?<GroupDomain>.*)"
`comment("4756 uses 'Account', not 'Group'")` | rex "Group:\s+[^\n]+\s+Account Name:\s+(?<Group>.*)" | rex "Group:\s+[^\n]+\s+[^\n]+\s+Account Domain:\s+(?<GroupDomain>.*)"
| rename ComputerName as "Computer", name as "EventDescription", Change_Type as "4764_Note"
| table Time, GroupDomain, Group, EventCode, ActionBy, ActionByDomain, Computer, EventDescription, 4764_Note

Tip: For a more compact view, remove “4764_Note” from the “table” line

For changes made to Builtin Local or non-AD groups local to computers, add “Group_Domain” to the query:

  • “Users” group on “Computer01” computer: Group_Domain=”Computer01″ AND Group_Name=”Users”
  • “Administrators” of the Builtin Local group: Group_Domain=”Builtin” AND Group_Name=”Administrators”
    • Omitting Group_Domain will work too: Group_Name=”Administrators”

Search for Group and Membership Changes

This query will comb through the last 30 days (within the “MyDomain” domain) to locate all 1) AD group membership changes, including who made the change and who was added or removed, 2) AD group creations, deletions, changes, and 3) AD group Type changes.

Membership Changes and Group Adds, Deletes, Changes

index="*" earliest=-30d latest=now() source=WinEventLog:Security
Group_Name="*" AND Group_Domain="MyDomain"
(EventCode=4764 OR EventCode=4731 OR EventCode=4734 OR EventCode=4735 OR EventCode=4727 OR EventCode=4730 OR EventCode=4737 OR EventCode=4754 OR EventCode=4758 OR EventCode=4755) OR
(EventCode=4728 OR EventCode=4729 OR EventCode=4732 OR EventCode=4733 OR EventCode=4756 OR EventCode=4757)
| eval Time=strftime(_time, "%m/%d/%y %H:%M:%S") | sort -_time
| eval EventCode=case(EventCode==4799, "4799 (e) DL/BL Sec ENUM", EventCode==4764, "4764 (x) Type/Scope CHANGE", EventCode==4731, "4731 (a) DL/BL Sec CREATE", EventCode==4734, "4734 (d) DL/BL Sec DELETE", EventCode==4735, "4735 (c) DL/BL Sec CHANGE", EventCode==4727, "4727 (a) GG Sec CREATE", EventCode==4730, "4730 (d) GG Sec DELETE", EventCode==4737, "4737 (c) GG Sec CHANGE", EventCode==4754, "4754 (a) UG Sec CREATE", EventCode==4758, "4758 (d) UG Sec DELETE", EventCode==4755, "4755 (c) UG Sec CHANGE", 1=1, EventCode)
| eval EventCode=case(EventCode==4728, "4728 [+] GG Sec", EventCode==4729, "4729 [-] GG Sec", EventCode==4732, "4732 [+] DL/BL Sec", EventCode==4733, "4733 [-] DL/BL Sec", EventCode==4756, "4756 [+] UG Sec", EventCode==4757, "4757 [-] UG Sec", 1=1, EventCode)
| rex "Subject:\s+[^\n]+\s+Account Name:\s+(?<ActionBy>.*)" | rex "Subject:\s+[^\n]+\s+[^\n]+\s+Account Domain:\s+(?<ActionByDomain>.*)"
| rex "Member:\s+\w+\s\w+:\s+(?<Member>.*)" | rex "Member:\s+Security ID:[^\n]+\s+Account Name:\s+(?<MemberDN>.*)"
| rex "Group:\s+[^\n]+\s+Group Name:\s+(?<Group>.*)" | rex "Group:\s+[^\n]+\s+[^\n]+\s+Group Domain:\s+(?<GroupDomain>.*)"
`comment("4756 uses 'Account', not 'Group'")` | rex "Group:\s+[^\n]+\s+Account Name:\s+(?<Group>.*)" | rex "Group:\s+[^\n]+\s+[^\n]+\s+Account Domain:\s+(?<GroupDomain>.*)"
| rename ComputerName as "Computer", name as "EventDescription", Change_Type as "4764_Note"
| table Time, GroupDomain, Group, EventCode, Member, MemberDN, ActionBy, ActionByDomain, Computer, EventDescription, 4764_Note

Tip: For a more compact view, remove “MemberDN” and “4764_Note” from the “table” line

Note on Multiple Matches

When an Event’s message body has multiple values for the same field, some challenges will be encountered. Look at the below screenshots of Event IDs 4732 and 4764. Notice that “Security ID” and “Account Name” have multiple values:

Now, let’s look at Event ID 4732 more closely. It occurred when “Michael.Yuen” (Member) was added to the Group, “Mike-Test”. The change was made by “Mike-Admin” (Subject).

The “Security ID” field occurs three times. If you were to reference it in your search, the result would be:

YUENX\Mike-Admin
YUENX\Michael.Yuen
YUENX\Mike-Test2

In other words, the “Security_ID” array contains 3 values, and each is assigned an index number (0-2, in this case).

  • Index 0 = “YUENX\Mike-Admin”, 1 = “YUENX\Michael.Yuen”, and 2 = “YUENX\Mike-Test”

Each index can be programmatically  obtained using “mvindex”:

  • “eval FirstId=mvindex(Security_ID,0)” assigns “FirstId” = “YUENX\Mike-Admin”
  • “eval SecondId=mvindex(Security_ID,1)” assigns “SecondId” = “YUENX\Michael.Yuen”
  • “eval ThirdId=mvindex(Security_ID,2)” assigns “ThirdId” = “YUENX\Mike-Test”

You may also utilize RegEx (learn it here) to grab the values you need instead.

Read on to see why being aware that a field may have multiple values becomes crucial in finding the data you need.

Note on “Membership Changes Made TO and BY an Account”

When “Account Name” occurred twice in Group Membership Change events, such as with ID 4732 (see screenshot below), trying to see what happened with one or the other presented challenges.

Thankfully, I noticed that the name format in both Account Name fields were different:

  • Under “Subject” header (marked with “0) is “Mike-Admin”, a sAMAccountName
  • Under “Member” header (marked with “1”) is “CN=Mike Yuen,OU=Admins,DC=YuenX,DC=com”, a DistinguishedName

With that observation in mind, I was now able to create the queries I needed. One would utilize for “Account_Name” its sAMAccountName and the other would DistinguishedName.

Helpful?

Before I embarked on my mission to create Splunk queries to investigate Group Membership changes, I did not realize how involving it would become, prompting the write-up of this article. I hope that it will be useful in your security research.

Tip: For investigating Account-related events, see my Account Lockouts and Modifications post.

Credits:
– Featured Image by Chang Duong via Unsplash