<< Back to Script Library

Shadow Session

Shadows Citrix, VMware, RDSH and WVD sessions
Version: 1.3.13
Created: 2020-10-15
Modified: 2020-10-20
Creator: Trentent Tye
Downloads: 329
Tags: citrix horizon parallels ras rdp shadow vdi
The Script Copy Script Copied to clipboard
<#
    .SYNOPSIS
        Shadow a user session

    .DESCRIPTION
        Shadow a remote user session

    .EXAMPLE
        . .\ShadowSession.ps1 -SessionId 1 -ComputerName W2019-001 -RemoteControlSession true -AllowUserConsent false
        Connects to a session and starts shadowing

    .PARAMETER  <SessionId <Int>>
        Session ID integer of the target session

    .PARAMETER  <ComputerName <string>>
        Remote Computer target user resides on

    .PARAMETER  <RemoteControlSession <boolean>>
        true  == grant view/mouse/keyboard control to the shadower
        false == only view the remote session

    .PARAMETER  <AllowUserConsent <boolean>>
        true  == user must consent to the shadow request
        false == bypass user consent

    .PARAMETER  <PromptForAlternativeCredentials <boolean>>
        true  == require the user to specify credentials to connect and shadow
        false == connect using your current credentials

    .PARAMETER  <Override <boolean>>
        true  == Attempt to enable the Shadow firewall rule (if the firewall is enabled) on the target machine and apply your shadow parameters to the GPO on the target machine
        false == respect the GPO on the target machine and do not touch the firewall

    .CONTEXT
        Console

    .NOTES
        In order to grant some of the more advanced functionality (bypass user consent, force remote control of the session) the initiator of this
        Script Action must have local admin rights of the target machine.  If the initiator does not have local admin rights on the target machine
        then the script will connect with whatever capabilities it can as dictated by policy.

    .MODIFICATION_HISTORY
        Created TTYE : 2020-10-08


    AUTHOR: Trentent Tye/Marcel Calef
#>

[CmdLetBinding()]
Param (
    [Parameter(Mandatory=$true,HelpMessage='Session ID of the target user')][ValidateNotNullOrEmpty()]                 [int]$SessionId,
    [Parameter(Mandatory=$true,HelpMessage='Name of the target machine user resides on')][ValidateNotNullOrEmpty()] [string]$ComputerName,
    [Parameter(Mandatory=$false,HelpMessage='Enable Remote Control of the target session?')]                        [string]$RemoteControlSession = "false",
    [Parameter(Mandatory=$false,HelpMessage='Allow the user to consent to the shadow request?')]                    [string]$AllowUserConsent = "false",
    [Parameter(Mandatory=$false,HelpMessage='Prompt to use other credentials for shadowing')]                       [string]$PromptForAlternativeCredentials = "false",
    [Parameter(Mandatory=$false,HelpMessage='Attempts to override GPO and enforce shadowing')]                      [string]$Override = "false"
)


#$ErrorActionPreference = "Stop"
#Set-StrictMode -Version Latest
$verbosePreference = "Continue"
#requires -version 4

#Convert string to boolean variable
[bool]$RemoteControlSession            = [bool]::Parse($RemoteControlSession)
[bool]$AllowUserConsent                = [bool]::Parse($AllowUserConsent)
[bool]$PromptForAlternativeCredentials = [bool]::Parse($PromptForAlternativeCredentials)
[bool]$Override                        = [bool]::Parse($Override)

