A Pentester’s Guide to Group Scoping

Scopes for Active Directory groups were always a bit murky for me. For anyone with an AD sysadmin background, this topic is probably second nature, but it wasn’t until I read this SS64 entry that everything started to fall into place. I wanted to document some relevant notes on the topic (as I understand it) in case anyone else had the same confusion I did. I’ll also cover how these group scopes interact with the forest global catalog and domain trusts, sprinkling in new PowerView functionality along the way.

Active Directory Groups

Active Directory groups can have one of two types: distribution groups and security groups. “Distribution groups” are used for email distribution lists and cannot be used to control access to resources, so we don’t really care about them for our purposes. Most groups are “security groups” which CAN be used to control access and added into discretionary access control lists (DACLs). Whether or not a group is a security or distribution group is stored as a bit in its groupType property, detailed after the graphic below.

Security groups can have one of three scopes. A group’s scope affects what types of group objects can be added to it and what other groups the group can be nested in. From ss64.com:

  • Global groups can be nested within Domain Local groups, Universal groups and within other Global groups in the same domain.
  • Universal groups can be nested within Domain Local groups and within other Universal groups in any domain.
  • A Domain Local group cannot be nested within a Global or a Universal group.
https://ss64.com/nt/syntax-groups.html

The groupType property is a binary bitfield, with the possible values broken out at the bottom of this page under ‘Remarks’. In order to search a binary field with LDAP, we need to use the LDAP_MATCHING_RULE_BIT_AND LDAP search syntax. This is the way to search binary fields through LDAP. Here are the filters for the associated searches:

  • Domain Local scope: ‘(groupType:1.2.840.113556.1.4.803:=4)’
  • Global scope: ‘(groupType:1.2.840.113556.1.4.803:=2)’
  • Universal scope: ‘(groupType:1.2.840.113556.1.4.803:=8)’
  • Security: ‘(groupType:1.2.840.113556.1.4.803:=2147483648)’
  • Distribution: ‘(!(groupType:1.2.840.113556.1.4.803:=2147483648))’
  • “Created by the system”: ‘(groupType:1.2.840.113556.1.4.803:=1)’

I recently pushed a commit to PowerView’s dev branch that abstracts all of this away for the Get-DomainGroup cmdlet. Here are the parameters equivalent to the above LDAP filters, as well as some negation shortcuts:

  • Domain Local scope: Get-DomainGroup -GroupScope DomainLocal
  • Not Domain Local scope: Get-DomainGroup -GroupScope NotDomainLocal
  • Global scope: Get-DomainGroup -GroupScope Global
  • Not Global scope: Get-DomainGroup -GroupScope NotGlobal
  • Universal scope: Get-DomainGroup -GroupScope Universal
  • Not Universal scope: Get-DomainGroup -GroupScope NotUniversal
  • Security: Get-DomainGroup -GroupProperty Security
  • Distribution: Get-DomainGroup -GroupProperty Distribution
  • “Created by the system”: Get-DomainGroup -GroupProperty CreatedBySystem

Here’s an example of using PowerView to search for all universal groups in the current domain:

And here’s an example of using PowerView to search for all non-domain local (i.e. universal or global) groups in the current domain:

In addition, another recent commit allows PowerView to automatically parse the groupType property (as well as userAccountControl, accountexpires, and samaccounttype) of found group results into human-readable enums, allowing for easier triage. I think these should be the last binary/non-readable default properties left for me to parse. Here’s what a single result looks like:

Security Group Scopings

Domain Local

These are the easiest to explain- these are groups that are local to the current domain. That is, domain local groups are intended to help manage access to resources within a single domain. The fact that domain local groups can’t be added to global groups is an intended design effect: (domain local) groups that grant access to specific resources can not be added to organizational groups (i.e. global groups), which can prevent some accidental group nestings that may lead to unintended access later on.

Can be nested in: only other domain local groups, from the same domain

Can contain: global groups, universal groups, and foreign trust members

