Here is the general workflow over a PSRemoting session over SSH, for example spawned with Invoke-Command -HostName foo { 'test' }
:
sequenceDiagram
box Client Machine
actor C as Pwsh Client
participant S1 as ssh client
end
box Remote Server
participant S2 as ssh server
actor S as Pwsh Server
end
C->>S1: Start ssh process and<br>requests powershell subsystem<br>with redirected stdio<br>CreateProcess on Win<br>fork + exec on *nix
S1->>S2: Creates SSH connection and<br>authenticates to server
S2->>S: Starts powershell subsystem<br>pwsh -NoLogo -sshs
loop During PSSession lifetime
C-->>S1: Writes PSRemoting<br>input XML payloads<br>to stdin stream
S1-->>S2: Encapsulate input<br>over SSH data packet to<br>target server proc stdin
S2-->>S: Unwrap SSH data packet and<br>write to pwsh stdin
S-->>S2: Write PSRemoting output<br>XML payloads to stdout
S2-->>S1: Read stdout and<br>encapsulate over SSH<br>data packet
S1-->C: Reads stdout from ssh and<br>processes incoming<br>PSRemoting payloads
end
The general workflow here is the same for other OutOfProc remoting based transports like the named pipe, process jobs, Hyper-V direct except that instead of the ssh client and server there is some other broker in between to exchange the data either on localhost or to a remote server. There may not even be that broker and the PowerShell client and server are connected directly, for example with the named pipe client and server.
Using New-PSSessionLogger.ps1 we can capture the raw PSRemoting packets that are sent from the PowerShell client and server.
. ./New-PSSessionLogger.ps1
# Creates the PSSession that logs the raw data to pssession.log
$s = New-PSSessionLogger -LogPath pssession.log
Invoke-Command $s { 'test' }
$s | Remove-PSSession
# Scans pssession.log and formats the payloads into something more human readable
Watch-PSSessionLog -ScanHistory -Path pssession.log | Format-PSSessionPacket
The OutOfProc
payloads exchange look something like this
<!--Contains PSRemoting fragments-->
<Data Stream='Default' PSGuid='00000000-0000-0000-0000-000000000000'>base64 of PSRemoting payload fragments</Data>
<!--Send by server to acknowledge receipt of Data above-->
<DataAck PSGuid='00000000-0000-0000-0000-000000000000' />
<!--Send by client to request a new PowerShell pipeline, is followed by Data payloads-->
<Command PSGuid='8d8a4469-6d63-4667-865c-8fd0589d0e4d' />
<!--Reply from server acknowledging command is started-->
<CommandAck PSGuid='8d8a4469-6d63-4667-865c-8fd0589d0e4d' />
There is also a Close/CloseAsk
and Signal/SignalAck
that closes the pipeline/runspace or sends the stop pipeline signal respectively. The contents of the PSRemoting
payloads are MS-PSRP fragments which in turn are combined to form a MS-PSRP message. These messages contain a CLIXML serialized object representing the action to perform.
The first action of PSRP is to create a Runspace Pool. This represents the New-PSSession
/New-PSSessionLogger
call that creates the PSSession.
Type : Data
PSGuid : 00000000-0000-0000-0000-000000000000
Stream : Default
Fragments : ObjectID : 1
FragmentID : 0
Start : True
End : True
Length : 202
ObjectID : 2
FragmentID : 0
Start : True
End : True
Length : 3783
Messages : ObjectID : 1
Destination : Server
MessageType : SESSION_CAPABILITY
RPID : 56733941-913e-49fc-b31f-870c2966bda4
PID : 00000000-0000-0000-0000-000000000000
Object :
<Obj RefId="0">
<MS>
<Version N="protocolversion">2.3</Version>
<Version N="PSVersion">2.0</Version>
<Version N="SerializationVersion">1.1.0.1</Version>
</MS>
</Obj>
ObjectID : 2
Destination : Server
MessageType : INIT_RUNSPACEPOOL
RPID : 56733941-913e-49fc-b31f-870c2966bda4
PID : 00000000-0000-0000-0000-000000000000
<Obj RefId="0">
<MS>
<I32 N="MinRunspaces">1</I32>
<I32 N="MaxRunspaces">1</I32>
<Obj N="PSThreadOptions" RefId="1">
...
</Obj>
<Obj N="ApplicationArguments" RefId="3">
...
</Obj>
</MS>
</Obj>
Type : Data
PSGuid : 00000000-0000-0000-0000-000000000000
Stream : Default
Fragments : ObjectID : 1
FragmentID : 0
Start : True
End : True
Length : 202
Messages : ObjectID : 1
Destination : Client
MessageType : SESSION_CAPABILITY
RPID : 00000000-0000-0000-0000-000000000000
PID : 00000000-0000-0000-0000-000000000000
Object :
<Obj RefId="0">
<MS>
<Version N="protocolversion">2.3</Version>
<Version N="PSVersion">2.0</Version>
<Version N="SerializationVersion">1.1.0.1</Version>
</MS>
</Obj>
Type : Data
PSGuid : 00000000-0000-0000-0000-000000000000
Stream : Default
Fragments : ObjectID : 2
FragmentID : 0
Start : True
End : True
Length : 1585
Messages : ObjectID : 2
Destination : Client
MessageType : APPLICATION_PRIVATE_DATA
RPID : 56733941-913e-49fc-b31f-870c2966bda4
PID : 00000000-0000-0000-0000-000000000000
Object :
<Obj RefId="0">
<MS>
<Obj N="ApplicationPrivateData" RefId="1">
...
</Obj>
</MS>
</Obj>
Type : Data
PSGuid : 00000000-0000-0000-0000-000000000000
Stream : Default
Fragments : ObjectID : 3
FragmentID : 0
Start : True
End : True
Length : 103
Messages : ObjectID : 3
Destination : Client
MessageType : RUNSPACEPOOL_STATE
RPID : 56733941-913e-49fc-b31f-870c2966bda4
PID : 00000000-0000-0000-0000-000000000000
Object :
<Obj RefId="0">
<MS>
<I32 N="RunspaceState">2</I32>
</MS>
</Obj>
Type : DataAck
PSGuid : 00000000-0000-0000-0000-000000000000
Stream :
Fragments :
Messages :
We can see the client sends the SESSION_CAPABILITY
and INIT_RUNSPACEPOOL
message to the server. The server then replies with its own SESSION_CAPABILITY
to negotiate the available APIs on each side. From there the client sends an APPLICATION_PRIVATE_DATA
message with data to store with the runspace being created and finally the server replies with RUNSPACEPOOL_STATE
to signal the runspace is opened. The last payload sent from the server is the DataAck
to end this exchange of messages from the original Data
payload sent by the client.
Once the Runspace Pool is created the client can then send a pipeline containing the commands and arguments to run. This represents the Invoke-Command $session { 'test' }
portion. In this example no extra arguments were provided but if -ArgumentList
was used it would be embedded in the CREATE_PIPELINE
payload from the client.
Type : Command
PSGuid : 1f9de7bb-994b-46bc-96cc-ae1452443691
Stream :
Fragments :
Messages :
Type : CommandAck
PSGuid : 1f9de7bb-994b-46bc-96cc-ae1452443691
Stream :
Fragments :
Messages :
Type : Data
PSGuid : 1f9de7bb-994b-46bc-96cc-ae1452443691
Stream : Default
Fragments : ObjectID : 5
FragmentID : 0
Start : True
End : True
Length : 2169
Messages : ObjectID : 5
Destination : Server
MessageType : CREATE_PIPELINE
RPID : 56733941-913e-49fc-b31f-870c2966bda4
PID : 1f9de7bb-994b-46bc-96cc-ae1452443691
Object :
<Obj RefId="0">
<MS>
<Obj N="PowerShell" RefId="1">
<MS>
<Obj N="Cmds" RefId="2">
<TN RefId="0">
<T>System.Collections.Generic.List`1[[System.Management.Automation.PSObject, System.Management.Automation, Version=7.4.6.500, Culture=neutral, PublicKeyToken=31bf3856ad36
4e35]]</T>
<T>System.Object</T>
</TN>
<LST>
<Obj RefId="3">
<MS>
<S N="Cmd"> 'test' </S>
<B N="IsScript">true</B>
<Nil N="UseLocalScope" />
...
</MS>
</Obj>
</LST>
</Obj>
</MS>
</Obj>
<B N="NoInput">true</B>
</MS>
</Obj>
Type : DataAck
PSGuid : 1f9de7bb-994b-46bc-96cc-ae1452443691
Stream :
Fragments :
Messages :
Type : Data
PSGuid : 1f9de7bb-994b-46bc-96cc-ae1452443691
Stream : Default
Fragments : ObjectID : 6
FragmentID : 0
Start : True
End : True
Length : 54
Messages : ObjectID : 6
Destination : Client
MessageType : PIPELINE_OUTPUT
RPID : 56733941-913e-49fc-b31f-870c2966bda4
PID : 1f9de7bb-994b-46bc-96cc-ae1452443691
Object :
<S>test</S>
Type : Data
PSGuid : 1f9de7bb-994b-46bc-96cc-ae1452443691
Stream : Default
Fragments : ObjectID : 7
FragmentID : 0
Start : True
End : True
Length : 103
Messages : ObjectID : 7
Destination : Client
MessageType : PIPELINE_STATE
RPID : 56733941-913e-49fc-b31f-870c2966bda4
PID : 1f9de7bb-994b-46bc-96cc-ae1452443691
Object :
<Obj RefId="0">
<MS>
<I32 N="PipelineState">4</I32>
</MS>
</Obj>
Type : Close
PSGuid : 1f9de7bb-994b-46bc-96cc-ae1452443691
Stream :
Fragments :
Messages :
Type : CloseAck
PSGuid : 1f9de7bb-994b-46bc-96cc-ae1452443691
Stream :
Fragments :
Messages :
The client will first send the <Command PSGuid='...' />
payload where PSGuid
is the Pipeline ID to be created. If successful the server responds with a CommandAck
to acknowledge the pipe is created and ready to receive the setup information. From there the client sends a CREATE_PIPELINE
PSRP payload that contains the -ScriptBlock
, and arguments/parameters to run with it and extra metadata like stream merging or $Host
information. Once received the server will response with a DataAck
and start running the pipeline.
After the pipe is run and output is generated, the server will send PIPELINE_OUTPUT
containing the raw CLIXML serialized output data from the payload. In the case above we are just outputting a single string so the server is going to send one PIPELINE_OUTPUT
message with the value of <S>test</S>
.
Once the pipeline has finished, the server sends a PIPELINE_STATE
with the identifier to indicate the pipe is done and not more output will be emitted. The client then sends a <Close PSGuid='...' />
payload targeting the pipeline ID to clear up any remaining resources on the server which the server acknowledges with a CLoseAck
.
The final step is closing the PSSession. This represents the $s | Remove-PSSession
portion of the example.
Type : Close
PSGuid : 00000000-0000-0000-0000-000000000000
Stream :
Fragments :
Messages :
Type : Data
PSGuid : 00000000-0000-0000-0000-000000000000
Stream : Default
Fragments : ObjectID : 8
FragmentID : 0
Start : True
End : True
Length : 103
Messages : ObjectID : 8
Destination : Client
MessageType : RUNSPACEPOOL_STATE
RPID : 56733941-913e-49fc-b31f-870c2966bda4
PID : 00000000-0000-0000-0000-000000000000
Object :
<Obj RefId="0">
<MS>
<I32 N="RunspaceState">4</I32>
</MS>
</Obj>
Type : Data
PSGuid : 00000000-0000-0000-0000-000000000000
Stream : Default
Fragments : ObjectID : 9
FragmentID : 0
Start : True
End : True
Length : 103
Messages : ObjectID : 9
Destination : Client
MessageType : RUNSPACEPOOL_STATE
RPID : 56733941-913e-49fc-b31f-870c2966bda4
PID : 00000000-0000-0000-0000-000000000000
Object :
<Obj RefId="0">
<MS>
<I32 N="RunspaceState">3</I32>
</MS>
</Obj>
Type : CloseAck
PSGuid : 00000000-0000-0000-0000-000000000000
Stream :
Fragments :
Messages :
In this case the client sends a <Close PSGuid='00000000-0000-0000-0000-000000000000' />
where the 0'd GUID represents the Runspace Pool should be closed. The server emits a few RUNSPACEPOOL_STATE
messages until finally it is closed and the CloseAck
is sent.
Once the PSSession is closed, the client transport implementation will shut down it's end, for ssh this means stopping the ssh
process that it spawned.