Find and Terminate Remote Sessions

Hey Sam *guitar riff*

“WTF is locking out this account!?” you find yourself asking. Yes there’s tools out there to help identify this, however this PowerShell script is a more direct approach in my opinion.

TLDR: Let’s use PowerShell to find (and terminate if you want) remote sessions for an account of your choosing. Sound good? Cool let’s go.

You can find this on github here https://github.com/VinceCarbone/RemoteSessions

The account you’re running this with will need administrative access to the servers it’s executing against. The easiest thing to do is run as a domain admin.

This will identify both RDP sessions (Remote Desktop) and also if you have some remote UNC paths / files being accessed with the account.

First things first, let’s establish two parameters. $samaccount is just that, the SamAccount you want to search for.

$logoff will be an optional switch. If you call it when you run the script, it will terminate any remote sessions it finds. Otherwise if you don’t call it, it’ll just display a list of what it finds

param (    
    [Parameter(Mandatory=$true)][string]$samaccount,
    [switch]$logoff
)

Next up is a simple check to make sure you’ve specified a valid user

If(-not(Get-ADUser $samaccount -erroraction SilentlyContinue)){
    Write-Host "Cannot find $samaccount in the domain" -ForegroundColor Red
    Exit
}

Now, let’s grab all the servers in the domain

$servers = Get-ADComputer -filter {enabled -eq $true} -properties serviceprincipalname, operatingsystem | Where-Object operatingsystem -like "*server*"
explain this

If only it were this easy. While the $servers array should contain all the computer objects that are servers, this will need to be narrowed down. Right now it may also contain cluster objects, and dead computer objects. So I give you the following. This will check the SPNs (service principal names) of the computer objects to determine if it is actually a Windows server and NOT a cluster object. If it is a Windows server, it will then do a ping test to ensure it is actually reachable. When it’s all said and done, the $realservers array will contain all pingable windows servers.

$i = 0
$realservers = @(
    ForEach($server in $servers){
        $is_server = $false
        $spns = $server | Select-Object -ExpandProperty serviceprincipalname
        ForEach($spn in $spns){
            If($spn -like "*TERMSRV*"){$is_server = $true}
        }
        If($is_server -eq $true){
            If(Test-Connection -ComputerName $server.name -Count 1 -BufferSize 16 -ErrorAction SilentlyContinue){Write-Output $server}
        }
        Write-Progress -Activity "Pinging $($server.name)" -PercentComplete (($i / $servers.count)*100)    
        $i++
    }
)
Write-Progress -Activity "Pinging" -Status Ready -Completed

Now that we have a valid list of servers, we can use Invoke-Command on the list to get the list of sessions on them. This will store all sessions to the $RemoteSessions array

Write-Host "Querying sessions on all pingable servers"
$RemoteSessions = @(Invoke-Command -ComputerName $($realservers.name) -ScriptBlock {qwinsta | foreach {(($_.trim() -replace “  +”,”,”))} | ConvertFrom-Csv} -ErrorAction SilentlyContinue)
bad example i ran on my local PC

Since we only care about sessions related to the $samaccount specified, let’s narrow down the list. This will find RDP Sessions and sessions related to things like UNC paths that are accessed with the account. You can see it matches on both the username and then the sessionname to populate the $SessionsToKill array.

$SessionsToKill = @($RemoteSessions | Where-Object username -eq $samaccount)
$SessionsToKill += @($RemoteSessions | Where-Object sessionname -eq $samaccount)
Again, bad example but you get the idea

Finally, once the $SessionsToKill array is populated (and assuming it found at least 1 result), this will display a list of everything it found. If you’ve included the -logoff switch, it will also terminte the sessions except if the session is on the computer you’re running the script on. This is a safety check I included after booting myself off the server I was executing the script on when I was trying to figure out what was locking out my account lol. Oh, I also configured it to unlock the user account too when you include the -logoff switch, because why not.

If($SessionsToKill){
    $SessionsToKill | Format-Table sessionname, username, id, state, pscomputername -AutoSize

    # Terminates the sessions and unlocks the user account
    If($logoff){
        ForEach($RemoteSession in $SessionsToKill){
            If($RemoteSession.pscomputername -ne $env:COMPUTERNAME){
                Write-Host "Logging off $($RemoteSession.pscomputername)"        
                If($RemoteSession.sessionname -eq $samaccount){rwinsta /server:$($RemoteSession.pscomputername) $RemoteSession.username}
                If($RemoteSession.username -eq $samaccount){rwinsta /server:$($RemoteSession.pscomputername) $RemoteSession.ID}
            }
        }

        # Unlocks the user account
        Unlock-ADAccount -Identity $samaccount
    }
}Else{Write-Host "Unable to find any remote sessions for $samaccount"}

“Vince, shut up and just paste the whole script” FINE

param (    
    [Parameter(Mandatory=$true)][string]$samaccount,
    [switch]$logoff
)

# Checks to make sure the samaccount is valid
If(-not(Get-ADUser $samaccount -erroraction SilentlyContinue)){
    Write-Host "Cannot find $samaccount in the domain" -ForegroundColor Red
    Exit
}

#Gets all servers in Domain
$servers = Get-ADComputer -filter {enabled -eq $true} -properties serviceprincipalname, operatingsystem | Where-Object operatingsystem -like "*server*"

#Removes clusters and un-pingable servers from list
$i = 0
$realservers = @(
    ForEach($server in $servers){
        $is_server = $false
        $spns = $server | Select-Object -ExpandProperty serviceprincipalname
        ForEach($spn in $spns){
            If($spn -like "*TERMSRV*"){$is_server = $true}
        }
        If($is_server -eq $true){
            If(Test-Connection -ComputerName $server.name -Count 1 -BufferSize 16 -ErrorAction SilentlyContinue){Write-Output $server}
        }
        Write-Progress -Activity "Pinging $($server.name)" -PercentComplete (($i / $servers.count)*100)    
        $i++
    }
)
Write-Progress -Activity "Pinging" -Status Ready -Completed

# Runs PowerShell against all servers to find all remote sessions
Write-Host "Querying sessions on all pingable servers"
$RemoteSessions = @(Invoke-Command -ComputerName $($realservers.name) -ScriptBlock {qwinsta | foreach {(($_.trim() -replace “  +”,”,”))} | ConvertFrom-Csv} -ErrorAction SilentlyContinue)

# Narrows down list of results to just sessions for the user listed above
$SessionsToKill = @($RemoteSessions | Where-Object username -eq $samaccount)
$SessionsToKill += @($RemoteSessions | Where-Object sessionname -eq $samaccount)

# Displays list of remote sessions
If($SessionsToKill){
    $SessionsToKill | Format-Table sessionname, username, id, state, pscomputername -AutoSize

    # Terminates the sessions and unlocks the user account
    If($logoff){
        ForEach($RemoteSession in $SessionsToKill){
            If($RemoteSession.pscomputername -ne $env:COMPUTERNAME){
                Write-Host "Logging off $($RemoteSession.pscomputername)"        
                If($RemoteSession.sessionname -eq $samaccount){rwinsta /server:$($RemoteSession.pscomputername) $RemoteSession.username}
                If($RemoteSession.username -eq $samaccount){rwinsta /server:$($RemoteSession.pscomputername) $RemoteSession.ID}
            }
        }

        # Unlocks the user account
        Unlock-ADAccount -Identity $samaccount
    }
}Else{Write-Host "Unable to find any remote sessions for $samaccount"}

Leave a Reply

Your email address will not be published. Required fields are marked *