diff --git a/kippo.cfg.dist b/kippo.cfg.dist index ffd407fb..90ee6555 100644 --- a/kippo.cfg.dist +++ b/kippo.cfg.dist @@ -87,6 +87,9 @@ dsa_private_key = data/ssh_host_dsa_key # (default: false) exec_enabled = true +# sftp_enabled enables the sftp subsystem +sftp_enabled = true + # IP address to bind to when opening outgoing connections. Used exclusively by # the wget command. # diff --git a/kippo/core/fs.py b/kippo/core/fs.py index 2e91c297..0a38c8ca 100644 --- a/kippo/core/fs.py +++ b/kippo/core/fs.py @@ -1,7 +1,7 @@ # Copyright (c) 2009-2014 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, \ @@ -177,4 +177,195 @@ class HoneyPotFilesystem(object): 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/ssh.py b/kippo/core/ssh.py index e667866b..744b89dc 100644 --- a/kippo/core/ssh.py +++ b/kippo/core/ssh.py @@ -4,15 +4,20 @@ import twisted from twisted.cred import portal from twisted.conch import avatar, interfaces as conchinterfaces -from twisted.conch.ssh import factory, userauth, connection, keys, session, transport -from twisted.python import log +from twisted.conch.ssh import factory, userauth, connection, keys, session, 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.python import log, components from zope.interface import implements + + import os +import copy import time import ConfigParser -from kippo.core import ttylog, utils +from kippo.core import ttylog, utils, fs from kippo.core.config import config import kippo.core.auth import kippo.core.honeypot @@ -215,6 +220,11 @@ class HoneyPotAvatar(avatar.ConchUser): userdb = core.auth.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: @@ -304,4 +314,180 @@ def getDSAKeys(): privateKeyString = f.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:offset+length] + + def writeChunk(self, offset, data): + self.server.fs.lseek(self.fd, offset, os.SEEK_SET) + self.server.fs.write(self.fd, data) + self.bytes_written += 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(copy.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 et sw=4 et: