Caveats With Enumerating Local Administrators

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).

Azure AD Users in local Administrator group

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?

Get-LocalGroupMember Error when AzureAD joined

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?

Enumerating Azure AD Local Admins with WMI

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.


If you enjoyed this post consider sharing it on , , , or , and .