Where My Admins At? (GPO Edition)

[Edit 6/14/16] I was mistaken on a few points in the Local Account Management – Restricted Groups section, which I have now corrected. Thanks to @DougSec for the question/catch.

Enumerating the membership of the Administrators local group on various computers is something we do on most of our engagements. This post will cover how to do this with Group Policy Object (GPO) correlation and without sending packets to every machine we’re enumerating these memberships for. I touched on this briefly in the Tracking Local Administrators by Group Policy Objects section of my “Local Group Enumeration” post back in March, but with a number of recent bug fixes in the development branch of PowerView and a better understanding of the problem, I wanted to revisit the topic. I’m going to dive a bit deeper this time around and explain the full implications and associated challenges.

Long story short, you can identify machines in a domain where a given user or group is a member of a specified local group (‘Administrators’ or ‘Remote Desktop Users’) by only communicating with a domain controller. You can also figure out the members of those local groups for a particular machine with only domain controller communication and get a complete mapping of all object -> machine local group memberships. If you don’t know why this would be incredible useful on an engagement, check out these posts before continuing.

Before I get into the really cool effects, I’m going to have to cover some background so the entire approach makes sense. If you’re impatient/just want results/don’t care how the process works, feel free to jump to PowerView and GPOs and Operational Usage sections.


This all started when Skip Duckwall (@passingthehash) pinged me about correlating GPO and OU objects, with the goal of figuring out what machines a particular group policy Globally Unique Identifier (GUID) applied to. This was so we could figure out what machines a particular GPP password set instead of spraying passwords across a network and wishing for the best. We’re fans of data analysis and targeted compromise rather than mass pwnage, and the “GPP and PowerView” post covered how to trace a GPP GUID name back to the computers under the realm of a certain policy. Thumbs up, this helped us a lot on several engagements.

Last fall while on an engagement I ran into a specific situation that warranted some on-the-spot development. We were able to compromise a number of cross-trust domain accounts but we could only reach computers in the target domain through RDP due to network restrictions. Our normal Get-NetLocalGroup trickey didn’t work so we couldn’t figure out where these compromised accounts could log into remotely. I banged my head against a wall for a bit but had a breakthrough after some back and forth with Sean Metcalf (@pyrotek3). But first, let me explain what happens with a computer boots up, and how this process interfaces with group policy.

Bootstrapping Group Policy

When a machine starts, it needs some way to determine what accounts have what rights on it in addition to what other policies should be applied. While some accounts are manually added to the local administrators group of specific machines, organizations beyond a certain size need to assign administrative roles in an automated fashion. So how does the machine determine this when booting up?

When a machine boots but before any user logs in, any network access attempted by processes running as SYSTEM takes on the privileges of the local machine account. This is because a machine needs to be able to authenticate to its domain controller and retrieve group policy information before any user logs in, partially in order to determine who can log in! This also explains why machine accounts can authenticate on the domain and why you can execute Get-GPPPassword without any users logged into the system, something I was always curious about.

So after the computer starts up and authenticates to its domain controller two things happen. The DC will determine what organizational unit (OU) the machine is a member of using existing Active Directory database information. If sites/subnets are configured in AD as well, and the IP address the computer has when it first communicates to the domain controller falls under a configured subnet, the computer also retrieves information for the associated subnet. OUs are static for machines, while sites/subnets can be flexible depending on where the machine turns up in the network.

The client will then enumerate information for its OU (and possibly site) and will determine the group policies it should apply. How does the client determine this? By enumerating the gPLink attribute of the returned OU/site objects, which is a standard attribute stored for these types of objects. The associated group policy settings are then retrieved by the computer and applied on boot. This is because a given GPO is applied (and linked) to either a particular organizational unit or a site.


Local Account Management – Restricted Groups

There are two ways to manage local accounts through built in Active Directory functionality: Restricted Groups and Group Policy Preferences.

