Skip to content

Instantly share code, notes, and snippets.

@jborean93
Last active July 27, 2024 19:38
Show Gist options
  • Save jborean93/7d4cb107fa06251b080fa10ec844893e to your computer and use it in GitHub Desktop.
Save jborean93/7d4cb107fa06251b080fa10ec844893e to your computer and use it in GitHub Desktop.
Windows PowerShell SSH Remoting Stub
<#
.SYNOPSIS
Windows PowerShell SSH Server Subsystem Shim.
.DESCRIPTION
Used as a basic wrapper for Windows PowerShell that allows it to be used as a target for SSH based remoting sessions.
This allows a PowerShell client to target a Windows host through SSH without having PowerShell 7 installed.
.NOTES
This is experimental and used as a POC.
It is not guaranteed to be stable or bug free.
See https://gist.github.com/jborean93/7d4cb107fa06251b080fa10ec844893e?permalink_comment_id=4093819#gistcomment-4093819 for more info.
#>
[CmdletBinding()]
param ()
$ErrorActionPreference = 'Stop'
Add-Type -Namespace PSSSH -Name NativeMethods -MemberDefinition @'
[DllImport("Kernel32.dll", EntryPoint = "GetStdHandle", SetLastError = true)]
private static extern IntPtr GetStdHandleNative(
int nStdHandle);
public static Microsoft.Win32.SafeHandles.SafeFileHandle GetStdHandle(int handleId)
{
IntPtr handle = GetStdHandleNative(handleId);
if (handle == (IntPtr)(-1)) {
throw new System.ComponentModel.Win32Exception();
}
// Std handles should not be freed.
return new Microsoft.Win32.SafeHandles.SafeFileHandle(handle, false);
}
'@
# Cannot use [System.Console]::OpenStandardInput() as it's a ConsoleStream and
# will be unable to read input from an SSH based process.
# https://github.com/PowerShell/PowerShell/issues/14478#issuecomment-1064764004
$stdin = [PSSSH.NativeMethods]::GetStdHandle(-10)
$stdinFS = New-Object System.IO.FileStream $stdin, "Read"
$version = $PSVersionTable.PSVersion
$proc = New-Object System.Diagnostics.Process
$proc.StartInfo.FileName = "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"
$proc.StartInfo.Arguments = "-Version $($version.Major).$($version.Minor) -NoLogo -ServerMode"
$proc.StartInfo.CreateNoWindow = $true
$proc.StartInfo.RedirectStandardInput = $true
$proc.StartInfo.UseShellExecute = $false
$null = $proc.Start()
$utf8 = New-Object System.Text.UTF8Encoding $false
$stdinSR = New-Object System.IO.StreamReader $stdinFS, $utf8
while ($true) {
$line = $stdinSR.ReadLine()
$proc.StandardInput.WriteLine($line)
$proc.StandardInput.Flush()
# Sent by the server to indicate the Runspace Pool is closed.
if ($line.StartsWith("<CloseAck PSGuid='00000000-0000-0000-0000-000000000000' />")) {
break
}
}
$proc | Stop-Process -Force
@jborean93
Copy link
Author

Currently PSRemoting over PowerShell requires a PowerShell (Core) v6+ to be installed on the remote host and configured as a subsystem as per https://docs.microsoft.com/en-us/powershell/scripting/learn/remoting/ssh-remoting-in-powershell-core?view=powershell-7.2#install-the-ssh-service-on-a-windows-computer. While it is possible to get the subsystem to start Windows PowerShell by specifying C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -Version 5.1 -NoLogo -ServerMode as the subsystem entry there is a unique combination of scenarios that stop this from just working PowerShell/PowerShell#14478 (comment).

To bypass this problem I've created a basic PowerShell script that reads from the raw stdin pipe through a FileStream rather than through the ConsoleStream that [Console]::OpenStandardInput() returns as the ConsoleStream has the blocking problem documented in https://sourceware.org/legacy-ml/cygwin/2013-12/msg00345.html. As it's bypassing this problem the process is able to read the stdin from the SSH pipe and it just passes along the data to the Windows PowerShell server process it spawns. There's no need to manually close the process as SSH will kill and child processes once it exits for you.

To get this working place this script in a folder on your host, in my example I placed it in C:\ProgramData\ssh\win_powershell_ssh.ps1. In the C:\ProgramData\ssh\sshd_config file add the following entry

Subsystem win_powershell C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -NoLogo -ExecutionPolicy ByPass -File C:\ProgramData\ssh\win_powershell_ssh.ps1

Reload sshd with Restart-Service -Name sshd and the changes are applied.

Connecting to the WinPS instance is as simple as adding -Subsystem win_powershell to the PSRemoting cmdlets like so;

Invoke-Command -HostName server -Subsystem win_powershell { $PSVersionTable }

# For interactive purposes
Enter-PSSession -HostName server -Subsystem win_powershell

You can also set the label to be powershell if you wish that to be the default configuration that PowerShell targets.

Please note this is in no way supported or endorsed by Microsoft or the PowerShell. I was curious as to how to get this working and this was the end result.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment