A better way to find a logged on user remotely using PowerShell

8 minute read

There are a mountain of different ways to get logged on users, but I have a favorite! Using qwinsta, the only problem is that it returns a string and PowerShell likes objects :( But never fear! We can fix that.

qwinsta

Prereqs

I ran into an error 5 access denied issue when I first started using qwinsta, there is a registry fix for that (thank you StackOverflow:

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Terminal Server]
"AllowRemoteRPC"=dword:00000001

Since this is a PowerShell blog, here is the PowerShell syntax to add it to a remote computer, since we will likely be working on remote computers.

$ComputerName = 'Computer' #replace with the computer name
$LMtype = [Microsoft.Win32.RegistryHive]::LocalMachine
$LMkey = "SYSTEM\CurrentControlSet\Control\Terminal Server"
$LMRegKey = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($LMtype,$ComputerName)
$regKey = $LMRegKey.OpenSubKey($LMkey,$true)
If($regKey.GetValue("AllowRemoteRPC") -ne 1)
{
    $regKey.SetValue("AllowRemoteRPC",1)
    Start-Sleep -Seconds 1
}
$regKey.Dispose()
$LMRegKey.Dispose()

Syntax

The syntax of qwinsta is fairly straightforward:

qwinsta /server:ServerName

And the output looks rather unmenacing:

 SESSIONNAME       USERNAME                 ID  STATE   TYPE        DEVICE
 services                                    0  Disc
 console           Anthony                   1  Active
 rdp-tcp                                 65536  Listen

The only problem is that if you were looking at a RDS server, there is no easy way to filter strings in PowerShell. So you couldn’t do something like:

qwinsta /server:RDSServer | Where-Object USERNAME -like "Anth*"

But we can fix that! Lets wrap qwinsta so that we can objectify that output!

Wrapping qwinsta

We’re going to create a PowerShell function here, reader beware.

Wringing out the string for its values

Besides all the parameter stuff, which we’ll get to, the first core thing we’ll do is to direct the output to a variable:

$result = qwinsta /server:$ComputerName

Now $result will look just like the output from before, but it will be formatted as a string array (string[]). We can, of course, verify that with Get-Member:

($result | Get-Member).TypeName[0]

So the next thing is to retrieve just the info we want. I’ve decided on a for loop, skipping the first line since we don’t need the headers:

ForEach($line in $result[1..$result.count])

For each line, what is each value delimited with? Spaces! So we can use the .Split() method on strings and split with each space. The only caveat is that we’ll also need to test for value on each split since you can split multiple spaces and get null values.

$tmp = $line.split(" ") | ?{$_.length -gt 0}

One thing to note is that the output from qwinsta is fixed-width, meaning that the width doesn’t change regardless of how long any of the values are. We can take advantage of that and use certain indexes to our advantage.

First thing to do is to determine which lines actually contain a user. We can count characters and find that the USERNAME value starts at the 19th character (the first character is actually the space, look carefully). So if the 19th character is not empty, we have a user.

If(($line[19] -ne " "))

The next thing I noticed in testing with qwinsta is that the SessionName property only has value if the session is in the ‘Active’ state. So we also need to check to see if the State is ‘Active’, which we can do by checking the index of that state for an ‘A’

If($line[48] -eq "A")

If it is ‘Active’, the $tmp variable will have different amounts of values.

Active:

#raw
 rdp-tcp#0         anthony                   2  Active                      
#$tmp variable (split)
rdp-tcp#0
anthony
2
Active

Not Active:

#raw
                   anthony                   2  Disc                        
#$tmp variable (split)
anthony
2
Disc

Converting the values to usable objects

Once we have all the values figured out, we can create a new PSObject, my favorite! If you have PowerShellv5 you can use the [pscustomobject]{} type accelerator (x), but I’ll be using the New-Object cmdlet here for ultimate compatibility.

New-Object PSObject -Property @{
    "ComputerName" = $ComputerName
    "SessionName" = $tmp[0]
    "UserName" = $tmp[1]
    "ID" = $tmp[2]
    "State" = $tmp[3]
    "Type" = $tmp[4]
}

In this case since we know the session is active, the first string in $tmp is the session name, and then, in order, you have the rest of the properties. It is possible that ‘Type’ or ‘SessionName’ can come back empty.

If the state is not ‘Active’ we’d switch that to:

New-Object PSObject -Property @{
    "ComputerName" = $ComputerName
    "SessionName" = $null
    "UserName" = $tmp[0]
    "ID" = $tmp[1]
    "State" = $tmp[2]
    "Type" = $null
}

The final product

Since this isn’t a post on creating functions (x), I do promise to get to that, I’m going to skip going over the other syntax and show you the final function I wrote. This does include some best practice stuff like checking for connectivity and admin rights before getting into the exciting stuff.

Function Get-ActiveSessions{
    Param(
        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [string]$Name
        ,
        [switch]$Quiet
    )
    Begin{
        $return = @()
    }
    Process{
        If(!(Test-Connection $Name -Quiet -Count 1)){
            Write-Error -Message "Unable to contact $Name. Please verify its network connectivity and try again." -Category ObjectNotFound -TargetObject $Name
            Return
        }
        If([bool](([System.Security.Principal.WindowsIdentity]::GetCurrent()).groups -match "S-1-5-32-544")){ #check if user is admin, otherwise no registry work can be done
            #the following registry key is necessary to avoid the error 5 access is denied error
            $LMtype = [Microsoft.Win32.RegistryHive]::LocalMachine
            $LMkey = "SYSTEM\CurrentControlSet\Control\Terminal Server"
            $LMRegKey = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($LMtype,$Name)
            $regKey = $LMRegKey.OpenSubKey($LMkey,$true)
            If($regKey.GetValue("AllowRemoteRPC") -ne 1){
                $regKey.SetValue("AllowRemoteRPC",1)
                Start-Sleep -Seconds 1
            }
            $regKey.Dispose()
            $LMRegKey.Dispose()
        }
        $result = qwinsta /server:$Name
        If($result){
            ForEach($line in $result[1..$result.count]){ #avoiding the line 0, don't want the headers
                $tmp = $line.split(" ") | ?{$_.length -gt 0}
                If(($line[19] -ne " ")){ #username starts at char 19
                    If($line[48] -eq "A"){ #means the session is active ("A" for active)
                        $return += New-Object PSObject -Property @{
                            "ComputerName" = $Name
                            "SessionName" = $tmp[0]
                            "UserName" = $tmp[1]
                            "ID" = $tmp[2]
                            "State" = $tmp[3]
                            "Type" = $tmp[4]
                        }
                    }Else{
                        $return += New-Object PSObject -Property @{
                            "ComputerName" = $Name
                            "SessionName" = $null
                            "UserName" = $tmp[0]
                            "ID" = $tmp[1]
                            "State" = $tmp[2]
                            "Type" = $null
                        }
                    }
                }
            }
        }Else{
            Write-Error "Unknown error, cannot retrieve logged on users"
        }
    }
    End{
        If($return){
            If($Quiet){
                Return $true
            }
            Else{
                Return $return
            }
        }Else{
            If(!($Quiet)){
                Write-Host "No active sessions."
            }
            Return $false
        }
    }
}

Get-ActiveSessions

You can find this in my Utilities GitHub repo. This is my first published script! Hooray! So lets go over how to use it:

Simple usage

To retrieve the users logged into a remote or local computer, we would simply use:

Get-ActiveSessions ComputerName

And this should return something similar to:

ID           : 2
SessionName  :
Type         :
UserName     : anthony
ComputerName : dc01
State        : Disc

And if you pass it to a variable and look at the type name, we should have a PSCustomObject, meaning we can use all the other useful cmdlets for filtering and whatnot!

$sessions = Get-ActiveSessions ComputerName
($sessions | Get-Member).TypeName[0]

System.Management.Automation.PSCustomObject

Advanced usage

We have two things going for us here, we can accept pipeline input AND it ouputs an object. So we can do stuff before and after Get-ActiveSessions.

What if you want to find all servers in your environment that have a particular user logged into them?

Get-ADComputer -Filter {OperatingSystem -like '*Server*'} | Get-ActiveSessions | Where-Object UserName -eq 'Anthony'

Or how about pull a list of computers from a text file and output the list of unique users logged into them all?

get-content C:\temp\computers.txt | Get-ActiveSessions | Select-Object -Unique UserName -ExpandProperty UserName | Out-File C:\temp\users.txt

If you come up with any useful uses of Get-ActiveSessions, comment!

Conclusion

Hopefully you have an appreciation for why I would write a function to do this stuff for me. I honestly couldn’t count the number of times I’ve used this to make my work easier.

Anyways, I have a couple of notable posts coming up, particularly the partner to Get-ActiveSessions: Close-ActiveSessions.

Spoiler:

Get-ActiveSessions Computer | Close-ActiveSessions

And also: ‘Creating Scheduled Tasks for PowerShell Scripts’, which will be a trip since I haven’t done any images yet!

Leave a Comment