I was recently investigating Java file transfer when I stumbled across an interesting set of issues. The discovery of these issues was sparked by a post with some example code. This post described a system for executing SCP commands with Java using popular library JSch. As I was reading through it, I quickly noticed something concerning about their approach:
// exec 'scp -f rfile' remotely String command = "scp -f " + from; Channel channel = session.openChannel("exec"); ((ChannelExec) channel).setCommand(command); // get I/O streams for remote scp OutputStream out = channel.getOutputStream(); InputStream in = channel.getInputStream(); channel.connect();
If you’ve ever looked at how to safely pass arguments to a command line, this is not how it’s done. Line two simply appends an arbitrary string to the end of the command without any form of sanitization. This is a textbook scenarion leading to command injection.
Quick Background Info
It’s worth noting a few important pieces of background information before going further down the rabbit hole:
- This command runs server-side. Part of how the SCP client operates is by SSHing into the remote server (the execution channel in the code above) and running the SCP command server-side.
- Due to the fact this runs server-side, there’s not really any good libraries in Java I was able to find that allow escaping commands. Most information I found recommend using Java’s
Runtimeclasses, however these only work for actually running a process locally, not preparing it to run on a server.
In the example above, the command is expected to look like this:
scp -f /some/user/provided/path
However, given that the path is not properly escaped at any point, it could just as easily look like this:
# path = '/; touch /tmp/foo' scp -f /; touch /tmp/foo
Which will run
scp -f /, then execute
touch /tmp/foo. After investigating further, I realized this example code was pretty much a direct copy+paste from the JSch source code, meaning their example code was vulnerable to the same attack. I reported this to the maintainers of this project, and figured the problem would either be resolved, or they’d simply ignore me. This was not the case.
The day after I reported the bug in JSch, I received a response. While investigating my initial report, the maintainers of the project noticed that both OpenSSH’s SCP and Rsync had the same general issue. For instance, running this command:
scp /tmp/foo [email protected]:/tmp/bar\;touch\ /tmp/foo
touch /tmp/foo on the remote server, even though arguments are correctly escaped client-side. This is because escaping this at an application level does not cause it to be escaped when run remotely. The server at
remotehost will run
scp -f /tmp/bar; touch /tmp/foo for the example above.
Rsync has a similar issue, although after reporting this to them, they introduced me to a flag (
-s/--protect-args) that will prevent it from being exploitable.
I had a very interesting back/forth with the OpenSSH maintainers around this. A few key points they made:
- This issue is well known, but not well alerted/documented
- SCP as a protocol is “completely busted”
- SCP2, based on SFTP as a replacement never took off
- They are unable to change this issue without breaking compatibility
- They do not feel that escaping shell arguments is impossible in this scenario due to the wide variety of shells supported
As a developer who has used SCP extensively, it was very surprising to me to learn that SCP is considered broken beyond repair. I feel that it would be worthwhile for them to provide proper documentation around this issue, and point users at better alternatives(such as SFTP or RSync), given the number of libraries and snippets I’ve seen that don’t take this into consideration.
Moral of the Story
For personal/manual usage, tools like Rsync and SCP are fine, for anything that uses untrusted or user provided paths/filenames, you should use SFTP or use
rsync -s. Anything that uses SCP with untrusted user input should not be trusted unless paths are sufficiently escaped before they are escaped again as command line arguments.
- Initial report to JSch
- JSch response (points for quick response!)
- Report to OpenSSH
- OpenSSH response
- Report to Rsync
- Response from Rsync (already handled!)
- JSch fixed issue, present in next release
- JSch v1.5.5 released, fixing the sample code
Thanks to the folks over at JSch for the quick response and resolution as well as the further discovery of potential SCP/Rsync issues