Recursively Listing Security Group Members with PowerShell, Version 2
October 4, 2009 9 Comments
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:
- Comparing the domain/server portion of the group parameter to the current hostname
- Comparing the domain/server portion of the group parameter to the name of the current domain context it is running in ($Domain=[ADSI]” “)
- 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:
- 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.
- 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.