Well, I am back for yet another Powershell script. This is one that I found pretty useful actually.
As one of the people really pushing automation in my group at work, I was tasked with getting a list of all users in the “Domain Admins” group for all domains in our Active Directory forest.
One of the challenges in doing this is that you may have a bunch of nested groups and we needed to dump users from all nested groups, etc. I do realize that there are probably tools and what-not that would this for me, but what fun is that and why spend the bucks if you can script it out. I like this approach, too, because I can force the output to be whatever I want and in whichever format is best for what I am trying to accomplish.
From my days in college as a computer science kinda guy, I figured we could use recursion to help walk us through all nested groups.
So for those of you unfamilar with this idea of recursion, I will summarize:
It is basically a function that calls itself until a certain condition is met. At this point the function exits. I know all you CS types may take exception to such a simplified definition, so please google for it or hit up Wikipedia for more detailed info on recursion.
How can this possible help us in our quest to enumeration group memberships? Well let’s break it down a little.
- I start with a group I care about. Let’s say it is “Domain Admins” for domain “office1.contoso.com”.
- I have a function (“get-groupmembers”) that I use to enumerate the members of this group and do something with them (output, write to file, etc)
- As I am enumerating them, I find a member that is of type “group” called something like “Corp Admins”
- I now call my function (“get-groupmembers”) with this nested group (“Corp Admins”) to enumerate the members
- As I am enumerating them, I find ANOTHER group called “Help Desk On-Call” inside of “Corp Admins”
- I can now call my funciton (“get-groupmembers”) ANOTHER time and keep going until I only have users and have walked all of the nested groups
I know this may sound a little weird “that I am calling myself” over and over again, but it is actually pretty efficient.
Let’s jump into some code now.
For starters, I need to have some functions that take a Fully Qualified Domain Name (for an Active Directory Domain) and convert it into an LDAP-ish format. For example, I needed “office1.contoso.com” to be transformed into “DC=office1,DC=contoso,DC=com”. I know this isn’t rocket-surgery, but I just threw some stuff together for it.
function Convert-DNStoDN ([string]$DNSName)
{
# Create an array of each item in the string separated by "."
$DNSArray = $DNSName.Split(".")
# Let's go through our new array and do something with each item
for ($x = 0; $x -lt $DNSArray.Length ; $x++)
{
#I don't want a comma after my last item, so check to see if I am on my last one and set
# $Separator equal to nothing.
# Remember that we need to go to Length-1 because arrays are "0 based indexes"
if ($x -eq ($DNSArray.Length - 1)){$Separator = ""}else{$Separator =","}
[string]$DN += "DC=" + $DNSArray[$x] + $Separator
}
return $DN
}
We will also need to be able to split the FQDN of the DOMAIN out from the DN of a group or user. So, I have something like “CN=Me,OU=User1,DC=office1,DC=contoso,DC=com” and want to get the FQDN of this domain. For this example this would output “office1.contoso.com”.
function Convert-DNtoDNS ([string]$DN)
{
$DNArray = $DN.Split(",")
# Let's go through our new array and do something with each item
for ($x = 0; $x -lt $DNArray.Length ; $x++)
{
#I don't want a period after my last item, so check to see if I am on my last one and set
# $Separator equal to nothing.
# Remember that we need to go to Length-1 because arrays are "0 based indexes"
if ($x -eq ($DNArray.Length - 1)){$Separator = ""}else{$Separator ="."}
# Now we have to see if we look like "DC=". If it does, we will
# start to construct our DNS name.
if ($DNArray[$x].Split("=")[0] -ilike "DC")
{
# Let's grab the "contoso" side of the "DC=contoso"
[string]$DNS += $DNArray[$x].Split("=")[1] + $Separator
}
}
return $DNS
}
Now that we have those little “cameo” functions, we will move on to the more of the meat-and-potatoes of the script.
The next function will be to actually enumerate a group in Active Directory without using the Quest Tools for Active Directory (if you don’t have those yet, you need to them).
We are actually going to use some .NET calls to get the directory objects. My colleague Mike Hays actually did a lot of this part of the code.
function get-groupmember([string]$domain, [string]$groupName)
{
# I have passed in the FQDN and Groupname I am interested in
# I just need to convert my FQDN into an LDAP style name using my previous function
$DN = convert-DNStoDN($Domain)
$domainLDAPUrl = "LDAP://" + $DN
# Setup my directory connection using .NET call
$ent = [System.DirectoryServices.DirectoryEntry] ( $domainLDAPUrl )
# Define my "searcher" object to query the directory
$srch = [System.DirectoryServices.DirectorySearcher] ( $ent )
# Setup my search criteria.. looking for all Groups with CN=GroupName
$groupNameFilter = "(&(objectClass=group)(CN=" + $groupName + "))"
$srch.Filter = $groupNameFilter
# Now go execute my query to and put the results in $coll
$coll = [System.DirectoryServices.SearchResultCollection] $srch.FindAll()
foreach ($rs in $coll)
{
# Now get a collection of properties for that object
$resultPropColl = [System.DirectoryServices.ResultPropertyCollection] $rs.Properties
# Cycle through all group members
foreach ($memberColl in $resultPropColl["member"])
{
# Build my membership array
[array]$gpMemberEntry += [System.DirectoryServices.DirectoryEntry] ( "LDAP://" + $memberColl )
}
}
# Send back my group members.
return $gpMemberEntry
}
Okay.. now that we have THAT setup let’s talk about the next bits of code.
This is where we will have our recursive function “Get-AllMembers”. In it, you will a call to itself. One of the biggest concerns is that you can end up in an unending or infinite cycle. I don’t really do any checking in this little scripty-do-dad, so that may be something for later.
function get-allmembers($objectName, $OF, $GN)
{
# Split out my domain name (should be FQDN) and the group name
$domainName = $objectname.split("\")[0]
$ObjectName = $objectname.split("\")[1]
$members = get-groupmember "$DomainName" "$ObjectName"
if ($members -ne $NULL)
{
foreach ($member in $members)
{
# Grab the domain DNS name out of the object DN
$ObjDomain = convert-DNtoDNS $Member.DistinguishedName
if ($member.objectclass -contains "group")
{
#If my group member is, itself, a group We get to do some recursion
$out = $objDomain + "\" + $member.name
Write-Host $out
# Call myself with the nested group name
get-allmembers -ObjectName $out -OF $of -GN $GN
}
else
{
# If I get back a user, then see if the user is disabled or not
$userAndDomain = $objDomain + "\" + $member.name
# The UserAccountControl property contains several "flags"
# that we can interrogate. By doing a Binary AND we are seeing if the 2nd flag is set.
[bool]$accountIsDisabled = [int]$member.userAccountControl.ToSTring() -band 2
# Setup our output (I am choosing to construct a comma-delimited type of output
$OutText = "'" + $GN + "','" + $objDomain + "','" + $Member.Samaccountname + "','" + $Member.displayName + "','" + $member.distinguishedName + "','" + $objectname + "','" + $accountIsDisabled + "'"
Out-File -FilePath $OF -inputobject $Outtext -append -Encoding "ASCII"
Write-Host $OutText
}
}
}
else
{
Write-Host "No Members or no Group:" $ObjectName "in Domain:" $DomainName -Foreground RED
}
}
###########################
## Main
###########################
$GroupName = "Domain Admins"
$Today = Get-Date -format "yyyyMMddhh"
$OutputFolder = "C:\Temp\"
if ((Test-Path $outputFolder) -eq $False)
{
New-Item -Path $OutputFolder -Type Directory > $NULL
}
# Grab my forest info
$forest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
#Setup my output file
$OutHeader = "'Group','UserDomain','SAMAccount','DisplayName','DN','MemberofGroup','IsDisabled'"
# Define output file name
$of = $OutputFolder + $Today +"_"+ $GroupName + "-AuditReport.txt"
Out-File -FilePath $OF -inputobject $OutHeader -Encoding "ASCII"
# Cycle through all the child domains in the forest root to query for Group.
foreach ($domain in $forest.Domains)
{
$FullGroupName = $domain.name + "\" + $GroupName
get-allmembers -ObjectName $FullGroupName -OF $of -GN $FullGroupName
}
Just take all of the script blocks from above and paste them into your script. I am trying to keep these posts a bit shorter, so you may see upcoming posts broken out into parts.
Well, I think I am done here with this one. Please let me know if you have questions, concerns, or comments.
Please keep in mind that this script will attempt to enumerate the Group in ALL child domains in your current AD Forest. If you have a large Forest with lots of child domains…..this could take a while.
I will be happy to help out with requested changes if they seem like they would be beneficial overall, but I am also a STRONG advocate of doing-it-yourself.
Every bit of Powershell and scripting I have learned by grabbing it and going with it.
Anyway, as always…thanks for stopping by and happy scripting!!!
– Mark