/dev/posts/

OpenSSH ProxyUseFdPass

Published:

Updated:

While looking at the OpenSSH ssh_config manpage, I found the ProxyUseFdpass configuration I did not know about. It is apparently not widely known or used.

Update 2017-08-02: netcat (nc) has an option (nc.openbsd -F www.example.com 80) to pass the created file descriptor using the "fdpass" mechanism. In addition to the straightforward connect(), it can pass a file descriptor having initiated a connection through a SOCKS proxy (with -x proxy.example.com) or with HTTP connect (-x proxy.example.com -X connect).

Update (2021-02-09): replaced fds.fromstring() with fds.frombytes().

ProxyCommand

OpenSSH client has a ProxyCommand configuration which can be used to use a command as a transport to the server:

Specifies the command to use to connect to the server. The command string extends to the end of the line, and is executed using the user's shell ‘exec’ directive to avoid a lingering shell process.

Instead of opening a socket to the server itself, the OpenSSH client spawns the specified command and use its standard input and output to communicate with the server.

The man page suggests to use (the OpenBSD variant of) netcat to connect through an HTTP (or SOCKS) proxy:

ProxyCommand /usr/bin/nc -X connect -x 192.0.2.0:8080 %h %p

A typical usage is to use a relay/bastion/jump/gateway[^jump] SSH server with ssh -W[1]:

Host gateway.example.com
ProxyCommand none

Host *.example.com
ProxyCommand ssh gateway.example.com -W %h:%p

Using ProxyJump instead

OpenSSH 7.3 includes special support for SSH jump servers with the ProxyJump configuration,

Host *.example.com
ProxyJump gateway.example.com

or the -J flag:

ssh -J gateway.example.com foo.example.com

Code injection through ProxyCommand (update 2023-12-21)

Using ProxyCommand with hostname (%h) or username (%u) expansions may expose you to shell command injections: it is possible to inject arbitrary shell commands through the hostname (%h) or username (%u) (CVE-2023-51385).

For this to be exploited, an attacker would have to trick you into SSH-ing into a host with a weird (containing shell metacharacters) name or using a weird username. This can for example happen when doing a recursive git clone of a malicious git repository.

This has been mitigated in OpenSSH 9.6.p1 by rejecting host and username containing shell meta-characters (which should mostly work on a regular shell but might not work if using an exotic shell).

ProxyUseFdPass

While looking at the new ProxyJump configuration, I found a ProxyUseFdpass option which:

Specifies that ProxyCommand will pass a connected file descriptor back to ssh(1) instead of continuing to execute and pass data. The default is “no”.

When enabled, instead of communicating with the server through the ProxyCommand standard input and output, the SSH client expects the command to give it a file descriptor to use. The idea is to avoid having an uncessary lingering process and extra write/reads when it is not necessary[2].

The documentation does not explain how it is supposed to work exactly and I did not find any working example or any suggestion of a program which would be able to pass the file descriptor.

The spawned command is expected to:

  1. setup a file descriptor;
  2. send this file descriptor to the OpenSSH client process through its (own) standard output (sendmsg with SCM_RIGHTS using a one-byte message;
  3. exit(0).

A minimal program which does the job is:

#!/usr/bin/env python3

import sys
import socket
import array

# Create the file descriptor:
s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM, 0)
s.connect((sys.argv[1], int(sys.argv[2])))

# Pass the file descriptor:
fds = array.array("i", [s.fileno()])
ancdata = [(socket.SOL_SOCKET, socket.SCM_RIGHTS, fds)]
socket.socket(fileno = 1).sendmsg([b'\0'], ancdata)

Which can be used with:

ProxyCommand /path/to/passfd %h %p
ProxyUseFdpass yes

In its current form, it does not do much. It creates a socket the same way the OpenSSH client would have and pass it to the OpenSSH client. However, it can extended in order to do things such as:

For testing purpose this receiving program can be used:

#!/usr/bin/env python3

import os
import sys
import socket
import array

(a, b) = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM, 0)
pid = os.fork()

def recv_fd(sock):
    fds = array.array("i")
    cmsg_len = socket.CMSG_LEN(fds.itemsize)
    msg, ancdata, flags, addr = sock.recvmsg(1, cmsg_len)
    for cmsg_level, cmsg_type, cmsg_data in ancdata:
        if (cmsg_level, cmsg_type) == (socket.SOL_SOCKET, socket.SCM_RIGHTS):
            fds.frombytes(cmsg_data)
            return fds[0]
    sys.exit(1)

if pid == 0:
    # Exec specified command in the child:
    a.close()
    os.dup2(b.fileno(), 0)
    os.dup2(b.fileno(), 1)
    b.close()
    os.execvp(sys.argv[1], sys.argv[1:])
else:
    # Receive file descriptor and wait in the parent:
    b.close()
    s = recv_fd(a)
    os.waitpid(pid, 0)
    print(s)

Which can be used as:

fdrecv fdpass localhost 80

  1. It is often suggested to use this configuration instead:

    ProxyCommand ssh gateway.example.com nc %h %p
    

    This requires netcat to be available on the server. ssh -W only needs client-side support which is available in OpenSSH since 5.4 (released in 2010) and the SSH server to accept TCP forwarding. ↩︎

  2. It is not usable for a SSH jump server but can be used in simpler cases. ↩︎