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 : UserXPerfect. Let’s change the Primary Group of UserX and re-test.
PS C:\> Get-ADGroupMember "GroupA" -Recursive
PS C:\> (Get-ADGroupMember "GroupA" -Recursive).Count
0While 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
-ADGroupNameparameter - By passing it an
ADPrincipalobject that references a group (for example, fromGet-ADGroupoutput)
Examples
PS C:\> Get-ADGroupAllMembers -ADGroupName GroupA | select Name, Enabled | ft -AutoSize
Name Enabled
---- -------
UserX TruePS C:\> Get-ADGroup GroupA | Get-ADGroupAllMembers | select Name, Enabled | ft -AutoSize
Name Enabled
---- -------
UserX TrueLong 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.