Recursively Listing Security Group Members with PowerShell, Version 2

In this post, I described the use of PowerShell for the purpose of generating a report of all members of a particular group, including nested groups.  The script utilized ADSI calls to the WinNT:// provider to recursively retrieve both local and domain group members.   However, it was brought to my attention that the WinNT:// provider cannot access group objects located in non-default OU’s, but only the default Users container.   In order to access group objects located in OU’s, the ADSI queries have to be targeted to the LDAP:// provider.   This posed a bit of a challenge in correcting due to the fact that LDAP:// can’t access local group members so both the WinNT:// and LDAP:// providers had to be selectively utilized.

An updated version of this script can be downloaded here.  This version utilizes the same command line syntax:  powershell.exe c:\scripts\listusers.ps1 “Domain_or_Server_Name\Group_Name” 5.   The first parameter can be a domain name or server name followed by a backslash and the group name.  If the group name contains spaces, this parameter must be wrapped in quotes.  The second parameter is the recursion depth, how many levels of nested groups the script will traverse and report on.  The script outputs group membership details to the command window as well as a text file (located in the same directory as the script) named with the group name. 

The inner workings of this script became more complicated in this version, largely because I wanted the script to automatically select which provider to use in the ADSI calls.  The functional overview of the script is as follows:

On the first iteration, the script attempts to determine whether the initial group is a domain or local group.  It calculates this by:

  1. Comparing the domain/server portion of the group parameter to the current hostname
  2. Comparing the domain/server portion of the group parameter to the name of the current domain context it is running in ($Domain=[ADSI]” “)
  3. Performing an LDAP query to attempt to locate the group name in the current domain context (new-object DirectoryServices.DirectorySearcher([ADSI]””)). 

If the current domain name matches the domain portion of the input parameter, or if the group name is found in an ADSI search, it uses the LDAP provider to retrieve group members.  Otherwise, it uses WinNT://

After the first iteration, the logic changes a bit as it is no longer required to determine if the group to enumerate is local.   How groups are located can take one of two forms:

  1. If the starting point is a local group (or a group in a trusting domain) and the WinNT:// provider is used initially, the LDAP paths of the next groups to enumerate are not known and the script has to engage a DirectoryService.DirectorySearcher to locate the LDAP path for each group member (that is a group to be recursed) in this iteration.
  2. For any group member enumerations obtained through the LDAP provider (i.e. the starting point is a domain group, or the script is in the third iteration or later when starting with a local group), the LDAP path of the group member is retrieved along with the name.   So in the next iteration, an LDAP ADSI call can be made without having to search for the group first. 

Where this all gets even more complicated is when dealing with trusting domains.   Assuming the current user context has the appropriate rights to enumerate ADSI objects in trusting domains, there is no problem enumerating the trusting domain groups once the script is working purely within the context of the LDAP provider (i.e. the starting point is a domain group or the script is in the third iteration or later when starting with a local group).   This is because with the LDAP provider, the fully qualified LDAP path can be retrieved when enumerating the group members.   However, if the starting point is a local group and that group (or one of the first level nested groups) contains a group from a trusting domain, or the starting group is outside of the current domain context, the LDAP path of the trusting domain groups is not known, and the Directory Searcher (which works within the context of the current domain without providing a qualified domain path) will not find the group.  In this case, the script engages the WinNT:// provider to attempt to enumerate the trusting domain group members.   This will work if the trusting domain groups are in the default A/D container (Users) and not in OU’s.

So to illustrate the scenarios:

  • Starting point is a local group or domain group and all nested group membership is within the context of a single domain:   No problem
  • Starting point is a domain group and nested group membership includes local and trusting domain groups:  No problem
  • Starting point is a local group and nested group membership includes local and trusting domain groups, but the trusting domain objects are at least two levels deep in the nesting hierarchy:  No problem
  • Starting point is a local group and nested group membership includes local and trusting domain groups and the trusting domain objects are members of the local group or first nesting level, but the trusting domain groups are located in the default Users A/D container:  No problem
  • Starting point is a local group and the nested group membership includes local and trusting domain groups and the trusting domain objects are members of the local group or first nesting level, and the trusting domain groups are in non-default Organizational Units:  Problem – some groups will not be properly enumerated
  • Starting point is a domain or local group in a trusting domain (not the local domain context where the script is being executed) and any of the nested groups are in non-default Organizational Units:  Problem – some groups will not be properly enumerated

