Skip to content

Instantly share code, notes, and snippets.

@jborean93
Created December 12, 2024 00:53
Show Gist options
  • Save jborean93/95cbf2add869ed6242f27882efaf2198 to your computer and use it in GitHub Desktop.
Save jborean93/95cbf2add869ed6242f27882efaf2198 to your computer and use it in GitHub Desktop.
Breakdown over what happens with PSRemoting over SSH

PSRemoting over SSH

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

Loading

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.

Creating Runspace Pool

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.

Running PowerShell Pipeline

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.

Closing Runspace Pool

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.

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