When administrators connect to domain controllers via RDP for routine maintenance or troubleshooting, they often disconnect rather than properly sign out. While this seems harmless, these disconnected sessions:
- Maintain memory allocations and process handles
- Keep user profiles loaded on the system
- Create potential security risks through dormant but authenticated sessions
- Can interfere with maintenance tasks or scheduled reboots
Microsoft recommends regularly cleaning up these sessions, but many organizations lack a standardized process for doing so. Today, I'll share a PowerShell script I developed to address this issue head-on.
I created a PowerShell script that:
- Identifies all domain controllers in your environment
- Queries each for both active and disconnected sessions
- Displays findings in organized tables
- Offers administrators the option to safely terminate disconnected sessions
- Specifically excludes critical service sessions to prevent system disruption
First, let's examine how we identify sessions on domain controllers:
# Get all domain controllers in the current domain
$domainControllers = Get-ADDomainController -Filter * | Select-Object -ExpandProperty Name
foreach ($dc in $domainControllers) {
$sessionOutput = query session /server:$dc 2>$null
# ...processing logic continues
}
The query session
command returns output similar to:
SESSIONNAME USERNAME ID STATE TYPE DEVICE
services 0 Disc
console 1 Conn
rdp-tcp#0 bear.manager 2 Active
rdp-tcp#1 helpdesk 3 Disc
Pitfalls and Challenges
During development, I encountered several pitfalls worth highlighting:
The query session
command produces fixed-width output that can be tricky to parse. My initial attempt used substring extraction:
# NOT RECOMMENDED
$sessionName = $line.Substring(1, 19).Trim()
$username = $line.Substring(21, 24).Trim()
This approach failed because the formatting varies between Windows versions and session types. A more resilient solution uses tokenization:
# Use tokenization for reliable results
$cleanLine = $line.Trim() -replace '\s+', ' '
$tokens = $cleanLine -split ' '
Another challenge was distinguishing between user sessions and critical system sessions. Terminating service sessions can cause system instability:
# Only add disconnected sessions that are NOT "services"
if ($state -eq "Disc" -and $sessionName -ne "services") {
$disconnectedResults += $sessionInfo
}
Running commands across multiple domain controllers requires robust error handling:
try {
# Execute the query session command and capture the output
$sessionOutput = query session /server:$dc 2>$null
# Processing logic...
}
catch {
Write-Host "Error querying $dc : $_" -ForegroundColor Red
}
# Get all domain controllers in the current domain
$domainControllers = Get-ADDomainController -Filter * | Select-Object
-ExpandProperty Name
# Create arrays to store the results
$activeResults = @()
$disconnectedResults = @()
# Loop through each domain controller
foreach ($dc in $domainControllers) {
Write-Host "Querying sessions on $dc..." -ForegroundColor Cyan
try {
# Execute the query session command and capture the output
$sessionOutput = query session /server:$dc 2>$null
# Skip the first line (header)
$dataLines = $sessionOutput | Select-Object -Skip 1
foreach ($line in $dataLines) {
# Skip empty lines
if ([string]::IsNullOrWhiteSpace($line)) { continue }
# Clean up the line and split it into tokens
$cleanLine = $line.Trim() -replace '\s+', ' '
$tokens = $cleanLine -split ' '
# Check if we have enough tokens and if "Active" or "Disc" is present
if (($tokens -contains "Active") -or ($tokens -contains "Disc")) {
# Determine the session state
$state = if ($tokens -contains "Active") { "Active" } else { "Disc" }
# Find the session ID (should be numeric and appear before the state)
$stateIndex = [array]::IndexOf($tokens, $state)
$id = $tokens[$stateIndex - 1]
# Session name is typically the first token
$sessionName = $tokens[0]
# Username is typically before the ID (but after session name)
$usernameIndex = $stateIndex - 2
$username = if ($usernameIndex -gt 0) { $tokens[$usernameIndex] }
else { "" }
# Create a result object
$sessionInfo = [PSCustomObject]@{
ServerName = $dc
SessionName = $sessionName
Username = $username
ID = $id
State = $state
}
# Add to appropriate results array
if ($state -eq "Active") {
$activeResults += $sessionInfo
} elseif ($state -eq "Disc" -and $sessionName -ne "services") {
# Only add disconnected sessions that are NOT "services"
$disconnectedResults += $sessionInfo
}
}
}
}
catch {
Write-Host "Error querying $dc : $_" -ForegroundColor Red
}
}
# Display active session results
Write-Host "`nActive User Sessions on Domain Controllers:" -ForegroundColor Green
$activeResults | Format-Table -AutoSize
# Display disconnected sessions (excluding services)
Write-Host "`nDisconnected Sessions on Domain Controllers (excluding services):"
-ForegroundColor Yellow
$disconnectedResults | Format-Table -AutoSize
# Create a log file in the script's directory
$scriptPath = $PSScriptRoot
if ([string]::IsNullOrEmpty($scriptPath)) {
$scriptPath = (Get-Location).Path
}
$logFileName = "DC_Session_Cleanup_$(Get-Date -Format 'yyyyMMdd').log"
$logFilePath = Join-Path -Path $scriptPath -ChildPath $logFileName
if (-not (Test-Path $logFilePath)) {
New-Item -Path $logFilePath -ItemType File -Force | Out-Null
Add-Content -Path $logFilePath -Value "DC Session Cleanup Log - Started $(Get-Date)`n"
}
# Ask if user wants to end disconnected sessions
if ($disconnectedResults.Count -gt 0) {
$endSessions = Read-Host "Do you want to end all disconnected sessions
(excluding services)? (Y/N)"
if ($endSessions -eq "Y" -or $endSessions -eq "y") {
Add-Content -Path $logFilePath -Value "`nSession cleanup started at $(Get-Date)"
foreach ($session in $disconnectedResults) {
try {
Write-Host "Ending session ID $($session.ID) on server
$($session.ServerName)..." -ForegroundColor Cyan
# Get the session details before attempting to reset
$beforeReset = query session /server:$($session.ServerName)
$($session.ID) 2>&1
# Execute the reset command
$null = reset session /server:$($session.ServerName) $($session.ID) 2>&1
# Give the command time to take effect
Start-Sleep -Seconds 2
# Verify if the session still exists
$afterReset = query session /server:$($session.ServerName)
$($session.ID) 2>&1
if ($afterReset -match "No session exists|not found|does not
exist|not active") {
Write-Host "Successfully ended session." -ForegroundColor Green
$logMessage = "SUCCESS: Ended session ID $($session.ID) for user
$($session.Username) on server $($session.ServerName)"
} else {
# Check if the session data has changed (reset might have worked
differently than expected)
if ($beforeReset -ne $afterReset) {
Write-Host "Session state changed but may not be fully terminated."
-ForegroundColor Yellow
$logMessage = "PARTIAL: Session ID $($session.ID) for user
$($session.Username) on server $($session.ServerName) state changed
but may not be fully terminated"
} else {
Write-Host "Session appears to still be active. You may need to
check manually." -ForegroundColor Red
$logMessage = "FAILED: Could not end session ID $($session.ID)
for user $($session.Username) on server $($session.ServerName)"
}
}
# Log the result
Add-Content -Path $logFilePath -Value "$(Get-Date -Format
'yyyy-MM-dd HH:mm:ss') - $logMessage"
}
catch {
$errorMessage = "ERROR: Exception when processing session ID
$($session.ID) on server $($session.ServerName): $_"
Write-Host $errorMessage -ForegroundColor Red
Add-Content -Path $logFilePath -Value "$(Get-Date -Format
'yyyy-MM-dd HH:mm:ss') - $errorMessage"
}
}
Add-Content -Path $logFilePath -Value "Session cleanup completed at $(Get-Date)`n"
}
}
# Report on script completion
Write-Host "`nScript execution completed." -ForegroundColor Cyan
Write-Host "Log file saved to: $logFilePath" -ForegroundColor Cyan
This is what the script looks like when its run, when you answer yes you get those disconnected session added to a log file.
Conclusion
Disconnected sessions on domain controllers may seem like a minor issue, but they represent both a security risk and administrative oversight. This script provides a simple yet effective solution to identify and clean up these sessions while preserving critical system services.
By incorporating regular session cleanup into your maintenance routines, you'll maintain better-performing, more secure domain controllers—a small effort that pays significant dividends in the long run.