diff --git a/CHANGELOG.md b/CHANGELOG.md index 47b49fb..45aa87f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ + +* 2015-02-25 Internals for dblog/ modules changed completely. Now accepts structured logging arguments, and uses eventids instead of regex parsing +* 2015-02-20 Removed screen clear/reset on logout +* 2015-02-19 Configuration directives have changed! ssh_addr has become listen_addr and ssh_port has become listen_port. The old keywords are still accepted for backwards compatibility + * default behaviour is changed to disable the exit jail * sftp support * exec support @@ -6,7 +11,7 @@ * allow wget download over non-80 port * simple JSON logging to kippo.json * accept log and deny publickey authentication -* add uname -r command +* add uname -r, -m flags * add working sleep command * enabled ssh diffie-hellman-group-exchange-sha1 algorithm * add 'bash -c' support (no effect option) @@ -33,3 +38,4 @@ * add 'poweroff' 'halt' and 'reboot' aliases for shutdown * add environment passing to commands * added 'which', 'netstat' and 'gcc' from kippo-extra +* logging framework allows for keyword use diff --git a/kippo.cfg.dist b/kippo.cfg.dist index a73fcf6..24171d5 100644 --- a/kippo.cfg.dist +++ b/kippo.cfg.dist @@ -16,12 +16,12 @@ # IP addresses to listen for incoming SSH connections. # # (default: 0.0.0.0) = any address -#ssh_addr = 0.0.0.0 +#listen_addr = 0.0.0.0 # Port to listen for incoming SSH connections. # # (default: 2222) -ssh_port = 2222 +#listen_port = 2222 # Hostname for the honeypot. Displayed by the shell prompt of the virtual # environment. @@ -216,7 +216,7 @@ interact_port = 5123 #logfile = log/kippo-textlog.log # JSON based logging module -#[database_jsonlog] +[database_jsonlog] logfile = log/kippolog.json #[database_hpfeeds] diff --git a/kippo.tac b/kippo.tac index 1135836..b92bbc9 100644 --- a/kippo.tac +++ b/kippo.tac @@ -3,13 +3,11 @@ import sys, os if sys.platform == 'win32': - import os, inspect # this is when just running on win32 sys.path.insert(0, os.path.abspath(os.getcwd())) # and this is when running as a service #os.chdir(os.path.dirname(inspect.getfile(inspect.currentframe()))) -from twisted.internet import reactor, defer from twisted.application import internet, service from twisted.cred import portal from twisted.conch.ssh import factory, keys @@ -23,7 +21,6 @@ if not os.path.exists('kippo.cfg'): sys.exit(1) from kippo.core.config import config -import kippo.core.auth import kippo.core.honeypot import kippo.core.ssh from kippo import core @@ -42,16 +39,27 @@ factory.privateKeys = {'ssh-rsa': keys.Key.fromString(data=rsa_privKeyString), 'ssh-dss': keys.Key.fromString(data=dsa_privKeyString)} cfg = config() -if cfg.has_option('honeypot', 'ssh_addr'): - ssh_addr = cfg.get('honeypot', 'ssh_addr') + +if cfg.has_option('honeypot', 'listen_addr'): + listen_addr = cfg.get('honeypot', 'listen_addr') +elif cfg.has_option('honeypot', 'ssh_addr'): + # ssh_addr for backwards compatibility + listen_addr = cfg.get('honeypot', 'ssh_addr') else: - ssh_addr = '0.0.0.0' + listen_addr = '0.0.0.0' + +if cfg.has_option('honeypot', 'listen_port'): + listen_port = int(cfg.get('honeypot', 'listen_port')) +elif cfg.has_option('honeypot', 'ssh_port'): + # ssh_port for backwards compatibility + listen_port = int(cfg.get('honeypot', 'ssh_port')) +else: + listen_port = 2222 application = service.Application('honeypot') -for i in ssh_addr.split(): - service = internet.TCPServer( - int(cfg.get('honeypot', 'ssh_port')), factory, - interface=i) +for i in listen_addr.split(): + service = internet.TCPServer( listen_port, + factory, interface=i) service.setServiceParent(application) if cfg.has_option('honeypot', 'interact_enabled') and \ @@ -59,7 +67,6 @@ if cfg.has_option('honeypot', 'interact_enabled') and \ ('yes', 'true', 'on'): iport = int(cfg.get('honeypot', 'interact_port')) from kippo.core import interact - from twisted.internet import protocol service = internet.TCPServer(iport, interact.makeInteractFactory(factory)) service.setServiceParent(application) diff --git a/kippo/commands/base.py b/kippo/commands/base.py index 2e73bd3..61e9fe9 100644 --- a/kippo/commands/base.py +++ b/kippo/commands/base.py @@ -230,7 +230,9 @@ class command_passwd(HoneyPotCommand): self.exit() def lineReceived(self, line): - log.msg( 'INPUT (passwd):', line ) + #log.msg( 'INPUT (passwd):', line ) + log.msg( eventid='KIPP0008', realm='passwd', input=line, + format='INPUT (%(realm)s): %(input)s' ) self.password = line.strip() self.callbacks.pop(0)(line) commands['/usr/bin/passwd'] = command_passwd @@ -428,7 +430,9 @@ class command_perl(HoneyPotCommand): self.exit() def lineReceived(self, line): - log.msg( 'INPUT (perl):', line ) + #log.msg( 'INPUT (perl):', line ) + log.msg( eventid='KIPP0008', realm='perl', input=line, + format='INPUT (%(realm)s): %(input)s' ) commands['/usr/bin/perl'] = command_perl @@ -492,7 +496,9 @@ class command_php(HoneyPotCommand): self.exit() def lineReceived(self, line): - log.msg( 'INPUT (php):', line ) + #log.msg( 'INPUT (php):', line ) + log.msg( eventid='KIPP0008', realm='php', input=line, + format='INPUT (%(realm)s): %(input)s' ) commands['/usr/bin/php'] = command_php diff --git a/kippo/commands/uname.py b/kippo/commands/uname.py index 21227d4..5070fa4 100644 --- a/kippo/commands/uname.py +++ b/kippo/commands/uname.py @@ -12,6 +12,8 @@ class command_uname(HoneyPotCommand): self.honeypot.hostname) elif len(self.args) and self.args[0].strip() in ('-r', '--kernel-release'): self.writeln( '2.6.26-2-686' ) + elif len(self.args) and self.args[0].strip() in ('-m', '--machine'): + self.writeln( 'i686' ) else: self.writeln('Linux') diff --git a/kippo/commands/wget.py b/kippo/commands/wget.py index e915169..ae721d7 100644 --- a/kippo/commands/wget.py +++ b/kippo/commands/wget.py @@ -143,29 +143,34 @@ class command_wget(HoneyPotCommand): def success(self, data, outfile): if not os.path.isfile(self.safeoutfile): - print "there's no file " + self.safeoutfile + log.msg("there's no file " + self.safeoutfile) self.exit() shasum = hashlib.sha256(open(self.safeoutfile, 'rb').read()).hexdigest() hash_path = '%s/%s' % (self.download_path, shasum) - msg = 'SHA sum %s of URL %s in file %s' % \ - (shasum, self.url, self.fileName) - print msg - self.honeypot.logDispatch(msg) - + # if we have content already, delete temp file if not os.path.exists(hash_path): - print "moving " + self.safeoutfile + " -> " + hash_path os.rename(self.safeoutfile, hash_path) else: - print "deleting " + self.safeoutfile + " SHA sum: " + shasum os.remove(self.safeoutfile) + log.msg("Not storing duplicate content " + shasum) + + self.honeypot.logDispatch( format='Downloaded URL (%(url)s) with SHA-256 %(shasum)s to %(outfile)s', + eventid='KIPP0007', url=self.url, outfile=hash_path, shasum=shasum ) + + log.msg( format='Downloaded URL (%(url)s) with SHA-256 %(shasum)s to %(outfile)s', + eventid='KIPP0007', url=self.url, outfile=hash_path, shasum=shasum ) + + # link friendly name to hash os.symlink( shasum, self.safeoutfile ) + + # FIXME: is this necessary? self.safeoutfile = hash_path - print "Updating realfile to " + hash_path + # update the honeyfs to point to downloaded file f = self.fs.getfile(outfile) - f[9] = hash_path + f[A_REALFILE] = hash_path self.exit() def error(self, error, url): @@ -224,10 +229,6 @@ class HTTPProgressDownloader(client.HTTPDownloader): (self.wget.url,) ) self.fileName = os.path.devnull self.nomore = True - else: - msg = 'Saving URL (%s) to %s' % (self.wget.url, self.fileName) - self.wget.honeypot.logDispatch(msg) - log.msg( msg ) self.wget.writeln('Saving to: `%s' % self.fakeoutfile) self.wget.honeypot.terminal.nextLine() diff --git a/kippo/core/auth.py b/kippo/core/auth.py index 3bb2a79..f1e3eed 100644 --- a/kippo/core/auth.py +++ b/kippo/core/auth.py @@ -16,7 +16,7 @@ from twisted.python import log, failure from twisted.conch import error from twisted.conch.ssh import keys -from kippo.core.config import config +from config import config # by Walter de Jong class UserDB(object): @@ -156,12 +156,18 @@ class HoneypotPasswordChecker: return defer.succeed(username) return defer.fail(UnauthorizedLogin()) - def checkUserPass(self, username, password): - if UserDB().checklogin(username, password): - log.msg( 'login attempt [%s/%s] succeeded' % (username, password) ) + def checkUserPass(self, theusername, thepassword): + if UserDB().checklogin(theusername, thepassword): + #log.msg( 'login attempt [%s/%s] succeeded' % (theusername, thepassword) ) + log.msg( eventid='KIPP0002', + format='login attempt [%(username)s/%(password)s] succeeded', + username=theusername, password=thepassword ) return True else: - log.msg( 'login attempt [%s/%s] failed' % (username, password) ) + #log.msg( 'login attempt [%s/%s] failed' % (theusername, thepassword) ) + log.msg( eventid='KIPP0003', + format='login attempt [%(username)s/%(password)s] failed', + username=theusername, password=thepassword ) return False # vim: set sw=4 et: diff --git a/kippo/core/dblog.py b/kippo/core/dblog.py index 57106e7..f08b69e 100644 --- a/kippo/core/dblog.py +++ b/kippo/core/dblog.py @@ -3,59 +3,60 @@ import re import time +import abc + +# dblog now operates based on eventids, no longer on regex parsing of the entry. +# add an eventid using keyword args and it will be picked up by the dblogger +# the KIPPxxxx naming convention is still subject to change. + +# KIPP0001 : create session +# KIPP0002 : succesful login +# KIPP0003 : failed login +# KIPP0004 : TTY log opened +# KIPP0005 : handle command +# KIPP0006 : handle unknown command +# KIPP0007 : file download +# KIPP0008 : INPUT +# KIPP0009 : SSH Version +# KIPP0010 : Terminal Size +# KIPP0011 : Connection Lost +# KIPP0012 : TTY log closed class DBLogger(object): + __metaclass__ = abc.ABCMeta + def __init__(self, cfg): self.cfg = cfg self.sessions = {} self.ttylogs = {} - self.re_connected = re.compile( - '^New connection: ([0-9.]+):([0-9]+) \(([0-9.]+):([0-9]+)\) ' + \ - '\[session: ([0-9]+)\]$') self.re_sessionlog = re.compile( '.*HoneyPotTransport,([0-9]+),[0-9.]+$') - # :dispatch: means the message has been delivered directly via - # logDispatch, instead of relying on the twisted logging, which breaks - # on scope changes. - self.re_map = [(re.compile(x[0]), x[1]) for x in ( - ('^connection lost$', - self._connectionLost), - ('^login attempt \[(?P.*)/(?P.*)\] failed', - self.handleLoginFailed), - ('^login attempt \[(?P.*)/(?P.*)\] succeeded', - self.handleLoginSucceeded), - ('^Opening TTY log: (?P.*)$', - self.handleTTYLogOpened), - ('^:dispatch: Command found: (?P.*)$', - self.handleCommand), - ('^:dispatch: Command not found: (?P.*)$', - self.handleUnknownCommand), - ('^:dispatch: Saving URL \((?P.*)\) to (?P.*)$', - self.handleFileDownload), - ('^:dispatch: SHA sum (?P.*) of URL (?P.*) in file (?P.*)$', - self.handleShaSum), - ('^:dispatch: Updated outfile (?P.*) to (?P.*) with SHA sum (?P.*)$', - self.handleUpdatedFile), - ('^INPUT \((?P[a-zA-Z0-9]+)\): (?P.*)$', - self.handleInput), - ('^Terminal size: (?P[0-9]+) (?P[0-9]+)$', - self.handleTerminalSize), - ('^Remote SSH version: (?P.*)$', - self.handleClientVersion), - )] + # KIPP0001 is special since it kicks off new logging session, + # and is not handled here + self.events = { + 'KIPP0002': self.handleLoginSucceeded, + 'KIPP0003': self.handleLoginFailed, + 'KIPP0004': self.handleTTYLogOpened, + 'KIPP0005': self.handleCommand, + 'KIPP0006': self.handleUnknownCommand, + 'KIPP0007': self.handleFileDownload, + 'KIPP0008': self.handleInput, + 'KIPP0009': self.handleClientVersion, + 'KIPP0010': self.handleTerminalSize, + 'KIPP0011': self._connectionLost, + } + self.start(cfg) - def logDispatch(self, sessionid, msg): - if sessionid not in self.sessions.keys(): - return - for regex, func in self.re_map: - match = regex.match(msg) - if match: - func(self.sessions[sessionid], match.groupdict()) - break + # used when the HoneypotTransport prefix is not available. + def logDispatch(self, *msg, **kw): + ev = kw + ev['message'] = msg + self.emit(ev) - def start(): + def start(self, cfg): + """Hook that can be used to set up connections in dbloggers""" pass def getSensor(self): @@ -68,28 +69,42 @@ class DBLogger(object): return int(time.mktime(time.gmtime()[:-1] + (-1,))) def emit(self, ev): - if not len(ev['message']): + # ignore stdout and stderr + if 'printed' in ev: return - match = self.re_connected.match(ev['message'][0]) - if match: - sessionid = int(match.groups()[4]) - self.sessions[sessionid] = \ + + # ignore anything without eventid + if not 'eventid' in ev: + return + + # connection event is special. adds to list + if ev['eventid'] == 'KIPP0001': + sessionno = ev['sessionno'] + self.sessions[sessionno] = \ self.createSession( - match.groups()[0], int(match.groups()[1]), - match.groups()[2], int(match.groups()[3])) + ev['src_ip'], ev['src_port'], ev['dst_ip'], ev['dst_port'] ) return - match = self.re_sessionlog.match(ev['system']) - if not match: + + # use explicit sessionno if coming from dispatch + if 'sessionno' in ev: + sessionno = ev['sessionno'] + del ev['sessionno'] + # else extract session id from the twisted log prefix + elif 'system' in ev: + match = self.re_sessionlog.match(ev['system']) + if not match: + return + sessionno = int(match.groups()[0]) + + if sessionno not in self.sessions.keys(): return - sessionid = int(match.groups()[0]) - if sessionid not in self.sessions.keys(): - return - message = ev['message'][0] - for regex, func in self.re_map: - match = regex.match(message) - if match: - func(self.sessions[sessionid], match.groupdict()) - break + + if 'eventid' in ev: + if ev['eventid'] in self.events: + self.events[ev['eventid']]( self.sessions[sessionno], ev ) + return + + print "error, unknown eventid %s" % repr(ev) def _connectionLost(self, session, args): self.handleConnectionLost(session, args) @@ -106,7 +121,8 @@ class DBLogger(object): f.close() return ttylog - # We have to return an unique ID + # We have to return a unique ID + @abc.abstractmethod def createSession(self, peerIP, peerPort, hostIP, hostPort): return 0 @@ -150,12 +166,4 @@ class DBLogger(object): def handleFileDownload(self, session, args): pass - # args has: shasum, url, outfile - def handleShaSum(self, session, args): - pass - - # args has: outfile, dl_file, shasum - def handleUpdatedFile(self, session, args): - pass - # vim: set sw=4 et: diff --git a/kippo/core/fs.py b/kippo/core/fs.py index 0dc9c59..ffcf257 100644 --- a/kippo/core/fs.py +++ b/kippo/core/fs.py @@ -8,7 +8,7 @@ import re import stat import errno -from kippo.core.config import config +from config import config A_NAME, \ A_TYPE, \ diff --git a/kippo/core/honeypot.py b/kippo/core/honeypot.py index 3fc8a65..7b67453 100644 --- a/kippo/core/honeypot.py +++ b/kippo/core/honeypot.py @@ -9,9 +9,10 @@ import copy import pickle from twisted.python import log -from kippo.core import fs -from kippo.core.config import config -import kippo.core.exceptions + +import fs +import exceptions +from config import config class HoneyPotCommand(object): def __init__(self, protocol, *args): @@ -124,12 +125,15 @@ class HoneyPotShell(object): rargs.append(arg) cmdclass = self.honeypot.getCommand(cmd, envvars['PATH'].split(':')) if cmdclass: - log.msg( 'Command found: %s' % (line,) ) - self.honeypot.logDispatch('Command found: %s' % (line,)) + #log.msg( 'Command found: %s' % (line,) ) + log.msg( eventid='KIPP0005', input=line, format='Command found: %(input)s' ) + #self.honeypot.logDispatch('Command found: %s' % (line,)) self.honeypot.call_command(cmdclass, *rargs) else: - self.honeypot.logDispatch('Command not found: %s' % (line,)) - log.msg( 'Command not found: %s' % (line,) ) + #log.msg( 'Command not found: %s' % (line,) ) + log.msg( eventid='KIPP0006', + input=line, format='Command not found: %(input)s' ) + #self.honeypot.logDispatch('Command not found: %s' % (line,)) if len(line): self.honeypot.writeln('bash: %s: command not found' % cmd) runOrPrompt() diff --git a/kippo/core/interact.py b/kippo/core/interact.py index 0dcf860..0079b50 100644 --- a/kippo/core/interact.py +++ b/kippo/core/interact.py @@ -6,7 +6,7 @@ import time from twisted.internet import protocol from twisted.conch import telnet, recvline -from kippo.core import ttylog +import ttylog class Interact(telnet.Telnet): diff --git a/kippo/core/protocol.py b/kippo/core/protocol.py index 0f552f3..73eedbb 100644 --- a/kippo/core/protocol.py +++ b/kippo/core/protocol.py @@ -3,20 +3,16 @@ import os import time -import struct import socket import copy from twisted.conch import recvline -from twisted.conch.ssh import transport from twisted.conch.insults import insults -from twisted.internet import protocol from twisted.python import log -from kippo.core import ttylog, fs -from kippo.core.config import config -import kippo.core.honeypot -from kippo import core +import honeypot +import ttylog +from config import config class HoneyPotBaseProtocol(insults.TerminalProtocol): def __init__(self, avatar, env): @@ -33,10 +29,10 @@ class HoneyPotBaseProtocol(insults.TerminalProtocol): self.password_input = False self.cmdstack = [] - def logDispatch(self, msg): + def logDispatch(self, *msg, **args): transport = self.terminal.transport.session.conn.transport - msg = ':dispatch: ' + msg - transport.factory.logDispatch(transport.transport.sessionno, msg) + args['sessionno']=transport.transport.sessionno + transport.factory.logDispatch(*msg,**args) def connectionMade(self): self.displayMOTD() @@ -47,7 +43,6 @@ class HoneyPotBaseProtocol(insults.TerminalProtocol): self.realClientPort = transport.transport.getPeer().port 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() @@ -61,7 +56,7 @@ class HoneyPotBaseProtocol(insults.TerminalProtocol): else: # Hack to get ip s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.connect(("8.8.8.8",80)) + s.connect(("8.8.8.8", 80)) self.kippoIP = s.getsockname()[0] s.close() @@ -71,17 +66,16 @@ class HoneyPotBaseProtocol(insults.TerminalProtocol): except: pass - # this doesn't seem to be called upon disconnect, so please use - # HoneyPotTransport.connectionLost instead + # this is only called on explicit logout, not on disconnect def connectionLost(self, reason): - pass + log.msg( eventid='KIPP0011', format='Connection lost') # 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(core.honeypot.HoneyPotCommand): + class command_txtcmd(honeypot.HoneyPotCommand): def call(self): log.msg( 'Reading txtcmd from "%s"' % txt ) f = file(txt, 'r') @@ -149,9 +143,9 @@ class HoneyPotExecProtocol(HoneyPotBaseProtocol): def connectionMade(self): HoneyPotBaseProtocol.connectionMade(self) - self.terminal.transport.session.conn.transport.stdinlog_open = True + self.terminal.stdinlog_open = True - self.cmdstack = [core.honeypot.HoneyPotShell(self, interactive=False)] + self.cmdstack = [honeypot.HoneyPotShell(self, interactive=False)] self.cmdstack[0].lineReceived(self.execcmd) class HoneyPotInteractiveProtocol(HoneyPotBaseProtocol, recvline.HistoricRecvLine): @@ -164,7 +158,7 @@ class HoneyPotInteractiveProtocol(HoneyPotBaseProtocol, recvline.HistoricRecvLin HoneyPotBaseProtocol.connectionMade(self) recvline.HistoricRecvLine.connectionMade(self) - self.cmdstack = [core.honeypot.HoneyPotShell(self)] + self.cmdstack = [honeypot.HoneyPotShell(self)] transport = self.terminal.transport.session.conn.transport transport.factory.sessions[transport.transport.sessionno] = self @@ -234,22 +228,27 @@ class HoneyPotInteractiveProtocol(HoneyPotBaseProtocol, recvline.HistoricRecvLin self.lineBuffer = self.lineBuffer[self.lineBufferIndex:] self.lineBufferIndex = 0 - class LoggingServerProtocol(insults.ServerProtocol): + """ + Wrapper for ServerProtocol that implements TTY logging + """ 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'), transport.transportId ) - log.msg( 'Opening TTY log: %s' % transport.ttylog_file ) - ttylog.ttylog_open(transport.ttylog_file, time.time()) - transport.ttylog_open = True - transport.stdinlog_file = '%s/%s-%s-stdin.log' % \ + self.ttylog_file = transport.ttylog_file + log.msg( eventid='KIPP0004', logfile=transport.ttylog_file, + format='Opening TTY Log: %(logfile)s') + + ttylog.ttylog_open(transport.ttylog_file, time.time()) + self.ttylog_open = True + + self.stdinlog_file = '%s/%s-%s-stdin.log' % \ (config().get('honeypot', 'download_path'), time.strftime('%Y%m%d-%H%M%S'), transport.transportId ) - transport.stdinlog_open = False + self.stdinlog_open = False insults.ServerProtocol.connectionMade(self) @@ -257,25 +256,39 @@ class LoggingServerProtocol(insults.ServerProtocol): transport = self.transport.session.conn.transport for i in transport.interactors: i.sessionWrite(bytes) - if transport.ttylog_open and not noLog: + if self.ttylog_open and not noLog: ttylog.ttylog_write(transport.ttylog_file, len(bytes), ttylog.TYPE_OUTPUT, time.time(), bytes) + insults.ServerProtocol.write(self, bytes) def dataReceived(self, data, noLog = False): transport = self.transport.session.conn.transport - if transport.ttylog_open and not noLog: + if self.ttylog_open and not noLog: ttylog.ttylog_write(transport.ttylog_file, len(data), ttylog.TYPE_INPUT, time.time(), data) - if transport.stdinlog_open and not noLog: - f = file( transport.stdinlog_file, 'ab' ) + if self.stdinlog_open and not noLog: + log.msg( "Saving stdin log: %s" % self.stdinlog_file ) + f = file( self.stdinlog_file, 'ab' ) f.write(data) f.close + insults.ServerProtocol.dataReceived(self, data) - # this doesn't seem to be called upon disconnect, so please use - # HoneyPotTransport.connectionLost instead + # override super to remove the terminal reset on logout + def loseConnection(self): + self.transport.loseConnection() + + # FIXME: this method is called 4 times on logout.... + # it's called once from Avatar.closed() if disconnected def connectionLost(self, reason): + # log.msg( "received call to LSP.connectionLost" ) + transport = self.transport.session.conn.transport + if self.ttylog_open: + log.msg( eventid='KIPP0012', format='Closing TTY Log: %(ttylog)s', + ttylog=transport.ttylog_file) + ttylog.ttylog_close(transport.ttylog_file, time.time()) + self.ttylog_open = False insults.ServerProtocol.connectionLost(self, reason) # vim: set sw=4 et: diff --git a/kippo/core/ssh.py b/kippo/core/ssh.py index a49c459..1f43035 100644 --- a/kippo/core/ssh.py +++ b/kippo/core/ssh.py @@ -21,13 +21,16 @@ from twisted.conch.ssh.common import NS, getNS import ConfigParser -from kippo.core import ttylog, utils, fs, sshserver -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 +import utils +import fs +import sshserver +import auth +import honeypot +import ssh +import protocol +import sshserver +import exceptions +from config import config class HoneyPotSSHUserAuthServer(userauth.SSHUserAuthServer): def serviceStarted(self): @@ -63,11 +66,11 @@ class HoneyPotSSHFactory(factory.SSHFactory): } # Special delivery to the loggers to avoid scope problems - def logDispatch(self, sessionid, msg): + def logDispatch(self, *msg, **args): for dblog in self.dbloggers: - dblog.logDispatch(sessionid, msg) + dblog.logDispatch(*msg, **args) for output in self.output_plugins: - output.logDispatch(sessionid, msg) + output.logDispatch(*msg, **args) def __init__(self): cfg = config() @@ -168,7 +171,7 @@ class HoneyPotRealm: def __init__(self): # I don't know if i'm supposed to keep static stuff here - self.env = core.honeypot.HoneyPotEnvironment() + self.env = honeypot.HoneyPotEnvironment() def requestAvatar(self, avatarId, mind, *interfaces): if conchinterfaces.IConchUser in interfaces: @@ -177,43 +180,35 @@ class HoneyPotRealm: else: raise Exception, "No supported interfaces found." -class HoneyPotTransport(kippo.core.sshserver.KippoSSHServerTransport): +class HoneyPotTransport(sshserver.KippoSSHServerTransport): """ - @ivar logintime: time of login - - @ivar interactors: interactors - - @ivar ttylog_open: whether log is open - - @ivar transportId: UUID of this transport - - @ivar _hadVersion: used so we only send key exchange after receive version info """ - _hadVersion = False - ttylog_open = False - interactors = [] - transportId = '' - def connectionMade(self): self.logintime = time.time() self.transportId = uuid.uuid4().hex[:8] + self.interactors = [] - log.msg( '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) ) + #log.msg( '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) ) + log.msg( eventid='KIPP0001', + format='New connection: %(src_ip)s:%(src_port)s (%(dst_ip)s:%(dst_port)s) [session: %(sessionno)s]', + src_ip=self.transport.getPeer().host, src_port=self.transport.getPeer().port, + dst_ip=self.transport.getHost().host, dst_port=self.transport.getHost().port, + sessionno=self.transport.sessionno ) - kippo.core.sshserver.KippoSSHServerTransport.connectionMade(self) + sshserver.KippoSSHServerTransport.connectionMade(self) def sendKexInit(self): # Don't send key exchange prematurely if not self.gotVersion: return - kippo.core.sshserver.KippoSSHServerTransport.sendKexInit(self) + sshserver.KippoSSHServerTransport.sendKexInit(self) def dataReceived(self, data): - kippo.core.sshserver.KippoSSHServerTransport.dataReceived(self, data) + 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: @@ -230,8 +225,8 @@ class HoneyPotTransport(kippo.core.sshserver.KippoSSHServerTransport): log.msg('KEXINIT: client supported MAC: %s' % macCS ) log.msg('KEXINIT: client supported compression: %s' % compCS ) log.msg('KEXINIT: client supported lang: %s' % langCS ) - log.msg( 'Remote SSH version: %s' % self.otherVersionString,) - return kippo.core.sshserver.KippoSSHServerTransport.ssh_KEXINIT(self, packet) + log.msg( eventid='KIPP0009', version=self.otherVersionString, format='Remote SSH version: %(version)s' ) + return sshserver.KippoSSHServerTransport.ssh_KEXINIT(self, packet) def lastlogExit(self): starttime = time.strftime('%a %b %d %H:%M', @@ -245,15 +240,13 @@ class HoneyPotTransport(kippo.core.sshserver.KippoSSHServerTransport): # this seems to be the only reliable place of catching lost connection def connectionLost(self, reason): + log.msg( "Connection Lost in SSH Transport" ) 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 - kippo.core.sshserver.KippoSSHServerTransport.connectionLost(self, reason) + sshserver.KippoSSHServerTransport.connectionLost(self, reason) class HoneyPotSSHSession(session.SSHSession): @@ -277,10 +270,17 @@ class HoneyPotSSHSession(session.SSHSession): log.msg('request_x11: %s' % repr(data) ) return 0 + # this is reliably called on session close/disconnect and calls the avatar + def closed(self): + session.SSHSession.closed(self) + def loseConnection(self): self.conn.sendRequest(self, 'exit-status', "\x00"*4) session.SSHSession.loseConnection(self) + def channelClosed(self): + log.msg( "Called channelClosed in SSHSession") + # FIXME: recent twisted conch avatar.py uses IConchuser here @implementer(conchinterfaces.ISession) class HoneyPotAvatar(avatar.ConchUser): @@ -291,6 +291,7 @@ class HoneyPotAvatar(avatar.ConchUser): self.env = env self.fs = fs.HoneyPotFilesystem(copy.deepcopy(self.env.fs)) self.hostname = self.env.cfg.get('honeypot', 'hostname') + self.protocol = None self.channelLookup.update({'session': HoneyPotSSHSession}) self.channelLookup['direct-tcpip'] = KippoOpenConnectForwardingClient @@ -300,41 +301,52 @@ class HoneyPotAvatar(avatar.ConchUser): if ( self.env.cfg.get('honeypot', 'sftp_enabled') == "true" ): self.subsystemLookup['sftp'] = filetransfer.FileTransferServer - self.uid = self.gid = core.auth.UserDB().getUID(self.username) + self.uid = self.gid = auth.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 openShell(self, proto): + serverProtocol = protocol.LoggingServerProtocol( + protocol.HoneyPotInteractiveProtocol, self, self.env) + self.protocol = serverProtocol + serverProtocol.makeConnection(proto) + proto.makeConnection(session.wrapProtocol(serverProtocol)) + #self.protocol = serverProtocol + self.protocol = proto def getPty(self, terminal, windowSize, attrs): - log.msg( 'Terminal size: %s %s' % windowSize[0:2] ) + #log.msg( 'Terminal size: %s %s' % windowSize[0:2] ) + log.msg( eventid='KIPP0010', width=windowSize[0], height=windowSize[1], + format='Terminal Size: %(width)s %(height)s' ) + self.windowSize = windowSize return None - def execCommand(self, protocol, cmd): + def execCommand(self, proto, cmd): cfg = config() if not cfg.has_option('honeypot', 'exec_enabled') or \ cfg.get('honeypot', 'exec_enabled').lower() not in \ ('yes', 'true', 'on'): log.msg( 'Exec disabled. Not executing command: "%s"' % cmd ) - raise core.exceptions.NotEnabledException, \ + raise exceptions.NotEnabledException, \ 'exec_enabled not enabled in configuration file!' return log.msg( '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)) + serverProtocol = protocol.LoggingServerProtocol( + protocol.HoneyPotExecProtocol, self, self.env, cmd) + self.protocol = serverProtocol + serverProtocol.makeConnection(proto) + proto.makeConnection(session.wrapProtocol(serverProtocol)) + self.protocol = serverProtocol + # this is reliably called on both logout and disconnect + # we notify the protocol here we lost the connection def closed(self): - pass + if self.protocol: + self.protocol.connectionLost("disconnected") def eofReceived(self): pass diff --git a/kippo/core/utils.py b/kippo/core/utils.py index 8c3bc53..ab4f99a 100644 --- a/kippo/core/utils.py +++ b/kippo/core/utils.py @@ -1,7 +1,7 @@ # Copyright (c) 2010-2014 Upi Tamminen # See the COPYRIGHT file for more information -from kippo.core.config import config +from config import config def addToLastlog(message): f = file('%s/lastlog.txt' % config().get('honeypot', 'data_path'), 'a') diff --git a/kippo/dblog/jsonlog.py b/kippo/dblog/jsonlog.py index 52c779d..7d70b6b 100644 --- a/kippo/dblog/jsonlog.py +++ b/kippo/dblog/jsonlog.py @@ -1,19 +1,44 @@ +# Copyright (c) 2015 Michel Oosterhof +# All rights reserved. # -# this module uses the dblog feature to create a JSON logfile -# ..so not exactly a dblog. +# 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. import datetime import uuid import json -from kippo.core import dblog -from twisted.enterprise import adbapi -from twisted.internet import defer -from twisted.python import log +from ..core import dblog class DBLogger(dblog.DBLogger): + def __init__(self, cfg): + self.sensor = "" + self.outfile = "" + dblog.DBLogger.__init__(self, cfg) + def start(self, cfg): self.outfile = file(cfg.get('database_jsonlog', 'logfile'), 'a') @@ -38,6 +63,9 @@ class DBLogger(dblog.DBLogger): def handleConnectionLost(self, session, args): logentry = { 'message': 'Connection lost' } self.write( session, logentry ) + #ttylog = self.ttylog(session) + #if ttylog: + # self.write( session, { 'message': repr(ttylog) } ) def handleLoginFailed(self, session, args): logentry = { 'message' : 'Login failed [%s/%s]' % (args['username'], args['password']), 'username' : args['username'], 'password' : args['password'] } @@ -68,12 +96,7 @@ class DBLogger(dblog.DBLogger): self.write( session, logentry ) def handleFileDownload(self, session, args): - logentry = { 'message' : 'File download: [%s] -> %s' % (args['url'], args['outfile']), 'url' : args['url'] } - self.write( session, logentry ) - - def handleShaSum(self, session, args): - logentry = { 'message' : 'File SHA sum: %s [%s] -> %s' % \ - (args['shasum'], args['url'], args['outfile']), 'shasum' : args['shasum'], 'url' : args['url'] } + logentry = { 'message' : 'File download: [%s] -> %s' % (args['url'], args['outfile']), 'url' : args['url'], 'shasum' : args['shasum'] } self.write( session, logentry ) # vim: set sw=4 et: diff --git a/kippo/dblog/mysql.py b/kippo/dblog/mysql.py index 612751d..0e049a4 100644 --- a/kippo/dblog/mysql.py +++ b/kippo/dblog/mysql.py @@ -142,13 +142,8 @@ class DBLogger(dblog.DBLogger): def handleFileDownload(self, session, args): self.simpleQuery('INSERT INTO `downloads`' + \ - ' (`session`, `timestamp`, `url`, `outfile`)' + \ + ' (`session`, `timestamp`, `url`, `outfile`, `shasum`)' + \ ' VALUES (%s, FROM_UNIXTIME(%s), %s, %s)', - (session, self.nowUnix(), args['url'], args['outfile'])) - - def handleShaSum(self, session, args): - self.simpleQuery('UPDATE `downloads` SET `shasum` = %s' + \ - ' WHERE `outfile` = %s', - (args['shasum'], args['outfile'])) + (session, self.nowUnix(), args['url'], args['outfile'], args['shasum'])) # vim: set sw=4 et: diff --git a/kippo/dblog/textlog.py b/kippo/dblog/textlog.py index d49810e..9f8169c 100644 --- a/kippo/dblog/textlog.py +++ b/kippo/dblog/textlog.py @@ -50,15 +50,7 @@ class DBLogger(dblog.DBLogger): self.write(session, 'Client version: [%s]' % (args['version'],)) def handleFileDownload(self, session, args): - self.write(session, 'File download: [%s] -> %s' % \ - (args['url'], args['outfile'])) - - def handleShaSum(self, session, args): - self.write(session, 'File SHA sum: %s [%s] -> %s' % \ - (args['shasum'], args['url'], args['outfile'])) - - def handleUpdatedFile(self, session, args): - self.write(session, 'Updated wget outfile %s to %s' % \ - (args['outfile'], args['dl_file'])) + self.write(session, 'File download: [%s] -> %s with SHA-256 %s' % \ + (args['url'], args['outfile'], args['shasum'])) # vim: set sw=4 et: diff --git a/start.sh b/start.sh index f9a4d9d..8a0289e 100755 --- a/start.sh +++ b/start.sh @@ -24,7 +24,5 @@ then . $VENV/bin/activate fi -twistd --version - echo "Starting kippo in the background..." twistd -y kippo.tac -l log/kippo.log --pidfile kippo.pid