Powershell : Disconnected Sessions on Domain Controllers


I have over the years encountered a common issue that often goes unaddressed in many environments: disconnected user sessions on domain controllers. These orphaned sessions consume resources, create potential security vulnerabilities, and can even cause performance degradation over time.

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.

The Solution: Automated Session Management

I created a PowerShell script that:

  1. Identifies all domain controllers in your environment
  2. Queries each for both active and disconnected sessions
  3. Displays findings in organized tables
  4. Offers administrators the option to safely terminate disconnected sessions
  5. Specifically excludes critical service sessions to prevent system disruption
Understanding the Session Query Process

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:

1. Inconsistent Output Formatting

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 ' '
2. Identifying Service Sessions

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
}
3. Error Handling for Remote Commands

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
}
Script : adds-rdpsessions.ps1
# 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
Visual Demo

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.

Previous Post Next Post

نموذج الاتصال