The Admin’s First Steps: Local Group Membership

The Admin’s First Steps: Local Group Membership

  • Comments 4
  • Likes

Summary: Richard Siddaway talks about using Windows PowerShell to discover the membership of local groups.

Hey, Scripting Guy! Question Hey, Scripting Guy! I’ve just starting using Windows PowerShell to administer my systems, and I’ve been told I need to check the membership of local groups on all my servers. How can I do that?

— AK

Hey, Scripting Guy! Answer Hello AK,

Honorary Scripting Guy, Richard Siddaway here today—filling in for my good friend, The Scripting Guy. Windows PowerShell is great because you have a number of ways to perform most tasks. You’ll see an example of that in this post, where there are three ways to solve this issue. This is a strength but also a weakness because it can confuse people who are starting to use Windows PowerShell. My advice has always been to find something that works and stick with it. You get more issues solved that way.

When we look at local groups, the most important is the Administrators group. This bestows full control of the system and allows you to do anything to that machine. We’ll use that group as an example throughout this post, but the ideas can be applied to any local group.

Of the three ways to find local group membership, the oldest method uses the WinNT ADSI provider. ADSI is a scripting interface to directory services. It’s normally used for scripting against Active Directory, but you can also use it against local machines.

$group = [ADSI]"WinNT://./Administrators"

 @($group.Invoke("Members")) |

foreach {$_.GetType().InvokeMember("Name", 'GetProperty', $null, $_, $null)}

The ADSI scripting interface isn’t directly available in Windows PowerShell in the same way it is in VBScript. Instead, you have to use the System.DirectoryServices.DirectoryEntry .NET Framework class, which provides a wrapper for ADSI. A shortcut, known as a type accelerator has been provided so you don’t have to type the whole class name. To add confusion, the type accelerator is known as [adsi] or [ADSI]—case doesn’t matter.

The WinNT provider is used to access the local machine by using “.” to represent the system together with the name of the group—in this case, Administrators. You then have to use the Invoke() method of the $group object to get the members that you can pipe to Foreach-Object, which uses the InvokeMember() method to get the member name. The output will look something like this:

Administrator

Richard

Personally, I don’t like using “.” to represent the local machine. It’s easy to overlook, and I have seen problems when using it with WMI. I prefer to use $env:COMPUTERNAME, and pick the machine’s name from the environmental variables:

$group =[ADSI]"WinNT://$($env:COMPUTERNAME)/Administrators"

 @($group.Invoke("Members")) |

foreach {$_.GetType().InvokeMember("Name", 'GetProperty', $null, $_, $null)}

This is OK if your machine is in a workgroup; but in a domain, you might get answers like this:

Administrator

Domain Admins

Richard

It’s not easy to separate local users from domain accounts by using this approach. Luckily, you have some alternatives. One of the alternatives involves using WMI. When I’m working with WMI, I find that the CIM cmdlets introduced in Windows PowerShell 3.0 are the easiest to work with:

$group = Get-CimInstance -ClassName Win32_Group  -Filter "Name = 'Administrators'"

Get-CimAssociatedInstance -InputObject $group -ResultClassName Win32_UserAccount |

select -ExpandProperty Caption

Use the Win32_Group class to get the WMI object that represents the group. Use that object in Get-CimAssociatedInstance to find the Win32_UserAccount instances that are associated with that group. On a workgroup machine, you get this:

RSLAPTOP01\Administrator

RSLAPTOP01\Richard

And on a domain machine, you get this:

WIN12R2\Administrator

MANTICORE\Richard

Now you can see which of the results are local accounts and which are domain accounts. But (and isn’t there always a but?) you don’t get the nested domain groups. You need to find the associated groups, which simply involves another call to Get-CimAssociatedInstance:

$group = Get-CimInstance -ClassName Win32_Group  -Filter "Name = 'Administrators'"

Get-CimAssociatedInstance -InputObject $group -ResultClassName Win32_UserAccount |

select -ExpandProperty Caption

Get-CimAssociatedInstance -InputObject $group -ResultClassName Win32_Group |

select -ExpandProperty Caption

Because you are displaying the same data, you can output from multiple calls (generally its viewed as a bad thing), and you get something like this:

WIN12R2\Administrator

MANTICORE\Richard

MANTICORE\Domain Admins

Now you can see the domain and machine name, so you know which entries are local, and you get the nested groups. If you can’t use the CIM cmdlets, you can fall back on the WMI cmdlets.

$query = "ASSOCIATORS OF {Win32_Group.Domain='$($env:COMPUTERNAME)',Name='Administrators'} WHERE ResultClass = Win32_UserAccount"

Get-WmiObject -Query $query | Select -ExpandProperty Caption

$query = "ASSOCIATORS OF {Win32_Group.Domain='$($env:COMPUTERNAME)',Name='Administrators'} WHERE ResultClass = Win32_Group"

Get-WmiObject -Query $query | Select -ExpandProperty Caption

This is a bit more complicated than the CIM cmdlet version because you have to create a WMI Query Language (WQL) query to discover the associated Win32_UserAccount and Win32_Group instances. The difficult part is getting the contents of the {} correct. The easiest way is to look at the __RELPATH property of the returned object when you use the following command:

Get-WmiObject -Class Win32_Group -Filter "Name = 'Administrators'" | fl *

Remember to change the double quotes to single quotes to make the query work.

You get the same results as with the CIM cmdlets:

WIN12R2\Administrator

MANTICORE\Richard

MANTICORE\Domain Admins

The third and final way to get this data is to drop back to the .NET Framework class and use the System.DirectoryServices.AccountManagement classes that were introduced in .NET Framework 3.5.

Add-Type -AssemblyName System.DirectoryServices.AccountManagement

