Private/AD/Core/Get-ADPrivilegedMembers.ps1
|
# PSGuerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0 # https://github.com/jimrtyler/PSGuerrilla | https://creativecommons.org/licenses/by/4.0/ # AI/LLM use: see AI-USAGE.md for required attribution function Get-ADPrivilegedMembers { [CmdletBinding()] param( [Parameter(Mandatory)] [hashtable]$Connection, [switch]$Quiet ) $domainDN = $Connection.DomainDN $result = @{ PrivilegedGroups = @{} AllPrivilegedUsers = @() AdminSDHolderACL = $null AdminCountOrphans = @() KrbtgtAccount = $null ProtectedUsersMembers = @() Errors = @{} } # ── Member properties to retrieve ───────────────────────────────── $memberProperties = @( 'sAMAccountName', 'distinguishedName', 'objectClass', 'objectSid', 'userAccountControl', 'pwdLastSet', 'lastLogonTimestamp', 'adminCount', 'memberOf', 'servicePrincipalName', 'whenCreated', 'displayName', 'description' ) # ── Helper: Get the DN of a group by its well-known SID/RID ─────── $domainSidString = '' try { $domainRoot = New-LdapSearchRoot -Connection $Connection -SearchBase $domainDN $domainObj = Invoke-LdapQuery -SearchRoot $domainRoot ` -Filter '(objectClass=domainDNS)' ` -Properties @('objectSid') ` -Scope Base if ($domainObj.Count -gt 0 -and $domainObj[0].ContainsKey('objectsid')) { $domainSidString = $domainObj[0]['objectsid'] } } catch { Write-Verbose "Failed to retrieve domain SID: $_" $result.Errors['DomainSID'] = $_.Exception.Message } # Helper function to find a group DN by SID $findGroupBySid = { param([string]$SidString, [System.DirectoryServices.DirectoryEntry]$SearchRoot) try { $sidObj = New-Object System.Security.Principal.SecurityIdentifier($SidString) $sidBytes = $sidObj.GetSidBytes() $escapedSid = ($sidBytes | ForEach-Object { '\' + $_.ToString('x2') }) -join '' $results = @(Invoke-LdapQuery -SearchRoot $SearchRoot ` -Filter "(objectSid=$escapedSid)" ` -Properties @('distinguishedName', 'cn', 'sAMAccountName') ` -SizeLimit 1) if ($results.Count -gt 0) { return $results[0] } } catch { Write-Verbose "Failed to find group by SID $SidString`: $_" } return $null } # Helper function to find a group DN by name $findGroupByName = { param([string]$GroupName, [System.DirectoryServices.DirectoryEntry]$SearchRoot) try { $results = @(Invoke-LdapQuery -SearchRoot $SearchRoot ` -Filter "(&(objectClass=group)(sAMAccountName=$GroupName))" ` -Properties @('distinguishedName', 'cn', 'sAMAccountName') ` -SizeLimit 1) if ($results.Count -gt 0) { return $results[0] } } catch { Write-Verbose "Failed to find group by name $GroupName`: $_" } return $null } # ── Define privileged groups to enumerate ───────────────────────── # Domain-relative groups use the domain SID + RID # Builtin groups use well-known SIDs $privilegedGroupDefs = [ordered]@{} if ($domainSidString) { $privilegedGroupDefs['Domain Admins'] = @{ SID = "$domainSidString-512"; RID = 512 } $privilegedGroupDefs['Enterprise Admins'] = @{ SID = "$domainSidString-519"; RID = 519 } $privilegedGroupDefs['Schema Admins'] = @{ SID = "$domainSidString-518"; RID = 518 } } else { # Fallback: search by name if we cannot construct SID $privilegedGroupDefs['Domain Admins'] = @{ Name = 'Domain Admins' } $privilegedGroupDefs['Enterprise Admins'] = @{ Name = 'Enterprise Admins' } $privilegedGroupDefs['Schema Admins'] = @{ Name = 'Schema Admins' } } # Builtin groups (well-known SIDs) $privilegedGroupDefs['Administrators'] = @{ SID = 'S-1-5-32-544'; Builtin = $true } $privilegedGroupDefs['Account Operators'] = @{ SID = 'S-1-5-32-548'; Builtin = $true } $privilegedGroupDefs['Server Operators'] = @{ SID = 'S-1-5-32-549'; Builtin = $true } $privilegedGroupDefs['Print Operators'] = @{ SID = 'S-1-5-32-550'; Builtin = $true } $privilegedGroupDefs['Backup Operators'] = @{ SID = 'S-1-5-32-551'; Builtin = $true } # DnsAdmins: no well-known builtin SID; RID is typically 1101 but not guaranteed if ($domainSidString) { $privilegedGroupDefs['DnsAdmins'] = @{ SID = "$domainSidString-1101"; RID = 1101; FallbackName = 'DnsAdmins' } } else { $privilegedGroupDefs['DnsAdmins'] = @{ Name = 'DnsAdmins' } } # ── 1. Resolve group DNs and enumerate recursive members ────────── $allPrivilegedUsersDN = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) $allPrivilegedUsersMap = @{} # DN -> member object foreach ($groupEntry in $privilegedGroupDefs.GetEnumerator()) { $groupLabel = $groupEntry.Key $groupDef = $groupEntry.Value if (-not $Quiet) { Write-ProgressLine -Phase AUDITING -Message 'Enumerating privileged group' -Detail $groupLabel } # Resolve the group DN $groupObj = $null $groupDN = '' # Determine search root based on whether this is a builtin group $searchRootDN = if ($groupDef.ContainsKey('Builtin') -and $groupDef.Builtin) { "CN=Builtin,$domainDN" } else { $domainDN } try { $groupSearchRoot = New-LdapSearchRoot -Connection $Connection -SearchBase $searchRootDN if ($groupDef.ContainsKey('SID') -and $groupDef.SID) { $groupObj = & $findGroupBySid $groupDef.SID $groupSearchRoot } # Fallback to name search if SID lookup failed if (-not $groupObj) { $fallbackName = if ($groupDef.ContainsKey('FallbackName')) { $groupDef.FallbackName } elseif ($groupDef.ContainsKey('Name')) { $groupDef.Name } else { $groupLabel } # Search from domain root for name-based lookups $domainSearchRoot = New-LdapSearchRoot -Connection $Connection -SearchBase $domainDN $groupObj = & $findGroupByName $fallbackName $domainSearchRoot } } catch { Write-Verbose "Failed to resolve group $groupLabel`: $_" $result.Errors["Group_$groupLabel"] = $_.Exception.Message } if (-not $groupObj) { Write-Verbose "Group '$groupLabel' not found in this domain" $result.PrivilegedGroups[$groupLabel] = @() continue } $groupDN = $groupObj['distinguishedname'] Write-Verbose "Resolved group '$groupLabel' to DN: $groupDN" # Use LDAP_MATCHING_RULE_IN_CHAIN for recursive membership # This returns all objects that are transitively a member of the group $memberFilter = "(memberOf:1.2.840.113556.1.4.1941:=$groupDN)" $members = @() try { $memberSearchRoot = New-LdapSearchRoot -Connection $Connection -SearchBase $domainDN $members = Invoke-LdapQuery -SearchRoot $memberSearchRoot ` -Filter $memberFilter ` -Properties $memberProperties } catch { Write-Verbose "Failed to enumerate members of $groupLabel`: $_" $result.Errors["Members_$groupLabel"] = $_.Exception.Message } # Also check builtin container for nested builtin group members if ($groupDef.ContainsKey('Builtin') -and $groupDef.Builtin) { try { $builtinSearchRoot = New-LdapSearchRoot -Connection $Connection -SearchBase "CN=Builtin,$domainDN" $builtinMembers = Invoke-LdapQuery -SearchRoot $builtinSearchRoot ` -Filter $memberFilter ` -Properties $memberProperties if ($builtinMembers.Count -gt 0) { $members = @($members) + @($builtinMembers) } } catch { Write-Verbose "Failed to enumerate builtin members of $groupLabel`: $_" } } Write-Verbose "Group '$groupLabel' has $($members.Count) recursive member(s)" # Build normalized member objects $groupMembers = [System.Collections.Generic.List[hashtable]]::new() foreach ($member in $members) { $memberDN = if ($member.ContainsKey('distinguishedname')) { $member['distinguishedname'] } else { '' } # Skip duplicates within this group $existingInGroup = $groupMembers | Where-Object { $_.DistinguishedName -eq $memberDN } if ($existingInGroup) { continue } $sam = if ($member.ContainsKey('samaccountname')) { $member['samaccountname'] } else { '' } $uac = if ($member.ContainsKey('useraccountcontrol')) { [int]$member['useraccountcontrol'] } else { 0 } $objectClasses = if ($member.ContainsKey('objectclass')) { $oc = $member['objectclass'] if ($oc -is [array]) { $oc } else { @($oc) } } else { @() } # Determine the primary object class $primaryClass = if ($objectClasses -contains 'computer') { 'computer' } elseif ($objectClasses -contains 'group') { 'group' } elseif ($objectClasses -contains 'user') { 'user' } elseif ($objectClasses -contains 'msDS-GroupManagedServiceAccount') { 'msDS-GroupManagedServiceAccount' } elseif ($objectClasses -contains 'msDS-ManagedServiceAccount') { 'msDS-ManagedServiceAccount' } else { ($objectClasses | Select-Object -Last 1) } $uacFlags = Get-UACFlags -UserAccountControl $uac # Service principal names $spns = @() if ($member.ContainsKey('serviceprincipalname')) { $spnVal = $member['serviceprincipalname'] $spns = if ($spnVal -is [array]) { @($spnVal) } else { @($spnVal) } } # MemberOf $memberOfList = @() if ($member.ContainsKey('memberof')) { $moVal = $member['memberof'] $memberOfList = if ($moVal -is [array]) { @($moVal) } else { @($moVal) } } $memberObj = @{ SamAccountName = $sam DistinguishedName = $memberDN DisplayName = if ($member.ContainsKey('displayname')) { $member['displayname'] } else { '' } ObjectClass = $primaryClass UserAccountControl = $uac UACFlags = $uacFlags Enabled = -not $uacFlags.ACCOUNTDISABLE PwdLastSet = if ($member.ContainsKey('pwdlastset')) { $member['pwdlastset'] } else { $null } LastLogonTimestamp = if ($member.ContainsKey('lastlogontimestamp')) { $member['lastlogontimestamp'] } else { $null } AdminCount = if ($member.ContainsKey('admincount')) { [int]$member['admincount'] } else { 0 } MemberOf = $memberOfList SID = if ($member.ContainsKey('objectsid')) { $member['objectsid'] } else { '' } ServicePrincipalName = $spns IsServiceAccount = ($spns.Count -gt 0 -or $sam -match '^svc[_-]' -or $sam -match '_svc$' -or $primaryClass -eq 'msDS-GroupManagedServiceAccount' -or $primaryClass -eq 'msDS-ManagedServiceAccount') IsComputer = ($primaryClass -eq 'computer') IsGroup = ($primaryClass -eq 'group') WhenCreated = if ($member.ContainsKey('whencreated')) { $member['whencreated'] } else { $null } Description = if ($member.ContainsKey('description')) { $member['description'] } else { '' } } $groupMembers.Add($memberObj) # Track across all groups for deduplication if ($primaryClass -ne 'group' -and $memberDN -and -not $allPrivilegedUsersDN.Contains($memberDN)) { [void]$allPrivilegedUsersDN.Add($memberDN) $allPrivilegedUsersMap[$memberDN] = $memberObj } } $result.PrivilegedGroups[$groupLabel] = @($groupMembers) } # ── 2. Build deduplicated list of all privileged users ──────────── $result.AllPrivilegedUsers = @($allPrivilegedUsersMap.Values) if (-not $Quiet) { Write-ProgressLine -Phase AUDITING -Message "Total unique privileged accounts: $($result.AllPrivilegedUsers.Count)" } # ── 3. AdminSDHolder ACL ────────────────────────────────────────── if (-not $Quiet) { Write-ProgressLine -Phase AUDITING -Message 'Reading AdminSDHolder security descriptor' } try { $adminSDHolderDN = "CN=AdminSDHolder,CN=System,$domainDN" $adminSDRoot = New-LdapSearchRoot -Connection $Connection -SearchBase $adminSDHolderDN $adminSDResults = @(Invoke-LdapQuery -SearchRoot $adminSDRoot ` -Filter '(objectClass=container)' ` -Properties @('ntSecurityDescriptor') ` -Scope Base) if ($adminSDResults.Count -gt 0 -and $adminSDResults[0].ContainsKey('ntsecuritydescriptor')) { $sdBytes = $adminSDResults[0]['ntsecuritydescriptor'] if ($sdBytes -is [byte[]]) { try { $sd = New-Object System.DirectoryServices.ActiveDirectorySecurity $sd.SetSecurityDescriptorBinaryForm($sdBytes) $result.AdminSDHolderACL = $sd } catch { Write-Verbose "Failed to parse AdminSDHolder security descriptor: $_" $result.AdminSDHolderACL = $sdBytes # Return raw bytes as fallback } } else { $result.AdminSDHolderACL = $sdBytes } } } catch { Write-Verbose "Failed to read AdminSDHolder: $_" $result.Errors['AdminSDHolder'] = $_.Exception.Message } # ── 4. AdminCount orphans ───────────────────────────────────────── # Users with adminCount=1 who are NOT in any protected/privileged group if (-not $Quiet) { Write-ProgressLine -Phase AUDITING -Message 'Identifying adminCount orphans' } try { $adminCountFilter = '(&(objectCategory=person)(objectClass=user)(adminCount=1))' $adminCountRoot = New-LdapSearchRoot -Connection $Connection -SearchBase $domainDN $adminCountResults = Invoke-LdapQuery -SearchRoot $adminCountRoot ` -Filter $adminCountFilter ` -Properties $memberProperties $orphans = [System.Collections.Generic.List[hashtable]]::new() foreach ($acUser in $adminCountResults) { $acDN = if ($acUser.ContainsKey('distinguishedname')) { $acUser['distinguishedname'] } else { '' } # If this user is in our privileged users set, they are not an orphan if ($allPrivilegedUsersDN.Contains($acDN)) { continue } $sam = if ($acUser.ContainsKey('samaccountname')) { $acUser['samaccountname'] } else { '' } $uac = if ($acUser.ContainsKey('useraccountcontrol')) { [int]$acUser['useraccountcontrol'] } else { 0 } $uacFlags = Get-UACFlags -UserAccountControl $uac $spns = @() if ($acUser.ContainsKey('serviceprincipalname')) { $spnVal = $acUser['serviceprincipalname'] $spns = if ($spnVal -is [array]) { @($spnVal) } else { @($spnVal) } } $memberOfList = @() if ($acUser.ContainsKey('memberof')) { $moVal = $acUser['memberof'] $memberOfList = if ($moVal -is [array]) { @($moVal) } else { @($moVal) } } $orphanObj = @{ SamAccountName = $sam DistinguishedName = $acDN DisplayName = if ($acUser.ContainsKey('displayname')) { $acUser['displayname'] } else { '' } ObjectClass = 'user' UserAccountControl = $uac UACFlags = $uacFlags Enabled = -not $uacFlags.ACCOUNTDISABLE PwdLastSet = if ($acUser.ContainsKey('pwdlastset')) { $acUser['pwdlastset'] } else { $null } LastLogonTimestamp = if ($acUser.ContainsKey('lastlogontimestamp')) { $acUser['lastlogontimestamp'] } else { $null } AdminCount = 1 MemberOf = $memberOfList SID = if ($acUser.ContainsKey('objectsid')) { $acUser['objectsid'] } else { '' } ServicePrincipalName = $spns WhenCreated = if ($acUser.ContainsKey('whencreated')) { $acUser['whencreated'] } else { $null } Description = if ($acUser.ContainsKey('description')) { $acUser['description'] } else { '' } } $orphans.Add($orphanObj) } $result.AdminCountOrphans = @($orphans) Write-Verbose "Found $($orphans.Count) adminCount orphan(s)" } catch { Write-Verbose "Failed to identify adminCount orphans: $_" $result.Errors['AdminCountOrphans'] = $_.Exception.Message } # ── 5. krbtgt account ───────────────────────────────────────────── if (-not $Quiet) { Write-ProgressLine -Phase AUDITING -Message 'Retrieving krbtgt account info' } try { $krbtgtRoot = New-LdapSearchRoot -Connection $Connection -SearchBase $domainDN $krbtgtResults = @(Invoke-LdapQuery -SearchRoot $krbtgtRoot ` -Filter '(&(objectClass=user)(sAMAccountName=krbtgt))' ` -Properties @( 'sAMAccountName', 'distinguishedName', 'objectSid', 'pwdLastSet', 'whenCreated', 'whenChanged', 'userAccountControl', 'msDS-KeyVersionNumber' ) ` -SizeLimit 1) if ($krbtgtResults.Count -gt 0) { $k = $krbtgtResults[0] $kUac = if ($k.ContainsKey('useraccountcontrol')) { [int]$k['useraccountcontrol'] } else { 0 } $result.KrbtgtAccount = @{ SamAccountName = if ($k.ContainsKey('samaccountname')) { $k['samaccountname'] } else { 'krbtgt' } DistinguishedName = if ($k.ContainsKey('distinguishedname')) { $k['distinguishedname'] } else { '' } SID = if ($k.ContainsKey('objectsid')) { $k['objectsid'] } else { '' } PwdLastSet = if ($k.ContainsKey('pwdlastset')) { $k['pwdlastset'] } else { $null } WhenCreated = if ($k.ContainsKey('whencreated')) { $k['whencreated'] } else { $null } WhenChanged = if ($k.ContainsKey('whenchanged')) { $k['whenchanged'] } else { $null } UserAccountControl = $kUac UACFlags = Get-UACFlags -UserAccountControl $kUac KeyVersionNumber = if ($k.ContainsKey('msds-keyversionnumber')) { [int]$k['msds-keyversionnumber'] } else { 0 } } # Calculate password age in days if ($result.KrbtgtAccount.PwdLastSet) { $pwdAge = ([datetime]::UtcNow - $result.KrbtgtAccount.PwdLastSet).TotalDays $result.KrbtgtAccount['PwdAgeDays'] = [math]::Round($pwdAge, 1) } else { $result.KrbtgtAccount['PwdAgeDays'] = -1 } } } catch { Write-Verbose "Failed to retrieve krbtgt account: $_" $result.Errors['KrbtgtAccount'] = $_.Exception.Message } # ── 6. Protected Users group members ────────────────────────────── if (-not $Quiet) { Write-ProgressLine -Phase AUDITING -Message 'Enumerating Protected Users group' } try { # Protected Users group has well-known RID 525 $protectedUsersDN = '' if ($domainSidString) { $puSid = "$domainSidString-525" $puSearchRoot = New-LdapSearchRoot -Connection $Connection -SearchBase $domainDN $puGroup = & $findGroupBySid $puSid $puSearchRoot if ($puGroup) { $protectedUsersDN = $puGroup['distinguishedname'] } } # Fallback to name search if (-not $protectedUsersDN) { $puNameRoot = New-LdapSearchRoot -Connection $Connection -SearchBase $domainDN $puByName = & $findGroupByName 'Protected Users' $puNameRoot if ($puByName) { $protectedUsersDN = $puByName['distinguishedname'] } } if ($protectedUsersDN) { Write-Verbose "Protected Users group DN: $protectedUsersDN" $puMemberFilter = "(memberOf:1.2.840.113556.1.4.1941:=$protectedUsersDN)" $puMemberRoot = New-LdapSearchRoot -Connection $Connection -SearchBase $domainDN $puMembers = Invoke-LdapQuery -SearchRoot $puMemberRoot ` -Filter $puMemberFilter ` -Properties $memberProperties $puMemberList = [System.Collections.Generic.List[hashtable]]::new() foreach ($pu in $puMembers) { $puSam = if ($pu.ContainsKey('samaccountname')) { $pu['samaccountname'] } else { '' } $puUac = if ($pu.ContainsKey('useraccountcontrol')) { [int]$pu['useraccountcontrol'] } else { 0 } $puFlags = Get-UACFlags -UserAccountControl $puUac $puObj = @{ SamAccountName = $puSam DistinguishedName = if ($pu.ContainsKey('distinguishedname')) { $pu['distinguishedname'] } else { '' } ObjectClass = 'user' UserAccountControl = $puUac UACFlags = $puFlags Enabled = -not $puFlags.ACCOUNTDISABLE SID = if ($pu.ContainsKey('objectsid')) { $pu['objectsid'] } else { '' } } $puMemberList.Add($puObj) } $result.ProtectedUsersMembers = @($puMemberList) Write-Verbose "Protected Users group has $($puMemberList.Count) member(s)" } else { Write-Verbose 'Protected Users group not found (may not exist at this domain functional level)' } } catch { Write-Verbose "Failed to enumerate Protected Users: $_" $result.Errors['ProtectedUsers'] = $_.Exception.Message } # ── Summary ─────────────────────────────────────────────────────── if (-not $Quiet) { $totalPriv = $result.AllPrivilegedUsers.Count $orphanCount = $result.AdminCountOrphans.Count $serviceAccts = @($result.AllPrivilegedUsers | Where-Object { $_.IsServiceAccount }).Count $computerAccts = @($result.AllPrivilegedUsers | Where-Object { $_.IsComputer }).Count $disabledAccts = @($result.AllPrivilegedUsers | Where-Object { -not $_.Enabled }).Count $puCount = $result.ProtectedUsersMembers.Count $summary = "Privileged accounts: $totalPriv total, $disabledAccts disabled, $orphanCount orphaned adminCount" if ($serviceAccts -gt 0) { $summary += ", $serviceAccts service" } if ($computerAccts -gt 0) { $summary += ", $computerAccts computer" } $summary += ", $puCount in Protected Users" if ($result.KrbtgtAccount -and $result.KrbtgtAccount.PwdAgeDays -ge 0) { $summary += " | krbtgt pwd age: $($result.KrbtgtAccount.PwdAgeDays)d" } if ($result.Errors.Count -gt 0) { $summary += " ($($result.Errors.Count) error(s))" } Write-ProgressLine -Phase AUDITING -Message $summary } return $result } |