Restricted Groups is more of the old school method but many organizations still take advantage of it. I don’t know exactly why but I couldn’t quite get my head around this approach until reading this post by Morgan Simonsen. In essence, a ‘Restricted Groups’ setting in group policy (GPO\Computer Configuration\Windows Settings\Security Settings\Restricted Groups) lets you modify the memberships of sensitive groups on a host (think local Administrators or ‘Remote Desktop Users’). These settings are stored in a file named GptTmpl.inf, an .ini type file stored in the $GPOPath\MACHINE\Microsoft\Windows NT\SecEdit\ folder in SYSVOL.

There are two ways that users/groups can be set as local members through this approach. If you set the group to be local ‘Administrators’ (SID: S-1-5-32-544) and set its members to be particular domain users or groups, that will wipe the existing members of that group and add the new set. Here’s an example:


Here’s a kink- when administrators add a user/group to the members of a group, they can either type the name or ‘lookup’ the user/group value to resolve it to a proper SID. If the name is typed that raw name will be a part of the specification, but if the object is ‘looked up’ then the object *SID will be the data. Here’s what the GptTmpl.inf file looks like for the setting by the previous screenshot:


Conversely, if you won’t want to modify the existing membership of ‘Administrators’, [Edit 6/14/16] you can create a new group (say ‘BackupAdmins’), add domain members to it, you can set the ‘Group Name’ to be an already created domain group and set the memberof for the group to be ‘Administrators’. This will add that domain group (and consequently all of its members) to ‘Administrators’:



[Edit 6/14/16]: because I didn’t do my homework properly, I didn’t realize that Microsoft does not allow nesting of local groups, as explained here. A well known local group like ‘Administrators’ and ‘Remote Desktop Users’ can have its members modified, and a well known domain group can have a memberof set through Restricted Groups, but no other scenarios are possible. The following chart from Microsoft shows the possible combinations:


So if we want to combine and correlate all of this information, what do we really care about?

We want the *S-1-5-32-544__members (‘Administrators’) and the name/SID of any domain group with a ‘GROUP__memberof = *S-1-5-32-544’ set, meaning that group is a member of local administrators. Keep in mind that I’m focusing on the BUILTIN\Administrators group here but this is the same for ‘Remote Desktop Users’ (SID: S-1-5-32-555) or any other local group SID you specify.

Local Account Management – Group Policy Preferences

‘Restricted Groups’ isn’t the only game in town with determining local group membership; Group Policy Preferences (GPP) is the new(er) kid on the block. Many pentesters have heard of GPP, but often only in the Get-GPPPassword sense to enumerate poorly managed local passwords. Group Policy Preferences is way more than just local password manipulation and Groups.xml files hold a lot more value for pentesters than many of us have realized.

Groups.xml can fully determine/manipulate the local user and group memberships for any computers the policy is applied to. Groups can be Created, Replaced, Updated, or Deleted, and the group name itself can be modified with Rename to. Local users/groups can be ADDed or REMOVEd from groups and the existing group membership can be wiped with Delete all member users/groups. Like with ‘restricted groups’, a username can be added raw or ‘looked up’ to resolve it to a proper SID:


This brings me to what I really don’t enjoy (from an offensive perspective) about Group Policy Preferences, filters, accessed through Item-level targeting in the interface. These settings allow you specify granular checks that a host can use when determining whether to apply a pushed Group.xml policy. The simplest filters and commons ones we’ve see are Computer Name and Organizational Unit, but there are a number of other options you can specify:


The standard ‘restricted groups’ policy is fairly narrow in its flexibility, and is limited to domain, site, and/or OU linking options with an option for layering some WMI trickery for additional targeting. Group Policy Preferences allow for much more granular targeting when setting these local groups (as you can see in the screenshot above); however, this makes it a bit more complicated when trying to perform mass enumeration of what machines a particular policy applies to.

PowerView and GPOs

So given all of this information, let’s pull everything together and get a result that we can use for offensive engagements. Let’s say we want to determine what machines where a particular user or group is a member of local administrators (or again, any other well-known local group SID). The PowerView function that executes this functionality is Find-GPOLocation. Note: this is currently only in the development branch of PowerSploit/PowerView.

