Collecting members of the Local Administrators group

A while ago I had to collect the members of the local administrators group via ConfigMgr. This data has come in handy a number of times so it’s certainly one of those inventory items I don’t plan on stopping just yet. Security being such a big deal, it’s nice to have a way to review local admins for your servers en masse.

Unfortunately, there isn’t a nice WMI class that contains this information (at least I certainly hope not otherwise I’m gonna look really stupid). So, to get this data requires running a script. Since I wanted to be able to collect the data via hardware inventory I chose to create a script which would be used in a Compliance Setting (aka DCM) to create a WMI class and populate the data on a routine basis. Then, hardware inventory just needs to be extended to collect the data from that class. I know it’s not a pretty solution, and perhaps it can be argued that it’s not even the best solution, but this is how I chose to do it.

Early in the testing of my script I realized that it would only work for English because I was using names as they appear in English. So I tried to find a way to make it work with any language. I believe I’ve done that by using SIDs but perhaps there are some outliers that would not work correctly; if you know of an issue please let me know so I can try to fix it!

Before I paste the PowerShell script I use, let me just mention a little about the CI. I used the PowerShell script as the “Remediation script” and for the “Discovery script” I simply used “wscript.echo 3” (VBScript). Then I set the compliance rule like so:

This ensures that every time the compliance setting runs the discovery script is not compliant and therefore the remediation script is run – which in turns populates the data we’re after.

Here’s the PowerShell script I use as the remediation script (where all the real magic happens):

Clear-Host

#*****************************************************
#  HWINV_LocalAdmins Class Creation
#*****************************************************
$WMIClassName = "HWINV_LocalAdmins"
$WMINameSpace = "root\cimv2"
#Delete the class if it exists; if it doesn't don't stop because of the error
Remove-WmiObject -Class $WMIClassName -Namespace $WMINameSpace -ErrorAction SilentlyContinue

#Create the new Class 
$NewClass = New-Object System.Management.ManagementClass($WMINameSpace, [String]::Empty, $null); 
$NewClass["__CLASS"] = $WMIClassName; 
$NewClass.Qualifiers.Add("Static", $true)

$NewClass.Properties.Add("Member",[System.Management.CimType]::String, $false)
$NewClass.Properties["Member"].Qualifiers.Add("Key", $true)
    
$NewClass.Put()

#*****************************************************
#  Script to Populate the HWINV_LocalAdmins class
#*****************************************************

