Remotely Checking for Local Administrators

I got a call from a very good friend of mine recently who was tasked with finding out which machines had users designated as local admins.  I told him I didn’t have anything in my bag-o-tricks for that but off the top of my head it would require:

  • Querying AD for a list of machines
  • Checking to see if the machine was online
  • Connecting to the asset and checking to see if users were added to the local admins group (e.g.: net localgroup administrators)

Thinking this might prove valuable here, I figured I’d try my hand at it.  I began my AD query targeting a specific OU and while contemplating how best to proceed, it occurred to me: Surely someone has already done this.
And sure enough, the Intarwebs hath provided abundantly.

I decided to go with the net localgroup administrators approach from PowerShell.org not because its better than ADSI or some of the other methods out there, but only because that’s where my head was at at the time.

Please do note that my approach is not nearly as elegant (or complete) as Boe Prox’s solution on the TechNet Script Centre and likely where I would go if I needed to this in our environment.

I opted to feed the script an array of OU’s versus doing something like

$PCs = Get-ADComputer -Filter * -Properties Name,DistinguishedName | ? { $_.DistinguishedName -like "*OU=LocalAdmins,*" } | select Name,DistinguishedName

I don’t know that I can support one method over the other but they both work fine.

Here’s where I ended up:

[string[]]$cstm_LocalGroupName = 'Administrators'
$cstm_arrSearchBase = @('OU=OU,OU=Test,OU=Some,DC=domain,DC=tld')
Try
    {
        $cstmRslt_arrLocalMembers = @()
        Foreach($cstm_SearchBase in $cstm_arrSearchBase) { $cstm_Computer += Get-ADComputer -SearchBase $cstm_SearchBase -SearchScope Subtree -Filter * -Properties Name,DistinguishedName -ErrorAction Stop | select Name,DistinguishedName }

        Foreach($cstm_Machine in $cstm_Computer)
            {
                if($cstm_ADRetrieved -ne $true) { $cstm_Machine = [pscustomobject]@{$cstm_Machine = $cstm_Machine; Name = $cstm_Machine; DistinguishedName = 'LOCAL'} }
                if(Test-ComputerOnline $cstm_Machine.Name)
                    {
                        Try
                            {
                                Foreach($cstm_LocalGroup in $cstm_LocalGroupName) { $cstmRslt_arrLocalMembers += Invoke-Command -ComputerName $cstm_Machine.Name -ScriptBlock {[pscustomobject]@{Computername = $env:COMPUTERNAME;DistinguishedName = $args[1];Group = $args[0];Members = $(net localgroup $args[0] | where { $_ -AND $_ -notmatch "command completed successfully" } | select -skip 4)}} -ArgumentList $cstm_LocalGroup,$($cstm_Machine.DistinguishedName) -HideComputerName -ErrorAction Stop | Select * -ExcludeProperty RunspaceID }
                                write-host "Online`t$($cstm_Machine.Name)`tSUCCESS" -ForegroundColor Green
                            }
                        Catch
                            {
                                $cstmRslt_arrLocalMembers += [pscustomobject]@{Computername = $($cstm_Machine.Name);DistinguishedName = $($cstm_Machine.DistinguishedName);Group = $cstm_LocalGroup;Members = "ERROR: $($_.exception.GetType().FullName)"}
                                write-host "Online`t$($cstm_Machine.Name)`tERROR" -ForegroundColor Red
                            }
                    }
                Else
                    {
                        $cstmRslt_arrLocalMembers += [pscustomobject]@{Computername = $($cstm_Machine.Name);DistinguishedName = "OFFLINE";Group = $cstm_LocalGroup;Members = "OFFLINE"}
                        write-host "Offline`t$($cstm_Machine.Name)`tOFFLINE" -ForegroundColor Gray
                    }
            }
    }
Catch { $_ }

# Raw Results
$cstmRslt_arrLocalMembers

After targeting a couple of OU’s and touching a few hundred machines, I got some really unusual results.
Instead of seeing what I expected to see, I saw some odd entries within

