Skip to main content
  1. Posts/

SSH Multiplexing and Master Control Sockets: An Advanced Red Team Operator's Guide

··1205 words·6 mins·
Table of Contents

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:

  1. TCP handshake (SYN, SYN-ACK, ACK)
  2. Protocol negotiation (versions, KEX, ciphers)
  3. Authentication (password, key, MFA)
  4. 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:

  1. The first SSH command establishes the connection and creates a local socket file (say, /tmp/ssh-socket).
  2. Subsequent ssh, scp, and sftp commands look for that socket. If it’s there, they hand their data to the existing client process and ride the open TCP connection.
  3. 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.
Warning

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 600

A few notes:

  • ControlPath is where the socket file lives. Lock down the directory (chmod 700 ~/.ssh/sockets).
  • ControlMaster auto handles the master/slave decision for you.
  • ControlPersist is the part I actually care about. Exit your shell, the TCP connection stays alive in the background. Run ssh target five 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.5

This 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.5

This 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
#

  1. You land on a developer machine or jump box.

  2. You enumerate /tmp (or ~/.ssh) and find a socket file:

    find /tmp -type s 2>/dev/null
    # /tmp/ssh-UserA-ServerB
  3. The socket is owned by UserA.

  4. If you’re root, or the surrounding directory is too permissive, you can use it. (Socket file mode 777 itself usually doesn’t help much; what matters is whether your UID can open the file.)

  5. 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/sockets at 0700.
  • Don’t put sockets in shared locations like /tmp if you can avoid it.
  • Watch for ssh processes 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-target

Same 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 pipe

You 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"
fi

A 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"
}
Validate inputs in any real version of this. Passing untrusted strings into $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.


References
#

UncleSp1d3r
Author
UncleSp1d3r
As a computer security professional, I’m passionate about building secure systems and exploring new technologies to enhance threat detection and response capabilities. My experience with Rails development has enabled me to create efficient and scalable web applications. At the same time, my passion for learning Rust has allowed me to develop more secure and high-performance software. I’m also interested in Nim and love creating custom security tools.