Demystifying SFTP
Introduction
SFTP is an auxiliary protocol of SSH that allows file transfer between two hosts over the SSH protocol.
Typically, using SFTP to connect to a remote host requires establishing a separate SSH session, which naturally also requires separate authentication. As a result, if we need to both log in to the remote host to run commands and transfer files, we need to authenticate separately and establish separate sessions. However, SSH itself supports port forwarding and multiplexing. Is there a way to achieve both command execution and file transfer through the same SSH session?
OpenSSH episode
OpenSSH’s design is unique among applications with network communication capabilities. Its client program, ssh(1), can either open a socket connection to the SSH server itself or use the ProxyCommand option (see the man page for ssh_config(5)) to run a command to connect to the SSH server. The data sent to the server needs to be sent to the standard input of the child process running this command, and the data returned by the server is obtained through the standard output of the child process. If the ProxyUseFdpass option is enabled, this command does not need to run continuously, but can terminate after transferring the successfully connected socket to its parent process through a Unix domain socket redirected to its standard output. This allows ssh(1) to use proxy servers without having to implement support for them directly, but by running applications that support connections through proxy servers in child processes.
The design of SFTP is similar. Its client program, sftp(1), does not have network communication capabilities itself, but instead connects to the SSH server by running a command (usually ssh(1) with a set of default options) in its child process. After successful authentication and login, the remote SSH server will start the SFTP server program sftp-server(8) (located in /usr/lib/openssh/ by default). The data sent by the client will be sent to the standard input of the child process and eventually reach the standard input of sftp-server(8). sftp-server(8) will return data through its standard output, which will eventually reach the standard output of the client’s child process and be delivered to the client. sftp-server(8) is designed to provide services through its standard input and output rather than listening to sockets.
Therefore, in theory, as long as SFTP can connect to the standard input and output of an sftp-server(8) through the command it runs in its child process, the SFTP protocol can function normally. sftp(1) also provides the -D option to achieve this. The original intention of this option is to allow sftp(1) to directly start the local sftp-server(8) for testing, but this does not prevent us from using it to run other commands that meet the requirements of SFTP.
“Plaintext SFTP” over TCP
The design of sftp-server(8), which provides services through its standard input and output, is very similar to that of a sub-server in the super-server and sub-server combination commonly found on early UNIX systems. In this combination, the super-server (typically inetd) is only responsible for listening to sockets. Once a connection is received, the super-server replicates itself through fork(2) and redirects the connection socket returned by accept(2) to the standard input and output of the child process through dup2(2). Finally, the child process starts the sub-server through exec(2), which implements interaction through its standard input and output. The specific operation of ssh(1) and sftp(1) using child processes to achieve network communication is similar to this.
Since sftp-server(8) conforms to the characteristics of a sub-server, we can get a “plaintext SFTP” server over TCP by using it with a super-server. The Nmap project’s Ncat tool is a feature-rich netcat implementation, and its -e, –exec and -k, –keep-open options can work together as a super-server. Running
$ ncat -lp ${port} -ke "/usr/lib/openssh/sftp-server"
on a host will start such a plaintext SFTP server. Other hosts can run
$ sftp -D "ncat ${host} ${port}"
to connect to this server and transfer files. Note: This mode of operation has neither encryption nor authentication, so it is only suitable for use in highly trusted network environments (such as a home LAN). The access permission of the server depends on the user who started the server, and the initial directory accessed by the client after connecting is the current directory where the server was started. If you want to specify a different directory, you can use the -d option of sftp-server(8), such as -d %d to set the initial directory to the home directory of the remote account (see the sftp-server(8) man page for details).
sshfs(1) can also connect to plaintext SFTP servers, but sshfs(1) does not support executing arbitrary commands to achieve network communication. However, you can use the -o directport=PORT option to connect directly to the plaintext SFTP server over TCP.
“SFTP over TLS”
“Plaintext SFTP” is neither encrypted nor authenticated. However, if we use a super-server that supports TLS, we can construct an “SFTP over TLS” file server with encryption but only authentication for server. Ncat happens to support TLS. Another TLS server that can be used as a super-server is stunnel.
The simplest server authentication can be achieved by certificate fingerprint: Start the server like this:
$ ncat -lp ${port} -v --ssl -ke "/usr/lib/openssh/sftp-server ..."
Ncat will generate a temporary TLS self-signed certificate in memory and print its fingerprint from Ncat’s standard error output with the -v option. The client connects to the server with:
sftp -D "ncat -v --ssl ${host} ${port}"
Ncat will also print the fingerprint of the received TLS certificate to its standard error output. By comparing the certificate fingerprints, it is possible to detect potential man-in-the-middle attacks, and all interactions in between are encrypted with TLS.
More formal server authentication can also be used: Server:
$ ncat -lp ${port} --ssl-cert ${cert} --ssl-key ${key} -ke "/usr/lib/openssh/sftp-server ..."
Client:
sftp -D "ncat --ssl-verify --ssl-trustfile ${ca-cert} ${host} ${port}"
If ${cert} is a publicly recognized TLS certificate, “–ssl-trustfile ${ca-cert}” can be omitted. If ${cert} is a self-signed certificate, it can be used in the ${ca-cert} parameter position on the client side. However, the Common Name field in the certificate must match the ${host} used when the client connects, otherwise the certificate verification will fail.
Of course, to build a formal file server, the server needs to be further strengthened. The specific implementation is beyond the scope of this article and is omitted here.
sshfs(1) cannot directly connect to an “SFTP over TLS” server, but you can use Ncat locally to unwrap TLS and convert it to a “plaintext SFTP” server, then connect directly with sshfs(1).
Plaintext SFTP on Unix Domain Sockets, One SSH Session for Both Command Execution and File Transfer
By combining a plaintext SFTP server and ssh’s port forwarding capability, a single SSH session can be used to achieve both command execution and file transfer. To prevent unauthorized access, we can let the server listen to a Unix domain socket and forward it to the localhost using SSH.
Before logging in to the remote host, we first create a temporary directory on the local machine that is only accessible to the local account using $ mktemp -d
, let it be /tmp/ltmpdir.
ssh(1) can dynamically add port forwarding at runtime, which is not activated by default. However, we can activate this feature by adding the -oEnableEscapeCommandline=yes option when logging in. Otherwise, an error message “commandline disabled” will be prompted when trying to activate the ssh(1) internal prompt.
After logging in to the remote host, we also create a temporary directory that is only accessible to our account on the remote host using $ mktemp -d
, let it be /tmp/rtmpdir.
Start a plaintext SFTP server in the background on the remote host that listens to the Unix domain socket located in /tmp/rtmpdir:
$ ncat -lU /tmp/rtmpdir/sftp -ke "/usr/lib/openssh/sftp-server ..." &
Then press enter, the SSH escape character (default is ‘~’), and uppercase C in sequence to activate the ssh(1) internal prompt, add port forwarding: -L /tmp/ltmpdir/sftp:/tmp/rtmpdir/sftp
, if “Forwarding port.” is displayed after pressing enter, it means that the dynamic port forwarding is successfully added. At this time, /tmp/ltmpdir/sftp will appear on the local machine.
Finally, run:
$ sftp -D "ncat -U /tmp/ltmpdir/sftp"
on the local machine to connect to the plaintext SFTP server forwarded to the local machine via the Unix domain socket. After using sftp, before logging out of the remote host, you should close the plaintext SFTP server and delete the temporary directories containing the Unix domain sockets on both the local machine and the remote host.
sshfs(1) cannot directly connect to plaintext SFTP servers on Unix domain sockets, but you can use Ncat to convert it to a plaintext SFTP server on TCP sockets, and then connect directly with sshfs(1).