Can be assigned permissions in: the same domain

Memberships replicated in the global catalog: no

tl;dr: if you want a group that can grant access only to resources in the same domain, but can contain any other group scope (including users across an external trust) use a domain local scope.

Global

Global groups are probably the trickiest of the three scopes to understand. They are usually used as an organizational structure for users who share comparable network access requirements. Global groups also can not be nested across domains, meaning a global group from one domain can’t be nested in a group in another domain. Also, users/computers from one domain can’t be nested in a global group in another domain, which is why users from one domain aren’t eligible for a membership in “Domain Admins” in a foreign domain (due to its global scope). Because global groups are not replicated in the global catalog (terrible naming conflict, I know) you can modify the membership of global groups without causing replication traffic to other domains in the forest.

Can be nested in: universal and domain local groups

Can contain: only global groups, from the same domain

Can be assigned permissions in: any domain

Memberships replicated in the global catalog: no

tl;dr: if you want a group that can be used in any domain in a forest or trusting domain, but can only contain users from that group’s domain, use a global scope.

Universal

If you need a group that contains members from one or more domains within the same forest, and can be granted access to any resource in that forest, you need a universal scope. For nested group membership, all groups can be members of the same group type (for global this only applies to other global groups in the same domain). For universal groups specifically, any changes in the membership will propagate to the global catalog. I’ll cover all of these global catalog interactions in the next section.

Can be nested in: domain local groups and other universal groups

Can contain: global groups and other universal groups

Can be assigned permissions in: any domain or forest

Memberships replicated in the global catalog: yes

tl;dr: if you want a group that can be given access to anything in the forest, and can contain any user/group/computer in the forest, use a universal scope.

The Global Catalog

The global catalog is a partial copy of all objects in an Active Directory forest, meaning that some object properties (but not all) are contained within it. This data is replicated among all domain controllers marked as global catalogs for the forest. One point of the global catalog is to allow for object searching and deconfliction quickly without the need for referrals to other domains (more information here). The nice side effect from an offensive perspective is that we can quickly query information about all domains and objects in a forest with simple queries to our primary domain controller, but more on this later.

The properties that are replicated are marked in the forest schema as the “partial attribute set”. You can easily enumerate these property names with PowerView:

Get-DomainObject -SearchBase "CN=Schema,CN=Configuration,DC=testlab,DC=local" -LDAPFilter "(isMemberOfPartialAttributeSet=TRUE)" | Select name

Sidenote: the initial global catalog is generated on the first domain controller created in the first domain in the forest. The first domain controller for each new child domain is also set as a global catalog by default, but others can be added.

Finding Global Catalogs

Before you can interact with the global catalog it helps to know where all of them are. There are obviously options through tools like nslookup and dsquery, but we’ll use a bit of PowerShell again. .NET has this functionality nicely built in:

$Forest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
$Forest.FindAllGlobalCatalogs()

With PowerView, you can use Get-ForestGlobalCatalog:

Searching the Global Catalog

To search a global catalog with PowerView, replace “LDAP://…” with “GC://” when specifying an LDAP search string for -SearchBase. In practice you can usually use “-SearchBase “GC://domain.com” which will map to the global catalog for that domain. This is usually my preference. For an operational example, here is how you could enumerate ALL computer DNS host names in the current forest by using the PRIMARY.testlab.local parent domain global catalog as shown above:

And here are the results from running the same query against another global catalog in another domain the same forest, SECONDARY.dev.testlab.local:

Group Scopes and the Global Catalog

Group scopes matter when it comes to global catalog replication. The group memberships of domain local groups and global groups are not replicated to the global catalog, although the group objects themselves are. Universal groups are replicated along with their full memberships. Stated in another way by Microsoft, “Groups with universal scope, and their members, are listed exclusively in the global catalog. Groups with global or domain local scope are also listed in the global catalog, but their members are not.” So let’s try to tease out the operational implications from an offensive perspective.

