Intercept PowerShell Remoting (PSRP)
PowerShell remoting over SSH (PSRP over SSH) lets Windows and Linux clients run remote PowerShell sessions without WinRM by using the SSH subsystem mechanism. SSH-MITM intercepts these sessions transparently — logging credentials and relaying the binary PSRP stream verbatim to the real server.
How PowerShell remoting over SSH works
The client (Enter-PSSession, Invoke-Command, or New-PSSession)
connects to the SSH server and requests the powershell subsystem. The
server must have this subsystem registered in /etc/ssh/sshd_config:
Subsystem powershell /usr/bin/pwsh -sshs -NoLogo
Once the subsystem is granted, both sides exchange PSRP data over the SSH channel for the full lifetime of the session.
PSRP wire format over SSH
PSRP over SSH does not use a raw binary stream. Instead, each logical message is wrapped in an XML envelope and the binary payload is base64-encoded (MS-PSRP specification §2.2.4, “SSH transport”):
<Data Stream='Default' PSGuid='00000000-0000-0000-0000-000000000000'>
AAAAAAAAAAE...BASE64...
</Data>
Each <Data> element contains exactly one PSRP fragment encoded in
base64. A fragment has the following 21-byte binary header followed by a
variable-length blob:
Offset Size Field Description
------ ---- ----------- ------------------------------------------
0 8 ObjectId uint64 big-endian — groups fragments into
one logical message
8 8 FragmentId uint64 big-endian — sequence number within
the object
16 1 Flags bit 0 = start fragment; bit 1 = end fragment
17 4 BlobLength uint32 big-endian — length of the blob below
21 N Blob raw bytes of this fragment's payload
Fragments with the same ObjectId are concatenated in order. When
Flags & 0x01 (start) and Flags & 0x02 (end) are both set on a single
fragment, that fragment is a complete, self-contained message.
The reassembled blob forms a PSRP message with a 40-byte header:
Offset Size Field Description
------ ---- ----------- ------------------------------------------
0 4 Destination uint32 little-endian (1 = client, 2 = server)
4 4 MessageType uint32 little-endian (see table below)
8 16 RPID UUID (runspace pool ID)
24 16 PID UUID (pipeline ID, or all-zeros)
40 * MessageData CLIXML (UTF-8 XML, optional BOM)
Common MessageType values:
Code |
Name |
Content |
|---|---|---|
0x00010002 |
|
Protocol version negotiation |
0x00010004 |
|
Pool parameters (min/max threads, ApartmentState, …) |
0x00010007 |
|
|
0x00021006 |
|
|
0x00041002 |
|
Serialised result objects in CLIXML |
0x00041007 |
|
|
0x00041008 |
|
Plain string |
0x00041009 |
|
|
Analysing sessions with SSH-MITM
The log-session plugin is active by default. It parses the PSRP protocol
on the fly and makes two types of output available: a human-readable transcript
file and a structured JSON log.
Human-readable transcript file (recommended)
Pass --psrp-transcript-dir to write one plain-text file per session:
ssh-mitm server --remote-host <target> \
--psrp-transcript-dir /tmp/psrp-transcripts/
Each file is named <session-id>.log. See Logging and transcripts below
for the full format description.
Structured JSON log
When SSH-MITM’s output is piped, it automatically switches to JSON format. Pipe directly to jq to filter live:
ssh-mitm server --remote-host <target> \
| jq 'select(.event == "psrp_message")'
To keep a log file and follow it at the same time:
# Terminal 1 — start server, write JSON log
ssh-mitm server --remote-host <target> > sshmitm.log
# Terminal 2 — follow and filter
tail -f sshmitm.log | jq 'select(.event == "psrp_message")'
Useful jq queries:
# Show all executed commands
jq -r 'select(.message_type == "CreatePipeline")
| "\(.timestamp) \(.commands[]?)"' sshmitm.log
# Show all errors
jq 'select(.message_type == "ErrorRecord")' sshmitm.log
Prerequisites on the target host
The SSH server that SSH-MITM forwards to must have PowerShell Core (pwsh)
installed and the powershell subsystem registered in sshd_config.
openSUSE Tumbleweed
Register the Microsoft package repository and install PowerShell Core:
# Import the Microsoft signing key
sudo rpm --import https://packages.microsoft.com/keys/microsoft.asc
# Register the repository (adjust the URL for the current release if needed —
# see https://learn.microsoft.com/en-us/powershell/scripting/install/install-rhel)
sudo zypper addrepo https://packages.microsoft.com/rhel/8/prod microsoft-prod
sudo zypper refresh
# Install PowerShell Core
sudo zypper install -y powershell
After installation verify the binary path:
which pwsh # → /usr/bin/pwsh
pwsh --version # → PowerShell 7.x.x
Register the subsystem with OpenSSH and restart the service:
# Append the Subsystem line if it is not present yet
grep -q "^Subsystem powershell" /etc/ssh/sshd_config || \
echo "Subsystem powershell $(which pwsh) -sshs -NoLogo" \
| sudo tee -a /etc/ssh/sshd_config
sudo systemctl restart sshd
# Confirm the line is active
sudo sshd -T | grep "subsystem powershell"
Ubuntu / Debian
# Install PowerShell Core from the Microsoft repository
# (see https://learn.microsoft.com/en-us/powershell/scripting/install/install-ubuntu)
sudo apt-get install -y powershell
grep -q "^Subsystem powershell" /etc/ssh/sshd_config || \
echo "Subsystem powershell $(which pwsh) -sshs -NoLogo" \
| sudo tee -a /etc/ssh/sshd_config
sudo systemctl restart sshd
Other distributions and Windows
For other Linux distributions (Fedora, RHEL, Alpine, …) and for Windows Server
(OpenSSH) refer to the
Microsoft documentation on PowerShell remoting over SSH.
The sshd_config entry and the SSH-MITM workflow are identical regardless of
the operating system on the target host.
Setting up a local test environment
The following steps let you test PowerShell interception on a single openSUSE Tumbleweed machine without a separate target host.
Requirements: PowerShell Core and OpenSSH server installed (see above).
# 1 — verify the powershell subsystem is registered (see Prerequisites above)
sudo sshd -T | grep "subsystem powershell"
# 2 — start SSH-MITM pointing at localhost port 22
ssh-mitm server --remote-host 127.0.0.1 --remote-port 22 --listen-port 10022
In a second terminal, open an intercepted PowerShell session:
pwsh -Command "Enter-PSSession -HostName 127.0.0.1 -Port 10022 -UserName $USER"
Accept the host-key warning (SSH-MITM presents its own generated key), enter your password, and you will land in a remote PowerShell session that has been routed transparently through SSH-MITM.
Intercepting a session against a real target
1. Start SSH-MITM
ssh-mitm server --remote-host <target-host>
By default SSH-MITM listens on port 10022.
2. Connect through SSH-MITM
From Linux (PowerShell Core):
pwsh -Command "Enter-PSSession -HostName <mitm-host> -Port 10022 -UserName <user>"
Or non-interactively:
pwsh -Command "Invoke-Command -HostName <mitm-host> -Port 10022 -UserName <user> -ScriptBlock { hostname }"
From Windows (PowerShell):
Enter-PSSession -HostName <mitm-host> -Port 10022 -UserName <user>
3. Check the intercepted credentials
SSH-MITM logs the credentials as soon as authentication succeeds:
Logging and transcripts
The log-session plugin is active by default. It parses the PSRP protocol
and logs every command, output, error, and state-change message.
To write a human-readable transcript file for each session, add
--psrp-transcript-dir:
ssh-mitm server --remote-host <target> \
--psrp-transcript-dir /tmp/psrp-transcripts/
Each session produces one file named <session-id>.log:
When --session-log-dir is already configured, the transcript is written
there automatically even without --psrp-transcript-dir.
Writing a custom forwarder plugin
To inspect or modify the raw PSRP stream, subclass
PowerShellForwarder.
See Developing Plugins for examples, the full API reference, and
registration instructions.
Limitations
PipelineOutput detail — the
log-sessionplugin extracts all CLIXML scalar values (strings, integers, doubles, booleans, dates, …) from pipeline output. The values are joined with spaces; property names are not included. Deeply nested or binary objects may not produce human-readable output.Certificate-based authentication — if the client is configured to use SSH certificate authentication, SSH-MITM can intercept the session only when
--accept-first-publickeyis used or a matching CA key is available.Known-hosts pinning — clients that pin the server’s host key will reject SSH-MITM’s generated key. Remove the old entry from
~/.ssh/known_hostsbefore testing, or pass a real host key with--host-key.