Earlier this week I was helping someone out on a problem working with the local administrators group. There are a variety of ways to enumerate the members of a local group. The code he was using involved WMI. I hadn't really worked with the WMI approach in any great detail so I thought I'd see how this might work in PowerShell. I ended up with a function to enumerate members of the local administrators group on a computer, as well as test if an account belongs to the group.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
The first function, Get-LocalAdministrators, will connect to a remote computer (it defaults to the local) and returns an object for each member like this:
[cc lang="DOS"]
Name : LocalAdmins
Fullname :
Caption : JDHLAB\LocalAdmins
Description :
Domain : JDHLAB
SID : S-1-5-21-3957442467-353870018-3926547339-1148
LocalAccount : False
Disabled :
Computer : CLIENT1
[/cc]
If I simply wanted a name, that would be pretty easy and I'd use a different approach. But I wanted richer information so that I could sort out what accounts were local, or disabled. I worked under the assumption that I would query a group of machines and save the data to a CSV file so I could later slice and dice the data. Or work with it further in PowerShell. Here's the function.
[cc lang="PowerShell"]
Function Get-LocalAdministrators {
[cmdletbinding()]
Param(
[Parameter(Position=0,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
[ValidateNotNullorEmpty()]
[string[]]$Computername=$env:computername,
[switch]$AsJob)
Begin {
Set-StrictMode -Version latest
Write-Verbose "Starting $($myinvocation.mycommand)"
#define an new array for computernames if this is run as a job
$computers=@()
}
Process {
foreach ($computer in $computername) {
$computers+=$Computer
$sb={Param([string]$computer=$env:computername)
Try {
Write-Verbose "Querying $computer"
$AdminsGroup=Get-WmiObject -Class Win32_Group -computername $Computer -Filter "SID='S-1-5-32-544' AND LocalAccount='True'" -errorAction "Stop"
Write-Verbose "Getting members from $($AdminsGroup.Caption)"
$AdminsGroup.GetRelated() | Where {$_.__CLASS -match "Win32_UserAccount|Win32_Group"} |
Select Name,Fullname,Caption,Description,Domain,SID,LocalAccount,Disabled,
@{Name="Computer";Expression={$Computer.ToUpper()}}
}
Catch {
Write-Warning "Failed to get administrators group from $computer"
Write-Error $_
}
} #end scriptblock
if (!$AsJob) {
Invoke-Command -ScriptBlock $sb -ArgumentList $computer
}
} #foreach computer
} #process
End {
#create a job is specified
if ($AsJob) {
Write-Verbose "Creating remote job"
#create a single job targeted against all the computers. This will execute on each
#computer remotely
Invoke-Command -ScriptBlock $sb -ComputerName $computers -asJob
}
Write-Verbose "Ending $($myinvocation.mycommand)"
}
} #end function
[/cc]
The main part of the function uses WMI to query the Win32_Group class. Because the group may have been renamed, the filter searches for it by well-known SID.
[cc lang="PowerShell"]
$AdminsGroup=Get-WmiObject -Class Win32_Group -computername $Computer -Filter "SID='S-1-5-32-544' AND LocalAccount='True'" -errorAction "Stop""
[/cc]
Once the group is found, you could use an Associators Of query to find all the related objects, whcih would include the group members. But Associators Of queries are not easy to construct, assuming you even knew about them. Instead, in PowerShell the WMI object has a method called GetRelated(). This method in essence runs an Associators Of query for you. But obviously this is much easier.
[cc lang="PowerShell"]
$AdminsGroup.GetRelated() | Where {$_.__CLASS -match "Win32_UserAccount|Win32_Group"} |
Select Name,Fullname,Caption,Description,Domain,SID,LocalAccount,Disabled,
@{Name="Computer";Expression={$Computer.ToUpper()}}
[/cc]
The method allows you to specify a resultant class as a parameter, but it doesn't speed up the process. It only filters the output. So I didn't bother and instead piped the results to Where-Object to get user and group accounts. I also select a few key properties to write to the pipeline. This query takes a little time to run and there isn't any way to speed it up. Although I have something to alleviate the pain.
I decided this would be a good reason to use a background job. So I included a function parameter to run the entire command as a job. You'll notice that in the process block I'm creating a script block.
[cc lang="PowerShell"]
Process {
foreach ($computer in $computername) {
$computers+=$Computer
$sb={Param([string]$computer=$env:computername)
Try {
[/cc]
The script block takes a computername as a parameter. If I'm running the command normally (ie no job), then the script block executes for each computer passed as a parameter or piped in.
[cc lang="PowerShell"]
if (!$AsJob) {
Invoke-Command -ScriptBlock $sb -ArgumentList $computer
}
[/cc]
The command runs locally and queries the remote computer. But if I decide to run this as a job, I wait until the End scriptblock since I wanted to create one job. In order for this to work, I need to keep track of all the pipelined computer names so I keep adding them to $computers in the Process script block. After I've built the list, I again use Invoke-Command, but this time I also specify that the command runs ON the remote machines.
[cc lang="PowerShell"]
if ($AsJob) {
Write-Verbose "Creating remote job"
#create a single job targeted against all the computers. This will execute on each
#computer remotely
Invoke-Command -ScriptBlock $sb -ComputerName $computers -asJob
}
[/cc]
The end result is a job created locally with child jobs that run on all the computers specified. This allows me to keep working in PowerShell, and get the results when I want. Because the command is executing simultaneously it runs a little faster overall. I realize the WMI queries aren't the speediest, but I end up with valuable information.
The other function is a simple test: does this account belong to the administrators group?
[cc lang="PowerShell"]
Function Test-IsLocalAdministrator {
[cmdletbinding()]
Param(
[Parameter(Position=0,HelpMessage="Enter a user or group name in the domain\username format")]
[ValidatePattern("\w+\\\w+")]
[string]$Name="$env:userdomain\$env:username",
[Parameter(Position=1)]
[ValidateNotNullorEmpty()]
[string]$Computername=$env:computername
)
Set-StrictMode -Version latest
Write-Verbose "Starting $($myinvocation.mycommand)"
#Split Name into domain and name parts
$Domain=$Name.Split("\")[0]
$Member=$Name.Split("\")[1]
Try {
Write-Verbose "Querying $computername"
$AdminsGroup=Get-WmiObject -Class Win32_Group -computername $Computername -Filter "SID='S-1-5-32-544' AND LocalAccount='True'" -errorAction "Stop"
Write-Verbose "Getting members from $($AdminsGroup.Caption)"
Write-Verbose "Testing $($name.ToUpper())"
$Found=$AdminsGroup.GetRelationships("Win32_GroupUser") | Where {$_.PartComponent -match "Domain=""$Domain"",Name=""$Member"""}
If ($found) {
Write $True
}
else {
Write $False
}
}
Catch {
Write-Warning "Failed to get administrators group from $computername"
Write-Error $_
}
Finally {
Write-Verbose "Ending $($myinvocation.mycommand)"
}
} #end function
[/cc]
Here I took a slightly different approach. I still get the admins group with the same WMI query. But this time I use the GetRelationships() method which is a .NET equivalent of a References WMI query.
[cc lang="PowerShell"]
$Found=$AdminsGroup.GetRelationships("Win32_GroupUser") | Where {$_.PartComponent -match "Domain=""$Domain"",Name=""$Member"""}
[/cc]
This type of query is quick, at least for groups and returns WMI paths of related objects. All I have to do is parse the PartComponent property and use a regular expression match to see if the domain and account name match. You have to specify the name in domain\name format. If $found exists, the function writes $True. I think Test functions should be simple, quick and concise.
These functions have limited error handling and don't support alternate credentials, although you could certainly add that. I'll be the first to admit that these may not be the best ways to achieve these results, but they are viable options and I am happy with the way I incorporated support for background jobs in the function.
If you would like to try these out, download get-wmiadmin.ps1 and load the functions into your PowerShell session.
net localgroup Administrators
Nothing wrong with an old school approach although you can’t use this remotely as is.
Hi,
When I run the script, the groups aren’t reported. Any ideas as to how solve that?
BJT
The script file only defines the function so if you just run the script nothing will happen. You need to either copy and paste the function into your PowerShell session or dot source the script like this:
PS C:\> . path-to-script \get-wmiadmin.ps1
Then you should be able to run: Help Get-LocalAdministrators
If that works, you can then run the function, Get-LocalAdministrators
OK, I forgot to mention that I put the function in a script from which I called the function (last line: Get-Content ‘G:ScriptsnUser – hbwToevoegenEnOpvragenLocalAdminsservers.txt’ | Get-LocalAdministrators -AsJob)
I do get the usernames, but no groupnames; they stay empty. I also changed the line with the WMI filter to only report the groups, but that had no effect either.
$AdminsGroup.GetRelated() | Where-Object {$_.__CLASS -match “Win32_UserAccount|Win32_Group”}
I always find it difficult to find the correct classes, and I’m not sure this Win32_Group class is the good…
Can you get the function to work properly for the local computer?
Than the script seems to hang; tried it on 2 different servers (W2K3 and W2K8).
These are member servers,right? Does using -verbose provide any more info? Does it work if you run interactively on those servers?
This function doesn’t list groups, it only retrieves the local Administrators group and then enumerates the members. Or are you saying it doesn’t return groups that belong to the local administrators group?
Correct: it doesn’t return the groups that are added to the local admin group. I know there should be at least one domain admin group.
It took me awhile to get there but I get the same results as well, although I thought it tested fine before. I don’t get groups where querying remotely. Double-checking my function.
I think I was misled earlier because some of my account names looked like groups but they weren’t. The bottom line is that this won’t work as written without some extra steps on your part. As I suspected, the command to get the Win32_Groups objects involves another hop to query a domain controller. This type of 2nd hop is not permitted by default for security reasons. Now, you can get this to work, but you must enable CredSSP on the client, where you are running the script and all the computers you want to query. I don’t have room to go into all the gory details now. Take a look at http://rkeithhill.wordpress.com/2009/05/02/powershell-v2-remoting-on-workgroup-joined-computers-%E2%80%93-yes-it-can-be-done/ on how to get started. The same concepts apply to a domain. Once implemented you can use Invoke-Command. I put the function in a script followed by a command to run the function.
PS S:\> invoke-command -FilePath C:\work\get-admins.ps1 -comp chi-fp01.globomantics.local -cred $cred -auth credssp
You have to include a credential object and specify the authentication.