One nice side effect: we can easily enumerate the members of ANY universal group for ANY domain in a forest by just communicating with a domain controller in our same domain. This means that we only initiate traffic with a domain controller in our current domain. With PowerView we can enumerate all groups with memberships for a global catalog in testlab.local with:

Get-DomainGroup -SearchBase "GC://testlab.local" -Properties grouptype,name,member -LDAPFilter '(member=*)'

These results demonstrate the differences in replication. Above there is only one group from dev.testlab.local that returned with membership results, due to its universal scope. Here are the complete group membership results through straight LDAP/non-global catalog querying. You can see the additional group results, as well as their global/domain local scopes:

Get-DomainGroup -LDAPFilter '(member=*)' -Domain dev.testlab.local -Properties distinguishedname,grouptype | fl

There is one slight ‘exception’ to this, which Microsoft explains here. I’ll talk about this more in an upcoming post, but member/memberof are linked attributes, meaning that the value of memberof (the back link) is calculated on the value of member (the forward link). Because the membership of universal groups are replicated in the global catalog, a user’s membership in a universal group should be replicated to all GCs in the forest. However, because the membership data for domain local/global groups is not replicated to the global catalog, memberof results for users will vary, as those backlinks may not be able to be traced. Microsoft confirms this behavior: “Because of the way that groups are enumerated by the Global Catalog, the results of a back-link [i.e. memb search can vary, depending on whether you search the Global Catalog (port 3268) or the domain (port 389), the kind of groups the user belongs to (global groups vs. domain local groups), and whether the user belongs to groups outside the local domain“.

From what I can tell, if you bind to a global catalog in the same domain that the group membership the user is a part of, those (domain local or global) group memberships will populate memberof. Just remember that those results will vary depending on the domain/global catalog you bind to.

Group Scoping and External Trusts

Users that exist in external or forest trusts, external from the domain’s current forest, can still be added to domain local groups in the specified domain. These users show up as new entries in CN=ForeignSecurityPrincipals,DC=domain,DC=com. Or, as Microsoft explains it, “When a trust is established between a domain in a forest and a domain outside of that forest, security principals from the external domain can access resources in the internal domain. Active Directory creates a foreign security principal object in the internal domain to represent each security principal from the trusted external domain. These foreign security principals can become members of domain local groups in the internal domain“.

Remember that “security principals” means either groups, users, or computers, i.e. anything with a security identifier. You can enumerate these members quickly by setting the SearchBase for a search to be “CN=ForeignSecurityPrincipals,DC=domain,DC=com”.  For example:

You can see the two foreign domain SIDs at the bottom of those results. If any of these foreign users are members of groups in your target domain, the Get-DomainForeignGroupMember function should tease these out as well. But remember that the only way this is possible is if the group is a domain local scope:

Offensive Operations

I previously covered using the global catalog for command and control in and Active Directory environment (something that inspired a BlackHat talk ;) but there are a few additional reasons that I can think of to use the global catalog offensively when dealing with a multi-domain forest. The first is if you get a plain samaccountname for a user/group/computer and want to know what domain the account resides in. This is what we do with the BloodHound ingestor, as the samaccountnames returned from a Get-NetSession result don’t contain domain names. In fact, this is one of the main reasons the global catalog was built, for object deconfliction:

Another option is to quickly enumerate all objects of a certain type throughout the forest, i.e. get all computer DNS names for the entire setup, as we saw in the “Searching the Global Catalog” section. So let’s go a step further and enumerate all Kerberoastable accounts in the entire forest, since the servicePrincipalName property is replicated:

This also applies to domain trusts! Trusts can be enumerated through LDAP with the ‘(objectClass=trustedDomain)’ filter, so with some recent mods for PowerView we can run the following to quickly enumerate all domains in the trust mesh:

Note that this won’t be quite as accurate as Get-DomainTrustMapping (old Invoke-MapDomainTrust), but it’s a hell of a lot faster.

Unfortunately, again due to how the member/memberOf properties are linked, if a user is added to a group in a foreign domain/forest (i.e. not in a domain in the same forest) the member property of the particular group is updated with the foreign security principal distinguishedname, but the memberOf field for the user/group object added is not updated. Also, these foreign members can only be added to domain local groups, which are not replicated in the global catalog.

So we can use the global catalog to enumerate some of the inner-forest but cross-domain memberships, but for external/forest foreign memberships we’ll have to search the CN=ForeignSecurityPrincipals container domain by domain. Luckily for us, this data is replicated in the global catalog! Just use -LDAPFilter ‘(objectclass=foreignSecurityPrincipal)’:

Get-DomainObject -Properties name,objectsid,distinguishedname -SearchBase "GC://dev.testlab.local" -LDAPFilter '(objectclass=foreignSecurityPrincipal)' | ? {$_.objectsid -match '^S-1-5-.*-[1-9]\d{2,}$'} | fl

Note that we are current in the dev.testlab.local, and used that child domain’s global catalog, but were still able to enumerate the ForeignSecurityPrincipals in testlab.local.

Since we now know that external trust users can only be added to groups with a domain local scope, we can extract the domain the foreign user was added to from the distinguishedname, query that domain directly for domain local-scoped groups with members, and compare each against the list of foreign users:

# query the global catalog for foreign security principals with domain-based SIDs, and extract out all distinguishednames
$ForeignUsers = Get-DomainObject -Properties objectsid,distinguishedname -SearchBase "GC://dev.testlab.local" -LDAPFilter '(objectclass=foreignSecurityPrincipal)' | ? {$_.objectsid -match '^S-1-5-.*-[1-9]\d{2,}$'} | Select-Object -ExpandProperty distinguishedname
$Domains = @{}

$ForeignMemberships = ForEach($ForeignUser in $ForeignUsers) {
    # extract the domain the foreign user was added to
    $ForeignUserDomain = $ForeignUser.SubString($ForeignUser.IndexOf('DC=')) -replace 'DC=','' -replace ',','.'
    # check if we've already enumerated this domain
    if (-not $Domains[$ForeignUserDomain]) {
        $Domains[$ForeignUserDomain] = $True
        # enumerate all domain local groups from the given domain that have any membership set
        Get-DomainGroup -Domain $ForeignUserDomain -Scope DomainLocal -LDAPFilter '(member=*)' -Properties distinguishedname,member | ForEach-Object {
            # check if there are any overlaps between the domain local groups and the foreign users
            if ($($_.member | Where-Object {$ForeignUsers  -contains $_})) {
                $_
            }
        }
    }
}

$ForeignMemberships | fl

Wrap Up

I’m sure this topic may have been a bit dry for some, but I hope this helps those interested to clear up some of the same misunderstandings I had. Domain group scoping provides a few interesting offensive opportunities, imposes a few restrictions, and has implications for the architecture of a red forest (more on this another time ;) Hopefully you got a few operationally useful details out of this post that help on engagements going forward.

And as always, if I made a mistake somewhere in this post, please let me know and I’ll edit in a correction!

3 thoughts on “A Pentester’s Guide to Group Scoping”

  1. Alexandru Parpalea

    Nice article.

    Is there a way to find out if a Foreign Security Principal represents a user or a group without querying the trusted domain?

    For example, i have Group1 in Domain1 that contains Group2 from Domain2., when i perform a LDAP query, using DirectorySearcher, to Domain1 i only get a SID and a distinguished name. The objectClass is foreignSecurityPrincipal and objectCategory is CN=Foreign-Security-Principal…

    I found that the “msds-principalname” property gives me the Domain\Name but i couldn’t find anything that says if it is a group or a user.

    Is it possible to find out without performing another query to Domain2, another property that i should load?

  2. Is it possible to manually add a user pointer to the ForeignSecurityPrincipals container? I have been unable to. I keep getting error 65 – Object class violation.

  3. Pingback: Active Directory Kill Chain Attack 101 – syhack

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.