$ctype = [System.DirectoryServices.AccountManagement.ContextType]::Machine

$context = New-Object -TypeName System.DirectoryServices.AccountManagement.PrincipalContext -ArgumentList $ctype, $env:COMPUTERNAME

$idtype = [System.DirectoryServices.AccountManagement.IdentityType]::SamAccountName

$group = [System.DirectoryServices.AccountManagement.GroupPrincipal]::FindByIdentity($context, $idtype, 'Administrators')

$group.Members |

select @{N='Domain'; E={$_.Context.Name}}, samaccountName

This may look a bit more complicated, but most of it is concerned with defining the objects that you want to work with. The script starts by using Add-Type to load the System.DirectoryServices.AccountManagement .NET Framework classes into Windows PowerShell. Not all.NET Framework classes are loaded by default—only those that are thought to be of the most use to the most people.

After that, you use the ContextType class to say that you are looking at the local machine rather than the domain. The System.DirectoryServices.AccountManagement classes aren’t used much, which is a shame because they are powerful and they span local and domain level activities.

You can then use the context type and the local machine name to create a context. The IdentityType defines what you are using to identify objects—in this case, the SamAccountName.

Now you can use all of that data in the FindByIdentity() method of the GroupPrincipal class to find the group. The Members property holds the members as you might expect; but the final piece is that you have to dig into the Context of the member to discover whether the object comes from the domain or from the local machine.

Luckily the code runs much faster than I can write about it and you get results like this:

Domain             SamAccountName

------                  --------------

WIN12R2         Administrator

Manticore.org   Domain Admins

Manticore.org   Richard 

Working through these three methods illustrates an important point: If there are multiple methods available, you should investigate them to discover how they work, any issues that occur for you, and which one works best with your skills and way of working.

The last part of this journey is to turn your code into a function that you can use across multiple remote machines. I’m going to use the System.DirectoryServices.AccountManagement approach so I can illustrate how these classes can be used against remote machines without the need to rely on Windows PowerShell remoting.

function get-localgroupmember {

[CmdletBinding()]

param(

[parameter(ValueFromPipeline=$true,

   ValueFromPipelineByPropertyName=$true)]

   [string[]]$computername = $env:COMPUTERNAME

)

BEGIN {

Add-Type -AssemblyName System.DirectoryServices.AccountManagement

$ctype = [System.DirectoryServices.AccountManagement.ContextType]::Machine

}

 

PROCESS{

foreach ($computer in $computername) {

  $context = New-Object -TypeName System.DirectoryServices.AccountManagement.PrincipalContext -ArgumentList $ctype, $computer

  $idtype = [System.DirectoryServices.AccountManagement.IdentityType]::SamAccountName

  $group = [System.DirectoryServices.AccountManagement.GroupPrincipal]::FindByIdentity($context, $idtype, 'Administrators')

  $group.Members |

  select @{N='Server'; E={$computer}}, @{N='Domain'; E={$_.Context.Name}}, samaccountName

} # end foreach

} # end PROCESS

}

 

"Win12R2", "W12SUS" | get-localgroupmember

Start by defining the function and parameter. In this case, you’re only interested in the remote computer name. When you use an array as the type, you can also do this:

get-localgroupmember -computername Win12R2, w12sus

…but only if you have the Foreach loop in the PROCESS block. (You don’t have to capitalize these blocks of code—I do it to make my code more readable.)

You only need to load the .NET Framework classes, define the context, and identity types once. So those steps go into the BEGIN block, which executes once when the first object in the pipeline hits the function.

The PROCESS block loops through the computers and creates a context for each machine. The Administrators group is found and the membership list is extracted. You are working against remote machines so the computer is added to the output to identify to which machine the results apply.

You can use this function on the pipeline as shown or interactively by passing one or more computer names to the function.

If you want to extend the use of this script you could:

  • Make the group name a variable.
  • Save the results for comparison against a future examination to track changes.

AK, that’s how you use Windows PowerShell to check the membership of your local groups. Next time I’ll have another idea for you to try as you bring more automation into your environment.

If you would like to read more in this series, check out these posts:

Bye for now.

~Richard

Richard Siddaway is based out of the UK, and he spends his time automating anything and everything for Kelway, Ltd. A Windows PowerShell MVP since 2007, Richard is a prolific blogger, mainly about Windows PowerShell (see Richard Siddaway's Blog: Of PowerShell and Other Things), and a frequent speaker at user groups and Windows PowerShell conferences. He has written a number of Windows PowerShell books: PowerShell in Practice, PowerShell and WMI, PowerShell in Depth (co-author); and PowerShell Dive (co-editor). He is currently finishing Learn Active Directory Management in a Month of Lunches, which features a lot of Windows PowerShell. All of the books are available from Manning Publications Co.

Thanks, Richard.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment
  • Even if you have to fall back on Get-WmiObject, there's a slightly easier way to find the associated members.  Instead of writing an "ASSOCIATORS OF" query, you can do this:

    $group = Get-WmiObject Win32_Group -Filter 'Name="Administrators"'

    $group.GetRelated('Win32_UserAccount') | Select-Object -ExpandProperty Caption

  • Good Post!!!

  • Hi..Iam poor at scripting..how could we apply this script on list of servers...pls explain

  • The last method - using System.DirectoryServices.AccountManagement - will only work on machines which have no 'orphaned' SIDs, i.e. the SIDs can be resolved. If you have a normal network there will be orphaned SIDs from domain accounts or groups that have been deleted at some point. Those will cause the third method to fail.

    This is a design decision and has been true since at least July of 2009 (https://connect.microsoft.com/VisualStudio/feedback/details/453812/principaloperationexception-when-enumerating-the-collection-groupprincipal-members towards the bottom is MS support's comments). Microsoft has yet to fix this 5 years on.