Decrypting SSH traffic with Wireshark
2025-03-02
We were facing a strange bug at work in one of our tests, where a character seemingly went missing in a command sent over SSH. To ensure that the character was indeed properly sent by the client and not the result of some sort of corruption, I wanted to look at the raw traffic. SSH is of course encrypted which makes it harder.
Wireshark (4.2+) has the ability to decrypt SSH traffic. It requires dumping the shared secret resulting from the SSH key exchange, and the cookie linked to that key exchange, so that Wireshark knows which stream to decrypt using which shared secret. These need to be dumped in a file with a specific format: <cookie> SHARED_SECRED <secret_hex>.
For SSL/TLS, many implementations allow to dump the shared secret using the SSLKEYLOGFILE environment variable. There is unfortunately no such "standard" for SSH, and OpenSSH has no way to dump the secret even in debug mode. We mostly test with Paramiko as the SSH client, which does not support dumping the secret either, but luckily Paramiko being implemented in Python makes it easy to hack something in.
from paramiko.transport import Transport
old_set_K_H = Transport._set_K_H
def sshkeylogfile(self, k, h):
logpath_str = os.environ.get("SSHKEYLOGFILE", None)
if not logpath_str:
return
logpath = Path(logpath_str)
cookie = self._latest_kex_init[1:17].hex()
key_hex = hex(k)[2:]
key_hex = ((len(key_hex) % 2) * "0") + key_hex
with open(logpath, "a+") as handle:
print(
f"{cookie} SHARED_SECRET {key_hex}",
file=handle,
)
def patched_set_k_h(self, k, h):
sshkeylogfile(self, k, h)
old_set_K_H(self, k, h)
Transport._set_K_H = patched_set_k_h
To test this I ran the example server from the asyncssh library, captured the traffic with wireshark and started paramiko as the client with the above patch.
tshark -f 'port 8022' -i lo -w capture.pcap
import paramiko
from paramiko.client import SSHClient
client = SSHClient()
client.load_system_host_keys()
client.set_missing_host_key_policy(paramiko.WarningPolicy())
client.connect('localhost', port=8022, allow_agent=False, look_for_keys=True, username="guest", password="")
stdin, stdout, stderr = client.exec_command('')
print(stdout.read())
client.close()
The client logs the secret to the keylog file:
7d313898b4cfeb9c179d68e542dc2f4c SHARED_SECRET 743acb263818045c693347b8f9234214aa1b2af17a58ec7b5cf81827af2dce71
and looking at the capture, you can find the above cookie starting with 7d31 in the key exchange message:
We can inject the secret into the pcap using editcap:
editcap --inject-secrets ssh,keylog capture.pcap capture-with-secrets.pcap
and opening capture-with-secrets.pcap in Wireshark, we can see the decrypted SSH messages! (If using a non-standard SSH port like here, it has to be added to the Wireshark settings.)
Success!
I implemented the above in our test framework, which allowed to look at the SSH traffic the next time we reproduced the missing character bug. The character was present in the raw messages, which confirmed that the problem was on the server side. The exact cause of the bug is still elluding us, so to be continued!