Write-Verbose -Message "Parameters: `
  SessionId                       = $SessionId `
  ComputerName                    = $computerName `
  RemoteControlSession            = $RemoteControlSession `
  AllowUserConsent                = $AllowUserConsent `
  PromptForAlternativeCredentials = $PromptForAlternativeCredentials `
  Override                        = $Override"

function Check-RemoteFirewallSettings ([string]$ComputerName) {

    $FirewallStateResult = New-Object System.Collections.Generic.List[PSObject]
    $Profile                      = "Unknown"
    $FirewallStateEnabledProperty = "Unknown"
    $FirewallRuleEnabledState     = "Unknown"

    try {
        $CimSession = New-CimSession -ComputerName $computerName -ErrorAction Stop
    } catch {
        Write-Verbose -Message "Unable to connect to $computerName"
        $FirewallStateResult.Add(
            [PSCustomObject]@{
                Profile = $Profile
                FirewallStateEnabledProperty = $FirewallStateEnabledProperty
                FirewallRuleEnabledState = $FirewallRuleEnabledState
            }
        )
        return $FirewallStateResult
    }

    ## Get Active Network Connection Category (Domain, Private, Public)
    try {
        $ConnectedNetwork = Get-NetConnectionProfile -CimSession $CimSession
    } catch {
        Write-Verbose -Message "Failed to get the network connection profile for $computerName"
    }
    switch ($ConnectedNetwork.NetworkCategory) {
        "DomainAuthenticated" { $Profile = "Domain"  ; Break }
        "Private"             { $Profile = "Private" ; Break }
        "Public"              { $Profile = "Public"  ; Break }
        Default               { $Profile = "Unknown" ; Break }
    }

    Write-Verbose -Message "Detected Connected Network : $Profile"
    
    ## See if the firewall is enabled.  If not, we can skip checking
    try {
        $FirewallState = Get-NetFirewallProfile -Name $Profile -CimSession $CimSession
    } catch {
        Write-Verbose -Message "Failed to get the firewall state for $computerName"
    }
    Write-Verbose -Message "Firewall State for Network $Profile : $($FirewallState.Enabled)"

    switch ($FirewallState.Enabled) {
        "False"    { $FirewallStateEnabledProperty = "$False"   ; Break }
        "True"     { $FirewallStateEnabledProperty = "$True"    ; Break }
        Default    { $FirewallStateEnabledProperty = "Unknown"  ; Break }
    }
    if ($FirewallStateEnabledProperty -eq "$False") { Remove-CimSession -CimSession $CimSession }

    ## See if the RDP Shadow rule is enabled
    try {
        $FirewallRuleState = Get-NetFirewallRule -Name "RemoteDesktop-Shadow-In-TCP" -CimSession $CimSession
    } catch {
        Write-Verbose "Failed to get Firewall Rule State"
    }
    Write-Verbose "Firewall Rule State : $($FirewallRuleState.Enabled)"
    switch ($FirewallRuleState.Enabled) {
        "False"    { $FirewallRuleEnabledState = "$False"   ; Break }
        "True"     { $FirewallRuleEnabledState = "$True"    ; Break }
        Default    { $FirewallRuleEnabledState = "Unknown"  ; Break }
    }

    if ($FirewallRuleEnabledState -eq "$False") { Remove-CimSession -CimSession $CimSession }

    $FirewallStateResult.Add(
            [PSCustomObject]@{
                Profile = $Profile
                FirewallStateEnabledProperty = $FirewallStateEnabledProperty
                FirewallRuleEnabledState = $FirewallRuleEnabledState
            }
        )
    return $FirewallStateResult
}

function Check-ForShadowPolicies ([string]$ComputerName) {
    ## Check if policy is set on remote machine by GPO
    Write-Verbose "Check for Shadow Policies"
    try {
        $GPOConfigured = Invoke-Command -ComputerName $ComputerName -ScriptBlock { Get-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services" }
    } catch {
        Write-Verbose "***Unable to connect to $ComputerName***"
        return "Fail"
    }

    
    if ([bool]($GPOConfigured.PSobject.Properties.name -match "Shadow")) {
        Write-Verbose -Message "GPO Shadow Policy Configured."
        return $GPOConfigured.Shadow
    } else {
        Write-Verbose -Message "No Shadow Policies Configured."
        return $false
    }
}

function Create-ShadowPolicy ([string]$ComputerName, [bool]$RemoteControlSession, [bool]$AllowUserConsent, [bool]$RemovePolicy) {
    if ($RemovePolicy) {
        Write-Verbose -Message "Remove Shadow Policy"
        try {
            Invoke-Command -ComputerName $ComputerName -ScriptBlock { Remove-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services" -Name "Shadow" -Force }
        } catch {
            Write-Error "Failed to remove policy"
        }
        break
    }

    $GPOValue = 0
    if ($RemoteControlSession -and $AllowUserConsent) {
        Write-Verbose -Message "Policy : Full Control with user's permission" 
        $GPOValue = 1
    }
    if ($RemoteControlSession -and -not $AllowUserConsent) {
        Write-Verbose -Message "Policy : Full Control without user's permission" 
        $GPOValue = 2
    }

    if (-not $RemoteControlSession -and $AllowUserConsent) {
        Write-Verbose -Message "Policy : View Session with user's permission" 
        $GPOValue = 3
    }

    if (-not $RemoteControlSession -and -not $AllowUserConsent) {
        Write-Verbose -Message "Policy : View Session without user's permission" 
        $GPOValue = 4
    }

    Write-Verbose "GPO Value = $GPOValue"

    if ($GPOValue -eq 0) {
        Write-Output "Unable to detect what the shadow policy may be. This script may fail."
    }

    $DefineGPO = Invoke-Command -ComputerName $ComputerName -ScriptBlock { Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services" -Name "Shadow" -Type DWord -Value $args[0] -Force -PassThru } -ArgumentList $GPOValue
    Write-Debug -Message "$($DefineGPO | Format-List | Out-String)"
    if ([bool]($DefineGPO.PSobject.Properties.name -match "Shadow")) {
        if ($DefineGPO.Shadow -ne $GPOValue) {
            Write-Error "Failed to set shadow policies."
            return $false
        } else {
            return $true
        }
    } else {
        Write-Error "Failed to set shadow policies."
        return $false
    }
}

function Shadow-Session ([int]$sessionId, [string]$ComputerName, $RemoteControlSession, $AllowUserConsent, $PromptForAlternativeCredentials) {
    $StringBuilder = New-Object -TypeName "System.Text.StringBuilder"
    [void]$StringBuilder.Append("/shadow:$sessionId ")
    [void]$StringBuilder.Append("/v:$ComputerName ")
    if ($RemoteControlSession) {
        [void]$StringBuilder.Append("/control ")
    }
    if (-not($AllowUserConsent)) {
        [void]$StringBuilder.Append("/noConsentPrompt ")
    }
    if ($PromptForAlternativeCredentials) {
        [void]$StringBuilder.Append("/prompt ")
    }

    Write-Verbose "Executing : mstsc.exe $($StringBuilder.ToString())"
    Start-Process -FilePath mstsc.exe -ArgumentList @("$($StringBuilder.ToString())")
}

Write-Verbose "Checking $computerName to see if the Windows firewall is enabled"
$RemoteFirewallSettings = Check-RemoteFirewallSettings -ComputerName $ComputerName

Write-Verbose "Are we overriding the target machine's settings?"
if ($Override) {
    Write-Verbose "Yes"
    ##try to enable the firewall rules and overwrite the GPO settings.
    if ($RemoteFirewallSettings.FirewallStateEnabledProperty -eq $true -and $RemoteFirewallSettings.FirewallRuleEnabledState -eq $false) {
        Write-Output "Override : Attempting to enable the Shadow firewall rule"
        $CimSession = New-CimSession -ComputerName $computerName
        Set-NetFirewallRule -Name "RemoteDesktop-Shadow-In-TCP" -Enabled True -CimSession $CimSession
        $RemoteFirewallSettings.FirewallRuleEnabledState = "True"
        Remove-CimSession -CimSession $CimSession
    }
    ##try to overwrite GPO settings with parameters
    Create-ShadowPolicy -ComputerName $ComputerName -RemoteControlSession $RemoteControlSession -AllowUserConsent $AllowUserConsent
} else {
    Write-Verbose "No"
}

## Is the Firewall Enabled and the RDP TCP Shadow policy disabled?
if ($RemoteFirewallSettings.FirewallStateEnabledProperty -eq $true -and $RemoteFirewallSettings.FirewallRuleEnabledState -eq $false) {
    Write-Error "Windows Firewall is enabled but Rule: `"Remote Desktop - Shadow (TCP-In)`" was found disabled. `n`nPlease enable firewall rule `"Remote Desktop - Shadow (TCP-In)`" in order to enable shadowing."
    break
}

