From b84f83f8d1bfe56569497ff46f9a78ade518e42d Mon Sep 17 00:00:00 2001 From: Andrew Morris Date: Mon, 29 Dec 2014 15:00:12 -0500 Subject: [PATCH 1/9] added some new default creds, updated permissions on stop.sh --- data/userdb.txt | 10 +++++++++- stop.sh | 0 2 files changed, 9 insertions(+), 1 deletion(-) mode change 100644 => 100755 stop.sh diff --git a/data/userdb.txt b/data/userdb.txt index decb76c..b0b8501 100644 --- a/data/userdb.txt +++ b/data/userdb.txt @@ -1 +1,9 @@ -root:0:123456 +root:0:password1 +root:0:qwerty +root:0:p4ssw0rd +root:0:administrator +root:0:america +root:0:changeme +www:501:www +www-data:501:www-data + diff --git a/stop.sh b/stop.sh old mode 100644 new mode 100755 From 510720dcaf9e727cc970e9b0445c4e3b31803d98 Mon Sep 17 00:00:00 2001 From: Andrew Morris Date: Mon, 29 Dec 2014 15:08:02 -0500 Subject: [PATCH 2/9] updated wget to remove port 80 limitation --- kippo/commands/wget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kippo/commands/wget.py b/kippo/commands/wget.py index ce18937..73eb899 100644 --- a/kippo/commands/wget.py +++ b/kippo/commands/wget.py @@ -84,7 +84,7 @@ def download(self, url, fakeoutfile, outputfile, *args, **kwargs): host = parsed.hostname port = parsed.port or (443 if scheme == 'https' else 80) path = parsed.path or '/' - if scheme == 'https' or port != 80: + if scheme == 'https': self.writeln('Sorry, SSL not supported in this release') self.exit() return None From 488e231a7b5c406d40287025f88506ba5e2da97f Mon Sep 17 00:00:00 2001 From: Andrew Morris Date: Mon, 29 Dec 2014 15:14:16 -0500 Subject: [PATCH 3/9] added option to remove fake jail --- kippo/commands/base.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/kippo/commands/base.py b/kippo/commands/base.py index 721e65d..c0fe259 100644 --- a/kippo/commands/base.py +++ b/kippo/commands/base.py @@ -52,9 +52,15 @@ def call(self): class command_exit(HoneyPotCommand): def call(self): + cfg = config() + self.exit_jail = True + if cfg.has_option('honeypot', 'exit_jail'): + if (cfg.get('honeypot', 'exit_jail') == "false"): + self.exit_jail = False if 'PuTTY' in self.honeypot.clientVersion or \ 'libssh' in self.honeypot.clientVersion or \ - 'sshlib' in self.honeypot.clientVersion: + 'sshlib' in self.honeypot.clientVersion or \ + self.exit_jail is False: self.honeypot.terminal.loseConnection() return self.honeypot.terminal.reset() From 1475236e7e19945e9fa5b7b51c83b588ed52b50c Mon Sep 17 00:00:00 2001 From: Andrew Morris Date: Mon, 29 Dec 2014 15:14:32 -0500 Subject: [PATCH 4/9] updated config file --- kippo.cfg.dist | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/kippo.cfg.dist b/kippo.cfg.dist index 138a37e..30b67ad 100644 --- a/kippo.cfg.dist +++ b/kippo.cfg.dist @@ -37,6 +37,12 @@ download_path = dl # (default: 0) #download_limit_size = 10485760 +# Allow the attacker to exit the honeypot on request or try to 'trick' the attacker with another shell. +# note: depending on the attackers client (e.g. putty), will just quit regardless. +# +# (default: false) +exit_jail = false + # Directory where virtual file contents are kept in. # # This is only used by commands like 'cat' to display the contents of files. From a064b80327012e5282932997ae3031d803ee85e2 Mon Sep 17 00:00:00 2001 From: Andrew Morris Date: Mon, 29 Dec 2014 15:16:26 -0500 Subject: [PATCH 5/9] updated ssh version --- kippo.cfg.dist | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kippo.cfg.dist b/kippo.cfg.dist index 30b67ad..54bc7b6 100644 --- a/kippo.cfg.dist +++ b/kippo.cfg.dist @@ -135,8 +135,8 @@ private_key = private.key # SSH-2.0-OpenSSH_5.9p1 Debian-5ubuntu1 # SSH-2.0-OpenSSH_5.9 # -# (default: "SSH-2.0-OpenSSH_5.1p1 Debian-5") -ssh_version_string = SSH-2.0-OpenSSH_5.1p1 Debian-5 +# (default: "SSH-2.0-OpenSSH_5.1 Debian-5") +ssh_version_string = SSH-2.0-OpenSSH_5.1 Debian-5 # Banner file to be displayed before the first login attempt. # From f74e1870e590b20c3671cba25561587322673eaa Mon Sep 17 00:00:00 2001 From: Andrew Morris Date: Tue, 30 Dec 2014 17:09:36 -0500 Subject: [PATCH 6/9] updated with SFTP support --- kippo.cfg.dist | 8 +- kippo/core/fs.py | 192 ++++++++++++++++++++++++++++++++++++++++- kippo/core/honeypot.py | 187 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 382 insertions(+), 5 deletions(-) diff --git a/kippo.cfg.dist b/kippo.cfg.dist index 54bc7b6..bc9b2a0 100644 --- a/kippo.cfg.dist +++ b/kippo.cfg.dist @@ -12,13 +12,13 @@ # Port to listen for incoming SSH connections. # # (default: 2222) -ssh_port = 2222 +ssh_port = 22 # Hostname for the honeypot. Displayed by the shell prompt of the virtual # environment. # # (default: svr03) -hostname = svr03 +hostname = devops05 # Directory where to save log files in. # @@ -43,6 +43,9 @@ download_path = dl # (default: false) exit_jail = false +# sftp_enabled enables the sftp subsystem +sftp_enabled = true + # Directory where virtual file contents are kept in. # # This is only used by commands like 'cat' to display the contents of files. @@ -206,3 +209,4 @@ interact_port = 5123 #identifier = abc123 #secret = secret #debug=false + diff --git a/kippo/core/fs.py b/kippo/core/fs.py index 5001841..1c30fb1 100644 --- a/kippo/core/fs.py +++ b/kippo/core/fs.py @@ -1,7 +1,7 @@ # Copyright (c) 2009 Upi Tamminen # See the COPYRIGHT file for more information -import os, time, fnmatch +import os, time, fnmatch, re, stat, errno from kippo.core.config import config A_NAME, \ @@ -176,5 +176,195 @@ def is_dir(self, path): if l: return True return False + # additions for SFTP support, try to keep functions here similar to os.* + + def open(self, filename, openFlags, mode): + #print "fs.open %s" % filename + + #if (openFlags & os.O_APPEND == os.O_APPEND): + # print "fs.open append" + + #if (openFlags & os.O_CREAT == os.O_CREAT): + # print "fs.open creat" + + #if (openFlags & os.O_TRUNC == os.O_TRUNC): + # print "fs.open trunc" + + #if (openFlags & os.O_EXCL == os.O_EXCL): + # print "fs.open excl" + + if openFlags & os.O_RDWR == os.O_RDWR: + raise notImplementedError + + elif openFlags & os.O_WRONLY == os.O_WRONLY: + # ensure we do not save with executable bit set + realmode = mode & ~(stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH) + + #print "fs.open wronly" + # TODO: safeoutfile could contains source IP address + safeoutfile = '%s/%s_%s' % \ + (config().get('honeypot', 'download_path'), + time.strftime('%Y%m%d%H%M%S'), + re.sub('[^A-Za-z0-9]', '_', filename)) + #print "fs.open file for writing, saving to %s" % safeoutfile + + self.mkfile(filename, 0, 0, 0, stat.S_IFREG | mode) + fd = os.open(safeoutfile, openFlags, realmode) + self.update_realfile(self.getfile(filename), safeoutfile) + + return fd + + elif openFlags & os.O_RDONLY == os.O_RDONLY: + return None + + return None + + # FIXME mkdir() name conflicts with existing mkdir + def mkdir2(self, path): + dir = self.getfile(path) + if dir != False: + raise OSError(errno.EEXIST, os.strerror(errno.EEXIST), path) + return self.mkdir(path, 0, 0, 4096, 16877) + + def rmdir(self, path): + raise notImplementedError + + def utime(self, path, atime, mtime): + p = self.getfile(path) + if p == False: + raise OSError(errno.ENOENT, os.strerror(errno.ENOENT)) + p[A_CTIME] = mtime + + def chmod(self, path, perm): + p = self.getfile(path) + if p == False: + raise OSError(errno.ENOENT, os.strerror(errno.ENOENT)) + p[A_MODE] = stat.S_IFMT(p[A_MODE]) | perm + + def chown(self, path, uid, gid): + p = self.getfile(path) + if p == False: + raise OSError(errno.ENOENT, os.strerror(errno.ENOENT)) + if (uid != -1): + p[A_UID] = uid + if (gid != -1): + p[A_GID] = gid + + def remove(self, path): + p = self.getfile(path) + if p == False: + raise OSError(errno.ENOENT, os.strerror(errno.ENOENT)) + self.get_path(os.path.dirname(path)).remove(p) + return + + def readlink(self, path): + p = self.getfile(path) + if p == False: + raise OSError(errno.ENOENT, os.strerror(errno.ENOENT)) + if not (p[A_MODE] & stat.S_IFLNK): + raise OSError + return p[A_TARGET] + + def symlink(self, targetPath, linkPath): + raise notImplementedError + + def rename(self, oldpath, newpath): + #print "rename %s to %s" % (oldpath, newpath) + old = self.getfile(oldpath) + if old == False: + raise OSError(errno.ENOENT, os.strerror(errno.ENOENT)) + new = self.getfile(newpath) + if new != False: + raise OSError(errno.EEXIST, os.strerror(errno.EEXIST)) + + self.get_path(os.path.dirname(oldpath)).remove(old) + old[A_NAME] = os.path.basename(newpath) + self.get_path(os.path.dirname(newpath)).append(old) + return + + def read(self, fd, size): + # this should not be called, we intercept at readChunk + raise notImplementedError + + def write(self, fd, string): + return os.write(fd, string) + + def close(self, fd): + if (fd == None): + return True + return os.close(fd) + + def lseek(self, fd, offset, whence): + if (fd == None): + return True + return os.lseek(fd, offset, whence) + + def listdir(self, path): + names = [x[A_NAME] for x in self.get_path(path)] + return names + + def lstat(self, path): + + # need to treat / as exception + if (path == "/"): + p = { A_TYPE:T_DIR, A_UID:0, A_GID:0, A_SIZE:4096, A_MODE:16877, A_CTIME:time.time() } + else: + p = self.getfile(path) + + if p == False: + raise OSError(errno.ENOENT, os.strerror(errno.ENOENT)) + + return _statobj( + p[A_MODE], + 0, + 0, + 1, + p[A_UID], + p[A_GID], + p[A_SIZE], + p[A_CTIME], + p[A_CTIME], + p[A_CTIME]) + + def stat(self, path): + if (path == "/"): + p = { A_TYPE:T_DIR, A_UID:0, A_GID:0, A_SIZE:4096, A_MODE:16877, A_CTIME:time.time() } + else: + p = self.getfile(path) + + if (p == False): + raise OSError(errno.ENOENT, os.strerror(errno.ENOENT)) + + #if p[A_MODE] & stat.S_IFLNK == stat.S_IFLNK: + if p[A_TYPE] == T_LINK: + return self.stat(p[A_TARGET]) + + return self.lstat(path) + + def realpath(self, path): + return path + + def update_size(self, filename, size): + f = self.getfile(filename) + if (f == False): + return + if (f[A_TYPE] != T_FILE): + return + f[A_SIZE] = size + + +# transform a tuple into a stat object +class _statobj: + def __init__(self, st_mode, st_ino, st_dev, st_nlink, st_uid, st_gid, st_size, st_atime, st_mtime, st_ctime): + self.st_mode = st_mode + self.st_ino = st_ino + self.st_dev = st_dev + self.st_nlink = st_nlink + self.st_uid = st_uid + self.st_gid = st_gid + self.st_size = st_size + self.st_atime = st_atime + self.st_mtime = st_mtime + self.st_ctime = st_ctime # vim: set sw=4 et: diff --git a/kippo/core/honeypot.py b/kippo/core/honeypot.py index 6cd07ad..33788d3 100644 --- a/kippo/core/honeypot.py +++ b/kippo/core/honeypot.py @@ -4,11 +4,13 @@ import twisted from twisted.cred import portal, checkers, credentials, error from twisted.conch import avatar, recvline, interfaces as conchinterfaces -from twisted.conch.ssh import factory, userauth, connection, keys, session, common, transport +from twisted.conch.ssh import factory, userauth, connection, keys, session, common, transport, filetransfer +from twisted.conch.ssh.filetransfer import FXF_READ, FXF_WRITE, FXF_APPEND, FXF_CREAT, FXF_TRUNC, FXF_EXCL +import twisted.conch.ls from twisted.conch.insults import insults from twisted.application import service, internet from twisted.internet import reactor, protocol, defer -from twisted.python import failure, log +from twisted.python import failure, log, components from zope.interface import implements from copy import deepcopy, copy import sys, os, random, pickle, time, stat, shlex, anydbm @@ -457,6 +459,12 @@ def __init__(self, username, env): userdb = UserDB() self.uid = self.gid = userdb.getUID(self.username) + # sftp support enabled only when option is explicitly set + if self.env.cfg.has_option('honeypot', 'sftp_enabled'): + if ( self.env.cfg.get('honeypot', 'sftp_enabled') == "true" ): + self.subsystemLookup['sftp'] = filetransfer.FileTransferServer + + if not self.uid: self.home = '/root' else: @@ -729,4 +737,179 @@ def getRSAKeys(): privateKeyString = file(private_key).read() return publicKeyString, privateKeyString +class KippoSFTPFile: + implements(conchinterfaces.ISFTPFile) + + def __init__(self, server, filename, flags, attrs): + self.server = server + self.filename = filename + self.transfer_completed = 0 + self.bytes_written = 0 + openFlags = 0 + if flags & FXF_READ == FXF_READ and flags & FXF_WRITE == 0: + openFlags = os.O_RDONLY + if flags & FXF_WRITE == FXF_WRITE and flags & FXF_READ == 0: + openFlags = os.O_WRONLY + if flags & FXF_WRITE == FXF_WRITE and flags & FXF_READ == FXF_READ: + openFlags = os.O_RDWR + if flags & FXF_APPEND == FXF_APPEND: + openFlags |= os.O_APPEND + if flags & FXF_CREAT == FXF_CREAT: + openFlags |= os.O_CREAT + if flags & FXF_TRUNC == FXF_TRUNC: + openFlags |= os.O_TRUNC + if flags & FXF_EXCL == FXF_EXCL: + openFlags |= os.O_EXCL + if attrs.has_key("permissions"): + mode = attrs["permissions"] + del attrs["permissions"] + else: + mode = 0777 + fd = server.fs.open(filename, openFlags, mode) + if attrs: + self.server.setAttrs(filename, attrs) + self.fd = fd + + # cache a copy of file in memory to read from in readChunk + if flags & FXF_READ == FXF_READ: + self.contents = self.server.fs.file_contents(self.filename) + + def close(self): + if ( self.bytes_written > 0 ): + self.server.fs.update_size(self.filename, self.bytes_written) + return self.server.fs.close(self.fd) + + def readChunk(self, offset, length): + return self.contents[offset:offslength] + + def writeChunk(self, offset, data): + self.server.fs.lseek(self.fd, offset, os.SEEK_SET) + self.server.fs.write(self.fd, data) + self.bytes_writte= len(data) + + def getAttrs(self): + s = self.server.fs.fstat(self.fd) + return self.server._getAttrs(s) + + def setAttrs(self, attrs): + raise NotImplementedError + +class KippoSFTPDirectory: + + def __init__(self, server, directory): + self.server = server + self.files = server.fs.listdir(directory) + self.dir = directory + + def __iter__(self): + return self + + def next(self): + try: + f = self.files.pop(0) + except IndexError: + raise StopIteration + else: + s = self.server.fs.lstat(os.path.join(self.dir, f)) + longname = twisted.conch.ls.lsLine(f, s) + attrs = self.server._getAttrs(s) + return (f, longname, attrs) + + def close(self): + self.files = [] + +class KippoSFTPServer: + implements(conchinterfaces.ISFTPServer) + + def __init__(self, avatar): + self.avatar = avatar + # FIXME we should not copy fs here, but do this at avatar instantiation + self.fs = fs.HoneyPotFilesystem(deepcopy(self.avatar.env.fs)) + + def _absPath(self, path): + home = self.avatar.home + return os.path.abspath(os.path.join(home, path)) + + def _setAttrs(self, path, attrs): + if attrs.has_key("uid") and attrs.has_key("gid"): + self.fs.chown(path, attrs["uid"], attrs["gid"]) + if attrs.has_key("permissions"): + self.fs.chmod(path, attrs["permissions"]) + if attrs.has_key("atime") and attrs.has_key("mtime"): + self.fs.utime(path, attrs["atime"], attrs["mtime"]) + + def _getAttrs(self, s): + return { + "size" : s.st_size, + "uid" : s.st_uid, + "gid" : s.st_gid, + "permissions" : s.st_mode, + "atime" : int(s.st_atime), + "mtime" : int(s.st_mtime) + } + + def gotVersion(self, otherVersion, extData): + return {} + + def openFile(self, filename, flags, attrs): + print "SFTP openFile: %s" % filename + return KippoSFTPFile(self, self._absPath(filename), flags, attrs) + + def removeFile(self, filename): + print "SFTP removeFile: %s" % filename + return self.fs.remove(self._absPath(filename)) + + def renameFile(self, oldpath, newpath): + print "SFTP renameFile: %s %s" % (oldpath, newpath) + return self.fs.rename(self._absPath(oldpath), self._absPath(newpath)) + + def makeDirectory(self, path, attrs): + print "SFTP makeDirectory: %s" % path + path = self._absPath(path) + self.fs.mkdir2(path) + self._setAttrs(path, attrs) + return + + def removeDirectory(self, path): + print "SFTP removeDirectory: %s" % path + return self.fs.rmdir(self._absPath(path)) + + def openDirectory(self, path): + print "SFTP OpenDirectory: %s" % path + return KippoSFTPDirectory(self, self._absPath(path)) + + def getAttrs(self, path, followLinks): + print "SFTP getAttrs: %s" % path + path = self._absPath(path) + if followLinks: + s = self.fs.stat(path) + else: + s = self.fs.lstat(path) + return self._getAttrs(s) + + def setAttrs(self, path, attrs): + print "SFTP setAttrs: %s" % path + path = self._absPath(path) + return self._setAttrs(path, attrs) + + def readLink(self, path): + print "SFTP readLink: %s" % path + path = self._absPath(path) + return self.fs.readlink(path) + + def makeLink(self, linkPath, targetPath): + print "SFTP makeLink: %s" % path + linkPath = self._absPath(linkPath) + targetPath = self._absPath(targetPath) + return self.fs.symlink(targetPath, linkPath) + + def realPath(self, path): + print "SFTP realPath: %s" % path + return self.fs.realpath(self._absPath(path)) + + def extendedRequest(self, extName, extData): + raise NotImplementedError + +components.registerAdapter( KippoSFTPServer, HoneyPotAvatar, conchinterfaces.ISFTPServer) + # vim: set sw=4 et: From 03eb33f33358357f0f1dbcb5af42892d1528bf84 Mon Sep 17 00:00:00 2001 From: Jason Trost Date: Thu, 5 Feb 2015 12:31:29 -0500 Subject: [PATCH 7/9] restored kippo.cfg.dist --- kippo.cfg.dist | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/kippo.cfg.dist b/kippo.cfg.dist index bc9b2a0..138a37e 100644 --- a/kippo.cfg.dist +++ b/kippo.cfg.dist @@ -12,13 +12,13 @@ # Port to listen for incoming SSH connections. # # (default: 2222) -ssh_port = 22 +ssh_port = 2222 # Hostname for the honeypot. Displayed by the shell prompt of the virtual # environment. # # (default: svr03) -hostname = devops05 +hostname = svr03 # Directory where to save log files in. # @@ -37,15 +37,6 @@ download_path = dl # (default: 0) #download_limit_size = 10485760 -# Allow the attacker to exit the honeypot on request or try to 'trick' the attacker with another shell. -# note: depending on the attackers client (e.g. putty), will just quit regardless. -# -# (default: false) -exit_jail = false - -# sftp_enabled enables the sftp subsystem -sftp_enabled = true - # Directory where virtual file contents are kept in. # # This is only used by commands like 'cat' to display the contents of files. @@ -138,8 +129,8 @@ private_key = private.key # SSH-2.0-OpenSSH_5.9p1 Debian-5ubuntu1 # SSH-2.0-OpenSSH_5.9 # -# (default: "SSH-2.0-OpenSSH_5.1 Debian-5") -ssh_version_string = SSH-2.0-OpenSSH_5.1 Debian-5 +# (default: "SSH-2.0-OpenSSH_5.1p1 Debian-5") +ssh_version_string = SSH-2.0-OpenSSH_5.1p1 Debian-5 # Banner file to be displayed before the first login attempt. # @@ -209,4 +200,3 @@ interact_port = 5123 #identifier = abc123 #secret = secret #debug=false - From 7a12e1a784aece2d095eef35fa89ce5a54d24735 Mon Sep 17 00:00:00 2001 From: Jason Trost Date: Thu, 5 Feb 2015 12:33:01 -0500 Subject: [PATCH 8/9] added only the exit jail and stfp enabled config options --- kippo.cfg.dist | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/kippo.cfg.dist b/kippo.cfg.dist index 138a37e..a841242 100644 --- a/kippo.cfg.dist +++ b/kippo.cfg.dist @@ -37,6 +37,15 @@ download_path = dl # (default: 0) #download_limit_size = 10485760 +# Allow the attacker to exit the honeypot on request or try to 'trick' the attacker with another shell. +# note: depending on the attackers client (e.g. putty), will just quit regardless. +# +# (default: false) +exit_jail = false + +# sftp_enabled enables the sftp subsystem +sftp_enabled = true + # Directory where virtual file contents are kept in. # # This is only used by commands like 'cat' to display the contents of files. From 5dc08f254d5ce5f0d88cb509494e526635d0efac Mon Sep 17 00:00:00 2001 From: Andrew Morris Date: Wed, 11 Feb 2015 02:16:31 -0500 Subject: [PATCH 9/9] patched to not be vulnerable to external detection. thanks to moosterhof and thomas nicholson a la https://code.google.com/p/honssh/source/detail?r=10ffd2ccf076305af2d5eba8a0aa0317b0d9e7ec --- kippo/core/honeypot.py | 16 ++++---- kippo/core/sshserver.py | 87 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 7 deletions(-) create mode 100644 kippo/core/sshserver.py diff --git a/kippo/core/honeypot.py b/kippo/core/honeypot.py index 33788d3..e58633a 100644 --- a/kippo/core/honeypot.py +++ b/kippo/core/honeypot.py @@ -15,9 +15,10 @@ from copy import deepcopy, copy import sys, os, random, pickle, time, stat, shlex, anydbm -from kippo.core import ttylog, fs, utils +from kippo.core import ttylog, fs, utils, sshserver from kippo.core.userdb import UserDB from kippo.core.config import config +import kippo.core.honeypot import commands import ConfigParser @@ -518,7 +519,8 @@ def requestAvatar(self, avatarId, mind, *interfaces): else: raise Exception, "No supported interfaces found." -class HoneyPotTransport(transport.SSHServerTransport): +#class HoneyPotTransport(transport.SSHServerTransport): +class HoneyPotTransport(kippo.core.sshserver.KippoSSHServerTransport): hadVersion = False @@ -530,16 +532,16 @@ def connectionMade(self): self.interactors = [] self.logintime = time.time() self.ttylog_open = False - transport.SSHServerTransport.connectionMade(self) + kippo.core.sshserver.KippoSSHServerTransport.connectionMade(self) def sendKexInit(self): # Don't send key exchange prematurely if not self.gotVersion: return - transport.SSHServerTransport.sendKexInit(self) + kippo.core.sshserver.KippoSSHServerTransport.sendKexInit(self) def dataReceived(self, data): - transport.SSHServerTransport.dataReceived(self, data) + kippo.core.sshserver.KippoSSHServerTransport.dataReceived(self, data) # later versions seem to call sendKexInit again on their own if twisted.version.major < 11 and \ not self.hadVersion and self.gotVersion: @@ -548,7 +550,7 @@ def dataReceived(self, data): def ssh_KEXINIT(self, packet): print 'Remote SSH version: %s' % (self.otherVersionString,) - return transport.SSHServerTransport.ssh_KEXINIT(self, packet) + return kippo.core.sshserver.KippoSSHServerTransport.ssh_KEXINIT(self, packet) def lastlogExit(self): starttime = time.strftime('%a %b %d %H:%M', @@ -570,7 +572,7 @@ def connectionLost(self, reason): if self.ttylog_open: ttylog.ttylog_close(self.ttylog_file, time.time()) self.ttylog_open = False - transport.SSHServerTransport.connectionLost(self, reason) + kippo.core.sshserver.KippoSSHServerTransport.connectionLost(self, reason) from twisted.conch.ssh.common import NS, getNS class HoneyPotSSHUserAuthServer(userauth.SSHUserAuthServer): diff --git a/kippo/core/sshserver.py b/kippo/core/sshserver.py new file mode 100644 index 0000000..a36b582 --- /dev/null +++ b/kippo/core/sshserver.py @@ -0,0 +1,87 @@ +# Copyright (c) 2013 Thomas Nicholson +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. The names of the author(s) may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED +# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +from twisted.conch.ssh import transport +from twisted.python import log + +class KippoSSHServerTransport(transport.SSHServerTransport): + def connectionMade(self): + """ + Called when the connection is made to the other side. We sent our + version and the MSG_KEXINIT packet. + """ + self.transport.write('%s\r\n' % (self.ourVersionString,)) + self.currentEncryptions = transport.SSHCiphers('none', 'none', 'none', 'none') + self.currentEncryptions.setKeys('', '', '', '', '', '') + + def dataReceived(self, data): + """ + First, check for the version string (SSH-2.0-*). After that has been + received, this method adds data to the buffer, and pulls out any + packets. + @type data: C{str} + """ + self.buf = self.buf + data + if not self.gotVersion: + if self.buf.find('\n', self.buf.find('SSH-')) == -1: + return + lines = self.buf.split('\n') + for p in lines: + if p.startswith('SSH-'): + self.gotVersion = True + self.otherVersionString = p.strip() + remoteVersion = p.split('-')[1] + if remoteVersion not in self.supportedVersions: + self._unsupportedVersionReceived(remoteVersion) + return + i = lines.index(p) + self.buf = '\n'.join(lines[i + 1:]) + self.sendKexInit() + packet = self.getPacket() + while packet: + messageNum = ord(packet[0]) + self.dispatchMessage(messageNum, packet[1:]) + packet = self.getPacket() + + def sendDisconnect(self, reason, desc): + """ + http://kbyte.snowpenguin.org/portal/2013/04/30/kippo-protocol-mismatch-workaround/ + Workaround for the "bad packet length" error message. + @param reason: the reason for the disconnect. Should be one of the + DISCONNECT_* values. + @type reason: C{int} + @param desc: a descrption of the reason for the disconnection. + @type desc: C{str} + """ + if not 'bad packet length' in desc: + # With python >= 3 we can use super? + transport.SSHServerTransport.sendDisconnect(self, reason, desc) + else: + self.transport.write('Protocol mismatch.\n') + log.msg('[SERVER] - Disconnecting with error, code %s\nreason: %s' % (reason, desc)) + self.transport.loseConnection()