Get-ADGroupMember -Recursive Doesn't Return All Members
Posted on January 28, 2020
- and tagged as
- active-directory
This recently cropped up when I was running some auditing scripts to ensure Domain/Enterprise/etc Admins were part of the Protected Users group. It was a simple script that pulled members from the privileged groups, and compared them to members of the Protected Users group.
Quick tangent: The AD STIG recommends excluding one account from Protected Users to ensure availability if there are Kerberos issues.
I encountered some inconsistencies on domains where the Domain Admins group was a member of the Protected Users group. Some accounts were shown as missing from Protected Users. How could that be?
I determined that when a nested group is the Primary Group of a user account, the user account will not be returned by Get-ADGroupMember -Recursive
.
For example, if we have GroupA
, that has a member GroupB
, which in turn contains UserX
. If the primary group of UserX
is GroupB
, it will not be returned by the Get-ADGroupMember -Recursive
cmdlet. If the primary group is any other group, it will be returned.
The cause of this behavior relates to how AD stores information regarding group membership. If we look at AD attributes for a group, membership is stored in the member
attribute.
For GroupA
, this attribute only has a reference to GroupB
.
However, if we change the Primary Group of UserX
to GroupB
, we notice the member
attribute of GroupB
becomes empty, and instead, the primaryGroupID
attribute of the user account gets updated (5766
being a reference to the SID of the group).
Let’s jump into PowerShell and demonstrate this.
This configuration will work as expected, as the Primary Group is not GroupB
. Let’s verify:
PS C:\> Get-ADGroupMember "GroupA" -Recursive
distinguishedName : CN=UserX,OU=Test,DC=test,DC=local
name : UserX
objectClass : user
objectGUID : 9bc338f5-3256-43fa-a2d9-33a873be0d4a
SamAccountName : UserX
Perfect. Let’s change the Primary Group of UserX
and re-test.
PS C:\> Get-ADGroupMember "GroupA" -Recursive
PS C:\> (Get-ADGroupMember "GroupA" -Recursive).Count
0
While I don’t have inside knowledge of how Get-ADGroupMember
retrieves group membership, I would assume it may merely be querying the member
attribute of nested groups, potentially returning incomplete data.
Returning All AD Group Members
I’ve written a function that can be used to overcome this issue by recursively querying nested groups directly for their members. If a group is directly queried by Get-ADGroupMember
it will return all members.
The function returns an ArrayList of custom user PSObjects.
function Get-ADGroupAllMembers {
[CmdletBinding()]
Param (
[Parameter(Mandatory, ValueFromPipeline, ParameterSetName='ADGroupName')]
[string]$ADGroupName,
[Parameter(Mandatory, ValueFromPipeline, ParameterSetName='ADGroup')]
[Microsoft.ActiveDirectory.Management.ADPrincipal]$ADGroup,
[Parameter(DontShow)]
[System.Collections.ArrayList]$AllGroups = [System.Collections.ArrayList]@(),
[Parameter(DontShow)]
[System.Collections.ArrayList]$CheckedGroups = [System.Collections.ArrayList]@(),
[Parameter(DontShow)]
[System.Collections.ArrayList]$AllGroupMembers = [System.Collections.ArrayList]@()
)
if ($ADGroupName) {
try {
$ADGroup = Get-ADGroup -Identity $ADGroupName -ErrorAction Stop
$ADGroupName = $null
}
catch {
Write-Error $_.Exception.Message
break
}
}
$GroupMembers = $ADGroup | Get-ADGroupMember
$Users = $GroupMembers | ? objectClass -eq "user" | Get-ADUser -properties Enabled
$Users | % {
$User = $_ | select Name, GivenName, Surname, DistinguishedName, UserPrincipalName, Enabled, SID
$AllGroupMembers.Add($User) | Out-Null
}
$CurrentGroupSID = $ADGroup.SID.Value
$CheckedGroups.Add($CurrentGroupSID) | Out-Null
$AllGroups.Remove($ADGroup) | Out-Null
$Groups = $GroupMembers | ? objectClass -eq "group"
foreach ($Group in $Groups) {
if ($Group.SID.Value -notin $CheckedGroups) {
$AllGroups.Add($Group) | Out-Null
}
}
if ($AllGroups.Count -gt 0) {
Get-ADGroupAllMembers -ADGroup $AllGroups[0] -AllGroupMembers $AllGroupMembers -CheckedGroups $CheckedGroups -AllGroups $AllGroups
} else {
Write-Output $AllGroupMembers | Sort-Object -Property SID -Unique
}
}
Usage
As we’re using Active Directory classes, you’ll need to Import-Module ActiveDirectory
prior to running the function.
There are two ways the function can be called:
- By passing it the name of an AD group using the
-ADGroupName
parameter - By passing it an
ADPrincipal
object that references a group (for example, fromGet-ADGroup
output)
Examples
PS C:\> Get-ADGroupAllMembers -ADGroupName GroupA | select Name, Enabled | ft -AutoSize
Name Enabled
---- -------
UserX True
PS C:\> Get-ADGroup GroupA | Get-ADGroupAllMembers | select Name, Enabled | ft -AutoSize
Name Enabled
---- -------
UserX True
Long term solution
The function is aimed at getting over the immediate hurdle of needing a complete list of group members, regardless of the configured Primary Group.
The recommendation from Microsoft is to reset the Primary Group to Domain Users. There is a script at the linked page which can be used to facilitate this.
The one exception to this may be for security reasons, Domain Users does have some rights, such as adding workstations to a domain, which you may not want for things like service accounts.