In PuTTY, Scripted Passwords are Exposed Passwords
PuTTY is one of the oldest and most popular SSH clients, originally for Windows, but now available on several platforms. It has won corporate support and endorsement, and is prepared and bundled within several third-party repositories.
Unfortunately, the 0.74 stable PuTTY release does not safely guard plain-text passwords provided to it via the -pw
command line option for the psftp
, pscp
, and plink
utilities as the documentation clearly warns. There is evidence within the source code that the authors are aware of the problem, but the exposure is confirmed on Microsoft Windows, Oracle Linux, and the package prepared by the OpenBSD project.
After discussions with the original author of PuTTY, Simon Tatham developed a new -pwfile
option, which will read an SSH password from a file, removing it from the command line. This feature can be backported into the current 0.76 stable release. Full instructions for applying the backport and a .netrc
wrapper for psftp
are presented, also implemented in Windows under Busybox.
While the -pw
option is attractive for SSH users who are required to use passwords (and forbidden from using keys) for scripting activities, the exposure risk should be understood for any use of the feature. Users with security concerns should obtain the -pwfile
functionality, either by applying a patch to the 0.76 stable release, or using a snapshot release found on the PuTTY website.
Vulnerability
The psftp
, pscp
, and plink
utilities are able to accept a password on the command line, as their usage output describes:
$ psftp -h PuTTY Secure File Transfer (SFTP) client Release 0.76 Usage: psftp [options] [user@]host Options: -V print version information and exit -pgpfp print PGP key fingerprints and exit -b file use specified batchfile -bc output batchfile commands -be don't stop batchfile processing if errors -v show verbose messages -load sessname Load settings from saved session -l user connect with specified username -P port connect to specified port -pw passw login with specified password -1 -2 force use of particular SSH protocol version -ssh -ssh-connection force use of particular SSH protocol variant -4 -6 force use of IPv4 or IPv6 -C enable compression -i key private key file for user authentication -noagent disable use of Pageant -agent enable use of Pageant -no-trivial-auth disconnect if SSH authentication succeeds trivially -hostkey keyid manually specify a host key (may be repeated) -batch disable all interactive prompts -no-sanitise-stderr don't strip control chars from standard error -proxycmd command use 'command' as local proxy -sshlog file -sshrawlog file log protocol details to a file -logoverwrite -logappend control what happens when a log file already exists
The manual pages for the psftp
, pscp
, and plink
clients bundled within the EPEL package clearly document the risk of exposure with this option:
$ man psftp | sed -n '/pw password/,/commands/p' -pw password Set remote password to password. CAUTION: this will likely make the password visible to other users of the local machine (via commands such as `w').
Note that this documentation from the UNIX manual page above is not included in the Windows MSI installer. While this warning is found in the general documentation, the risk to Windows users is not prominent:
3.11.3.8 -pw: specify a passwordA simple way to automate a remote login is to supply your password on the command line. This is not recommended for reasons of security. If you possibly can, we recommend you set up public-key authentication instead.
This warning is genuine, as is easily demonstrated on Linux:
$ psftp -pw foobar4.foobar cfisher@localhost Using username "cfisher". Remote working directory is /home/cfisher psftp> !sh $ ps ax | grep psftp 7490 pts/1 S 0:00 psftp -pw foobar4.foobar cfisher@localhost sh-4.2$ cat /proc/7490/cmdline; echo psftp-pwfoobar4.foobarcfisher@localhost
Shell scripts relying upon the -pw
argument to automate psftp
, pscp
, or plink
do so at the cost of credential exposure, as any shell account can see the process list, on Linux via /proc/*/cmdline
and on OpenBSD by other mechanisms.
Windows users will likely contend that this issue does not impact their platform; they are mistaken, as a few clicks in task manager will show:
Passwords used with the -pw
option should be considered exposed and changed if any unprivileged users have had access to the process list.
Incomplete Remediation
While a common but imperfect solution, it appears that modern programs wipe passwords from their command lines by writing into the “argument vector” as defined in the C programming language. Below is an example:
$ cat clipurge.c #include <stdio.h> #include <string.h> #define BUF 1024 int main(int argc, char **argv) { int c; char junk[BUF]; for(c = 1; c < argc; c++) if(!strcmp("-pw", argv[c])) { int d = 0; while(*(d + argv[c + 1])) *(d++ + argv[c + 1]) = '*'; } fgets(junk, BUF, stdin); }
We can compile and test this code to see the removal:
$ cc -o clipurge clipurge.c $ ./clipurge foo bar -pw baz bada bing
From another shell, check the process list:
$ ps ax | grep clipurge 13500 pts/1 S+ 0:00 ./clipurge foo bar -pw *** bada bing
The technique has been tested to work on Oracle Linux and OpenBSD (notably, it does not work on legacy HP-UX, where the old hide.c can be used instead). Windows, unfortunately, was not remediated in testing with the Cygwin GCC compiler.
Since all of the PuTTY utilities are written in C, we can add this code to their source and prepare new binaries if a capable C compiler is available.
To create a patched version of the psftp
, pscp
, and plink
utilities on applicable platforms, place the following files in your home directory:
$ cat ~/cmdline.patch --- cmdline.c.orig 2021-09-26 11:15:52.386305592 -0500 +++ cmdline.c 2021-09-26 11:16:08.359152634 -0500 @@ -163,7 +163,7 @@ settings_set_default_port(port); conf_set_int(conf, CONF_port, port); } - +extern char **globargv; extern int globargc; int cmdline_process_param(const char *p, char *value, int need_save, Conf *conf) { @@ -575,12 +575,12 @@ if (conf_get_int(conf, CONF_protocol) != PROT_SSH) cmdline_error("the -pw option can only be used with the " "SSH protocol"); - else { + else { int c; cmdline_password = dupstr(value); /* Assuming that `value' is directly from argv, make a good faith * attempt to trample it, to stop it showing up in `ps' output * on Unix-like systems. Not guaranteed, of course. */ - smemclr(value, strlen(value)); + smemclr(value, strlen(value)); for(c=1;c<globargc;c++) if(!strcmp("-pw", globargv[c])) {int d=0; char *a=globargv[c+1]; while(*(d+a)) {*(d+a) = (d==0)?'X':0; d++;} } } }
$ cat ~/uxsftp.patch --- unix/uxsftp.c.orig 2021-09-26 11:07:24.900243423 -0500 +++ unix/uxsftp.c 2021-09-26 11:08:13.039656553 -0500 @@ -566,13 +566,13 @@ void platform_psftp_pre_conn_setup(LogPolicy *lp) {} const bool buildinfo_gtk_relevant = false; - +int globargc; char **globargv; /* * Main program: do platform-specific initialisation and then call * psftp_main(). */ int main(int argc, char *argv[]) -{ +{ globargc=argc; globargv=argv; uxsel_init(); return psftp_main(argc, argv); }
$ cat ~/uxplink.patch --- unix/uxplink.c.orig 2021-09-26 11:07:34.101329314 -0500 +++ unix/uxplink.c 2021-09-26 11:08:44.210306055 -0500 @@ -654,7 +654,7 @@ return false; /* terminate main loop */ return true; } - +int globargc; char **globargv; int main(int argc, char **argv) { int exitcode; @@ -664,7 +664,7 @@ bool just_test_share_exists = false; struct winsize size; const struct BackendVtable *backvt; - +globargc=argc; globargv=argv; /* * Initialise port and protocol to sensible defaults. (These * will be overridden by more or less anything.)
Notice above the text, “Assuming that ‘value’ is directly from argv, make a good faith attempt to trample it, to stop it showing up in ‘ps’ output on Unix-like systems. Not guaranteed, of course.” Unfortunately, this good faith attempt appears insufficient.
The patch presented here simply makes the original argv
available in the context where the wipe is designed to occur, then wipes all the -pw
parameters found. It might be preferable to determine exactly why the intended wipe of the incoming value does not perform as expected.
To apply these source code patches, download the 0.76 release of PuTTY, unpack it, change directory to its top level, and run the following commands:
$ patch -p0 < ~/cmdline.patch patching file cmdline.c $ patch -p0 < ~/uxsftp.patch patching file unix/uxsftp.c $ patch -p0 < ~/uxplink.patch patching file unix/uxplink.c
On systems with modern GCC or equivalent options, run this custom configuration command which enables all of the compiler safety controls (syntax confirmed on OpenBSD clang; note that -O3
has caused crashes):
CFLAGS='-O2 -D_FORTIFY_SOURCE=2 -fstack-protector-strong -fpic -pie' \ LDFLAGS='-Wl,-z,relro,-z,now -Wl,-z,now' ./configure
Then run make to trigger the build:
$ make
When the compiler finishes, the following programs should be visible:
$ ls -l plink pscp psftp -rwxr-xr-x. 1 fishecj itg 837424 Sep 24 12:17 plink -rwxr-xr-x. 1 fishecj itg 825336 Sep 24 12:17 pscp -rwxr-xr-x. 1 fishecj itg 838400 Sep 24 12:17 psftp
If you have the hardening-check
utility, you can confirm that the programs were compiled with safety controls:
$ hardening-check plink pscp psftp plink: Position Independent Executable: yes Stack protected: yes Fortify Source functions: yes (some protected functions found) Read-only relocations: yes Immediate binding: yes pscp: Position Independent Executable: yes Stack protected: yes Fortify Source functions: yes (some protected functions found) Read-only relocations: yes Immediate binding: yes psftp: Position Independent Executable: yes Stack protected: yes Fortify Source functions: yes (some protected functions found) Read-only relocations: yes Immediate binding: yes
Test the patched psftp
, pscp
, and plink
:
$ printf 'Password: '; stty -echo; read Pass; stty echo; echo Password: $ ./psftp -pw "$Pass" $USER@localhost Using username "fishecj". Remote working directory is /home/cfisher psftp> !sh $ ps ax | grep psftp 24095 pts/0 S 0:00 ./psftp -pw X cfisher@localhost $ cat /proc/24095/cmdline; echo ./psftp-pwXcfisher@localhost
This is not a completely effective patch; it is still able to leak a password. To demonstrate how this can take place, run the following shell script fragment:
$ while true; do ps ax | grep psftp | grep -v grep >> log; done
In another shell, run this fragment:
$ while true; do echo quit | ./psftp -pw "$Pass" $USER@localhost; done
Eventually, the password will appear in the log:
19681 pts/0 R+ 0:00 ./psftp -pw foobar4.foobar cfisher@localhost 19681 pts/0 R+ 0:00 ./psftp -pw X cfisher@localhost
For a brief time at program startup, the password is present on the command line before it is overwritten, and can be captured. A simple shell script fragment was able to record it with enough attempts, engaging in a race condition.
A carefully-coded C application that leveraged inotify might be able to dramatically increase the success rate. Applications that require the use of command line passwords cannot completely escape vulnerability by manipulating argv
. Still, this patch provides the imperfect obfuscation that the PuTTY authors intended.
Secure Backport with .netrc
Since manipulation of argv
cannot ensure complete security for sensitive credentials, Simon Tathum has added a new feature, -pwfile
, which will remove passwords from the command line by reading them from a named file.
This feature is currently available in the snapshot release on the PuTTY website, and the Windows psftp
binary has been successfully tested with this new functionality.
For UNIX users, the new feature can be applied as a patch to the stable 0.76 release. Place Simon's code in the following file in your home directory:
$ cat ~/simon.patch --- cmdline.c.orig 2021-10-01 09:33:17.000000000 -0500 +++ cmdline.c 2021-10-01 09:33:38.000000000 -0500 @@ -584,6 +584,32 @@ } } + if (!strcmp(p, "-pwfile")) { + RETURN(2); + UNAVAILABLE_IN(TOOLTYPE_NONNETWORK); + SAVEABLE(1); + /* We delay evaluating this until after the protocol is decided, + * so that we can warn if it's of no use with the selected protocol */ + if (conf_get_int(conf, CONF_protocol) != PROT_SSH) + cmdline_error("the -pwfile option can only be used with the " + "SSH protocol"); + else { + Filename *fn = filename_from_str(value); + FILE *fp = f_open(fn, "r", false); + if (!fp) { + cmdline_error("unable to open password file '%s'", value); + } else { + cmdline_password = chomp(fgetline(fp)); + if (!cmdline_password) { + cmdline_error("unable to read a password from file '%s'", + value); + } + fclose(fp); + } + filename_free(fn); + } + } + if (!strcmp(p, "-agent") || !strcmp(p, "-pagent") || !strcmp(p, "-pageant")) { RETURN(1);
To apply this patch, unpack a clean copy of the 0.76 PuTTY source code, without any of the patches in the previous section (the previous patches can be applied with Simon's patch, but are not completely effective due to the demonstrated race condition; for this reason, users requiring assured security should use only Simon's patch, as a wipe of argv
cannot be completely effective, and is not helpful on Windows). With pristine source and the patch in place, configure and execute your build:
$ cd putty-0.76/ $ patch -p0 < ~/simon.patch patching file cmdline.c $ CFLAGS='-O2 -D_FORTIFY_SOURCE=2 -fstack-protector-strong -fpic -pie' \ LDFLAGS='-Wl,-z,relro,-z,now -Wl,-z,now' ./configure $ make
When the configuration and build have completed, the psftp
, pscp
, and plink
executables now implement -pwfile
. Rename the new SFTP client, to distinguish it from any version installed in an OS package:
$ mv psftp pwpsftp
In testing this functionality, I will use a POSIX shell script as a wrapper to implement .netrc
capability for PuTTY psftp
.
The .netrc
format has long been used by classic FTP clients, and allows credential storage for many accounts in a secured text file, usually one's home directory. In this example, I will place the following example for testing:
$ echo 'machine 10.58.7.27 login fishecj password foobar4.foobar default login foo password bar' > ~/.netrc $ chmod 600 ~/.netrc
A competitor to psftp
, known as Curl or cURL, has implemented a --netrc
option for many years which can be used with SFTP transfers. PuTTY is a superior product to curl for many uses, as it implements a command interface that is similar to the original FTP which eases retooling of scripts, and PuTTY's cryptography is currently superior in all respects.
PuTTY's cryptography is specifically better than the version 7.79.1 curl-amd64
binary found on the download site. The curl version tested does not implement any AEAD ciphers (as are required for TLS 1.3 and are best practices for SSH). Also, of the supported MACs, none are of the “Encrypt-then-MAC” (ETM) variety. In detail, the version 7.79.1 curl-amd64
implements the following ciphers: aes256-ctr, aes192-ctr, aes128-ctr, aes256-cbc, aes192-cbc, aes128-cbc, rijndael-cbc@lysator.liu.se, arcfour, cast128-cbc, 3des-cbc, blowfish-cbc, and arcfour128. In addition, the referenced curl version implements the following MACs: hmac-sha2-512, hmac-sha2-256, hmac-ripemd160, hmac-ripemd160@openssh.com, hmac-sha1, hmac-sha1-96, hmac-md5, and hmac-md5-96.
Place the following script, and the previous pwpsftp
executable, in your PATH for testing, and mark the script with 755 permissions on UNIX:
$ cat pwpsftp #!/bin/sh # .netrc wrapper for PuTTY psftp -pwfile H= L= P= D= DL= DP= t="$1" unset IFS # Host Login Password Default/L/P target set -eu; shift # http://redsymbol.net/articles/unofficial-bash-strict-mode/ sftp () { PWF="$(mktemp)"; trap "rm -fv \"$PWF\"" EXIT; printf %s "$P" > "$PWF" [ -t 9 ] && exec 9<&-; pwpsftp -pwfile "$PWF" "${L}@${t}" "$@"; exit $?; } newL () { [ "$t" = "$H" -a "$L" -a "$P" ] && sftp "$@" [ "$D" ] && DL="$L" DP="$P"; true; } while read line <&9 # This feeds into psftp's stdin without alternate #< fd do for thisword in $line do case "$thisword" in machine) newL "$@"; D= L= P= H=_ continue ;; default) newL "$@"; H= L= P= D=_ continue ;; login) L=_ continue ;; password) P=_ continue ;; esac [ "X$H" = X_ ] && H="$thisword" [ "X$L" = X_ ] && L="$thisword" [ "X$P" = X_ ] && P="$thisword" done done 9< ~/.netrc # This feeds into psftp's stdin without alternate #< fd newL "$@"; [ "$DL" -a "$DP" ] && { L="$DL" P="$DP"; sftp "$@"; } # Check default
The script will extract a specific password from the .netrc
and store it in a file created by the mktemp
utility, which attempts to ensure that the temporary file “is only readable and writable by its owner.” The temporary file will be in place for the duration of the SFTP session, and it's deletion will be reported at the close. A test use is below:
$ psftprc 10.58.7.27 Using username "fishecj". Remote working directory is /home/fishecj psftp> !sh sh-4.2$ ps ax | grep psftp 9765 pts/1 S 0:00 /bin/dash /home/fishecj/putty-0.76/psftprc 10.58.7.27 9767 pts/1 S 0:00 pwpsftp -pwfile /tmp/tmp.VqMIsHfkCQ fishecj@10.58.7.27 sh-4.2$ ls -l /tmp/tmp.VqMIsHfkCQ -rw-------. 1 fishecj itg 14 Oct 1 13:13 /tmp/tmp.VqMIsHfkCQ sh-4.2$ cat /tmp/tmp.VqMIsHfkCQ; echo foobar4.foobar $ cat /proc/9767/cmdline; echo pwpsftp-pwfile/tmp/tmp.VqMIsHfkCQfishecj@10.58.7.27 sh-4.2$ exit psftp> exit removed ‘/tmp/tmp.VqMIsHfkCQ’
Notice above the use of dash
, which closely adheres to POSIX shell syntax and tolerates (very nearly) no BASHisms, indicating that this script should run in most UNIX shells. Also observe the verbose removal of the temporary file at the end of the SFTP session, triggered by the EXIT trap. A subshell launched as a background process that sleeps for a few seconds, then deletes the temporary file would increase security, especially for SFTP sessions of long duration.
The .netrc
format may also specify a default login, which will be used on all connections that are not otherwise explicitly defined in the file:
$ psftprc 10.58.23.22 Using username "foo". Remote working directory is /tmp psftp> quit removed ‘/tmp/tmp.0jQCj1xjMr’
The psftprc script also returns the exit code of the psftp
binary, allowing session failures to be detected and retried. A simple test is to shut down the master sshd
on the target, then configure and launch a batch transfer:
$ echo 'cd tinyssh dir' > stuff $ until psftprc 10.58.23.22 -b stuff; do sleep 5; done FATAL ERROR: Connection refused removed ‘/tmp/tmp.8eQUXu3ri1’ FATAL ERROR: Connection refused removed ‘/tmp/tmp.7lzVesQsDk’ ..
When the remote sshd
is restarted, the loop terminates. This technique is extremely helpful with unreliable connections, or transfers that must be retried on any and all failures.
FATAL ERROR: Connection refused removed ‘/tmp/tmp.gkcHN5O0yC’ Using username "foo". Remote working directory is /tmp Remote directory is now /tmp/tinyssh Listing directory /tmp/tinyssh drwxr-xr-x 4 fishecj itg 102 Jun 25 2020 . drwx------ 27 fishecj itg 4096 Oct 1 14:34 .. -rw-r--r-- 1 fishecj itg 233155 Jun 24 2020 20190101.tar.gz drwxr-xr-x 10 fishecj itg 4096 Jun 24 2020 tinyssh-20190101 drwxr-xr-x 2 fishecj itg 4096 Jun 25 2020 tinyssh-convert -rwxr-xr-x 1 fishecj itg 1861 Jun 25 2020 tinyssh-keyconvert removed ‘/tmp/tmp.ugODaSfWPm’
This script is also functional with the Windows Busybox port:
C:\Users\fishecj>busybox64 sh psftprc 10.58.7.27 Using username "oracle". Remote working directory is /home/oracle psftp> pwd Remote directory is /home/oracle psftp> quit removed 'C:/Users/FISH~1/AppData/Local/Temp/tmp.a18944' C:\Users\fishecj>copy con stuff cd Ora19/OPatch dir quit ^Z 1 file(s) copied. C:\Users\fishecj>busybox64 sh psftprc 10.58.7.27 -b stuff Using username "oracle". Remote working directory is /home/oracle Remote directory is now /home/oracle/Ora19/OPatch Listing directory /home/oracle/Ora19/OPatch drwxr-x--- 14 oracle dba 4096 Apr 21 2020 . drwxr-xr-x 71 oracle dba 4096 May 12 2020 .. -rw-r----- 1 oracle dba 2980 Apr 12 2019 README.txt drwxr-x--- 6 oracle dba 64 Apr 12 2019 auto drwxr-x--- 2 oracle dba 30 Apr 12 2019 config -rwxr-x--- 1 oracle dba 589 Apr 12 2019 datapatch drwxr-x--- 2 oracle dba 86 Apr 12 2019 docs -rwxr-x--- 1 oracle dba 23550 Apr 12 2019 emdpatch.pl drwxr-x--- 2 oracle dba 4096 Apr 21 2020 jlib drwxr-x--- 5 oracle dba 4096 Aug 16 2018 jre drwxr-x--- 9 oracle dba 4096 Apr 12 2019 modules drwxr-x--- 5 oracle dba 54 Apr 12 2019 ocm -rwxr-x--- 1 oracle dba 48493 Apr 12 2019 opatch -rwxr-x--- 1 oracle dba 2551 Apr 12 2019 opatch.pl -rwxr-x--- 1 oracle dba 4290 Apr 12 2019 opatch_env.sh -rwxr-x--- 1 oracle dba 1442 Apr 12 2019 opatchauto -rwxr-x--- 1 oracle dba 393 Apr 12 2019 opatchauto.cmd drwxr-x--- 4 oracle dba 59 Apr 12 2019 opatchprereqs -rwxr-x--- 1 oracle dba 3159 Apr 12 2019 operr -rw-r----- 1 oracle dba 3177 Apr 12 2019 operr_readme.txt drwxr-x--- 2 oracle dba 18 Apr 12 2019 oplan drwxr-x--- 3 oracle dba 20 Apr 12 2019 oracle_common drwxr-x--- 3 oracle dba 23 Apr 12 2019 plugins drwxr-x--- 2 oracle dba 4096 Apr 21 2020 scripts -rw-r----- 1 oracle dba 27 Apr 12 2019 version.txt removed 'C:/Users/FISH~1/AppData/Local/Temp/tmp.a25248'
On Windows, it might be helpful to carefully examine the permissions on the .netrc
file, as ACL access is much more complex than simple POSIX filesystem permissions. The following ACL adjustment might be prudent.
C:\Users\fishecj>cacls .netrc /e /r Administrators processed file: C:\Users\fishecj\.netrc
Also, on Windows, Simon's patch has proven effective on Cygwin:
$ uname CYGWIN_NT-10.0 $ echo foobar4.foobar > secret $ ./psftp -pwfile secret cfisher@myhost Using username "cfisher". Remote working directory is /home/cfisher psftp> quit
Closing all PuTTY psftp
security concerns will require the application of the methods presented in this section, and I would like to thank Simon Tatham for his contribution in allowing it to be written.
Conclusion
The consistent advice from the OpenSSH documentation is the avoidance of passwords when they cannot be entered interactively:
$ man sftp | sed -n '/automated/,/keygen/p' The final usage format allows for automated sessions using the -b option. In such cases, it is necessary to configure non-interactive authentica‐ tion to obviate the need to enter a password at connection time (see sshd(8) and ssh-keygen(1) for details).
Many psftp
users and administrators do not heed this advice, and disallow keys. This comes at a cost.
While PuTTY's stable release may be particularly difficult in the question of secure, scripted password handling (as the developers have repeatedly warned, and without aforementioned patches), we see here that common command line defenses can be circumvented for a much larger set of programs expressing sensitive content. A partial solution is to record credentials in a file, but so many incompatible formats have been adopted for this that choosing is not trivial (FTP/curl .netrc
, OpenSSL's -passin
formats, a smbclient
credential file, the particularly problematic web.config
, etc.). Perhaps a credential cache provided by the OS, designed for security and ease of use and allowing either a standardized file format or sqlite, should be considered for the replacement of these patchwork solutions that are prone to mismanagement and abuse. It would also be helpful if the kernel offered administrators a method to securely hide argv
.
The whole question of the standardization on SFTP for file transfer ignores so many problems: multiple failed protocol revisions in the SSH 1.X series, noted performance problems with the SFTP protocol, now-deprecated SCP, rsync advocacy despite license incompatibility, and the redesigns on the way. SSH offers file transfer as a sideline, not as a core component. Due to this turmoil, the choice of SSH as a file transfer standard seems premature; perhaps the approach that Wireguard took to VPN would be useful in designing a superior file transfer protocol and tool.
In any case, we should not advertise our passwords. Avoid doing so with PuTTY.
Once again, Simon Tathum deserves our thanks for his concise solution to this question of password exposure.