Most operators I’ve worked with treat SSH as encrypted Telnet and call it a day. That’s fine until you find yourself running the same scp ten times in a row and watching the auth log fill up.
This post is about SSH multiplexing and master control sockets: running multiple SSH sessions (shells, file transfers, port forwards) over a single authenticated TCP connection. It cuts the per-connection overhead, makes scripted work a lot faster, and — if you find one on a box you’ve already compromised — gives you a free ride past authentication.
What is SSH multiplexing?#
SSH multiplexing is an OpenSSH feature that lets separate SSH sessions share one underlying TCP connection. The plumbing for this is a master control socket: a Unix domain socket on Linux/macOS, or a named pipe on Windows.
The mechanics#
When you run ssh user@target cold, you pay for:
- TCP handshake (SYN, SYN-ACK, ACK)
- Protocol negotiation (versions, KEX, ciphers)
- Authentication (password, key, MFA)
- Channel setup and shell spawn
That’s noisy, and it happens every single time. Run an scp loop over ten files and you do the whole dance ten times.
With multiplexing:
- The first SSH command establishes the connection and creates a local socket file (say,
/tmp/ssh-socket). - Subsequent
ssh,scp, andsftpcommands look for that socket. If it’s there, they hand their data to the existing client process and ride the open TCP connection. - No new handshake, no new auth, no new shell setup.
Why it matters on an engagement#
- Scripted operations finish much faster — no per-command handshake tax.
- Network-side, you generate one long-lived connection instead of fifty short ones, which is fewer firewall events.
- You authenticate once, so
/var/log/auth.log(or Windows Event ID 4624) sees one login instead of fifty. - If you can leave a master connection running in the background, you can come back later without re-authenticating, as long as the connection is still up.
Multiplexing reduces the volume of auth events, but a single TCP connection that stays open for four days while moving 2 GB of data is itself a red flag for a halfway-decent network hunter. You’re trading one signal for another.
Configuration#
The clean way to manage this is ~/.ssh/config:
Host target-*
User operator
# The socket path. %r=remote_user, %h=host, %p=port
# %C is a hash of the above, useful to avoid
# "socket path too long" errors.
ControlPath ~/.ssh/sockets/%C
# Become master if no socket exists; reuse one if it does.
ControlMaster auto
# Keep the connection open for 10 minutes after the last session closes.
ControlPersist 600A few notes:
ControlPathis where the socket file lives. Lock down the directory (chmod 700 ~/.ssh/sockets).ControlMaster autohandles the master/slave decision for you.ControlPersistis the part I actually care about. Exit your shell, the TCP connection stays alive in the background. Runssh targetfive minutes later and it’s instant.
Manual control from the command line#
If you’re on a compromised box and don’t want to drop config files anywhere, do it all with flags.
1. The master#
# -M: master mode
# -S: socket path
# -f: fork to background
# -N: no command (just keep the connection alive)
ssh -M -S /tmp/pivot -fN user@10.10.10.5This authenticates, opens the connection, creates /tmp/pivot, and forks into the background.
2. Using the socket#
# Run a command
ssh -S /tmp/pivot user@10.10.10.5 "id"
# Transfer a file (scp uses -o for this)
scp -o "ControlPath=/tmp/pivot" payload.elf user@10.10.10.5:/tmp/3. Talking to the master at runtime#
You can send control commands to the running master to add or remove forwards without restarting the connection:
# Check status
ssh -S /tmp/pivot -O check user@10.10.10.5
# Add a SOCKS proxy on port 1080
ssh -S /tmp/pivot -O forward -D 1080 user@10.10.10.5
# Add a local forward
ssh -S /tmp/pivot -O forward -L 8080:localhost:80 user@10.10.10.5
# Stop the SOCKS proxy
ssh -S /tmp/pivot -O cancel -D 1080 user@10.10.10.5
# Kill the master connection
ssh -S /tmp/pivot -O exit user@10.10.10.5This is genuinely useful when scope or routing changes mid-engagement and you’d otherwise have to tear down and re-auth.
SSH socket hijacking#
A control socket is, for all practical purposes, a pre-authenticated session handle. Anyone who can read and write to that socket file can use the SSH connection as the authenticated user. No password, no key, no MFA prompt — the session is already up.
The scenario#
You land on a developer machine or jump box.
You enumerate
/tmp(or~/.ssh) and find a socket file:find /tmp -type s 2>/dev/null # /tmp/ssh-UserA-ServerBThe socket is owned by UserA.
If you’re root, or the surrounding directory is too permissive, you can use it. (Socket file mode
777itself usually doesn’t help much; what matters is whether your UID can open the file.)Pivot without credentials:
# Assuming you're root and found UserA's socket ssh -S /tmp/ssh-UserA-ServerB remote_host "mkdir /tmp/.hidden; chmod 777 /tmp/.hidden"
You’re now running commands on ServerB as UserA. You never saw a password or a private key, and you walked past MFA because someone else already passed it.
Defense#
- Keep
~/.ssh/socketsat0700. - Don’t put sockets in shared locations like
/tmpif you can avoid it. - Watch for
sshprocesses with odd parents, or socket files that survive the user’s shell.
Windows#
This works on Windows if you’re on the modern OpenSSH client (Windows 10/11, Server 2019+). Windows doesn’t have Unix domain sockets, so OpenSSH uses named pipes instead:
Host target
ControlPath \\.\pipe\ssh-targetSame idea, different transport. Worth knowing if you’re operating from a Windows implant.
Stale sockets#
The annoying part of multiplexing is what happens when the connection dies ungracefully — ISP blip, NAT timeout, whatever. The socket file stays around, the local SSH client still thinks it’s connected, and you get:
mux_client_request_session: read from master failed: Broken pipeYou have to clean it up by hand:
socket_path="/tmp/pivot"
if [ -S "$socket_path" ]; then
# Try a graceful exit, then force-remove if that fails
ssh -S "$socket_path" -O exit user@host 2>/dev/null || rm "$socket_path"
fiA wrapper I actually use#
Here’s a small function for running batches of commands. It checks whether a master is up, brings one online if not, and runs the command:
run_multiplexed() {
local target="$1"
local cmd="$2"
local socket="/tmp/ssh-mux-${target##*@}" # socket per host
# Is the master alive?
if ! ssh -S "$socket" -O check "$target" 2>/dev/null; then
echo "[*] Establishing master connection..."
rm -f "$socket" # in case there's a stale one
ssh -M -S "$socket" -fN "$target"
fi
echo "[+] Running: $cmd"
ssh -S "$socket" "$target" "$cmd"
}$cmd or $target is a local command-injection footgun.Wrapping up#
Multiplexing isn’t exotic. It’s been in OpenSSH forever. Most people just never bother turning it on. Use it for a week of scripted work and re-authenticating for every command starts to feel ridiculous.
And if you’re on the blue side: treat live control sockets the way you’d treat a logged-in console. That’s basically what they are.
UncleSp1d3r out.