Summary: Guest blogger, Bill Stewart, talks about using Windows PowerShell to find administrators.
Microsoft Scripting Guy, Ed Wilson, is here. Hello everyone. It is the weekend, and guest blogger, Bill Stewart is going to share his time and knowledge today. Bill is a scripting guru and a moderator for the Official Scripting Guys Forum...
As you know, an administrator of a Windows computer is a member of the computer’s local Administrators group. This is a special built-in group, so any user or group that’s a member of this special group is an administrator on the computer. We can see who the members of this group are by typing the command net localgroup Administrators at a cmd.exe or Windows PowerShell prompt.
Get local group membership by using ADSI
But what if we want to get the members of the local Administrators group on a remote computer? We can’t use the net localgroup command because it works only on the local computer. Since we’re talking Windows PowerShell, you might be aware that you can use the [ADSI] type accelerator to connect to a group on a computer; for example:
$localAdminGroup = [ADSI] "WinNT://remotecomputer/Administrators,Group"
This creates a System.DirectoryServices.DirectoryEntry object that references the local Administrators group on the computer RemoteComputer. This is an object, so we should be able to call its Members method to view the members of the group:
$localAdminGroup.Members()
However, this produces some peculiar output:
System.__ComObject
...
The members of the group are COM objects, but Windows PowerShell doesn’t know what to do with them because there’s no type library that tells Windows PowerShell the interface for the objects (that is, what the properties and methods are). The workaround is to call the InvokeMember method of each COM object’s base type to retrieve the name property, like this:
$localAdminGroup.Members() | ForEach-Object {
$_.GetType().InvokeMember("Name", "GetProperty", $NULL, $_, $NULL)
}
It’s ugly, but it works. But we have another issue. What if you’re using a non-English version of Windows? Or what if the Administrators group has been renamed? In either case, the ADSI technique isn’t going to work because the group might not be named Administrators.
Get Administrators group name by using WMI
The alternative is to use WMI. By using WMI, we can ask for the group by its SID, S-1-5-32-544, rather than by its name:
Get-WMIObject -Class Win32_Group `
-Filter "LocalAccount=TRUE and SID='S-1-5-32-544'" `
-Computer remotecomputer
This gives us output like this:
Caption Domain Name SID
------- ------ ---- ---
REMOTECOMPUTER\Administrators REMOTECOMPUTER Administrators S-1-5-32-544
Get-WMIObject supports credentials, so if you’re signed in with an account that doesn’t have permission to access the remote computer, you can create a credential object to make the connection:
$credential = Get-Credential
Get-WMIObject -Class Win32_Group `
-Filter "LocalAccount=TRUE and SID='S-1-5-32-544'" `
-ComputerName remotecomputer `
-Credential $credential
Combine ADSI and WMI?
Now that we’ve connected to the actual Administrators group, we can use the Name property, with ADSI and script in the previous section, to get the members of the group:
$credential = Get-Credential
$computerName = "remotecomputer"
$wmiObject = Get-WMIObject `
-Class Win32_Group `
-Filter "LocalAccount=TRUE and SID='S-1-5-32-544'" `
-ComputerName $computerName `
-Credential $credential
$adminGroupName = $wmiObject.Name
$adminGroup = [ADSI] "WinNT://$computerName/$adminGroupName,Group"
$adminGroup.Members() | ForEach-Object {
$_.GetType().InvokeMember("Name", "GetProperty", $NULL, $_, $NULL)
}
Now we’re getting somewhere. The output of this script is a list of names like this:
Administrator
Domain Admins
kendyer
...
This is good, but we’re missing something. We know that Administrator is probably a local account and Domain Admins is probably a domain group. But we don’t know if kendyer is a local account or a domain account. To avoid confusion, it would be really helpful if the output contained the domain name for domain accounts.
Also, there’s another issue with this approach: It’s relatively inefficient. We’re making an initial WMI connection to the remote computer to retrieve the Administrators group name, and then we’re making a second connection (connecting to the ADSI group object) to retrieve the group members. This may not be an issue on a fast network, but over slower connections, it doesn’t scale very well.
Use WMI to get members
To avoid making two separate connections to each remote computer, we can stick with WMI. When we connect to a computer and retrieve its Administrators group, we’re retrieving a .NET ManagementObject object:
$credential = Get-Credential
$computerName = "remotecomputer"
$wmiObject = Get-WMIObject `
-Class Win32_Group `
-Filter "LocalAccount=TRUE and SID='S-1-5-32-544'" `
-ComputerName $computerName `
-Credential $credential
".NET object type: {0}" -f $wmiObject.GetType().FullName
"WMI class: {0}" -f $wmiObject.__CLASS
The output of this command is:
.NET object type: System.Management.ManagementObject
WMI class: Win32_Group
WMI objects can be related to each other by using associator classes; in this case, WMI relates the Win32_Group class with its members by using the Win32_GroupUser associator class. The WMI GetRelated method can use the Win32_GroupUser class to retrieve the group’s members, like this:
$credential = Get-Credential
$computerName = "remotecomputer"
$wmiObject = Get-WMIObject `
-Class Win32_Group `
-Filter "LocalAccount=TRUE and SID='S-1-5-32-544'" `
-ComputerName $computerName `
-Credential $credential
$wmiObject.GetRelated("Win32_Account")
When I run this on my computer, though, the script seems to hang, and it never finishes. There are simply too many relationships in WMI for this call to the GetRelated method to complete in a reasonable amount of time. To get around this issue, I need to specify the parameters to the GetRelated method to tell WMI that I only want the members of this specific group—not every possible relationship:
$credential = Get-Credential
$computerName = "remotecomputer"
$wmiObject = Get-WMIObject `
-Class Win32_Group `
-Filter "LocalAccount=TRUE and SID='S-1-5-32-544'" `
-ComputerName $computerName `
-Credential $credential
$wmiObject.GetRelated("Win32_Account","Win32_GroupUser","","",
"PartComponent","GroupComponent",$FALSE,$NULL)
In this case, I use all eight parameters for the GetRelated method to focus on the exact relationship between the Win32_Group object instance (in the variable $wmiObject) and its related Win32_Account object instances. When I run the previous script, I get the desired output:
AccountType : 512
Caption : REMOTECOMPUTER\Administrator
Domain : REMOTECOMPUTER
SID : S-1-5-21...
FullName :
Name : Administrator
Caption : FABRIKAM\Domain Admins
Domain : FABRIKAM
Name : Domain Admins
SID : S-1-5-21...
Improve WMI performance
I’ve run into a final snag, though. When I connect to a remote computer over a slow network connection, I get the group members, but the GetRelated method only outputs one group member at a time. To speed things up, I would prefer to retrieve multiple members at a time; say, in batches of 50 members.
To do this, I need to create a System.Management.EnumerationOptions object, set its BlockSize property to 50, and use this object as the final parameter of the GetRelated method:
$wmiEnumOpts = new-object System.Management.EnumerationOptions
$wmiEnumOpts.BlockSize = 50
$credential = Get-Credential
$computerName = "remotecomputer"
$wmiObject = Get-WMIObject `
-Class Win32_Group `
-Filter "LocalAccount=TRUE and SID='S-1-5-32-544'" `
-ComputerName $computerName `
-Credential $credential
$wmiObject.GetRelated("Win32_Account","Win32_GroupUser","","",
"PartComponent","GroupComponent",$FALSE,$wmiEnumOpts)
Now, I get the same output, but it runs faster because the GetRelated method retrieves the local Administrator group’s membership in batches of 50 instead of one at a time.
User friendly output
When I use the GetRelated method, the object instances are output by the default formatter in Windows PowerShell. These output objects provide a lot of detail, but I only want to know three things:
- The computer name where the local Administrators group resides
- The name of the Administrators group (in case it’s not in English or has been renamed)
- The name of the member of the group
To do this, I’ll use the Select-Object cmdlet to output customized objects that contain only the information I want:
$wmiEnumOpts = new-object System.Management.EnumerationOptions
$wmiEnumOpts.BlockSize = 20
$credential = Get-Credential
$computerName = "remotecomputer"
$wmiObject = Get-WMIObject `
-Class Win32_Group `
-Filter "LocalAccount=TRUE and SID='S-1-5-32-544'" `
-ComputerName $computerName `
-Credential $credential
$groupName = $wmiObject.Name
$wmiObject.GetRelated("Win32_Account","Win32_GroupUser","","",
"PartComponent","GroupComponent",$FALSE,$wmiEnumOpts) |
Select-Object `
@{Name="ComputerName"; Expression={$_.__SERVER}},
@{Name="Name"; Expression={$groupName}},
@{Name="Member"; Expression={$_.Caption}}
Note that I’m retrieving the group’s name before calling the GetRelated method so I can include it in the output objects.
There is one final “tweak” I want to include. If the account is a local account, I want to omit the computer name from the Member output property; that is, I want my Member property to contain Administrator rather than RemoteComputer\Administrator. To get this effect, I’ll use the Windows PowerShell -replace operator to replace the computer name from the Caption property with an empty string. To do this, let’s replace the final line of script:
@{Name="Member"; Expression={$_.Caption}}
with this instead:
@{Name="Member"; Expression={$_.Caption -replace "^$($_.__SERVER)\\", ""}}
This code gives me the final output I want:
ComputerName Name Member
------------ ---- ------
REMOTECOMPUTER Administrators Administrator
REMOTECOMPUTER Administrators D1\Domain Admins
...
Put it all together
Now that we have the working script, all that’s left is to put it together so it’s easy to use. You can download the entire script from the TechNet Gallery, and it supports pipeline input for computer names: Get-LocalAdminGroupMember.ps1.
~Bill
Thanks Bill. Excellent post.
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