Forensics: Monitor Active Directory Privileged Groups with PowerShell

Someone just now added Jimmy to the Domain Admins group! How do I know? Because I used PowerShell to check. Let me show you how.

Some of the best customers that I visit get email pages when high value group memberships change. Obviously this is strongly encouraged for IT shops of any size. Of course you can buy products to do this, but here on my blog we build these tools ourselves. It’s more fun and FREE with PowerShell.

Finding Privileged Groups

What if someone adds a user to a group nested in Administrators? How can you tell?

There is a little feature in Active Directory called AdminSDHolder. Every hour by default the PDC will make sure that no one has attempted to escalate the privileges of groups like Server Operators, Account Operators, Administrators, etc. These protected groups have a special attribute: AdminCount = 1. Then, anyone who is a member of these groups, even if nested, gets that same attribute. That means we can find all privileged groups like this:

Get-ADGroup -Filter 'AdminCount -eq 1'

Same thing for users:

Get-ADUser -Filter 'AdminCount -eq 1'

So now we know the high-value targets in the environment.

Finding Group Membership Changes

In a previous post we explored group history using the Get-ADReplicationAttributeMetadata cmdlet in Windows Server 2012. This is one of my favorite cmdlets, especially for forensics. Go read the former post if you want to learn more about that part. You can also watch me explain how this works over at the MVA AD PowerShell videos here; look at module four on forensics.

Now let’s pair it up with what we just learned above. We can loop through all of the privileged groups, and then we’ll query the metadata to see any recent changes to group membership. Even better, we can put a filter on the date value to narrow the results to a specific timeframe.

Show Me Some ‘Shell

Here’s what the code looks like if we put it in a reusable function:

Function Get-PrivilegedGroupChanges {            
    $Server = (Get-ADDomainController -Discover | Select-Object -ExpandProperty HostName),            
    $Hour = 24            
    $ProtectedGroups = Get-ADGroup -Filter 'AdminCount -eq 1' -Server $Server            
    $Members = @()            
    ForEach ($Group in $ProtectedGroups) {            
        $Members += Get-ADReplicationAttributeMetadata -Server $Server `
            -Object $Group.DistinguishedName -ShowAllLinkedValues |            
         Where-Object {$_.IsLinkValue} |            
         Select-Object @{name='GroupDN';expression={$Group.DistinguishedName}}, `
            @{name='GroupName';expression={$Group.Name}}, *            
    $Members |            
        Where-Object {$_.LastOriginatingChangeTime -gt (Get-Date).AddHours(-1 * $Hour)}            
# Last 24 hours            
# Last week            
Get-PrivilegedGroupChanges -Hour (7*24)| Out-Gridview           
# Specific DC, 30 days (24 hours x 30 days)            
Get-PrivilegedGroupChanges -Server -Hour (24*30)            
# Last year of group changes            
Get-PrivilegedGroupChanges -Hour (365*24) |            
    Export-Csv -Path .\PrivGroupMembershipChanges.csv -NoTypeInformation            

Explore the code and the properties it returns. Adjust the output to suit your own needs.



In this screenshot we can see that the group nesting stacks up like this:

  • Guest
  • GroupLevel3
  • GroupLevel2
  • GroupLevel1
  • Domain Admins

The “AdminCount = 1” trick exposes any nesting that may make immediate group membership monitoring elusive. Now we can see that Guest is nested four levels under Domain Admins. The other columns in the report can show you the time of the change, and the DC that originated the change. Then you can review audit logs on that DC to track down who did it.

Also note in the data there is a LastOriginatingDeleteTime. If this date is any newer than the year 1601, then the change could have been a deletion instead of an addition. Study the output, and you’ll see very quickly that this could become very useful in any investigation.

Make It Your Own

Now that you see the power of stitching a few AD cmdlets together it’s your turn. Here are some ideas:

  • Pipe the output to a CSV file and attach it to an email notice using Send-MailMessage.
  • On a DC or tools server schedule your script to run hourly with a 1 hour query window.
  • Test the script by adding a member to a group and see if you can catch it. (Of course, then remove the user afterward.)
  • Try adding a user to a group nested in a privileged group. Did you catch that one?
  • Create your own view of the output, using filters and fields relevant to your needs.

Be the security hero at your company. Monitor your groups with AD PowerShell. Your boss will love the price tag, too! And you will rest easier knowing that PowerShell has your back.

“An ounce of prevention is worth a pound of cure.”

So today we learned how to monitor your privileged groups for membership changes. What if you could automatically revert any changes to these group members to enforce them and keep the bad guys out? Would you believe that is also free out-of-the-box? PowerShell Desired State Configuration can undo rogue changes to privileged groups. It would require writing a custom DSC resource. But that is a topic for another blog post…  😀

Comments (12)

  1. Matt McNabb says:

    Maybe a little off-topic but somethign I haven’t quite figured out: how does SDprop identify critical security principals? For instance in my AD, all the expected built-in principals like Domain Admins, etc are included, but a few groups that were created
    are returned as well. What identifies them as critical or protected?

  2. Matt McNabb says:

    Never mind, I see it now. It’s because one of the built-in principals is a member of the groups we created. Makes sense!

  3. DJ Grijalva says:


  4. anonymouscommenter says:

    Awesome as usual, thank you!

  5. Great post!!

    You can also use restricted groups to maintain your privileged groups. I am not sure how supported that is but it seems to work very well.

  6. anonymouscommenter says:

    awesome as always

  7. anonymouscommenter says:

    Darn, newest server we have is 2008 R2. "Get-ADReplicationAttributeMetadata" doesn’t work.

  8. LibertyGuy62 says:

    Darn, newest server we have is 2008 R2. "Get-ADReplicationAttributeMetadata" doesn’t work.

  9. anonymouscommenter says:

    Great post! I wanted to add that I used the ‘version’ numeric property (which increments with every addition and removal of a principal from the group’s membership) to determine if it was an add or delete. Since the first version is an add, the second
    a delete, the third an add, etc. you can mod the version number to determine which it is.

  10. anonymouscommenter says:

    Don’t cross the streams (objects and executables)!

  11. For those folks that don’t have the 2012 AD Replication cmdlets this should help:

    Get-ADPrivilegedGroupUpdates Function (featuring Repadmin.exe)

    Here’s my write-up:

    Use PowerShell and Repadmin to Check for Updates to High Privilege Groups


  12. anonymouscommenter says:

    Need some help and I know this is not related to this thread, hoping someone can help me. We are flattening our Active Directory structure and right now OUs are delegated with domain groups for delegated ability to write members but were going to move
    all groups into a single OU and i need to find a way to delegate these domain groups to have the ability to continue to manage those groups but not others. For example if i have 10 domain groups and i need to delegate helpdesk ability to manage 4 of them but
    operations to manage the other 6 how to write that script? Obviously I am talking about a heck of alot more then 10 groups and if it were that easy I would manually delegate each of them but I am talking about thousands.

Skip to main content