Testing for Local Administrator Privilege with PowerShell

When writing PowerShell scripts it is sometimes necessary to know whether the user account running the script has local administrator privileges.  A piece of code often used for this is:


([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")

However, this code only returns true when run in an elevated context, so it is not really a test of whether the user has local administrator privilege, but whether the code is running in an elevated context.

capture
Non-elevated
capture
Elevated

The reason for this is that since Windows Vista and the introduction of  User Account Control (UAC), an account that is a member of the local administrator group will get a split user access token.  For most tasks, the account will use a filtered access token, which has the same rights as a standard user token.  When a task is performed that requires elevation of privilege, the full access token will be used instead.  Hence we get different results from the above command depending on which access token is being used.

Another way of ensuring that code is run elevated (since PowerShell 4) is to use the requires statement


#requires –runasadministrator

But I want to know if the user is a member of the local administrator group.  One way to do that is simply get the username of the logged-on user from WMI, then use net localgroup:


$LoggedOnUsername = (Get-WmiObject -Class Win32_ComputerSystem -Property Username | Select -ExpandProperty Username).Split('\')[1]
Net localgroup administrators | Select-String $LoggedOnUsername

And here is another method using WMI:


$userToFind = (Get-WmiObject -Class Win32_ComputerSystem -Property Username | Select -ExpandProperty Username).Split('\')[1]
$administratorsAccount = Get-WmiObject Win32_Group -filter "LocalAccount=True AND SID='S-1-5-32-544'"
$administratorQuery = "GroupComponent = `"Win32_Group.Domain='" + $administratorsAccount.Domain + "',NAME='" + $administratorsAccount.Name + "'`""
$user = Get-WmiObject Win32_GroupUser -filter $administratorQuery | select PartComponent |where {$_ -match $userToFind}
$user 

The problem with both of these methods is that they wont work with nested groups.  So if a user is a member of a group that is a member of the local administrators group, rather than a direct member, I can’t use these methods.

It can be done, however.

If we go back to our original code we can see that we are creating a Windows Identity object for the current user.  This object has a Claims property, that lists the rights that the user has, mostly in the form of local or domain users or groups.  In the Value property of Claims, we can see the SIDs of those users and groups.  This will include nested groups, because even though the user may not be a direct member of the group, they still have the rights of that group.

capture
Partial listing of SIDs

SIDs are not too friendly by themselves, so lets create a custom object and translate the SIDs to group names:


$Claims = @()
[Security.Principal.WindowsIdentity]::GetCurrent().Claims | foreach {
    $Claim = New-Object psobject
    try
    {
        $SID = New-Object Security.Principal.SecurityIdentifier -ArgumentList $_.Value
        $SID = $SID.Translate([System.Security.Principal.NTAccount])
        Add-Member -InputObject $Claim -MemberType NoteProperty -Name Claim -Value $SID
        Add-Member -InputObject $Claim -MemberType NoteProperty -Name SID -Value $_.Value
        Add-Member -InputObject $Claim -MemberType NoteProperty -Name Type -Value $_.Type.Split('/')[8]
        Add-Member -InputObject $Claim -MemberType NoteProperty -Name Properties -Value $_.Properties.Values
        $Claims += $Claim
    }
    Catch {}
    }
$Claims | sort Claim

capture
User Identity Claims

You’ll notice that the “BUILTIN\Administrators” group is listed in the Claims.  This is the local Administrators group.  Since the SID for this group is the same on all systems, we can search the Claims of the User Identity for this SID, and thereby determine if he or she is a member of the local Administrators group, either directly or indirectly.

We can either use the HasClaim() method:


$LoggedOnUsername = (Get-WmiObject -Class Win32_ComputerSystem -Property Username | Select -ExpandProperty Username).Split('\')[1]
$ID = New-Object Security.Principal.WindowsIdentity -ArgumentList $LoggedOnUsername
$ID.HasClaim('http://schemas.microsoft.com/ws/2008/06/identity/claims/groupsid','S-1-5-32-544')

This will return True or False based on whether the Identity has the claim of that group.

Or we can also simply do a text search on the Value property:


$LoggedOnUsername = (Get-WmiObject -Class Win32_ComputerSystem -Property Username | Select -ExpandProperty Username).Split('\')[1]
$ID = New-Object Security.Principal.WindowsIdentity -ArgumentList $LoggedOnUsername
$ID.Claims.Value.Contains('S-1-5-32-544')

To make it even simpler, we can do a one-liner using the GetCurrent() method:


[Security.Principal.WindowsIdentity]::GetCurrent().Claims.Value.Contains('S-1-5-32-544')

Now I can use this in a script:


if ([Security.Principal.WindowsIdentity]::GetCurrent().Claims.Value.Contains('S-1-5-32-544'))
{
    "You got the power!"
}
Else
{
    "You don't got the power :("
}

Have I got the power? Oh yeah 🙂

capture

 

Detect an Active VPN Adapter During ConfigMgr Deployments

A common requirement with ConfigMgr deployments is to exclude clients that are connected to the corporate network via a VPN, when the total size of the content files for the deployment are too much to be throwing down a slow network link. There is more than one way to do this, but I have seen that not all are reliable and do not work in every case or for every VPN adapter out there.

For example, using PowerShell, you can run either of the following WMI queries to potentially detect an active VPN adapter (your VPN adapter description may be different):

Using Win32_NetworkAdapter


Get-WmiObject -Query "Select * from Win32_NetworkAdapter where Name like '%VPN%' and NetEnabled='True'"

Using Win32_NetworkAdapterConfiguration


Get-WmiObject -Query "Select * from Win32_NetworkAdapterConfiguration where Description like '%VPN%' and IPEnabled='True'"

Since Windows 8 / Server 2012 you can also use the Get-VPNConnection cmdlet:


(Get-VpnConnection -AllUserConnection).where{$_.Name -like "*VPN*" -and $_.ConnectionStatus -eq "Connected"}

Another method is simply:


ipconfig | Select-String 'PPP adapter'

But my preferred method is to check the IPv4 routing table. This is because VPN connections typically use their own subnet, so when connected they will add entries to the IP routing table for that subnet, and will remove them again when disconnected. If you know the subnets used by your VPN connections, you can query for them in WMI:


Get-WmiObject -Query "Select * from Win32_IP4RouteTable where Name like '10.0.99.%' or Name like '10.15.99.%' 

To use this with Application deployments in ConfigMgr, you can create a Global Condition with a script setting.  This condition could be used either to target or to exclude systems using VPN:

capture

Here is an example script that returns “VPN-Active” or “VPN-InActive” based on whether a VPN subnet is detected:


If (Get-WmiObject -Query "Select * from Win32_IP4RouteTable where Name like '10.0.99.%' or Name like '10.15.99.%'")
    {Write-host "VPN-Active"}
Else {Write-host "VPN-InActive"}

You can then add this as a requirement to an application:

capture

For task sequences, you can use a WMI query condition:

WMI Query


Select * from Win32_IP4RouteTable where Name like '10.0.99.%' or Name like '10.15.99.%'

 

capture

The only concession is if your VPN subnets ever change, you will need to update them in ConfigMgr.

Finding the ‘LastLogon’ Date from all Domain Controllers with PowerShell

In an Active Directory environment, probably the most reliable way to query the last logon time of a computer is to use the Last-Logon attribute.  The Last-Logon-Timestamp attribute could be used, but this will not likely be up-to-date due to the replication lag.  If you are using PowerShell, the LastLogonDate attribute can also be used, however this is also a replicated attribute which suffers from the same delay and potential inaccuracy.

The Last-Logon attribute is not replicated, however, it is only stored on the DC that the computer authenticated against.  If you have multiple domain controllers, you will get multiple values for this attribute depending on which DC the computer has authenticated with and when.

To find the Last-Logon date from the DC that the computer has most recently authenticated with, you need to query all domain controllers for this attribute, then select the most recent.

Following is a PowerShell script I wrote that will read a list of domain controllers from an Active Directory OU, query each one, then return the most recent Last-Logon value.  It uses parallel processing to return the result more quickly than processing each DC in turn, which is useful in a multi-DC environment.

To use the script, simply pass the computer name and optionally the AD OU containing your domain controllers, to the function.  You can hard-code the ‘DomainControllersOU’ parameter in the script if you prefer, so you don’t need to call it.  You need the Active Directory module installed to use this.

Example


Get-ADLastLogon -ComputerName PC001 -DomainControllersOU "OU=Domain Controllers,DC=contoso,DC=com"

capture

Get-ADLastLogon


function Get-ADLastLogon {

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory=$false,
                   ValueFromPipelineByPropertyName=$true,
                   Position=0)]
        [string]$DomainControllersOU = "OU=Domain Controllers,DC=contoso,DC=com",

        [string]
        $ComputerName
    )

    # Multithreading function
    function Invoke-InParallel {
        [CmdletBinding()]
        param(
            [parameter(Mandatory = $True,ValueFromPipeline=$true,Position = 0)]
            $InputObject,
            [parameter(Mandatory = $True)]
            [ScriptBlock]$Scriptblock,
            [string]$ComputerName,
            [parameter()]
            $ThrottleLimit = 32,
            [parameter()]
            [switch]$ShowProgress
        )

        Begin
        {
            # Create runspacepool, add code and parameters and invoke Powershell
                [void][runspacefactory]::CreateRunspacePool()
                $SessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
                $script:RunspacePool = [runspacefactory]::CreateRunspacePool(1,$ThrottleLimit,$SessionState,$host)
                $RunspacePool.Open()

            # Function to start a runspace job
            function Start-RSJob
            {
                param(
                    [parameter(Mandatory = $True,Position = 0)]
                    [ScriptBlock]$Code,
                    [parameter()]
                    $Arguments
                )
                if ($RunspacePool.GetAvailableRunspaces() -eq 0)
                    {
                        do {}
                        Until ($RunspacePool.GetAvailableRunspaces() -ge 1)
                    }

                $PowerShell = [powershell]::Create()
                $PowerShell.runspacepool = $RunspacePool
                [void]$PowerShell.AddScript($Code)
                foreach ($Argument in $Arguments)
                {
                    [void]$PowerShell.AddArgument($Argument)
                }
                $job = $PowerShell.BeginInvoke()

                # Add the job and PS instance to the arraylist
                $temp = '' | Select-Object -Property PowerShell, Job
                $temp.PowerShell = $PowerShell
                $temp.Job = $job
                [void]$Runspaces.Add($temp)  

            }

        # Start a 'timer'
        $Start = Get-Date

        # Define an arraylist to add the runspaces to
        $script:Runspaces = New-Object -TypeName System.Collections.ArrayList
        }

        Process
        {
            # Start an RS job for each computer
            $InputObject | ForEach-Object -Process {
                Start-RSJob -Code $Scriptblock -Arguments $_, $ComputerName
            }
        }

        End
        {
            # Wait for each script to complete
            foreach ($item in $Runspaces)
            {
                do
                {
                }
                until ($item.Job.IsCompleted -eq 'True')
            }

            # Grab the output from each script, and dispose the runspaces
            $return = $Runspaces | ForEach-Object -Process {
                $_.powershell.EndInvoke($_.Job)
                $_.PowerShell.Dispose()
            }
            $Runspaces.clear()
            [void]$RunspacePool.Close()
            [void]$RunspacePool.Dispose

            # Stop the 'timer'
            $End = Get-Date
            $TimeTaken = [math]::Round(($End - $Start).TotalSeconds,2)

            # Return the results
            $return
        }
    }

    # Get list of domain controllers from OU
    try {
    Import-Module ActiveDirectory | out-null
    $DomainControllers = Get-ADComputer -Filter * -SearchBase $DomainControllersOU -Properties Name -ErrorAction Stop | Select -ExpandProperty Name | Sort
    }
    catch {}

    # Define Code to run in each parallel runspace
    $Code = {
        param($DC,$ComputerName)
        Import-Module ActiveDirectory | out-null
        $Date = [datetime]::FromFileTime((Get-ADComputer -Identity $ComputerName -Server $DC -Properties LastLogon | select -ExpandProperty LastLogon))
        $Result = '' | Select 'Domain Controller','Last Logon'
        $Result.'Domain Controller' = $DC
        $Result.'Last Logon' = $Date
        Return $Result
    }

    # Run code in parallel
    $Result = Invoke-InParallel -InputObject $DomainControllers -Scriptblock $Code -ComputerName $ComputerName -ThrottleLimit 64

    # Return most recent logon date
    return $Result | sort 'Last Logon' -Descending | select -First 1
}