From c3c09adb024d914c38a12e78cfe163346bd10ec8 Mon Sep 17 00:00:00 2001 From: Upi Tamminen Date: Sat, 9 Aug 2014 23:48:34 +0300 Subject: [PATCH] restructuring and cleanup --- .gitignore | 11 - .gitignore | 66 ++++ kippo.tac | 15 +- kippo/commands/base.py | 2 +- kippo/core/{userdb.py => auth.py} | 56 ++- kippo/core/config.py | 2 +- kippo/core/dblog.py | 2 +- kippo/core/exceptions.py | 5 + kippo/core/fs.py | 2 +- kippo/core/honeypot.py | 589 +----------------------------- kippo/core/interact.py | 3 + kippo/core/protocol.py | 253 +++++++++++++ kippo/core/ssh.py | 307 ++++++++++++++++ kippo/core/ttylog.py | 2 +- kippo/core/utils.py | 2 +- 15 files changed, 702 insertions(+), 615 deletions(-) delete mode 100644 .gitignore create mode 100644 .gitignore rename kippo/core/{userdb.py => auth.py} (58%) create mode 100644 kippo/core/protocol.py create mode 100644 kippo/core/ssh.py diff --git a/ .gitignore b/ .gitignore deleted file mode 100644 index 9790710..0000000 --- a/ .gitignore +++ /dev/null @@ -1,11 +0,0 @@ -*.pyc -kippo.cfg -kippo.pid -data/lastlog.txt -data/ssh_host_dsa_key -data/ssh_host_dsa_key.pub -data/ssh_host_rsa_key -data/ssh_host_rsa_key.pub -dl/* -log/kippo.log -log/tty/* \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ca5599f --- /dev/null +++ b/.gitignore @@ -0,0 +1,66 @@ +kippo.cfg +kippo.pid +data/lastlog.txt +data/ssh_host_dsa_key +data/ssh_host_dsa_key.pub +data/ssh_host_rsa_key +data/ssh_host_rsa_key.pub +dl/* +log/kippo.log +log/tty/* +private.key +public.key + +# Created by .gitignore support plugin (hsz.mobi) + +### Python template +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo +*.pot + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ diff --git a/kippo.tac b/kippo.tac index 23c3a89..92bbdb5 100644 --- a/kippo.tac +++ b/kippo.tac @@ -22,15 +22,18 @@ if not os.path.exists('kippo.cfg'): print 'ERROR: kippo.cfg is missing!' sys.exit(1) -from kippo.core import honeypot from kippo.core.config import config +import kippo.core.auth +import kippo.core.honeypot +import kippo.core.ssh +from kippo import core -factory = honeypot.HoneyPotSSHFactory() -factory.portal = portal.Portal(honeypot.HoneyPotRealm()) +factory = core.ssh.HoneyPotSSHFactory() +factory.portal = portal.Portal(core.ssh.HoneyPotRealm()) -rsa_pubKeyString, rsa_privKeyString = honeypot.getRSAKeys() -dsa_pubKeyString, dsa_privKeyString = honeypot.getDSAKeys() -factory.portal.registerChecker(honeypot.HoneypotPasswordChecker()) +rsa_pubKeyString, rsa_privKeyString = core.ssh.getRSAKeys() +dsa_pubKeyString, dsa_privKeyString = core.ssh.getDSAKeys() +factory.portal.registerChecker(core.auth.HoneypotPasswordChecker()) factory.publicKeys = {'ssh-rsa': keys.Key.fromString(data=rsa_pubKeyString), 'ssh-dss': keys.Key.fromString(data=dsa_pubKeyString)} factory.privateKeys = {'ssh-rsa': keys.Key.fromString(data=rsa_privKeyString), diff --git a/kippo/commands/base.py b/kippo/commands/base.py index 6266088..89a798e 100644 --- a/kippo/commands/base.py +++ b/kippo/commands/base.py @@ -5,7 +5,7 @@ import os, time, anydbm, datetime from kippo.core.honeypot import HoneyPotCommand from twisted.internet import reactor from kippo.core.config import config -from kippo.core.userdb import UserDB +from kippo.core.auth import UserDB from kippo.core import utils commands = {} diff --git a/kippo/core/userdb.py b/kippo/core/auth.py similarity index 58% rename from kippo/core/userdb.py rename to kippo/core/auth.py index 9b87dbd..532ef37 100644 --- a/kippo/core/userdb.py +++ b/kippo/core/auth.py @@ -1,15 +1,18 @@ -# -# userdb.py for kippo -# by Walter de Jong -# -# adopted and further modified by Upi Tamminen -# +# Copyright (c) 2009-2014 Upi Tamminen +# See the COPYRIGHT file for more information -from kippo.core.config import config -import os import string -class UserDB: +import twisted +from twisted.cred import checkers, credentials, error +from twisted.internet import defer +from zope.interface import implements + +from kippo.core.config import config + +# by Walter de Jong +class UserDB(object): + def __init__(self): self.userdb = [] self.load() @@ -96,4 +99,39 @@ class UserDB: self.userdb.append((login, uid, passwd)) self.save() +class HoneypotPasswordChecker: + implements(checkers.ICredentialsChecker) + + credentialInterfaces = (credentials.IUsernamePassword, + credentials.IPluggableAuthenticationModules) + + def requestAvatarId(self, credentials): + if hasattr(credentials, 'password'): + if self.checkUserPass(credentials.username, credentials.password): + return defer.succeed(credentials.username) + else: + return defer.fail(error.UnauthorizedLogin()) + elif hasattr(credentials, 'pamConversion'): + return self.checkPamUser(credentials.username, + credentials.pamConversion) + return defer.fail(error.UnhandledCredentials()) + + def checkPamUser(self, username, pamConversion): + r = pamConversion((('Password:', 1),)) + return r.addCallback(self.cbCheckPamUser, username) + + def cbCheckPamUser(self, responses, username): + for response, zero in responses: + if self.checkUserPass(username, response): + return defer.succeed(username) + return defer.fail(error.UnauthorizedLogin()) + + def checkUserPass(self, username, password): + if UserDB().checklogin(username, password): + print 'login attempt [%s/%s] succeeded' % (username, password) + return True + else: + print 'login attempt [%s/%s] failed' % (username, password) + return False + # vim: set sw=4 et: diff --git a/kippo/core/config.py b/kippo/core/config.py index 1464016..b27ef3e 100644 --- a/kippo/core/config.py +++ b/kippo/core/config.py @@ -1,4 +1,4 @@ -# Copyright (c) 2009 Upi Tamminen +# Copyright (c) 2009-2014 Upi Tamminen # See the COPYRIGHT file for more information import ConfigParser, os diff --git a/kippo/core/dblog.py b/kippo/core/dblog.py index 59fba1b..6983d04 100644 --- a/kippo/core/dblog.py +++ b/kippo/core/dblog.py @@ -1,4 +1,4 @@ -# Copyright (c) 2009 Upi Tamminen +# Copyright (c) 2009-2014 Upi Tamminen # See the COPYRIGHT file for more information import re, time, socket diff --git a/kippo/core/exceptions.py b/kippo/core/exceptions.py index 894f7c0..e5697ae 100644 --- a/kippo/core/exceptions.py +++ b/kippo/core/exceptions.py @@ -1,3 +1,8 @@ +# Copyright (c) 2009-2014 Upi Tamminen +# See the COPYRIGHT file for more information + class NotEnabledException(Exception): """ Feature not enabled """ + +# vim: set sw=4 et: diff --git a/kippo/core/fs.py b/kippo/core/fs.py index 5001841..2e91c29 100644 --- a/kippo/core/fs.py +++ b/kippo/core/fs.py @@ -1,4 +1,4 @@ -# Copyright (c) 2009 Upi Tamminen +# Copyright (c) 2009-2014 Upi Tamminen # See the COPYRIGHT file for more information import os, time, fnmatch diff --git a/kippo/core/honeypot.py b/kippo/core/honeypot.py index 1aeb9b3..2c6db26 100644 --- a/kippo/core/honeypot.py +++ b/kippo/core/honeypot.py @@ -1,26 +1,16 @@ -# Copyright (c) 2009 Upi Tamminen +# Copyright (c) 2009-2014 Upi Tamminen # See the COPYRIGHT file for more information 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.insults import insults -from twisted.application import service, internet -from twisted.internet import reactor, protocol, defer -from twisted.python import failure, log -from zope.interface import implements from copy import deepcopy, copy -import sys, os, random, pickle, time, stat, shlex, anydbm, struct +import shlex -from kippo.core import ttylog, fs, utils -from kippo.core.userdb import UserDB +from kippo.core import fs from kippo.core.config import config -from kippo.core import exceptions +import kippo.core.exceptions +from kippo import core -import commands - -import ConfigParser +import pickle class HoneyPotCommand(object): def __init__(self, honeypot, *args): @@ -253,296 +243,6 @@ class HoneyPotShell(object): self.honeypot.lineBufferIndex = len(self.honeypot.lineBuffer) self.honeypot.terminal.write(newbuf) -class HoneyPotBaseProtocol(insults.TerminalProtocol): - def __init__(self, user, env): - self.user = user - self.env = env - self.hostname = self.env.cfg.get('honeypot', 'hostname') - self.fs = fs.HoneyPotFilesystem(deepcopy(self.env.fs)) - if self.fs.exists(user.home): - self.cwd = user.home - else: - self.cwd = '/' - # commands is also a copy so we can add stuff on the fly - self.commands = copy(self.env.commands) - self.password_input = False - self.cmdstack = [] - - def logDispatch(self, msg): - transport = self.terminal.transport.session.conn.transport - msg = ':dispatch: ' + msg - transport.factory.logDispatch(transport.transport.sessionno, msg) - - def connectionMade(self): - self.displayMOTD() - - transport = self.terminal.transport.session.conn.transport - - #transport = self.transport.transport.session.conn.transport - self.realClientIP = transport.getPeer().address.host - self.clientVersion = transport.otherVersionString - self.logintime = transport.logintime - self.ttylog_file = transport.ttylog_file - - # source IP of client in user visible reports (can be fake or real) - cfg = config() - if cfg.has_option('honeypot', 'fake_addr'): - self.clientIP = cfg.get('honeypot', 'fake_addr') - else: - self.clientIP = self.realClientIP - - def displayMOTD(self): - try: - self.writeln(self.fs.file_contents('/etc/motd')) - except: - pass - - # this doesn't seem to be called upon disconnect, so please use - # HoneyPotTransport.connectionLost instead - def connectionLost(self, reason): - pass - # not sure why i need to do this: - # scratch that, these don't seem to be necessary anymore: - #del self.fs - #del self.commands - - def txtcmd(self, txt): - class command_txtcmd(HoneyPotCommand): - def call(self): - print 'Reading txtcmd from "%s"' % txt - f = file(txt, 'r') - self.write(f.read()) - f.close() - return command_txtcmd - - def getCommand(self, cmd, paths): - if not len(cmd.strip()): - return None - path = None - if cmd in self.commands: - return self.commands[cmd] - if cmd[0] in ('.', '/'): - path = self.fs.resolve_path(cmd, self.cwd) - if not self.fs.exists(path): - return None - else: - for i in ['%s/%s' % (self.fs.resolve_path(x, self.cwd), cmd) \ - for x in paths]: - if self.fs.exists(i): - path = i - break - txt = os.path.abspath('%s/%s' % \ - (self.env.cfg.get('honeypot', 'txtcmds_path'), path)) - if os.path.exists(txt) and os.path.isfile(txt): - return self.txtcmd(txt) - if path in self.commands: - return self.commands[path] - return None - - def lineReceived(self, line): - if len(self.cmdstack): - self.cmdstack[-1].lineReceived(line) - - def writeln(self, data): - self.terminal.write(data) - self.terminal.nextLine() - - def call_command(self, cmd, *args): - obj = cmd(self, *args) - self.cmdstack.append(obj) - obj.start() - - def addInteractor(self, interactor): - transport = self.terminal.transport.session.conn.transport - transport.interactors.append(interactor) - - def delInteractor(self, interactor): - transport = self.terminal.transport.session.conn.transport - transport.interactors.remove(interactor) - - def uptime(self, reset = None): - transport = self.terminal.transport.session.conn.transport - r = time.time() - transport.factory.starttime - if reset: - transport.factory.starttime = reset - return r - -class HoneyPotInteractiveProtocol(HoneyPotBaseProtocol, recvline.HistoricRecvLine): - - def __init__(self, user, env): - recvline.HistoricRecvLine.__init__(self) - HoneyPotBaseProtocol.__init__(self, user, env) - - def connectionMade(self): - HoneyPotBaseProtocol.connectionMade(self) - recvline.HistoricRecvLine.connectionMade(self) - - self.cmdstack = [HoneyPotShell(self)] - - transport = self.terminal.transport.session.conn.transport - transport.factory.sessions[transport.transport.sessionno] = self - - self.keyHandlers.update({ - '\x04': self.handle_CTRL_D, - '\x15': self.handle_CTRL_U, - '\x03': self.handle_CTRL_C, - '\x09': self.handle_TAB, - }) - - # this doesn't seem to be called upon disconnect, so please use - # HoneyPotTransport.connectionLost instead - def connectionLost(self, reason): - HoneyPotBaseProtocol.connectionLost(self, reason) - recvline.HistoricRecvLine.connectionLost(self, reason) - - # Overriding to prevent terminal.reset() - def initializeScreen(self): - self.setInsertMode() - - def call_command(self, cmd, *args): - self.setTypeoverMode() - HoneyPotBaseProtocol.call_command(self, cmd, *args) - - def keystrokeReceived(self, keyID, modifier): - transport = self.terminal.transport.session.conn.transport - if type(keyID) == type(''): - ttylog.ttylog_write(transport.ttylog_file, len(keyID), - ttylog.TYPE_INPUT, time.time(), keyID) - recvline.HistoricRecvLine.keystrokeReceived(self, keyID, modifier) - - # Easier way to implement password input? - def characterReceived(self, ch, moreCharactersComing): - if self.mode == 'insert': - self.lineBuffer.insert(self.lineBufferIndex, ch) - else: - self.lineBuffer[self.lineBufferIndex:self.lineBufferIndex+1] = [ch] - self.lineBufferIndex += 1 - if not self.password_input: - self.terminal.write(ch) - - def handle_RETURN(self): - if len(self.cmdstack) == 1: - if self.lineBuffer: - self.historyLines.append(''.join(self.lineBuffer)) - self.historyPosition = len(self.historyLines) - return recvline.RecvLine.handle_RETURN(self) - - def handle_CTRL_C(self): - self.cmdstack[-1].ctrl_c() - - def handle_CTRL_U(self): - for i in range(self.lineBufferIndex): - self.terminal.cursorBackward() - self.terminal.deleteCharacter() - self.lineBuffer = self.lineBuffer[self.lineBufferIndex:] - self.lineBufferIndex = 0 - - def handle_CTRL_D(self): - self.call_command(self.commands['exit']) - - def handle_TAB(self): - self.cmdstack[-1].handle_TAB() - -class HoneyPotExecProtocol(HoneyPotBaseProtocol): - - def __init__(self, user, env, execcmd): - self.execcmd = execcmd - HoneyPotBaseProtocol.__init__(self, user, env) - - def connectionMade(self): - HoneyPotBaseProtocol.connectionMade(self) - - self.cmdstack = [HoneyPotShell(self, interactive=False)] - - print 'Running exec command "%s"' % self.execcmd - self.cmdstack[0].lineReceived(self.execcmd) - -class LoggingServerProtocol(insults.ServerProtocol): - def connectionMade(self): - transport = self.transport.session.conn.transport - - transport.ttylog_file = '%s/tty/%s-%s.log' % \ - (config().get('honeypot', 'log_path'), - time.strftime('%Y%m%d-%H%M%S'), - int(random.random() * 10000)) - print 'Opening TTY log: %s' % transport.ttylog_file - ttylog.ttylog_open(transport.ttylog_file, time.time()) - - transport.ttylog_open = True - - insults.ServerProtocol.connectionMade(self) - - def write(self, bytes, noLog = False): - transport = self.transport.session.conn.transport - for i in transport.interactors: - i.sessionWrite(bytes) - if transport.ttylog_open and not noLog: - ttylog.ttylog_write(transport.ttylog_file, len(bytes), - ttylog.TYPE_OUTPUT, time.time(), bytes) - insults.ServerProtocol.write(self, bytes) - - # this doesn't seem to be called upon disconnect, so please use - # HoneyPotTransport.connectionLost instead - def connectionLost(self, reason): - insults.ServerProtocol.connectionLost(self, reason) - -class HoneyPotSSHSession(session.SSHSession): - def request_env(self, data): - print 'request_env: %s' % (repr(data)) - -class HoneyPotAvatar(avatar.ConchUser): - implements(conchinterfaces.ISession) - - def __init__(self, username, env): - avatar.ConchUser.__init__(self) - self.username = username - self.env = env - self.channelLookup.update({'session': HoneyPotSSHSession}) - - userdb = UserDB() - self.uid = self.gid = userdb.getUID(self.username) - - if not self.uid: - self.home = '/root' - else: - self.home = '/home/' + username - - def openShell(self, protocol): - serverProtocol = LoggingServerProtocol( - HoneyPotInteractiveProtocol, self, self.env) - serverProtocol.makeConnection(protocol) - protocol.makeConnection(session.wrapProtocol(serverProtocol)) - - def getPty(self, terminal, windowSize, attrs): - print 'Terminal size: %s %s' % windowSize[0:2] - self.windowSize = windowSize - return None - - def execCommand(self, protocol, cmd): - cfg = config() - if not cfg.has_option('honeypot', 'exec_enabled') or \ - cfg.get('honeypot', 'exec_enabled').lower() not in \ - ('yes', 'true', 'on'): - print 'Exec disabled. Not executing command: "%s"' % cmd - raise exceptions.NotEnabledException, \ - 'exce_enabled not enabled in configuration file!' - return - - print 'exec command: "%s"' % cmd - serverProtocol = LoggingServerProtocol( - HoneyPotExecProtocol, self, self.env, cmd) - serverProtocol.makeConnection(protocol) - protocol.makeConnection(session.wrapProtocol(serverProtocol)) - - def closed(self): - pass - - def eofReceived(self): - pass - - def windowChanged(self, windowSize): - self.windowSize = windowSize - class HoneyPotEnvironment(object): def __init__(self): self.cfg = config() @@ -555,281 +255,4 @@ class HoneyPotEnvironment(object): self.fs = pickle.load(file( self.cfg.get('honeypot', 'filesystem_file'), 'rb')) -class HoneyPotRealm: - implements(portal.IRealm) - - def __init__(self): - # I don't know if i'm supposed to keep static stuff here - self.env = HoneyPotEnvironment() - - def requestAvatar(self, avatarId, mind, *interfaces): - if conchinterfaces.IConchUser in interfaces: - return interfaces[0], \ - HoneyPotAvatar(avatarId, self.env), lambda: None - else: - raise Exception, "No supported interfaces found." - -class HoneyPotTransport(transport.SSHServerTransport): - - hadVersion = False - - def connectionMade(self): - print 'New connection: %s:%s (%s:%s) [session: %d]' % \ - (self.transport.getPeer().host, self.transport.getPeer().port, - self.transport.getHost().host, self.transport.getHost().port, - self.transport.sessionno) - self.interactors = [] - self.logintime = time.time() - self.ttylog_open = False - transport.SSHServerTransport.connectionMade(self) - - def sendKexInit(self): - # Don't send key exchange prematurely - if not self.gotVersion: - return - transport.SSHServerTransport.sendKexInit(self) - - def dataReceived(self, data): - transport.SSHServerTransport.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: - self.sendKexInit() - self.hadVersion = True - - def ssh_KEXINIT(self, packet): - print 'Remote SSH version: %s' % (self.otherVersionString,) - return transport.SSHServerTransport.ssh_KEXINIT(self, packet) - - def lastlogExit(self): - starttime = time.strftime('%a %b %d %H:%M', - time.localtime(self.logintime)) - endtime = time.strftime('%H:%M', - time.localtime(time.time())) - duration = utils.durationHuman(time.time() - self.logintime) - clientIP = self.transport.getPeer().host - utils.addToLastlog('root\tpts/0\t%s\t%s - %s (%s)' % \ - (clientIP, starttime, endtime, duration)) - - # this seems to be the only reliable place of catching lost connection - def connectionLost(self, reason): - for i in self.interactors: - i.sessionClosed() - if self.transport.sessionno in self.factory.sessions: - del self.factory.sessions[self.transport.sessionno] - self.lastlogExit() - if self.ttylog_open: - ttylog.ttylog_close(self.ttylog_file, time.time()) - self.ttylog_open = False - transport.SSHServerTransport.connectionLost(self, reason) - - def sendDisconnect(self, reason, desc): - """ - 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('Disconnecting with error, code %s\nreason: %s' % (reason, desc)) - self.transport.loseConnection() - -from twisted.conch.ssh.common import NS, getNS -class HoneyPotSSHUserAuthServer(userauth.SSHUserAuthServer): - def serviceStarted(self): - userauth.SSHUserAuthServer.serviceStarted(self) - self.bannerSent = False - - def sendBanner(self): - if self.bannerSent: - return - cfg = config() - if not cfg.has_option('honeypot', 'banner_file'): - return - try: - data = file(cfg.get('honeypot', 'banner_file')).read() - except IOError: - print 'Banner file %s does not exist!' % \ - cfg.get('honeypot', 'banner_file') - return - if not data or not len(data.strip()): - return - data = '\r\n'.join(data.splitlines() + ['']) - self.transport.sendPacket( - userauth.MSG_USERAUTH_BANNER, NS(data) + NS('en')) - self.bannerSent = True - - def ssh_USERAUTH_REQUEST(self, packet): - self.sendBanner() - return userauth.SSHUserAuthServer.ssh_USERAUTH_REQUEST(self, packet) - -# As implemented by Kojoney -class HoneyPotSSHFactory(factory.SSHFactory): - services = { - 'ssh-userauth': HoneyPotSSHUserAuthServer, - 'ssh-connection': connection.SSHConnection, - } - - # Special delivery to the loggers to avoid scope problems - def logDispatch(self, sessionid, msg): - for dblog in self.dbloggers: - dblog.logDispatch(sessionid, msg) - - def __init__(self): - cfg = config() - - # protocol^Wwhatever instances are kept here for the interact feature - self.sessions = {} - - # for use by the uptime command - self.starttime = time.time() - - # convert old pass.db root passwords - passdb_file = '%s/pass.db' % (cfg.get('honeypot', 'data_path'),) - if os.path.exists(passdb_file): - userdb = UserDB() - print 'pass.db deprecated - copying passwords over to userdb.txt' - if os.path.exists('%s.bak' % (passdb_file,)): - print 'ERROR: %s.bak already exists, skipping conversion!' % \ - (passdb_file,) - else: - passdb = anydbm.open(passdb_file, 'c') - for p in passdb: - userdb.adduser('root', 0, p) - passdb.close() - os.rename(passdb_file, '%s.bak' % (passdb_file,)) - print 'pass.db backed up to %s.bak' % (passdb_file,) - - # load db loggers - self.dbloggers = [] - for x in cfg.sections(): - if not x.startswith('database_'): - continue - engine = x.split('_')[1] - dbengine = 'database_' + engine - lcfg = ConfigParser.ConfigParser() - lcfg.add_section(dbengine) - for i in cfg.options(x): - lcfg.set(dbengine, i, cfg.get(x, i)) - lcfg.add_section('honeypot') - for i in cfg.options('honeypot'): - lcfg.set('honeypot', i, cfg.get('honeypot', i)) - print 'Loading dblog engine: %s' % (engine,) - dblogger = __import__( - 'kippo.dblog.%s' % (engine,), - globals(), locals(), ['dblog']).DBLogger(lcfg) - log.startLoggingWithObserver(dblogger.emit, setStdout=False) - self.dbloggers.append(dblogger) - - def buildProtocol(self, addr): - cfg = config() - - # FIXME: try to mimic something real 100% - t = HoneyPotTransport() - - if cfg.has_option('honeypot', 'ssh_version_string'): - t.ourVersionString = cfg.get('honeypot','ssh_version_string') - else: - t.ourVersionString = "SSH-2.0-OpenSSH_5.1p1 Debian-5" - - t.supportedPublicKeys = self.privateKeys.keys() - - if not self.primes: - ske = t.supportedKeyExchanges[:] - ske.remove('diffie-hellman-group-exchange-sha1') - t.supportedKeyExchanges = ske - - t.factory = self - return t - -class HoneypotPasswordChecker: - implements(checkers.ICredentialsChecker) - - credentialInterfaces = (credentials.IUsernamePassword, - credentials.IPluggableAuthenticationModules) - - def requestAvatarId(self, credentials): - if hasattr(credentials, 'password'): - if self.checkUserPass(credentials.username, credentials.password): - return defer.succeed(credentials.username) - else: - return defer.fail(error.UnauthorizedLogin()) - elif hasattr(credentials, 'pamConversion'): - return self.checkPamUser(credentials.username, - credentials.pamConversion) - return defer.fail(error.UnhandledCredentials()) - - def checkPamUser(self, username, pamConversion): - r = pamConversion((('Password:', 1),)) - return r.addCallback(self.cbCheckPamUser, username) - - def cbCheckPamUser(self, responses, username): - for response, zero in responses: - if self.checkUserPass(username, response): - return defer.succeed(username) - return defer.fail(error.UnauthorizedLogin()) - - def checkUserPass(self, username, password): - if UserDB().checklogin(username, password): - print 'login attempt [%s/%s] succeeded' % (username, password) - return True - else: - print 'login attempt [%s/%s] failed' % (username, password) - return False - -def getRSAKeys(): - cfg = config() - public_key = cfg.get('honeypot', 'rsa_public_key') - private_key = cfg.get('honeypot', 'rsa_private_key') - if not (os.path.exists(public_key) and os.path.exists(private_key)): - print "Generating new RSA keypair..." - from Crypto.PublicKey import RSA - from twisted.python import randbytes - KEY_LENGTH = 2048 - rsaKey = RSA.generate(KEY_LENGTH, randbytes.secureRandom) - publicKeyString = keys.Key(rsaKey).public().toString('openssh') - privateKeyString = keys.Key(rsaKey).toString('openssh') - with file(public_key, 'w+b') as f: - f.write(publicKeyString) - with file(private_key, 'w+b') as f: - f.write(privateKeyString) - print "Done." - else: - with file(public_key) as f: - publicKeyString = f.read() - with file(private_key) as f: - privateKeyString = f.read() - return publicKeyString, privateKeyString - -def getDSAKeys(): - cfg = config() - public_key = cfg.get('honeypot', 'dsa_public_key') - private_key = cfg.get('honeypot', 'dsa_private_key') - if not (os.path.exists(public_key) and os.path.exists(private_key)): - print "Generating new DSA keypair..." - from Crypto.PublicKey import DSA - from twisted.python import randbytes - KEY_LENGTH = 1024 - dsaKey = DSA.generate(KEY_LENGTH, randbytes.secureRandom) - publicKeyString = keys.Key(dsaKey).public().toString('openssh') - privateKeyString = keys.Key(dsaKey).toString('openssh') - with file(public_key, 'w+b') as f: - f.write(publicKeyString) - with file(private_key, 'w+b') as f: - f.write(privateKeyString) - print "Done." - else: - with file(public_key) as f: - publicKeyString = f.read() - with file(private_key) as f: - privateKeyString = f.read() - return publicKeyString, privateKeyString - # vim: set sw=4 et: diff --git a/kippo/core/interact.py b/kippo/core/interact.py index c4d9169..9b112fa 100644 --- a/kippo/core/interact.py +++ b/kippo/core/interact.py @@ -1,3 +1,6 @@ +# Copyright (c) 2009-2014 Upi Tamminen +# See the COPYRIGHT file for more information + from twisted.internet import protocol from twisted.conch import telnet, recvline from kippo.core import ttylog diff --git a/kippo/core/protocol.py b/kippo/core/protocol.py new file mode 100644 index 0000000..3589049 --- /dev/null +++ b/kippo/core/protocol.py @@ -0,0 +1,253 @@ +# Copyright (c) 2009-2014 Upi Tamminen +# See the COPYRIGHT file for more information + +import os +import random +import time +import struct + +from twisted.conch import recvline +from twisted.conch.ssh import transport +from twisted.conch.insults import insults +from twisted.internet import protocol +from copy import deepcopy, copy + +from kippo.core import ttylog, fs +from kippo.core.config import config +from kippo.core import exceptions +from kippo import core + +class HoneyPotBaseProtocol(insults.TerminalProtocol): + def __init__(self, user, env): + self.user = user + self.env = env + self.hostname = self.env.cfg.get('honeypot', 'hostname') + self.fs = fs.HoneyPotFilesystem(deepcopy(self.env.fs)) + if self.fs.exists(user.home): + self.cwd = user.home + else: + self.cwd = '/' + # commands is also a copy so we can add stuff on the fly + self.commands = copy(self.env.commands) + self.password_input = False + self.cmdstack = [] + + def logDispatch(self, msg): + transport = self.terminal.transport.session.conn.transport + msg = ':dispatch: ' + msg + transport.factory.logDispatch(transport.transport.sessionno, msg) + + def connectionMade(self): + self.displayMOTD() + + transport = self.terminal.transport.session.conn.transport + + #transport = self.transport.transport.session.conn.transport + self.realClientIP = transport.getPeer().address.host + self.clientVersion = transport.otherVersionString + self.logintime = transport.logintime + self.ttylog_file = transport.ttylog_file + + # source IP of client in user visible reports (can be fake or real) + cfg = config() + if cfg.has_option('honeypot', 'fake_addr'): + self.clientIP = cfg.get('honeypot', 'fake_addr') + else: + self.clientIP = self.realClientIP + + def displayMOTD(self): + try: + self.writeln(self.fs.file_contents('/etc/motd')) + except: + pass + + # this doesn't seem to be called upon disconnect, so please use + # HoneyPotTransport.connectionLost instead + def connectionLost(self, reason): + pass + # not sure why i need to do this: + # scratch that, these don't seem to be necessary anymore: + #del self.fs + #del self.commands + + def txtcmd(self, txt): + class command_txtcmd(HoneyPotCommand): + def call(self): + print 'Reading txtcmd from "%s"' % txt + f = file(txt, 'r') + self.write(f.read()) + f.close() + return command_txtcmd + + def getCommand(self, cmd, paths): + if not len(cmd.strip()): + return None + path = None + if cmd in self.commands: + return self.commands[cmd] + if cmd[0] in ('.', '/'): + path = self.fs.resolve_path(cmd, self.cwd) + if not self.fs.exists(path): + return None + else: + for i in ['%s/%s' % (self.fs.resolve_path(x, self.cwd), cmd) \ + for x in paths]: + if self.fs.exists(i): + path = i + break + txt = os.path.abspath('%s/%s' % \ + (self.env.cfg.get('honeypot', 'txtcmds_path'), path)) + if os.path.exists(txt) and os.path.isfile(txt): + return self.txtcmd(txt) + if path in self.commands: + return self.commands[path] + return None + + def lineReceived(self, line): + if len(self.cmdstack): + self.cmdstack[-1].lineReceived(line) + + def writeln(self, data): + self.terminal.write(data) + self.terminal.nextLine() + + def call_command(self, cmd, *args): + obj = cmd(self, *args) + self.cmdstack.append(obj) + obj.start() + + def addInteractor(self, interactor): + transport = self.terminal.transport.session.conn.transport + transport.interactors.append(interactor) + + def delInteractor(self, interactor): + transport = self.terminal.transport.session.conn.transport + transport.interactors.remove(interactor) + + def uptime(self, reset = None): + transport = self.terminal.transport.session.conn.transport + r = time.time() - transport.factory.starttime + if reset: + transport.factory.starttime = reset + return r + +class HoneyPotExecProtocol(HoneyPotBaseProtocol): + + def __init__(self, user, env, execcmd): + self.execcmd = execcmd + HoneyPotBaseProtocol.__init__(self, user, env) + + def connectionMade(self): + HoneyPotBaseProtocol.connectionMade(self) + + self.cmdstack = [core.honeypot.HoneyPotShell(self, interactive=False)] + + print 'Running exec command "%s"' % self.execcmd + self.cmdstack[0].lineReceived(self.execcmd) + +class HoneyPotInteractiveProtocol(HoneyPotBaseProtocol, recvline.HistoricRecvLine): + + def __init__(self, user, env): + recvline.HistoricRecvLine.__init__(self) + HoneyPotBaseProtocol.__init__(self, user, env) + + def connectionMade(self): + HoneyPotBaseProtocol.connectionMade(self) + recvline.HistoricRecvLine.connectionMade(self) + + self.cmdstack = [core.honeypot.HoneyPotShell(self)] + + transport = self.terminal.transport.session.conn.transport + transport.factory.sessions[transport.transport.sessionno] = self + + self.keyHandlers.update({ + '\x04': self.handle_CTRL_D, + '\x15': self.handle_CTRL_U, + '\x03': self.handle_CTRL_C, + '\x09': self.handle_TAB, + }) + + # this doesn't seem to be called upon disconnect, so please use + # HoneyPotTransport.connectionLost instead + def connectionLost(self, reason): + HoneyPotBaseProtocol.connectionLost(self, reason) + recvline.HistoricRecvLine.connectionLost(self, reason) + + # Overriding to prevent terminal.reset() + def initializeScreen(self): + self.setInsertMode() + + def call_command(self, cmd, *args): + self.setTypeoverMode() + HoneyPotBaseProtocol.call_command(self, cmd, *args) + + def keystrokeReceived(self, keyID, modifier): + transport = self.terminal.transport.session.conn.transport + if type(keyID) == type(''): + ttylog.ttylog_write(transport.ttylog_file, len(keyID), + ttylog.TYPE_INPUT, time.time(), keyID) + recvline.HistoricRecvLine.keystrokeReceived(self, keyID, modifier) + + # Easier way to implement password input? + def characterReceived(self, ch, moreCharactersComing): + if self.mode == 'insert': + self.lineBuffer.insert(self.lineBufferIndex, ch) + else: + self.lineBuffer[self.lineBufferIndex:self.lineBufferIndex+1] = [ch] + self.lineBufferIndex += 1 + if not self.password_input: + self.terminal.write(ch) + + def handle_RETURN(self): + if len(self.cmdstack) == 1: + if self.lineBuffer: + self.historyLines.append(''.join(self.lineBuffer)) + self.historyPosition = len(self.historyLines) + return recvline.RecvLine.handle_RETURN(self) + + def handle_CTRL_C(self): + self.cmdstack[-1].ctrl_c() + + def handle_CTRL_U(self): + for i in range(self.lineBufferIndex): + self.terminal.cursorBackward() + self.terminal.deleteCharacter() + self.lineBuffer = self.lineBuffer[self.lineBufferIndex:] + self.lineBufferIndex = 0 + + def handle_CTRL_D(self): + self.call_command(self.commands['exit']) + + def handle_TAB(self): + self.cmdstack[-1].handle_TAB() + +class LoggingServerProtocol(insults.ServerProtocol): + def connectionMade(self): + transport = self.transport.session.conn.transport + + transport.ttylog_file = '%s/tty/%s-%s.log' % \ + (config().get('honeypot', 'log_path'), + time.strftime('%Y%m%d-%H%M%S'), + int(random.random() * 10000)) + print 'Opening TTY log: %s' % transport.ttylog_file + ttylog.ttylog_open(transport.ttylog_file, time.time()) + + transport.ttylog_open = True + + insults.ServerProtocol.connectionMade(self) + + def write(self, bytes, noLog = False): + transport = self.transport.session.conn.transport + for i in transport.interactors: + i.sessionWrite(bytes) + if transport.ttylog_open and not noLog: + ttylog.ttylog_write(transport.ttylog_file, len(bytes), + ttylog.TYPE_OUTPUT, time.time(), bytes) + insults.ServerProtocol.write(self, bytes) + + # this doesn't seem to be called upon disconnect, so please use + # HoneyPotTransport.connectionLost instead + def connectionLost(self, reason): + insults.ServerProtocol.connectionLost(self, reason) + +# vim: set sw=4 et: diff --git a/kippo/core/ssh.py b/kippo/core/ssh.py new file mode 100644 index 0000000..e667866 --- /dev/null +++ b/kippo/core/ssh.py @@ -0,0 +1,307 @@ +# Copyright (c) 2009-2014 Upi Tamminen +# See the COPYRIGHT file for more information + +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 zope.interface import implements + +import os +import time +import ConfigParser + +from kippo.core import ttylog, utils +from kippo.core.config import config +import kippo.core.auth +import kippo.core.honeypot +import kippo.core.ssh +import kippo.core.protocol +from kippo import core + +from twisted.conch.ssh.common import NS, getNS +class HoneyPotSSHUserAuthServer(userauth.SSHUserAuthServer): + def serviceStarted(self): + userauth.SSHUserAuthServer.serviceStarted(self) + self.bannerSent = False + + def sendBanner(self): + if self.bannerSent: + return + cfg = config() + if not cfg.has_option('honeypot', 'banner_file'): + return + try: + data = file(cfg.get('honeypot', 'banner_file')).read() + except IOError: + print 'Banner file %s does not exist!' % \ + cfg.get('honeypot', 'banner_file') + return + if not data or not len(data.strip()): + return + data = '\r\n'.join(data.splitlines() + ['']) + self.transport.sendPacket( + userauth.MSG_USERAUTH_BANNER, NS(data) + NS('en')) + self.bannerSent = True + + def ssh_USERAUTH_REQUEST(self, packet): + self.sendBanner() + return userauth.SSHUserAuthServer.ssh_USERAUTH_REQUEST(self, packet) + +# As implemented by Kojoney +class HoneyPotSSHFactory(factory.SSHFactory): + services = { + 'ssh-userauth': HoneyPotSSHUserAuthServer, + 'ssh-connection': connection.SSHConnection, + } + + # Special delivery to the loggers to avoid scope problems + def logDispatch(self, sessionid, msg): + for dblog in self.dbloggers: + dblog.logDispatch(sessionid, msg) + + def __init__(self): + cfg = config() + + # protocol^Wwhatever instances are kept here for the interact feature + self.sessions = {} + + # for use by the uptime command + self.starttime = time.time() + + # load db loggers + self.dbloggers = [] + for x in cfg.sections(): + if not x.startswith('database_'): + continue + engine = x.split('_')[1] + dbengine = 'database_' + engine + lcfg = ConfigParser.ConfigParser() + lcfg.add_section(dbengine) + for i in cfg.options(x): + lcfg.set(dbengine, i, cfg.get(x, i)) + lcfg.add_section('honeypot') + for i in cfg.options('honeypot'): + lcfg.set('honeypot', i, cfg.get('honeypot', i)) + print 'Loading dblog engine: %s' % (engine,) + dblogger = __import__( + 'kippo.dblog.%s' % (engine,), + globals(), locals(), ['dblog']).DBLogger(lcfg) + log.startLoggingWithObserver(dblogger.emit, setStdout=False) + self.dbloggers.append(dblogger) + + def buildProtocol(self, addr): + cfg = config() + + # FIXME: try to mimic something real 100% + t = HoneyPotTransport() + + if cfg.has_option('honeypot', 'ssh_version_string'): + t.ourVersionString = cfg.get('honeypot','ssh_version_string') + else: + t.ourVersionString = "SSH-2.0-OpenSSH_5.1p1 Debian-5" + + t.supportedPublicKeys = self.privateKeys.keys() + + if not self.primes: + ske = t.supportedKeyExchanges[:] + ske.remove('diffie-hellman-group-exchange-sha1') + t.supportedKeyExchanges = ske + + t.factory = self + return t + +class HoneyPotRealm: + implements(portal.IRealm) + + def __init__(self): + # I don't know if i'm supposed to keep static stuff here + self.env = core.honeypot.HoneyPotEnvironment() + + def requestAvatar(self, avatarId, mind, *interfaces): + if conchinterfaces.IConchUser in interfaces: + return interfaces[0], \ + HoneyPotAvatar(avatarId, self.env), lambda: None + else: + raise Exception, "No supported interfaces found." + +class HoneyPotTransport(transport.SSHServerTransport): + + hadVersion = False + + def connectionMade(self): + print 'New connection: %s:%s (%s:%s) [session: %d]' % \ + (self.transport.getPeer().host, self.transport.getPeer().port, + self.transport.getHost().host, self.transport.getHost().port, + self.transport.sessionno) + self.interactors = [] + self.logintime = time.time() + self.ttylog_open = False + transport.SSHServerTransport.connectionMade(self) + + def sendKexInit(self): + # Don't send key exchange prematurely + if not self.gotVersion: + return + transport.SSHServerTransport.sendKexInit(self) + + def dataReceived(self, data): + transport.SSHServerTransport.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: + self.sendKexInit() + self.hadVersion = True + + def ssh_KEXINIT(self, packet): + print 'Remote SSH version: %s' % (self.otherVersionString,) + return transport.SSHServerTransport.ssh_KEXINIT(self, packet) + + def lastlogExit(self): + starttime = time.strftime('%a %b %d %H:%M', + time.localtime(self.logintime)) + endtime = time.strftime('%H:%M', + time.localtime(time.time())) + duration = utils.durationHuman(time.time() - self.logintime) + clientIP = self.transport.getPeer().host + utils.addToLastlog('root\tpts/0\t%s\t%s - %s (%s)' % \ + (clientIP, starttime, endtime, duration)) + + # this seems to be the only reliable place of catching lost connection + def connectionLost(self, reason): + for i in self.interactors: + i.sessionClosed() + if self.transport.sessionno in self.factory.sessions: + del self.factory.sessions[self.transport.sessionno] + self.lastlogExit() + if self.ttylog_open: + ttylog.ttylog_close(self.ttylog_file, time.time()) + self.ttylog_open = False + transport.SSHServerTransport.connectionLost(self, reason) + + def sendDisconnect(self, reason, desc): + """ + 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('Disconnecting with error, code %s\nreason: %s' % \ + (reason, desc)) + self.transport.loseConnection() + +class HoneyPotSSHSession(session.SSHSession): + def request_env(self, data): + print 'request_env: %s' % (repr(data)) + +class HoneyPotAvatar(avatar.ConchUser): + implements(conchinterfaces.ISession) + + def __init__(self, username, env): + avatar.ConchUser.__init__(self) + self.username = username + self.env = env + self.channelLookup.update({'session': HoneyPotSSHSession}) + + userdb = core.auth.UserDB() + self.uid = self.gid = userdb.getUID(self.username) + + if not self.uid: + self.home = '/root' + else: + self.home = '/home/' + username + + def openShell(self, protocol): + serverProtocol = core.protocol.LoggingServerProtocol( + core.protocol.HoneyPotInteractiveProtocol, self, self.env) + serverProtocol.makeConnection(protocol) + protocol.makeConnection(session.wrapProtocol(serverProtocol)) + + def getPty(self, terminal, windowSize, attrs): + print 'Terminal size: %s %s' % windowSize[0:2] + self.windowSize = windowSize + return None + + def execCommand(self, protocol, cmd): + cfg = config() + if not cfg.has_option('honeypot', 'exec_enabled') or \ + cfg.get('honeypot', 'exec_enabled').lower() not in \ + ('yes', 'true', 'on'): + print 'Exec disabled. Not executing command: "%s"' % cmd + raise core.exceptions.NotEnabledException, \ + 'exec_enabled not enabled in configuration file!' + return + + print 'exec command: "%s"' % cmd + serverProtocol = kippo.core.protocol.LoggingServerProtocol( + kippo.core.protocol.HoneyPotExecProtocol, self, self.env, cmd) + serverProtocol.makeConnection(protocol) + protocol.makeConnection(session.wrapProtocol(serverProtocol)) + + def closed(self): + pass + + def eofReceived(self): + pass + + def windowChanged(self, windowSize): + self.windowSize = windowSize + +def getRSAKeys(): + cfg = config() + public_key = cfg.get('honeypot', 'rsa_public_key') + private_key = cfg.get('honeypot', 'rsa_private_key') + if not (os.path.exists(public_key) and os.path.exists(private_key)): + print "Generating new RSA keypair..." + from Crypto.PublicKey import RSA + from twisted.python import randbytes + KEY_LENGTH = 2048 + rsaKey = RSA.generate(KEY_LENGTH, randbytes.secureRandom) + publicKeyString = keys.Key(rsaKey).public().toString('openssh') + privateKeyString = keys.Key(rsaKey).toString('openssh') + with file(public_key, 'w+b') as f: + f.write(publicKeyString) + with file(private_key, 'w+b') as f: + f.write(privateKeyString) + print "Done." + else: + with file(public_key) as f: + publicKeyString = f.read() + with file(private_key) as f: + privateKeyString = f.read() + return publicKeyString, privateKeyString + +def getDSAKeys(): + cfg = config() + public_key = cfg.get('honeypot', 'dsa_public_key') + private_key = cfg.get('honeypot', 'dsa_private_key') + if not (os.path.exists(public_key) and os.path.exists(private_key)): + print "Generating new DSA keypair..." + from Crypto.PublicKey import DSA + from twisted.python import randbytes + KEY_LENGTH = 1024 + dsaKey = DSA.generate(KEY_LENGTH, randbytes.secureRandom) + publicKeyString = keys.Key(dsaKey).public().toString('openssh') + privateKeyString = keys.Key(dsaKey).toString('openssh') + with file(public_key, 'w+b') as f: + f.write(publicKeyString) + with file(private_key, 'w+b') as f: + f.write(privateKeyString) + print "Done." + else: + with file(public_key) as f: + publicKeyString = f.read() + with file(private_key) as f: + privateKeyString = f.read() + return publicKeyString, privateKeyString + +# vim: set et sw=4 et: diff --git a/kippo/core/ttylog.py b/kippo/core/ttylog.py index ecd2410..2a5198d 100644 --- a/kippo/core/ttylog.py +++ b/kippo/core/ttylog.py @@ -1,4 +1,4 @@ -# Copyright (c) 2009 Upi Tamminen +# Copyright (c) 2009-2014 Upi Tamminen # See the COPYRIGHT file for more information # Should be compatible with user mode linux diff --git a/kippo/core/utils.py b/kippo/core/utils.py index f14b087..8788a88 100644 --- a/kippo/core/utils.py +++ b/kippo/core/utils.py @@ -1,4 +1,4 @@ -# Copyright (c) 2010 Upi Tamminen +# Copyright (c) 2010-2014 Upi Tamminen # See the COPYRIGHT file for more information import time, anydbm