# Use the universal SID to get the Administrator group information regardless of language:
$AdminGroup = New-Object System.Security.Principal.SecurityIdentifier("S-1-5-32-544")
[String]$AdminGroupName = $AdminGroup.Translate([System.Security.Principal.NTAccount]).Value
# Remove the 'domain' from the group name:
$AdminGroupName = $AdminGroupName.Substring($AdminGroupName.IndexOf("\")+1)

# Get the local admins:
$LocalAdmins = net localgroup $AdminGroupName | Where {$_} | Select-Object -Skip 4 -ErrorAction Stop # Assuming all languages will return the same number of header lines

# Because $LocalAdmins is an array and the last item/index is the "command completed successfully" message we'll use a for loop and not do anything with the last item:
for ($i=0; $i -lt $LocalAdmins.Count-1; $i++) {
    [String]$CurAdmin = $LocalAdmins[$i]
    if ($CurAdmin -notmatch "\\") { # escape the backslash so the match doesn't throw an error
        # Add the computername as the account domain since no domain was given:
        $CurAdmin = "$($env:ComputerName)\$CurAdmin"

        # Because the Administrator account is listed without the domain it will fall into this loop; we want to ignore the Administrator account
        # but want to account for any language; so we'll get the account's SID and do a regex compare to ensure we don't include the Admin:
        $CurAdminObj = New-Object System.Security.Principal.NTAccount($CurAdmin)
        $CurAdminSID = $CurAdminObj.Translate([System.Security.Principal.SecurityIdentifier]) | Select -ExpandProperty Value

        if ($CurAdminSID -notmatch "S-1-5-21-\d+-\d+-\d+-500") { # The local Admin account SID always follows this pattern. Note: the SID is variable length hence "\d+" rather than "\d{n}"
            # The account is not the local Admin account so we'll add the account to the WMI class:
            Set-WMIInstance -Class $WMIClassName -Namespace $WMINameSpace -argument @{Member=$CurAdmin} -ErrorAction Stop
            #Write-Host $CurAdmin
        }
    }
    else {
        # The account already has the domain included so just add it directly to the WMI class:
        Set-WMIInstance -Class $WMIClassName -Namespace $WMINameSpace -argument @{Member=$CurAdmin} -ErrorAction Stop
        #Write-Host $CurAdmin
    }
} # End For Loop
# End Script

Since the class is deleted each time the script runs we ensure that the class only contains the latest data (as of script run time).

To collect this data via inventory, here’s the piece needed to add to your “SMS_DEF.mof”

[ SMS_Report (TRUE),
  SMS_Group_Name ("HWINV_LocalAdmins"),
  SMS_Class_ID ("MICROSOFT|HWINV_LocalAdmins|1.0"),
  Namespace ("root\\cimv2") ]
class HWINV_LocalAdmins : SMS_Class_Template
{
    [ SMS_Report (TRUE), key ]
    String     Member;
};

Then, you should be able to use the following (assuming you don’t update the script at all) to see your results in ConfigMgr:

SELECT  TOP 100 ResourceID
       ,Member0
  FROM dbo.v_GS_HWINV_LocalAdmins;

 

9 thoughts on “Collecting members of the Local Administrators group

  1. Hey – thanks for this! I noticed the formatter is removing the backslashes as escape characters, so copying it for other users will fail 🙂

    Like

    1. I’m adding this comment, hope it doesn’t remove the slashes again – try this one:
      #*****************************************************
      # HWINV_LocalAdmins Class Creation
      #*****************************************************

      $WMIClassName = “HWINV_LocalAdmins”
      $WMINameSpace = “root\cimv2”

      #Delete the class if it exists; if it doesn’t don’t stop because of the error
      Remove-WmiObject -Class $WMIClassName -Namespace $WMINameSpace -ErrorAction SilentlyContinue

      #Create the new Class
      $NewClass = New-Object System.Management.ManagementClass($WMINameSpace, [String]::Empty, $null);
      $NewClass[“__CLASS”] = $WMIClassName;
      $NewClass.Qualifiers.Add(“Static”, $true)

      $NewClass.Properties.Add(“Member”,[System.Management.CimType]::String, $false)
      $NewClass.Properties[“Member”].Qualifiers.Add(“Key”, $true)

      $NewClass.Put()

      #*****************************************************
      # Script to Populate the HWINV_LocalAdmins class
      #*****************************************************

      # Use the universal SID to get the Administrator group information regardless of language:
      $AdminGroup = New-Object System.Security.Principal.SecurityIdentifier(“S-1-5-32-544”)
      [String]$AdminGroupName = $AdminGroup.Translate([System.Security.Principal.NTAccount]).Value
      # Remove the ‘domain’ from the group name:
      $AdminGroupName = $AdminGroupName.Substring($AdminGroupName.IndexOf(“\”)+1)

      # Get the local admins:
      $LocalAdmins = net localgroup $AdminGroupName | Where-Object {$_} | Select-Object -Skip 4 -ErrorAction Stop # I believe all languages will return the same number of header lines

      # Because $LocalAdmins is an array and the last item/index is the “command completed successfully” message we’ll use a for loop and not do anything with the last item:
      for ($i=0; $i -lt $LocalAdmins.Count-1; $i++) {
      [String]$CurAdmin = $LocalAdmins[$i]
      if ($CurAdmin -notmatch “\\”) { # escape the backslash so the match doesn’t throw an error
      # Add the computername as the account domain since no domain was given:
      $CurAdmin = “$($env:ComputerName)\$CurAdmin”

      # Because the Administrator account is listed without the domain it will fall into this loop; we want to ignore the Administrator account
      # but want to account for any language; so we’ll get the account’s SID and do a regex compare to ensure we don’t include the Admin:
      $CurAdminObj = New-Object System.Security.Principal.NTAccount($CurAdmin)
      $CurAdminSID = $CurAdminObj.Translate([System.Security.Principal.SecurityIdentifier]) | Select -ExpandProperty Value

      if ($CurAdminSID -notmatch “S-1-5-21-\d+-\d+-\d+-500”) { # The local Admin account SID always follows this pattern. Note: the SID is variable length hence “d+” rather than “d{n}”
      # The account is not the local Admin account so we’ll add the account to the WMI class:
      Set-WMIInstance -Class $WMIClassName -Namespace $WMINameSpace -argument @{Member=$CurAdmin} -ErrorAction Stop
      #Write-Host $CurAdmin
      }
      }
      else {
      # The account already has the domain included so just add it directly to the WMI class:
      Set-WMIInstance -Class $WMIClassName -Namespace $WMINameSpace -argument @{Member=$CurAdmin} -ErrorAction Stop
      #Write-Host $CurAdmin
      }
      } # End For Loop
      # End Script

      Like

  2. Hi benjamin,

    You wrote on your blog about “Collecting members of the Local Administrators group”.
    I would like to collect each Local groups with its members.
    Did you already work on this one?

    Regards
    Pierre

    Like

Leave a comment