The first step is to figure out the target SID set we’re going after. If a user or group name is passed, this means retrieving the associated user/group object with Get-NetUser or Get-NetGroup (respectively) and then determining the SIDs of all groups the target object is a part of. This is done with Get-NetGroup -UserName $ObjectSamAccountName, which takes advantage of the ‘TokenGroups‘ constructed attribute. TokenGroups is, “A computed attribute that contains the list of SIDs due to a transitive group membership expansion operation on a given user or computer“, meaning the result isn’t a standard LDAP attribute we can query but it still possible through AD Directory Entries. Here’s how the PowerView code does it, and there’s more information here for anyone interested.

Once we have the set of all SIDs the target user/group is a part of the next step is to pull all ‘GPO set’ groups where GPOs (through restricted groups or GPP groups.xml) determine who is a member of ‘Administrators’. The PowerView function that executes this is Get-NetGPOGroup and here’s how it works:

  • All GPOs are enumerated for the current (or target) domain using Get-NetGPO.
  • For each GPO returned we first check if ‘$GPOPath\MACHINE\Microsoft\Windows NT\SecEdit\GptTmpl.inf’ exists, and if it does we parse the results with Get-GptTmpl. This function wraps Get-IniContent from ‘The Scripting Guys‘.
  • Parse out each element of ‘Group Membership’ returned, properly splitting up the found ‘X__members’ fields and translating found usernames to SIDs if the -ResolveMemberSIDs flag is passed. Also check if any group has “Y__memberof” set, and extract out the group name.
  • A custom object is returned that contains the GPO information (display name, GUID, path, etc.) along with the group name, translated group SID, and the memberof/members fields.
  • Then we check if ‘$GPOPath\MACHINE\Preferences\Groups\Groups.xml’ exists and parse the any Groups.xml files with Get-GroupsXML similarly to GptTmpl.inf files. Another custom object is returned per Groups.xml group membership with similar information.

This gives us all GPOs that set some kind of local group membership in the domain. This is what that data looks like for my sample environment:


For a particular user or group, we can then match up the target SID set with these results, determining what GPOs set local group membership with the target we’re after (matching GroupMembers if its set, otherwise the GroupSID if memberof if set). If no user/group is passed, all results are used so we can produce a complete object -> computer mapping.

Then for each ‘GPOGroup’ object we first get all organizational units with this GPO applied by executing Get-NetOU -GUID $GPOguid. Again, this takes advantage of the gPLink attribute and returns full OU objects. We then use Get-NetComputer with -ADSPath set to the OU path to pull all computers that are a part of the given OU. In the case of filters for Groups.xml files, we try to filter the results based on the specific criteria (this area of PowerView definitely needs work/expansion to properly cover additional filters).

Finally, we enumerate all sites with the GPO linked as well using Get-NetSite -GUID $GPOguid to take advantage of gPLink yet again. All results are returned as custom objects that include associated object, GPO, and computer information.

Here’s the condensed process once more:

  1. Resolve the user/group to its proper SID
  2. Enumerate all groups the user/group is a current part of and extract all target SIDs to build a target SID list
  3. Pull all GPOs that set ‘Restricted Groups’ or Groups.xml by calling Get-NetGPOGroup
  4. Match the target SID list to the queried GPO SID list to enumerate all GPOs that set local group memberships that include the target user/group
  5. Enumerate all OUs and sites that applicable GPO GUIDs are applied to through gPLink enumeration
  6. Query for all computers under the given OUs or sites

Here’s how the results look for specifying a particular user:


If a user/group name is not passed, we just return all mapping results instead of enumerating a TokenGroups and filtering by that SID set:


Since the returned ComputerName property is an array, if you want to export all this data to a CSV you need to do something like:

Find-GPOLocation | %{$_.ComputerName = $_.ComputerName -join ', '; $_} | Export-CSV -NoTypeInformation gpo_map.csv

If you want to determine the members of GPO set groups for a particular machine without sending packets to the target, you can use Find-GPOComputerAdmin instead of Get-NetLocalGroup. It does the inverse of Find-GPOLocation‘s functionality, so I won’t cover it in detail.

And a final note: this approach will not enumerate local group membership already set on particular machines, such the “Domain/Enterprise Admins” groups. It will only enumerate modifications to local groups set through group policy.