Computername      : COMPUTER001
DistinguishedName : CN=COMPUTER001,OU=LocalAdmins,OU=Queens,OU=NewYork,DC=domain,DC=tld
Group             : Administrators
Members           : {Administrator, DOMAIN\IT-Engineers, DOMAIN\ServiceAccounts}

Computername      : COMPUTER002
DistinguishedName : CN=COMPUTER002,OU=LocalAdmins,OU=Houston,OU=Texas,DC=domain,DC=tld
Group             : Administrators
Members           : {Administrator, DOMAIN\IT-Engineers, DOMAIN\User002}

IsReadOnly     : False
IsFixedSize    : False
IsSynchronized : False
Keys           : {Group, Computername, DistinguishedName, Members}
Values         : {Administrators, COMPUTER003, CN=COMPUTER003,OU=LocalAdmins,OU=Fresno,OU=California,DC=domain,DC=tld, Administrator DOMAIN\IT-Engineers DOMAIN\User003 DOMAIN\ServiceAccounts}
SyncRoot       : System.Object
Count          : 4

IsReadOnly     : False
IsFixedSize    : False
IsSynchronized : False
Keys           : {Group, Computername, DistinguishedName, Members}
Values         : {Administrators, COMPUTER004, CN=COMPUTER004,OU=LocalAdmins,OU=Fresno,OU=California,DC=domain,DC=tld, Administrator DOMAIN\IT-Engineers DOMAIN\ServiceAccounts}
SyncRoot       : System.Object
Count          : 4

Computername      : COMPUTER005
DistinguishedName : OFFLINE
Group             : Administrators
Members           : OFFLINE

Turns out those machines returning those objects are still running PowerShell 2.0!
So the work around:

# This works for PowerShell 2.0
Foreach($cstm_LocalGroup in $cstm_LocalGroupName) { $cstmRslt_arrLocalMembers += Invoke-Command -ComputerName $cstm_Machine.Name -ScriptBlock {New-Object -TypeName PSObject -Property @{'Computername' = $env:COMPUTERNAME;'DistinguishedName' = $args[1];'Group' = $args[0];'Members' = $(net localgroup $args[0] | where { $_ -AND $_ -notmatch "command completed successfully" } | select -skip 4)}} -ArgumentList $cstm_LocalGroup,$($cstm_Machine.DistinguishedName) -HideComputerName -ErrorAction Stop | Select * -ExcludeProperty RunspaceID }

# This works for PowerShell 1.0
Foreach($cstm_LocalGroup in $cstm_LocalGroupName) { $cstmRslt_arrLocalMembers += Invoke-Command -ComputerName $cstm_Machine.Name -ScriptBlock {$tmpPSO = New-Object -TypeName PSObject;$tmpPSO | Add-Member -MemberType NoteProperty -Name ComputerName -Value $env:COMPUTERNAME;$tmpPSO | Add-Member -MemberType NoteProperty -Name DistinguishedName -Value $args[1];$tmpPSO | Add-Member -MemberType NoteProperty -Name Group -Value $args[0];$tmpPSO | Add-Member -MemberType NoteProperty -Name Members -Value $(net localgroup $args[0] | where { $_ -AND $_ -notmatch "command completed successfully" } | select -skip 4);$tmpPSO} -ArgumentList $cstm_LocalGroup,$($cstm_Machine.DistinguishedName) -HideComputerName -ErrorAction Stop | Select * -ExcludeProperty RunspaceID }

Seeing that a chain is only as strong as its weakest link, I’m using the PowerShell 2.0 line above to work in our environment.  Also, to tidy up your PowerShell ISE or shell, you can use the following to clean up the variables:

Get-Variable -Name cstm_* | Remove-Variable | Out-Null
Get-Variable -Name cstmRslt_* | Remove-Variable | Out-Null

I hope my friend finds this useful, if anything from a PowerShell learning perspective but I’m going to steer him towards the other, more complete, solutions found elsewhere.

 

Good Providence to ya Buddy!

Advertisements

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s