I really wanted to keep the input for the script as simple as possible and avoid any kind of requirement to manually feed the script qualified LDAP path information, so I think that some compromise in enumerating trusting domain groups in some scenarios is an acceptable price to pay for the simplicity in the execution of the script. 

One other area to note is that of rights.   If the current user context that the script is executing under does not have the rights to enumerate group membership via WinNT or LDAP ADSI calls, the script will list the group name but no membership details and keep moving on.   In a hierarchical domain forest organization, this is likely to be the case if the script enumerates groups that have a forest domain group (such as Enterprise Admins).    So if the script output lists groups with no membership information, the first thing to check would be that the current user has the appropriate rights to enumerate those objects.

Advertisements

About Kristopher Bash
Kris is a Senior Program Manager at Microsoft, working on UNIX and Linux management features in Microsoft System Center. Prior to joining Microsoft, Kris worked in systems management, server administration, and IT operations for nearly 15 years.

9 Responses to Recursively Listing Security Group Members with PowerShell, Version 2

  1. Script_newb says:

    Hey Kris.

    Thanks for the updated script. It now correctly enumerates groups in AD.

    I have run into another small issue with the script.

    When I run the script on certain groups, using 3 levels or more of recursion I get this error. ( I have changed the domain\user info for security reasons.)

    Members of the Group: OMAIN\first.last
    ———————————————–
    Exception calling “Invoke” with “2” argument(s): “The network path was not found.

    At C:\listusers-1.ps1:64 char:36
    + $Members = @($Group.psbase.Invoke <<<< ("Members"))
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : DotNetMethodException

    Essentially what happens is that in one of the nested groups, the script will run against the user accounts in the group, it parses the domain\username but drops the first letter of the domain name, reading them as though they were groups and try to enumerate the user account. The above error is the result. The script will try all members of one group and then move on per normal.

    * I removed the 'D' intentionally from the 'DOMAIN' to show you what the script does as it parses the name.

    Any ideas are welcome.

    Thanks.

    • Kristopher Bash says:

      This is interesting. When it lists the members of the parent group does it list:
      Group Domain\first.last
      or does it list
      User Domain\first.last

      In short, can you tell if the script is mistaking users for groups when it enumerates them as members, or is it mistakingly trying to enumerate an object that it initially listed as a user? If that makes sense. I tested the script in quite a number of scenarios, but obviously haven’t seen this. I appreciate you bringing this to my attention, and I’m sure the issue is easily fixable, once we figure out what the issue is exactly.

    • Leo says:

      I’m new to scripting. For my job, I sometimes have to do audits on Domain Group memberships and have been searching for a script that would make this easier for me.

      I’m getting the exact same error as mentioned above. Has anyone firgured this out yet?

      I’m restricted to PowerShell v1. Could this be part of the cause?

  2. Script_newb says:

    It correctly identifies the object as a user, then attempts to mistakenly recurse the user object as though it were a group.

  3. Script_newb says:

    Hey Kris,

    Any luck on figuring out the enumeration issue.

    Thanks.

  4. Leo says:

    I’m just now learning how to script because of a forced need at work.

    I’m having to perform audits on group membership for active directory groups. I have yet to find a script that works for me to do this but from what i can tell, this is the closest one yet.

    I am however, getting the exact same error as mentioned above. I also tried the link that was posted without any success. Has anyone come up with a solution to this yet?

    Thanks.

  5. Don says:

    i would also like to pass the server value from a file, using the default search group as the ‘local administrators’ group, and push the output to a file. [ although piping it to a csv format seems to work ]

  6. Don says:

    to elaborate a bit more, if reading the server values from a txt file. I am wondering how to spit out resulting individual files for each server listed in the txt file.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: