With the PowerShell script I discuss in this post, you can find out who has administrator rights on specific computers.

One of my customers is a typical freewheeling creative media giant, with pool tables, ping-pong, free lattes, and a real lack of concern for network security—which is why they pay us to manage it for them.

Over the years, as I'm sure you appreciate, one or two more technically minded people in your organization may have asked to be local administrators on their computers. Or at least they wanted a separate local admin account created so they could continually update their equally awesome open-source development tool. Of course, no one has ever heard of this, and it doesn't have a management engine GPO can control.

Unfortunately for those free spirits, someone just bought out their company. And although the users can go along with the pool tables and the lattes, the new owner will not tolerate under any circumstances users with administrative permissions on their computers—quite rightly so.

So we face the easy option to enable restricted groups and wipe out any local admin permissions anyone may have had. Before doing this, I wanted to search those devices to see who actually did still have local admin permissions and whether we needed to inform any folks their party was over or if we could just silently roll out a restricted group.

PowerShell seemed like an easy option, but with no direct way to query local groups, we needed to search for a way to do it and additionally exclude our own local administrator accounts from the results.

Using this PowerShell script as a basis, I adapted it to exclude certain accounts from the results (for example, local administrator accounts authorized to be there), direct the query to a specific PC or to an entire organizational unit (OU), and also exclude PCs not active recently.

The base script uses WMI to query Win32_GroupUser and collect all users in all local groups. It then selects only those where the local group is administrators.

<#
.Synopsis
   Script to check membership of the local administrators group on client computers.
.DESCRIPTION
   Script to check membership of the local administrators group on client computers.
   Robert Pearman / WindowsServerEssentials.com
   Version 1.0 August 2018
   Requires use of remote WMI queries to client computers and the ActiveDirectory PowerShell Module.

.EXAMPLE

    LocalAdminGroupAudit.ps1 -ou "ou=myOU,ou=myCompany,dc=myDomain,dc=com" -excludeNames "Administrator","Domain Admins" -activeDays 30

    Search for computers active in the last 30 days in the myOU OU and for members of local administrators group excluding Administrator and the Domain Admins group.

.EXAMPLE

    LocalAdminGroupAudit.ps1 -pcName PC01 -excludeNames "Administrator","Domain Admins"

    Query PC01 for members of local administrators group excluding Administrator and the Domain Admins group.
#>

param(
[array]$excludeNames,
[Parameter(ParameterSetName="Domain")]
[string]$ou,
[int]$activeDays,
[Parameter(ParameterSetName="Computer")]
[string]$pcName
)
$padVal = 22
$adminLabel = "Admin Users".PadRight($padVal," ")
$badLabel = "Total invalid Accounts".PadRight($padVal," ")
$onlineLabel = "Online Computers".PadRight($padVal," ")
$offlineLabel = "Offline Computers".PadRight($padVal," ")
$activeLabel = "Computers Active Since".PadRight($padVal," ")
Write-Output "Local Admin Group membership audit tool. Searches computers for membership of Local Administrators group."
Write-Output "Accounts not listed in -excludeNames will be displayed."
if($ou)
{
    $searchLabel = "Searching Computers in".PadRight($padVal," ")
    Write-Output "$searchLabel : $ou"
    $now  = [datetime]::Now
    $searchDays = $now.AddDays(-$activedays)
    Write-Output "$activeLabel : $searchDays"
}
if($pcName)
{
    $searchLabel = "Searching Computer".PadRight($padVal," ")
    Write-Output "$searchLabel : $pcName"
}
Write-Output "$adminLabel : $excludeNames"
$compFoundLabel = "Computers Found".PadRight($padVal, " ")
$badAccounts = 0
$onlineComputer = 0
$offlineComputer = 0
if($ou)
{
    $computers = Get-AdComputer -filter {LastLogonDate -ge $searchDays } -searchbase $ou -properties LastLogonDate | sort Name -Descending
}
if($pcName)
{
    $computers = Get-AdComputer $pcName -properties *
}
$computerCount = ($computers | Measure-Object).Count
$pCounter = 0
Write-Output "$compFoundLabel : $computerCount"
foreach ($computer in $computers )
{
    $pCounter++
    Write-Progress -Activity "Searching.. Please Wait.." -Status $computer.Name -PercentComplete ($pCounter / $computerCount*100)
    try{
        $groupMembers = get-wmiobject win32_groupUser -ComputerName $computer.Name -ErrorAction Stop
        $onlineComputer++
        $groupMembers = $groupMembers | where { $_.GroupComponent -like "*Administrators*"}
        foreach ($member in $groupMembers)
        {
            $name = $member.PartComponent.Split("=")
            $uName = $name[2].Replace('"',"")
            $gName = $member.GroupComponent.Split("=")
            $gName = $gName[2].Replace('"',"")
            if(($excludeNames) -contains $uName)
            {
                # Skip
            }
            else
            {
                $badAccounts++
                $comGroupObj = New-Object System.Object
                $comGroupObj | Add-Member -MemberType NoteProperty -Name Name -Value $computer.Name
                $comGroupObj | Add-Member -MemberType NoteProperty -Name UserName -Value $uName
                $comGroupObj | Add-Member -MemberType NoteProperty -Name GroupName -Value $gName
                $comGroupObj
            }
        }
    }
    catch{
        $offlineComputer++
    }
}
Write-Output ""
Write-Output "$badLabel : $badAccounts"
Write-Output "$onlineLabel : $onlineComputer"
Write-Output "$offlineLabel : $offlineComputer"

In the example, we have queried an OU for any local Aadministrator accounts not either Administrator, REDACTED, or part of the Domain Admins on a PC active on the network since September 1. The result is 139 computers to search, 31 offline computers, and 4 accounts that fail our test.

We can now speak to those four individuals and warn them they'll soon lose their admin permissions.

Subscribe to 4sysops newsletter!

Local admin query

Local admin query

Of course the good thing about collating all of this information into $report is that we can then use the object to build a picture over time by exporting the result to a CSV or emailing the report to an auditor or anything you can think of!

avataravatar
43 Comments
  1. Melvin Backus 4 years ago

    Sweet script. Is it possible there’s a way I could modify it to include a default set of excluded names?  I never want to see Administrator or Domain Admins, and probably a few others in some groups of machines.

    • Author
      Robert Pearman 4 years ago

      Yes you can do that.

      At line 33, create a new array to include your default exclusions, then add that new array to the $excludeNames array.

      $defaultExclude = @(

      “Administrator”,

      “Domain Admins”

      )

      $excludeNames += $defaultExclude

      Save and run to test.

      avatar
  2. Melvin Backus 4 years ago

    PERFECT! Works great.

    Thank you

  3. John Wagner 4 years ago

    How about having this save to a file?

    • Author
      Robert Pearman 4 years ago

      Yes, can do that.

      Up around line 32, add in a new array.

      $report = @()

      Then line 93 add,

      $report += $comGroupObj

      Then at the end around line 111, you can do something like,

      $report | export-csv <my file path>

      Should work fine.

      • John Wagner 4 years ago

        So I added this in and it ends up generating the following error. I am running powershell with elevated rights so I should have full rights to the path I’m using.

        export-csv : Access to the path ‘C:\logs’ is denied.
        At C:\temp\LocalAdminGroupAudit.ps1:107 char:11
        + $report | export-csv C:\logs
        + ~~~~~~~~~~~~~~~~~~
        + CategoryInfo : OpenError: (:) [Export-Csv], UnauthorizedAccessException
        + FullyQualifiedErrorId : FileOpenFailure,Microsoft.PowerShell.Commands.ExportCsvCommand

        • Author
          Robert Pearman 4 years ago

          You should use a full file path, like c:\logs\mylog.csv

           

          • john wagner 4 years ago

            doh! thanks my bad!! 🙂
            works perfectly now.

          • john wagner 4 years ago

            OOps i spoke to soon.

            So the report file is generated but its empty. lol

          • john 4 years ago

            So i just added a redirect at the end of the cli call to send it to a text file and that works okay.

  4. John Wagner 4 years ago

    One issue I ran into in my environment.

    I have a  parent OU with several sub OUs and all have computer objects under them.  When I kick off the script it only scans the parent and doesn’t scan the subs.  How would I set it to scan parent and subs?

  5. David Figueroa 4 years ago

    That’s easy.. you just need to change this line (59):

    $computers = Get-AdComputer -filter {LastLogonDate -ge $searchDays } -searchbase $ou -properties LastLogonDate | sort Name -Descending

     
    Add the parameter -SearchScope Subtree, and that will pick up all the computers you need..

    $computers = Get-AdComputer -filter {LastLogonDate -ge $searchDays } -searchbase $ou -properties LastLogonDate -SearchScope Subtree| sort Name -Descending

    David F.

  6. David Figueroa 4 years ago

    Sorry.. that didn’t quote correctly..

    Original:
    $computers = Get-AdComputer -filter {LastLogonDate -ge $searchDays } -searchbase $ou -properties LastLogonDate | sort Name -Descending
     

    Updated:

    $computers = Get-AdComputer -filter {LastLogonDate -ge $searchDays } -searchbase $ou -properties LastLogonDate -SearchScope Subtree| sort Name -Descending

     
    David F.

    avatar
  7. John Wagner 4 years ago

    Awesome thanks very much!!!!

  8. Eddiberto Silvaz 4 years ago

    Can you make it run for multiple PC that you have a file with the PC like “pc.txt”

     

  9. moulouya 4 years ago

    Hello ,

    I receive this kind of error when i execute the script:

    Parameter set cannot be resolved using the specified named parameters.
    + CategoryInfo : InvalidArgument: (:) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : AmbiguousParameterSet

  10. moulouya 4 years ago

    I copy paste the script with changing  :

    ParameterSetName=”domain = i set my domain)

    ParameterSetName=”computer = i set the path of the OU”)

    Regards,

    • Author
      Robert Pearman 4 years ago

      OK that sounds like a bad idea.

      Copy the script ‘as is’, make no changes.

      When you run it, do the following..

      .\myscript.ps1 -pcname pc01

      This will query one pc, named PC01

      .\myscript.ps1 -ou “ou=myou,dc=domain,dc=com”

      This will query the whole OU.

  11. Eddiberto Silvaz 4 years ago

    Robert,

    I left the script as is and ran it against my domain that has 4500+ pc it runs great but the buffer gets full and before it can copy the $report | export-csv c:\logs\logs.csv it ran for days and I got no data.  if I break it down to groups of 100 it works.

    I had modified another script in VBS that I had set the files and created a log that passed the data results from each scan, so there were no buffering issues

    Option Explicit

    Const LogFile = “LocalAdmins.log”
    Const resultFile = “LocalAdministratorsMembership.csv”
    Const inputFile = “workstations.txt”

    Dim fso
    Set fso = CreateObject(“Scripting.FileSystemObject”)

    Dim shl
    Set shl = WScript.CreateObject(“WScript.Shell”)

    Dim fil
    Set fil = fso.OpenTextFile(inputFile)

    Dim results
    Set results = fso.CreateTextFile(resultFile, True)

    WriteToLog “Beginning Pass of ” & inputFile & ” at ” & Now()
    WScript.Echo “Beginning Pass of ” & inputFile & ” at ” & Now()
    ‘On Error Resume Next

    Dim grp
    Dim line
    Dim exec
    Dim pingResults
    Dim member

    What would you recomend I do to with your script to scan 4000+ systems and keep the data?

  12. Author
    Robert Pearman 4 years ago

    Are they all in one single OU?

    Im not particularly experience with large environments like that. How many objects does it process before it crashes? 1000?

  13. Eddiberto Silvaz 4 years ago

    Are they all in one single OU? YES computer is in one or broken into areas

    How many objects does it process before it crashes? 1000?  it doesn’t crash it continues and displays about 100 lines on the screen, but nothing gets passed over to the CSV file.  The script runs for days, I’m thinking to increase the buffer size and the number of buffers to see if it can keep all the data.

    I’m not very good using Powershell but I’m thinking if I add a line to move output to a temp log.txt and then push the log.txt to my log.csv when the script is completed?

  14. David Figueroa 4 years ago

    I can’t see the entire modified script.. but the basic idea isn’t too tough to do what you need Eddiberto..

    Change the array to an arraylist and use a .add() for each object, then use your $pCounter to export every 100 lines and then .Clear() it and continue on.  Here’s a quick example of what I’m talking about..

    $MyList = [System.Collections.ArrayList]::New()
    
    1..1000 | foreach-object{
        $thisNumber = $_
        $null = $MyList.Add($thisNumber)
        if ($thisNumber % 100 -eq 0)
        {
            $MyList.ToString() | Export-CSV -path c:\temp.csv -Append -NoTypeInformation
            $MyList.Clear()
        }
    }
    

    So, this should export every 100 numbers which will keep your memory from running out.  Base on the $pCounter you would use if ($pCounter % 100 -eq 0) { $var | export-csv -path <path> -append -notypeinformation }.

    Coralon

    • Eddiberto Silvaz 4 years ago

      David,

      the code is the original on top with the following added

      create a new array to include your default exclusions, then add that new array to the $excludeNames array

      $defaultExclude = @(“Administrator”,”Domain Admins”)

      $excludeNames += $defaultExclud

      ———————————————————————-

      Up around line 32, add in a new array.

      $report = @()

      Then line 93 add,

      $report += $comGroupObj

      Then at the end around line 111, you can do something like,

      $report | export-csv <my file path>

      where would you add your code?

  15. moulouya 4 years ago

    Cooooool, thx a lot , it works

  16. Jim 4 years ago

    I know this thread is a bit old, but I can run the script when I use -PCName. However when I use -ou it runs but returns nothing (No computers found). I've tried it on several different OUs. 

    • Swapnil Kambli 4 years ago

      Hi Jim, The script is running fine for me. Could you please provide the exact command you are running and the result.

      • Jim 4 years ago

        I figured it out actually, I was not adding in the rest of the variables after the ou section (-excludeNames, active days, etc)

        But thanks for replying!

        avatar
  17. Steve 4 years ago

    For some reason when I run it I get no results.  It shows:

     

    Local Admin Group membership audit tool. Searches computers for membership of Local Administrators group.
    Accounts not listed in -excludeNames will be displayed.
    Searching Computers in : <redacted> (it shows the proper OU)
    Computers Active Since : 04/14/2019 16:22:25
    Admin Users            : Administrators Domain Admins
    Computers Found        : 0

    Total invalid Accounts : 0
    Online Computers       : 0
    Offline Computers      : 0

     

    I even made sure to run it as an admin, and tried the Import-Module ActiveDirectory to see if that helped, but still get the same results.

     

  18. Steve 4 years ago

    Disregard, I didn't notice but I was searching the "Users" OU instead of computers.  It works now.

  19. Michael 4 years ago

    Hi Robert 

    This is excellent and does exactly what I need.

    However one thing is our hostname are pretty long so the left column does not show the full hostname like the below.

    Can you please advise where do I modify to make the left column to fit more character.

     

    thanks

    DT-4RFS… test                                      Administrators
    DT-5CG6… test                                      Administrators

    avatar
    • Swapnil Kambli 4 years ago

      Hi Michael,

      Please make use of Format-table to have the desired output

      Line no 93:  $comGroupObj | Format-Table -AutoSize

      • Author
        Robert Pearman 4 years ago

        Beat me to it!

      • Martin 2 years ago

        hi, when i use on line 93 $comGroupObj | ft -AutoSize outut is like this 

        Name       UserName   GroupName     
        —-       ——–   ———     
        pc1 monitoring Administrators

        Name       UserName   GroupName     
        —-       ——–   ———     
        pc2 monitoring Administrators

        Name       UserName   GroupName     
        —-       ——–   ———     
        pc3 monitoring Administrators

        can someone help me?

  20. michael 4 years ago

    Thanks so much Swaphnil and Robert, worked perfectly.

  21. Redi 3 years ago

    I have the same problem as Steve and i am searching a Computer OU.Can anyone please advise.

    thanks,

  22. Tilak 3 years ago

    Hi Friends,

    Need your help to get powershell script on the below criteria.

    Thanks in advance for helping me.

    Create a script that creates a list with percentage  of computers running with users as local admin.

    % for Windows 10 STD and a list of PC'c
    % for Windows 10 VC and a list of PC'c
    % for Windows 7 STD (Both x86 and x64) and a list of PC'c
    % for Windows 7 VC and a list of PC'c
    % for Windows XP (all) and a list of PC'c

    Thanks,

    Tilak.

  23. Siva 2 years ago

    PS C:\Users\Administrator\Desktop> .\localadmin.ps1 -ou “ou=myou,dc=domain,dc=com” | Export-CSV adminlist.csv
    Get-AdComputer : The supplied distinguishedName must belong to one of the following partition(s): ‘DC=aprim,DC=local ,
    CN=Configuration,DC=aprim,DC=local , CN=Schema,CN=Configuration,DC=aprim,DC=local ,
    DC=DomainDnsZones,DC=aprim,DC=local , DC=ForestDnsZones,DC=aprim,DC=local’.
    At C:\Users\Administrator\Desktop\localadmin.ps1:58 char:18
    + $computers = Get-AdComputer -filter {LastLogonDate -ge $searchDays } -search …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidArgument: (:) [Get-ADComputer], ArgumentException
    + FullyQualifiedErrorId : ActiveDirectoryCmdlet:System.ArgumentException,Microsoft.ActiveDirectory.Management.Comm
    ands.GetADComputer

    • Siva 2 years ago

      Getting above Error when i run a script

  24. RobG 2 years ago

    Hi Robert,

    I already have a list of server names, how could the script be modified to run this script against a .txt file of server names ?

    Thanks,
    Rob

  25. Kim Vogel 2 months ago

    Hi Robert,
    So far so good, I added the $report=@() steps and I get the output as a csv. However, I would like for the UserName column to show the contents as “some_domain\some_name” as we have users from a different domain with local admin permissions. Is there an easy solution for this? -Thx.

    • Author
      Robert Pearman 2 months ago

      Yes you can do that. The string containing the username is split and only the username section is kept.

      If you look at $uName, this is $name[2] replacing the , with nothing.

      So you will want to get $name[1] which will contain either the host name of the pc or the domain name. It will need some tidying as it will possibly contain other characters. Once you have it tidied up you can do something like $dname = $name[1]
      $uname = $dname, $name[2] -join “\”

      This is example code and won’t work if you just paste it in.

Leave a reply

Your email address will not be published.

*

© 4sysops 2006 - 2022

CONTACT US

Please ask IT administration questions in the forums. Any other messages are welcome.

Sending

Log in with your credentials

or    

Forgot your details?

Create Account