Forum Discussion
Get Privileged User Accounts and then associate those names to their AD Groups.... ???
Thank you for your review. Most appreciated. I exited PS to make sure it wasnt running anything cached. Yet it is stopping at the same spot... right after the 5th AD record is displayed to screen...
ForEach-Object : Cannot index into a null array.
At line:69 char:29
+ ForEach-Object {
+ ~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [ForEach-Object], RuntimeException
+ FullyQualifiedErrorId : NullArray,Microsoft.PowerShell.Commands.ForEachObjectCommand
I very much appreciate your efforts sir !!
Scott
Thanks, Scott.
The fact that it's working without error in my environment is making it a bit tricky to figure out which part of that ForEach-Object block is causing the issue.
What I might do first is add some more logging and then reach out to you via direct messaging, and only update this post once we've made progress - as the script itself is making this thread pretty hard to read.
Cheers,
Lain
- LainRobertsonSep 11, 2023Silver Contributor
Just to close out this question, this was the final version of the script, which went from being an example to a working solution.
Get-ADPrivilegedIdentities.ps1
[cmdletbinding()] Param() try { #region Global variables. $RootDSE = [adsi]"LDAP://RootDSE"; $Server = $RootDSE.dNSHostName[0]; $Domain = [adsi]"LDAP://$Server/$($RootDSE.defaultNamingContext)"; $DomainSid = [System.Security.Principal.SecurityIdentifier]::new($Domain.objectSid[0], 0).Value; $ProcessedUsers = [System.Collections.Generic.List[int]]::new(); $MemberOf = [System.Collections.Generic.List[string]]::new(); $WellknownGroups = [System.Collections.Generic.Dictionary[[int],[string]]]::new(); Write-Verbose -Message "Connected to $Server ..."; #endregion # Protected groups obtained from https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/plan/security-best-practices/appendix-c--protected-accounts-and-groups-in-active-directory#protected-accounts-and-groups-in-active-directory-by-operating-system. $ProtectedGroups = @( "S-1-5-32-548" # Account Operators , "S-1-5-32-544" # Administrators , "S-1-5-32-551" # Backup Operators , "$DomainSid-512" # Domain Admins , "$DomainSid-519" # Enterprise Admins , "S-1-5-32-550" # Print Operators , "$DomainSid-518" # Schema Admins , "S-1-5-32-549" # Server Operators ) #region Set up a directory searcher for each level of expansion. $Searcher = [adsisearcher]::new(); $Searcher.SearchRoot = $Domain; $Searcher.PropertiesToLoad.AddRange(@("distinguishedName")); $MemberSearcher = [adsisearcher]::new(); $MemberSearcher.SearchScope = [System.DirectoryServices.SearchScope]::Base; $MemberSearcher.PropertiesToLoad.AddRange(@("msds-memberTransitive", "name")); $ObjectSearcher = [adsisearcher]::new(); $ObjectSearcher.PropertiesToLoad.AddRange(@("accountExpires", "distinguishedName", "msds-memberOfTransitive", "msDS-UserPasswordExpiryTimeComputed", "name", "objectClass", "objectGUID", , "primaryGroupID", "userAccountControl", "userPrincipalName" )); $AdhocSearcher = [adsisearcher]::new(); $AdhocSearcher.SearchRoot = $Domain; $AdhocSearcher.SearchScope = [System.DirectoryServices.SearchScope]::Subtree; #endregion #region Fetch the well-known groups' primaryGroupToken and name for fast lookup. Write-Verbose -Message "Fetching well-known groups for fast lookup..."; $SecurityIdentifier = [System.Security.Principal.SecurityIdentifier]::new("$DomainSid-1024"); $SidAsBytes = [byte[]]::new($SecurityIdentifier.BinaryLength); $SecurityIdentifier.GetBinaryForm($SidAsBytes, 0); $AdhocSearcher.Filter = "(&(objectClass=group)(objectSid<=\$([System.BitConverter]::ToString($SidAsBytes).Replace("-", "\"))))"; $AdhocSearcher.PropertiesToLoad.AddRange(@("distinguishedName", "primaryGroupToken")); Write-Verbose -Message "Search filter = $($AdhocSearcher.Filter)"; $AdhocSearcher.FindAll() | ForEach-Object { $WellknownGroups.Add($_.Properties["primaryGroupToken"][0], $_.Properties["distinguishedName"][0]); } #endregion $ProtectedGroups | ForEach-Object { Write-Verbose -Message "`nProcessing $_ ..."; # Grab the RID from the SID while we're here for use later on. $RID = [int]($_.Split("-")[-1]); #region Convert the SID string template to a byte array first, and then to a useable LDAP search string. $SecurityIdentifier = [System.Security.Principal.SecurityIdentifier]::new($_); $SidAsBytes = [byte[]]::new($SecurityIdentifier.BinaryLength); $SecurityIdentifier.GetBinaryForm($SidAsBytes, 0); $Searcher.Filter = "(&(objectClass=group)(objectSid=\$([System.BitConverter]::ToString($SidAsBytes).Replace("-", "\"))))"; Write-Verbose -Message "Search filter = $($Searcher.Filter)"; #endregion # Find the privileged group's distinguishedName. $Searcher.FindOne() | ForEach-Object { $dn = $_.Properties["distinguishedName"]; Write-Verbose -Message "Enumerating memberships from $dn ..."; try { #region Perform a base search using the distinguishedName of the privileged group to retrieve the transitive memberships. $MemberSearcher.SearchRoot = [adsi]"LDAP://$Server/$dn"; $MemberSearcher.Filter = "(objectClass=*)"; Write-Verbose -Message "Member search root = $($MemberSearcher.SearchRoot.Path)"; $MemberSearcher.FindOne() | ForEach-Object { #region Iterate through each member of the privileged group. foreach ($Entry in $_.Properties["msds-memberTransitive"]) { #region Check we haven't already processed this user, and skip them if we have. if ($ProcessedUsers.Contains(($Checksum = $Entry.GetHashCode()))) { Write-Verbose -Message "Skipping $Entry."; continue; } else { Write-Verbose -Message "Enumerating transitive ""memberOf"" references for: $Entry."; $ProcessedUsers.Add($Checksum); } #endregion #region Perform a base search against the member to retrieve a transitive list of groups it is a member of $ObjectSearcher.SearchRoot = [adsi]"LDAP://$Server/$Entry"; $ObjectSearcher.SearchScope = [System.DirectoryServices.SearchScope]::Base; if ($null -ne ($Instance = $ObjectSearcher.FindOne())) { if (-not ($ObjectClass = $Instance.Properties["objectClass"][$Instance.Properties["objectClass"].Count-1]).Equals("group")) { #region Fetch the primary group details first. try { if ($WellknownGroups.ContainsKey(($PrimaryGroupID = $Instance.Properties["primaryGroupID"][0]))) { $MemberOf.Add($WellknownGroups[$PrimaryGroupID]); } else { $PrimaryGroup = [adsi]"LDAP://$Server/<SID=$DomainSid-$PrimaryGroupID>"; $MemberOf.Add($PrimaryGroup.Properties["distinguishedName"][0]); $PrimaryGroup.Dispose(); } } catch { # Do nothing. } #endregion if (($null -ne $Instance.Properties.Contains("msds-memberOfTransitive")) -and (0 -lt $Instance.Properties["msds-memberOfTransitive"].Count)) { $MemberOf.AddRange([string[]] ($Instance.Properties["msds-memberOfTransitive"])); $MemberOf.Sort(); } [PSCustomObject] @{ objectGUID = [guid]::new($Instance.Properties["objectGUID"][0]); objectClass = $ObjectClass; enabled = ($Instance.Properties["userAccountControl"][0] -band 0x2) -eq 0; name = $Instance.Properties["name"][0]; userPrincipalName = $Instance.Properties["userPrincipalName"][0]; neverExpires = $Instance.Properties["accountExpires"][0] -in (0, [int64]::MaxValue); passwordNeverExpires = $Instance.Properties["msDS-UserPasswordExpiryTimeComputed"][0] -eq [int64]::MaxValue; distinguishedName = $Instance.Properties["distinguishedName"][0]; memberOfTransitive = $MemberOf.ToArray(); } $MemberOf.Clear(); } } #endregion } #endregion } #endregion } catch [System.UnauthorizedAccessException] { Write-Warning -Message "Access denied on ""$dn"""; } catch { throw; } finally { if ($MemberSearcher.SearchRoot) { $MemberSearcher.SearchRoot.Dispose(); } } } # Find the objects where their primaryGroupID has been set to this group's RID. $Searcher.Filter = "(&(primaryGroupID=$RID))"; $Searcher.FindAll() | ForEach-Object { $dn = $_.Properties["distinguishedName"][0]; Write-Verbose -Message "Enumerating memberships from $dn ..."; # Check we haven't already processed this user, and skip them if we have. if ($ProcessedUsers.Contains(($Checksum = $dn.GetHashCode()))) { Write-Verbose -Message "Skipping $dn."; } else { Write-Verbose -Message "Enumerating transitive ""memberOf"" references for: $dn."; $ProcessedUsers.Add($Checksum); try { #region Perform a base search using the distinguishedName of the object to retrieve the transitive memberships. $ObjectSearcher.SearchRoot = [adsi]"LDAP://$Server/$dn"; $ObjectSearcher.Filter = "(objectClass=*)"; Write-Verbose -Message "Member search root = $($ObjectSearcher.SearchRoot.Path)"; $ObjectSearcher.FindOne() | ForEach-Object { $Instance = $_; #region Fetch the primary group details first. try { if ($WellknownGroups.ContainsKey(($PrimaryGroupID = $Instance.Properties["primaryGroupID"][0]))) { $MemberOf.Add($WellknownGroups[$PrimaryGroupID]); } } catch { # Do nothing. } #endregion if (($null -ne $Instance.Properties.Contains("msds-memberOfTransitive")) -and (0 -lt $Instance.Properties["msds-memberOfTransitive"].Count)) { $MemberOf.AddRange([string[]] ($Instance.Properties["msds-memberOfTransitive"])); $MemberOf.Sort(); } [PSCustomObject] @{ objectGUID = [guid]::new($Instance.Properties["objectGUID"][0]); objectClass = $ObjectClass; enabled = ($Instance.Properties["userAccountControl"][0] -band 0x2) -eq 0; name = $Instance.Properties["name"][0]; userPrincipalName = $Instance.Properties["userPrincipalName"][0]; neverExpires = $Instance.Properties["accountExpires"][0] -in (0, [int64]::MaxValue); passwordNeverExpires = $Instance.Properties["msDS-UserPasswordExpiryTimeComputed"][0] -eq [int64]::MaxValue; distinguishedName = $Instance.Properties["distinguishedName"][0]; memberOfTransitive = $MemberOf.ToArray(); } $MemberOf.Clear(); } #endregion } catch [System.UnauthorizedAccessException] { Write-Warning -Message "Access denied on ""$dn"""; } catch { throw; } finally { } } } } } catch { throw; } finally { $ProcessedUsers.Clear(); if ($Seacher) { $Searcher.Dispose(); } if ($Domain) { $Domain.Dispose(); } if ($RootDSE) { $RootDSE.Dispose(); } }Cheers,
Lain
- LainRobertsonSep 05, 2023Silver Contributor
Hi, Scott.
I sent you a direct message - you can read them if you click on your profile avatar menu in the top-right of your screen:
Try out that latest version and let me know how you go.
Some things I hadn't dealt with originally were:
- A user account can have a primaryGroupID that no longer exists;
- I'm not catering to referrals to different domains/forests when looking up the primaryGroupID.
Both of these can cause an exception when resolving the primaryGroupID (which is a RID and not a SID). Given how rare it is to see a user account where the primary group has been switched away from Domain Users, I didn't feel it was worth the additional effort to resolve, so I've just handled the error silently and continued on.
Hopefully these two oversights were the only ones and it works without error for you now.
Cheers,
Lain
- Scott_AZSep 05, 2023Copper Contributor
You're welcome, Lain
Will hold on for MS Tech DM
Scott