Write-Verbose "Checking $computerName for Shadow Policies"
$ShadowPolicy = Check-ForShadowPolicies -ComputerName $ComputerName

if ($ShadowPolicy -ne "Fail") {
    if ($ShadowPolicy -eq $false) {
        Write-Verbose "Shadow Policies not configured -- Attempting to use defined parameters."
        Create-ShadowPolicy -ComputerName $computerName -RemoteControlSession $RemoteControlSession -AllowUserConsent $AllowUserConsent
    } else {
        Write-Output "Group Policy Shadow Policies found. Overriding less restrictive parameters to match more restrictive policy"  ## if AllowUserConsent is True and $RemoteControlSession is False then we'll just enforce those options
        switch ($ShadowPolicy) {
            0       { Write-Output "Detected Policy : No remote control allowed" }
            1       { Write-Output "Detected Policy : Full Control with user's permission"    ; $AllowUserConsent=$true   ; break }
            2       { Write-Output "Detected Policy : Full Control without user's permission" ; break }
            3       { Write-Output "Detected Policy : View Session with user's permission"    ; $RemoteControlSession=$false ; $AllowUserConsent=$true   ; break }
            4       { Write-Output "Detected Policy : View Session without user's permission" ; $RemoteControlSession=$false ; break }
            Default { Write-Output "Detected Policy : Unknown Policy Value : $($GPOConfigured.Shadow) " }
        }
    }
} else {
    Write-Verbose "Unable to determine shadow policies, will attempt to connect with the most restrictive parameters."
    $RemoteControlSession=$false
    $AllowUserConsent=$true
}

Shadow-Session -sessionId $SessionID -ComputerName $ComputerName -RemoteControlSession $RemoteControlSession -AllowUserConsent $AllowUserConsent -PromptForAlternativeCredentials $PromptForAlternativeCredentials

Sleep 5  #sleep to allow mstsc.exe to connect to remote computer before switching policies back

if ($ShadowPolicy -eq $false) {
    Create-ShadowPolicy -ComputerName $ComputerName -RemovePolicy $true
}