A better way to find a logged on user remotely using PowerShell
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, 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