Caveats With Enumerating Local Administrators
Posted on December 28, 2021
- and tagged as
- powershell,
- windows
There are several ways to get a list of local administrators on a Windows system - be it a server or a client, but the accuracy of the data returned can vary based on the method and whether there is domain controller reachability (assuming the system in question is domain joined). In this post we’re going to use two PowerShell cmdlets and explore how the ability of the system to talk to a domain controller can impact the accuracy of the output.
Let’s begin by looking at the two methods under normal circumstances with the DC(s) being reachable.
Auditing Local Administrators with PowerShell
If you’re on a relatively recent OS or PowerShell version you can use Get-LocalGroupMember
.
PS C:\> Get-LocalGroupMember -Name Administrators
ObjectClass Name PrincipalSource
----------- ---- ---------------
Group LAB\Domain Admins ActiveDirectory
User LAB\md ActiveDirectory
User PC1\Administrator Local
We can get the same result using WMI, and this method may be preferable if we’re writing code that needs to work access older PowerShell versions.
$LocalAdminGroup = Get-WMIObject Win32_Group -filter "name='Administrators'"
$LocalAdmins = ($LocalAdminGroup.GetRelated('Win32_UserAccount')).Caption
$LocalAdmins += ($LocalAdminGroup.GetRelated('Win32_Group')).Caption
PS C:\> $LocalAdmins
PC1\Administrator
LAB\md
LAB\Domain Admins
Everything looks fine so far but next we’ll repeat the commands after removing network reachability to a domain controller.
Without Domain Controller Reachability
Let’s run the same commands when the device has come up without having connectivity to a Domain Controller. This may occur because the user is working from home and doesn’t have a VPN connected or they could be off-site and just checking emails when the audit script happens to run via whatever management platform is in place.
First up is Get-LocalGroupMember
:
PS C:\> Get-LocalGroupMember -Name Administrators
Get-LocalGroupMember : An unspecified error occurred: error code = 1789
At line:1 char:1
+ Get-LocalGroupMember -Name Administrators
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (Administrators:LocalGroup) [Get-LocalGroupMember], Win32InternalException
+ FullyQualifiedErrorId : Win32Internal,Microsoft.PowerShell.Commands.GetLocalGroupMemberCommand
While it doesn’t give us the results we want at least it throws an error and we know something went wrong.
Now let’s test the WMI method.
PS C:\> $LocalAdminGroup = Get-WMIObject Win32_Group -filter "name='Administrators'"
PS C:\> $LocalAdmins = ($LocalAdminGroup.GetRelated('Win32_UserAccount')).Caption
PS C:\> $LocalAdmins += ($LocalAdminGroup.GetRelated('Win32_Group')).Caption
PS C:\>
PS C:\> $LocalAdmins
PC1\Administrator
This time no error is thrown and we only get the local Administrator account returned. If we’re auditing systems to evaluate local admin privileges this can result in skewed and incorrect results due to missing data.
So how can we account for this, assuming we wish to use the WMI method? We need a simple and clean way to ensure the system can reach a domain controller to properly resolve the SIDs cached on the machine.
Testing Domain Controller Availability
The best way I’ve found is to stick a small method that queries a DC inside a try/catch block, as is shown here.
try {
[System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain() | Out-Null
$LocalAdminGroup = Get-WMIObject....*truncated*
}
catch {
throw $_
}
We can pipe the output to Out-Null
as we don’t care about the result, only that it doesn’t fail. Here’s an example of a failure:
PS C:\> try {[System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain()} catch {throw $_}
Exception calling "GetComputerDomain" with "0" argument(s): "The local computer is not joined to a domain or the domain cannot be contacted."
At line:1 char:6
+ try {[System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDo ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : ActiveDirectoryObjectNotFoundException
Determining whether a device is domain joined
One last thing we may want to verify is that the device we’re on is actually domain joined as some systems may not be. This is easily done with the following WMI query.
PS C:\> (Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain
True
As a sidenote I’ll mention that when querying WMI we usually want to use the more modern Get-CIMInstance
cmdlet, however, if we’re aiming for maximum compatibility with older PowerShell versions then Get-WmiObject
makes more sense.
Azure AD Considerations
Determining whether a device is Azure AD joined
I’ve not seen a clean and short WMI class or PowerShell cmdlet to tell us whether a device is AAD joined as we have for ADDS but there is the dsregcmd /status
command. Unfortunately Microsoft don’t always care about playing nice with PowerShell so if we want to incorporate this into scripts we need to parse the string output.
Here’s a truncated example of the output format:
PS C:\> dsregcmd /status
+----------------------------------------------------------------------+
| Device State |
+----------------------------------------------------------------------+
AzureAdJoined : YES
EnterpriseJoined : NO
DomainJoined : NO
Device Name : pc1
The following code can be used to take the above output and turn it into a usable PowerShell object.
$Output = [PSCustomObject]@{}; (dsregcmd /status | sls "(^.*?) : (.*$)").Matches.Value | % {$Line = $_.Split(":",2); $Output | Add-Member -Type NoteProperty -Name ($Line[0].Replace(" ","").Replace("-","")).Trim() -Value $Line[1].Trim()}
PS C:\> $Output
AzureAdJoined : YES
EnterpriseJoined : NO
DomainJoined : NO
DeviceName : pc1
*truncated*
PS C:\> $Output.AzureAdJoined
YES
Alternatively, if we want to avoid parsing string output we can instead hit the native Win32 APIs directly to get the Azure AD join status of a device.
function Get-AADJoinStatus() {
[CmdletBinding()]
Param()
try {
[Win32.NetAPI32] | Out-Null
}
catch {
$MemberDefinition = @'
[DllImport("netapi32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
public static extern int NetGetAadJoinInformation(string pcszTenantId, out IntPtr ppJoinInfo);
'@
Add-Type -MemberDefinition $MemberDefinition -Name NetAPI32 -Namespace Win32
}
$AADJoined=[IntPtr]::Zero
try {
[Win32.NetAPI32]::NetGetAadJoinInformation($null,[ref]$AADJoined) | Out-Null
[bool]($AADJoined.ToInt64())
}
catch {
Write-Output $false
}
}
PS C:\> Get-AADJoinStatus
True
Now that we have that out of the way, let’s move on to getting a list of Azure AD users in the local admin group.
Enumerating Azure AD accounts in the local Administrators group
Unfortunately none of the enumeration methods we’ve covered thus far will work for retrieving AAD entities in the local admin group. Let’s begin by looking at the actual configured Administrators group members on this lab machine (which is no longer part of any ADDS domain and is only Azure AD joined).
There’s a single AAD account called User and two AAD SIDs representing Global Admins and Device Admins.
What does Get-LocalGroupMember
have to say about this?
Not much it seems. This error is different to the one we had before and an issue has been raised but has not been fixed at the time of writing.
What about our WMI method?
Again, no data relating to our Azure AD local admins, however, we can get further by hitting the GetRelationships
method.
$LocalAdminGroup = Get-WMIObject Win32_Group -filter "name='Administrators'"
$LocalAdminGroup.GetRelationships('Win32_GroupUser').PartComponent
\\PC1\root\cimv2:Win32_UserAccount.Domain="PC1",Name="Administrator"
\\PC1\root\cimv2:Win32_UserAccount.Domain="AzureAD",Name="User"
The output is messy as there’s no nice Caption
property, but it does return the single user AAD account. It is however missing the two AAD admin SIDs. Depending on your requirements this may be acceptable.
We can use a little regex to clean up the output.
$Members = $LocalAdminGroup.GetRelationships('Win32_GroupUser').PartComponent
$Members | % {($_ | Select-String '"(.*?)"' -AllMatches).Matches.Value.Replace('"',"") -join "\"}
PC1\Administrator
AzureAD\User
So can you use this command for enumerating domain accounts? Sure, but it’s not ideal - it fails in a somewhat different way if a DC is not reachable. If the currently logged in user is a local administrator it will list that account correctly, however, it will not list any other accounts or groups (in our example, Domain Admins).
$LocalAdminGroup = Get-WMIObject Win32_Group -filter "name='Administrators'"
$LocalAdminGroup.GetRelationships('Win32_GroupUser').PartComponent
\\PC1\root\cimv2:Win32_UserAccount.Domain="PC1",Name="Administrator"
\\PC1\root\cimv2:Win32_UserAccount.Domain="LAB",Name="md"
cmd.exe net command
There is one last option which isn’t much better - the cmd.exe net
command.
Enumerating Azure AD local admin accounts using net localgroup
PS C:\> net localgroup Administrators
Alias name Administrators
Comment Administrators have complete and unrestricted access to the computer/domain
Members
-------------------------------------------------------------------------------
Administrator
AzureAD\User
The command completed successfully.
Again we’re missing the two AAD admin group SIDs and the output would need to be parsed but it does return the main data point we’re after.
Enumerating ADDS local admin accounts using net localgroup
For ADDS joined devices without DC reachability the net localgroup
command gives the same results as the GetRelationships
WMI method. It lists the currently logged in user if they’re a local admin, but nothing else.
Here is what that output looks like. No error or failure, and incomplete data returned.
PS C:\> net localgroup Administrators
Alias name Administrators
Comment Administrators have complete and unrestricted access to the computer/domain
Members
-------------------------------------------------------------------------------
Administrator
LAB\md
The command completed successfully.
That’s all for this one, I’ll leave it to the reader to combine the above methods to create a script that makes sense for their environment.