Operational Usage

If you’re still reading (or skipped ahead) you might be asking, “So what, how can I use this on an engagement, and why should I?”

We know all about limited time frame engagements, where sometimes you have to slam stuff through in a limited window in order to satisfy clients. Part of our goal at the Adaptive Threat Division with PowerView and other code is to help ‘bridge the gap‘ between pentesting and traditional red team operations by bringing this tradecraft to a wider audience.

So if you nab a token of a user that you think may have elevated privileges on other machines, try not running Find-LocalAdminAccess or spraying hashes/credentials around the network, rather take a step back and do a bit of data analysis with this new functionality. Find-GPOLocation can help you determine where your current or target rights can log in, allowing you to do more targeted compromise instead of mass pwnage.

And in case it wasn’t clear, you can gather all of this information from an unprivileged user context. Another nice side effect is that due to PowerView’s modular nature, you can already take advantage of all this functionality for cross-domain trust situations. Just pass -Domain X to Find-GPOLocation or Find-GPOComputerAdmin and the backend code should take care of you.

Endnote: Find-GPOLocation vs. Get-NetLocalGroup

So why use this approach over Get-NetLocalGroup? Find-GPOLocation/Find-GPOComputerAdmin will only enumerate changes to the local administrative groups that are pushed out through group policy, while Get-NetLocalGroup will capture the ‘ground truth’ through the WinNT service provider or the NetLocalGroupGetMembers() Win32 API call.

Luckily for us, local group modification through group policy is the most common way that larger organizations manage these components at scale, but there can be some exceptions. If users/groups are added to a system’s local administrator group manually or on some kind of  ‘gold image’ before the machines are deployed, the GPO correlation approach will not capture this. Also, if there’s some kind of third party software solution that manages local member passwords/group memberships then the GPO approach will provide another false negative. If you want to be 100% sure of a system’s local memberships, Get-NetLocalGroup [-API] will always provide the most accurate information.

So why build this then, why not just run Get-NetLocalGroup on every machine in the domain? For one, manually enumerating local groups on each machine can take a very long time in certain environments. We have threading options for this approach but you still have to reach out and communicate with each machine and these operations can be greatly slowed down with timeouts. Also, touching every machine is more likely to get you caught. This can even look like worm traffic to some internal network heuristics, with one machine touching every other it can find as quickly as possible over common Microsoft ports. And finally, as with the original motivation I mentioned for writing this functionality, you might not be able to directly reach all machines in a network with reasonable network segmentation. Luckily Find-GPOLocation/Find-GPOComputerAdmin can be reflected through specific domain controllers to get around this restriction ;)

So which method you use is going to depend on whether you’re trying to map massive local group memberships or specific machine information, what the network restrictions look like, your engagement time frame, tolerance for false negatives, and other environment specific factors. GPO local group correlation is a powerful weapon in the offensive arsenal and we hope to get feedback from anyone using it in the field!

1 thought on “Where My Admins At? (GPO Edition)”

  1. Just chiming in to say that as a relative newcomer/pen-tester-in-training to the world of offensive security, I’m finding a lot of stuff here tremendously informative and useful. (Which is not to claim that I understand 100% of the technical details of what I read in each post… at, the very least, not yet. But the is well-written enough that the vital points are coming across despite the knowledge gap. I think.) And this post in particular is an example of one I find myself rereading and studying; I may have known coming in that one can use facilities in Empire/ Powerview/other tools to find where a user can login in an Active Directory domain, but I really didn’t understand how and why those methods work. Or, for that matter, the real details how a machine gets Group Policy applied to it properly during boot. So… you definitely have my thanks for the insightful treatment.

    One question, if you perhaps happen to see this (or if someone else who knows the answer sees this): I’ve heard a couple of non-specific statements in some conference talks recently that in Windows Server 2016 Microsoft is (finally) starting to combat the ability of unprivileged users to easily do info gathering of what users can do what & where just by talking to the DC. Does any of that work potentially impact the use of Find-GPOLocation/Find-GPOComputerAdmin as you describe here?

    Again, great stuff. Thanks.

Leave a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.