From 88d377cfc6b42bc02f5225d467ed51e7ccec528e Mon Sep 17 00:00:00 2001 From: desaster Date: Sun, 22 Nov 2009 07:07:58 +0000 Subject: [PATCH] move core/ and commands/ to kippo/ git-svn-id: https://kippo.googlecode.com/svn/trunk@50 951d7100-d841-11de-b865-b3884708a8e2 --- kippo.tac | 4 +- kippo/__init__.py | 0 kippo/commands/__init__.py | 12 ++ kippo/commands/base.py | 293 +++++++++++++++++++++++++++++++++++ kippo/commands/dice.py | 45 ++++++ kippo/commands/ls.py | 121 +++++++++++++++ kippo/commands/ping.py | 52 +++++++ kippo/commands/ssh.py | 63 ++++++++ kippo/commands/tar.py | 66 ++++++++ kippo/commands/wget.py | 194 ++++++++++++++++++++++++ kippo/core/__init__.py | 0 kippo/core/config.py | 14 ++ kippo/core/fs.py | 126 +++++++++++++++ kippo/core/honeypot.py | 303 +++++++++++++++++++++++++++++++++++++ kippo/core/ttylog.py | 30 ++++ 15 files changed, 1321 insertions(+), 2 deletions(-) create mode 100644 kippo/__init__.py create mode 100644 kippo/commands/__init__.py create mode 100644 kippo/commands/base.py create mode 100644 kippo/commands/dice.py create mode 100644 kippo/commands/ls.py create mode 100644 kippo/commands/ping.py create mode 100644 kippo/commands/ssh.py create mode 100644 kippo/commands/tar.py create mode 100644 kippo/commands/wget.py create mode 100644 kippo/core/__init__.py create mode 100644 kippo/core/config.py create mode 100644 kippo/core/fs.py create mode 100644 kippo/core/honeypot.py create mode 100644 kippo/core/ttylog.py diff --git a/kippo.tac b/kippo.tac index b30a175e..a9534c72 100644 --- a/kippo.tac +++ b/kippo.tac @@ -13,8 +13,8 @@ from twisted.internet import reactor, defer from twisted.application import internet, service from twisted.cred import portal from twisted.conch.ssh import factory, keys -from core import honeypot -from core.config import config +from kippo.core import honeypot +from kippo.core.config import config factory = honeypot.HoneyPotSSHFactory() factory.portal = portal.Portal(honeypot.HoneyPotRealm()) diff --git a/kippo/__init__.py b/kippo/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/kippo/commands/__init__.py b/kippo/commands/__init__.py new file mode 100644 index 00000000..4fd9930e --- /dev/null +++ b/kippo/commands/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) 2009 Upi Tamminen +# See the COPYRIGHT file for more information + +__all__ = [ + 'base', + 'ls', + 'ping', + 'ssh', + 'tar', + 'wget', + 'dice', + ] diff --git a/kippo/commands/base.py b/kippo/commands/base.py new file mode 100644 index 00000000..79637ec7 --- /dev/null +++ b/kippo/commands/base.py @@ -0,0 +1,293 @@ +# Copyright (c) 2009 Upi Tamminen +# See the COPYRIGHT file for more information + +import os, time +from kippo.core.honeypot import HoneyPotCommand +from kippo.core.fs import * +from twisted.internet import reactor +import config + +commands = {} + +class command_whoami(HoneyPotCommand): + def call(self): + self.writeln(self.honeypot.user.username) +commands['/usr/bin/whoami'] = command_whoami + +class command_cat(HoneyPotCommand): + def call(self): + for arg in self.args: + path = self.fs.resolve_path(arg, self.honeypot.cwd) + if not path or not self.fs.exists(path): + self.writeln('bash: cat: %s: No such file or directory' % arg) + return + f = self.fs.getfile(path) + + realfile = self.fs.realfile(f, + '%s/%s' % (config.contents_path, path)) + if realfile: + f = file(realfile, 'rb') + self.write(f.read()) + f.close() +commands['/bin/cat'] = command_cat + +class command_cd(HoneyPotCommand): + def call(self): + if not self.args: + path = '/root' + else: + path = self.args[0] + try: + newpath = self.fs.resolve_path(path, self.honeypot.cwd) + newdir = self.fs.get_path(newpath) + except IndexError: + newdir = None + if newdir is None: + self.writeln('bash: cd: %s: No such file or directory' % path) + return + if not self.fs.is_dir(newpath): + self.writeln('-bash: cd: %s: Not a directory' % path) + return + self.honeypot.cwd = newpath +commands['cd'] = command_cd + +class command_rm(HoneyPotCommand): + def call(self): + for f in self.args: + path = self.fs.resolve_path(f, self.honeypot.cwd) + try: + dir = self.fs.get_path('/'.join(path.split('/')[:-1])) + except IndexError: + self.writeln( + 'rm: cannot remove `%s\': No such file or directory' % f) + continue + basename = path.split('/')[-1] + contents = [x for x in dir] + for i in dir[:]: + if i[A_NAME] == basename: + if i[A_TYPE] == T_DIR: + self.writeln( + 'rm: cannot remove `%s\': Is a directory' % \ + i[A_NAME]) + else: + dir.remove(i) +commands['/bin/rm'] = command_rm + +class command_mkdir(HoneyPotCommand): + def call(self): + for f in self.args: + path = self.fs.resolve_path(f, self.honeypot.cwd) + if self.fs.exists(path): + self.writeln( + 'mkdir: cannot create directory `%s\': File exists' % f) + return + ok = self.fs.mkdir(path, 0, 0, 4096, 16877) + if not ok: + self.writeln( + 'mkdir: cannot create directory `%s\': ' % f + \ + 'No such file or directory') + return +commands['/bin/mkdir'] = command_mkdir + +class command_rmdir(HoneyPotCommand): + def call(self): + for f in self.args: + path = self.fs.resolve_path(f, self.honeypot.cwd) + if len(self.fs.get_path(path)): + self.writeln( + 'rmdir: failed to remove `%s\': Directory not empty' % f) + continue + try: + dir = self.fs.get_path('/'.join(path.split('/')[:-1])) + except IndexError: + dir = None + if not dir or f not in [x[A_NAME] for x in dir]: + self.writeln( + 'rmdir: failed to remove `%s\': ' % f + \ + 'No such file or directory') + continue + for i in dir[:]: + if i[A_NAME] == f: + dir.remove(i) +commands['/bin/rmdir'] = command_rmdir + +class command_uptime(HoneyPotCommand): + def call(self): + self.writeln( + ' %s up 14 days, 3:53, 0 users, load average: 0.08, 0.02, 0.01' % \ + time.strftime('%H:%M:%S')) +commands['/usr/bin/uptime'] = command_uptime + +class command_w(HoneyPotCommand): + def call(self): + self.writeln( + ' %s up 14 days, 3:53, 0 users, load average: 0.08, 0.02, 0.01' % \ + time.strftime('%H:%M:%S')) + self.writeln('USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT') +commands['/usr/bin/w'] = command_w +commands['/usr/bin/who'] = command_w + +class command_echo(HoneyPotCommand): + def call(self): + self.writeln(' '.join(self.args)) +commands['/bin/echo'] = command_echo + +class command_exit(HoneyPotCommand): + def call(self): + #self.honeypot.terminal.loseConnection() + self.honeypot.terminal.reset() + self.writeln('Connection to server closed.') + self.honeypot.hostname = 'localhost' +commands['exit'] = command_exit + +class command_clear(HoneyPotCommand): + def call(self): + self.honeypot.terminal.reset() +commands['/usr/bin/clear'] = command_clear + +class command_vi(HoneyPotCommand): + def call(self): + self.writeln('E558: Terminal entry not found in terminfo') +commands['/usr/bin/vi'] = command_vi + +class command_hostname(HoneyPotCommand): + def call(self): + self.writeln(self.honeypot.hostname) +commands['/bin/hostname'] = command_hostname + +class command_uname(HoneyPotCommand): + def call(self): + if len(self.args) and self.args[0].strip() == '-a': + self.writeln( + 'Linux %s 2.6.26-2-686 #1 SMP Wed Nov 4 20:45:37 UTC 2009 i686 GNU/Linux' % \ + self.honeypot.hostname) + else: + self.writeln('Linux') +commands['/bin/uname'] = command_uname + +class command_ps(HoneyPotCommand): + def call(self): + if len(self.args) and self.args[0].strip().count('a'): + output = ( + 'USER PID %%CPU %%MEM VSZ RSS TTY STAT START TIME COMMAND', + 'root 1 0.0 0.1 2100 688 ? Ss Nov06 0:07 init [2] ', + 'root 2 0.0 0.0 0 0 ? S< Nov06 0:00 [kthreadd]', + 'root 3 0.0 0.0 0 0 ? S< Nov06 0:00 [migration/0]', + 'root 4 0.0 0.0 0 0 ? S< Nov06 0:00 [ksoftirqd/0]', + 'root 5 0.0 0.0 0 0 ? S< Nov06 0:00 [watchdog/0]', + 'root 6 0.0 0.0 0 0 ? S< Nov06 0:17 [events/0]', + 'root 7 0.0 0.0 0 0 ? S< Nov06 0:00 [khelper]', + 'root 39 0.0 0.0 0 0 ? S< Nov06 0:00 [kblockd/0]', + 'root 41 0.0 0.0 0 0 ? S< Nov06 0:00 [kacpid]', + 'root 42 0.0 0.0 0 0 ? S< Nov06 0:00 [kacpi_notify]', + 'root 170 0.0 0.0 0 0 ? S< Nov06 0:00 [kseriod]', + 'root 207 0.0 0.0 0 0 ? S Nov06 0:01 [pdflush]', + 'root 208 0.0 0.0 0 0 ? S Nov06 0:00 [pdflush]', + 'root 209 0.0 0.0 0 0 ? S< Nov06 0:00 [kswapd0]', + 'root 210 0.0 0.0 0 0 ? S< Nov06 0:00 [aio/0]', + 'root 748 0.0 0.0 0 0 ? S< Nov06 0:00 [ata/0]', + 'root 749 0.0 0.0 0 0 ? S< Nov06 0:00 [ata_aux]', + 'root 929 0.0 0.0 0 0 ? S< Nov06 0:00 [scsi_eh_0]', + 'root 1014 0.0 0.0 0 0 ? D< Nov06 0:03 [kjournald]', + 'root 1087 0.0 0.1 2288 772 ? S +# See the COPYRIGHT file for more information + +# Random commands when running new executables + +from kippo.core.honeypot import HoneyPotCommand + +commands = {} +clist = [] + +class command_orly(HoneyPotCommand): + def start(self): + self.orly() + + def orly(self): + self.writeln(' ___ ') + self.writeln(' {o,o}') + self.writeln(' |)__)') + self.writeln(' -"-"-') + self.write('O RLY? ') + + def lineReceived(self, data): + if data.strip().lower() in ('ya', 'yarly', 'ya rly', 'yes', 'y'): + self.writeln(' ___') + self.writeln(' {o,o}') + self.writeln(' (__(|') + self.writeln(' -"-"-') + self.writeln('NO WAI!') + self.exit() + return + self.orly() +clist.append(command_orly) + +class command_wargames(HoneyPotCommand): + def start(self): + self.write('Shall we play a game? ') + + def lineReceived(self, data): + self.writeln('A strange game. ' + \ + 'The only winning move is not to play. ' + \ + 'How about a nice game of chess?') + self.exit() +clist.append(command_wargames) + +# vim: set sw=4 et: diff --git a/kippo/commands/ls.py b/kippo/commands/ls.py new file mode 100644 index 00000000..eb0fd249 --- /dev/null +++ b/kippo/commands/ls.py @@ -0,0 +1,121 @@ +# Copyright (c) 2009 Upi Tamminen +# See the COPYRIGHT file for more information + +from kippo.core.honeypot import HoneyPotCommand +from kippo.core.fs import * +import stat, time + +commands = {} + +class command_ls(HoneyPotCommand): + + def uid2name(self, uid): + if uid == 0: + return 'root' + return uid + + def gid2name(self, gid): + if gid == 0: + return 'root' + return gid + + def call(self): + path = self.honeypot.cwd + paths = [] + if len(self.args): + for arg in self.args: + if not arg.startswith('-'): + paths.append(self.honeypot.fs.resolve_path(arg, + self.honeypot.cwd)) + + self.show_hidden = False + func = self.do_ls_normal + for x in self.args: + if x.startswith('-') and x.count('l'): + func = self.do_ls_l + if x.startswith('-') and x.count('a'): + self.show_hidden = True + + if not paths: + func(path) + else: + for path in paths: + func(path) + + def do_ls_normal(self, path): + try: + files = self.honeypot.fs.list_files(path) + except: + self.honeypot.writeln( + 'ls: cannot access %s: No such file or directory' % path) + return + if not len(files): + return + l = [x[A_NAME] for x in files \ + if self.show_hidden or not x[A_NAME].startswith('.')] + if not l: + return + count = 0 + maxlen = max([len(x) for x in l]) + perline = int(self.honeypot.user.windowSize[1] / (maxlen + 1)) + if self.show_hidden: + l.insert(0, '..') + l.insert(0, '.') + for f in l: + if count == perline: + count = 0 + self.nextLine() + self.write(f.ljust(maxlen + 1)) + count += 1 + self.nextLine() + + def do_ls_l(self, path): + try: + files = self.honeypot.fs.list_files(path)[:] + except: + self.honeypot.writeln( + 'ls: cannot access %s: No such file or directory' % path) + return + + largest = 0 + if len(files): + largest = max([x[A_SIZE] for x in files]) + + # FIXME: should grab these off the parents instead + files.insert(0, + ['..', T_DIR, 0, 0, 4096, 16877, time.time(), [], None]) + files.insert(0, + ['.', T_DIR, 0, 0, 4096, 16877, time.time(), [], None]) + for file in files: + perms = ['-'] * 10 + + if file[A_MODE] & stat.S_IRUSR: perms[1] = 'r' + if file[A_MODE] & stat.S_IWUSR: perms[2] = 'w' + if file[A_MODE] & stat.S_IXUSR: perms[3] = 'x' + + if file[A_MODE] & stat.S_IRGRP: perms[4] = 'r' + if file[A_MODE] & stat.S_IWGRP: perms[5] = 'w' + if file[A_MODE] & stat.S_IXGRP: perms[6] = 'x' + + if file[A_MODE] & stat.S_IROTH: perms[7] = 'r' + if file[A_MODE] & stat.S_IWOTH: perms[8] = 'w' + if file[A_MODE] & stat.S_IXOTH: perms[9] = 'x' + + if file[A_TYPE] == T_DIR: + perms[0] = 'd' + + perms = ''.join(perms) + ctime = time.localtime(file[A_CTIME]) + + l = '%s 1 %s %s %s %s %s' % \ + (perms, + self.uid2name(file[A_UID]), + self.gid2name(file[A_GID]), + str(file[A_SIZE]).rjust(len(str(largest))), + time.strftime('%Y-%m-%d %H:%M', ctime), + file[A_NAME]) + + self.honeypot.writeln(l) +commands['/bin/ls'] = command_ls + +# vim: set sw=4 et: diff --git a/kippo/commands/ping.py b/kippo/commands/ping.py new file mode 100644 index 00000000..988edd8c --- /dev/null +++ b/kippo/commands/ping.py @@ -0,0 +1,52 @@ +# Copyright (c) 2009 Upi Tamminen +# See the COPYRIGHT file for more information + +from kippo.core.honeypot import HoneyPotCommand +from twisted.internet import reactor +import time, re, random, md5 + +commands = {} + +class command_ping(HoneyPotCommand): + def start(self): + if not len(self.args): + for l in ( + 'Usage: ping [-LRUbdfnqrvVaA] [-c count] [-i interval] [-w deadline]', + ' [-p pattern] [-s packetsize] [-t ttl] [-I interface or address]', + ' [-M mtu discovery hint] [-S sndbuf]', + ' [ -T timestamp option ] [ -Q tos ] [hop1 ...] destination', + ): + self.writeln(l) + self.exit() + return + + self.host = self.args[0] + if re.match('^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$', + self.host): + self.ip = self.host + else: + s = md5.md5(self.host).hexdigest() + self.ip = '.'.join([str(int(x, 16)) for x in + (s[0:2], s[2:4], s[4:6], s[6:8])]) + + self.writeln('PING %s (%s) 56(84) bytes of data.' % \ + (self.host, self.ip)) + self.scheduled = reactor.callLater(0.2, self.showreply) + self.count = 0 + + def showreply(self): + ms = 40 + random.random() * 10 + self.writeln( + '64 bytes from %s (%s): icmp_seq=%d ttl=50 time=%.1f ms' % \ + (self.host, self.ip, self.count + 1, ms)) + self.count += 1 + self.scheduled = reactor.callLater(1, self.showreply) + + def ctrl_c(self): + self.scheduled.cancel() + self.writeln('--- %s ping statistics ---' % self.host) + self.writeln('%d packets transmitted, %d received, 0%% packet loss, time 907ms' % \ + (self.count, self.count)) + self.writeln('rtt min/avg/max/mdev = 48.264/50.352/52.441/2.100 ms') + self.exit() +commands['/bin/ping'] = command_ping diff --git a/kippo/commands/ssh.py b/kippo/commands/ssh.py new file mode 100644 index 00000000..6bfe2bf2 --- /dev/null +++ b/kippo/commands/ssh.py @@ -0,0 +1,63 @@ +# Copyright (c) 2009 Upi Tamminen +# See the COPYRIGHT file for more information + +from kippo.core.honeypot import HoneyPotCommand +from twisted.internet import reactor +import time + +commands = {} + +class command_ssh(HoneyPotCommand): + def start(self): + if not self.args: + for l in ( + 'usage: ssh [-1246AaCfgKkMNnqsTtVvXxY] [-b bind_address] [-c cipher_spec]', + ' [-D [bind_address:]port] [-e escape_char] [-F configfile]', + ' [-i identity_file] [-L [bind_address:]port:host:hostport]', + ' [-l login_name] [-m mac_spec] [-O ctl_cmd] [-o option] [-p port]', + ' [-R [bind_address:]port:host:hostport] [-S ctl_path]', + ' [-w local_tun[:remote_tun]] [user@]hostname [command]', + ): + self.writeln(l) + self.exit() + return + self.host = self.args[0].strip() + self.writeln('The authenticity of host \'187.42.2.9 (187.42.2.9)\' can\'t be established.') + self.writeln('RSA key fingerprint is 9d:30:97:8a:9e:48:0d:de:04:8d:76:3a:7b:4b:30:f8.') + self.write('Are you sure you want to continue connecting (yes/no)? ') + self.callbacks = [self.yesno, self.wait] + + def yesno(self, line): + host = line.strip() + self.writeln( + 'Warning: Permanently added \'%s\' (RSA) to the list of known hosts.' % \ + host) + self.write('%s\'s password: ' % self.host) + self.honeypot.password_input = True + + def wait(self, line): + reactor.callLater(2, self.finish, line) + + def finish(self, line): + self.pause = False + user, rest, host = 'root', self.host, 'localhost' + if self.host.count('@'): + user, rest = self.host.split('@', 1) + rest = rest.strip().split('.') + if len(rest) and rest[0].isalpha(): + host = rest[0] + self.honeypot.hostname = host + self.honeypot.password_input = False + self.writeln( + 'Linux %s 2.6.26-2-686 #1 SMP Wed Nov 4 20:45:37 UTC 2009 i686' % \ + self.honeypot.hostname) + self.writeln('Last login: %s from 192.168.9.4' % \ + time.ctime(time.time() - 123123)) + self.exit() + + def lineReceived(self, line): + if len(self.callbacks): + self.callbacks.pop(0)(line) +commands['/usr/bin/ssh'] = command_ssh + +# vim: set sw=4 et: diff --git a/kippo/commands/tar.py b/kippo/commands/tar.py new file mode 100644 index 00000000..2de7e334 --- /dev/null +++ b/kippo/commands/tar.py @@ -0,0 +1,66 @@ +# Copyright (c) 2009 Upi Tamminen +# See the COPYRIGHT file for more information + +from kippo.core.honeypot import HoneyPotCommand +from kippo.core.fs import * +from kippo.commands import dice +import time, random, tarfile, os + +commands = {} + +class command_tar(HoneyPotCommand): + def call(self): + if len(self.args) < 2: + self.writeln('tar: You must specify one of the `-Acdtrux\' options') + self.writeln('Try `tar --help\' or `tar --usage\' for more information.') + return + + filename = self.args[1] + + extract = False + if 'x' in self.args[0]: + extract = True + verbose = False + if 'v' in self.args[0]: + verbose = True + + path = self.fs.resolve_path(filename, self.honeypot.cwd) + if not path or not self.honeypot.fs.exists(path): + self.writeln('tar: %s: Cannot open: No such file or directory' % \ + filename) + self.writeln('tar: Error is not recoverable: exiting now') + self.writeln('tar: Child returned status 2') + self.writeln('tar: Error exit delayed from previous errors') + return + + f = self.fs.getfile(path) + if not f[A_REALFILE]: + self.writeln('tar: this does not look like a tar archive') + self.writeln('tar: skipping to next header') + self.writeln('tar: error exit delayed from previous errors') + return + + try: + t = tarfile.open(f[A_REALFILE]) + except: + self.writeln('tar: this does not look like a tar archive') + self.writeln('tar: skipping to next header') + self.writeln('tar: error exit delayed from previous errors') + return + + for f in t: + dest = self.fs.resolve_path(f.name.strip('/'), self.honeypot.cwd) + if verbose: + self.writeln(f.name) + if not extract or not len(dest): + continue + if f.isdir(): + self.fs.mkdir(dest, 0, 0, 4096, f.mode, f.mtime) + elif f.isfile(): + self.fs.mkfile(dest, 0, 0, f.size, f.mode, f.mtime) + self.honeypot.commands[dest] = random.choice(dice.clist) + else: + print 'tar: skipping [%s]' % f.name +commands['/bin/tar'] = command_tar + +# vim: set sw=4 et: diff --git a/kippo/commands/wget.py b/kippo/commands/wget.py new file mode 100644 index 00000000..452c9556 --- /dev/null +++ b/kippo/commands/wget.py @@ -0,0 +1,194 @@ +# Copyright (c) 2009 Upi Tamminen +# See the COPYRIGHT file for more information + +from kippo.core.honeypot import HoneyPotCommand +from kippo.core.fs import * +from twisted.web import client +from twisted.internet import reactor +import stat, time, urlparse, random, re + +commands = {} + +def tdiff(seconds): + t = seconds + days = int(t / (24 * 60 * 60)) + t -= (days * 24 * 60 * 60) + hours = int(t / (60 * 60)) + t -= (hours * 60 * 60) + minutes = int(t / 60) + t -= (minutes * 60) + + s = '%ds' % int(t) + if minutes >= 1: s = '%dm %s' % (minutes, s) + if hours >= 1: s = '%dh %s' % (hours, s) + if days >= 1: s = '%dd %s' % (days, s) + return s + +def sizeof_fmt(num): + for x in ['bytes','K','M','G','T']: + if num < 1024.0: + return "%d%s" % (num, x) + num /= 1024.0 + +# Luciano Ramalho @ http://code.activestate.com/recipes/498181/ +def splitthousands( s, sep=','): + if len(s) <= 3: return s + return splitthousands(s[:-3], sep) + sep + s[-3:] + +class command_wget(HoneyPotCommand): + def start(self): + url = None + for arg in self.args: + if arg.startswith('-'): + continue + url = arg.strip() + break + + if not url: + self.writeln('wget: missing URL') + self.writeln('Usage: wget [OPTION]... [URL]...') + self.nextLine() + self.writeln('Try `wget --help\' for more options.') + self.exit() + return + + if url and not url.startswith('http://'): + url = 'http://%s' % url + + urldata = urlparse.urlparse(url) + + outfile = urldata.path.split('/')[-1] + if not len(outfile.strip()) or not urldata.path.count('/'): + outfile = 'index.html' + + self.safeoutfile = '%s/%s_%s' % \ + (self.honeypot.env.cfg.get('honeypot', 'download_path'), + time.strftime('%Y%m%d%H%M%S'), + re.sub('[^A-Za-z0-9]', '_', url)) + self.deferred = self.download(url, outfile, + file(self.safeoutfile, 'wb')) + if self.deferred: + self.deferred.addCallback(self.success) + self.deferred.addErrback(self.error, url) + + def download(self, url, fakeoutfile, outputfile, *args, **kwargs): + scheme, host, port, path = client._parse(url) + if scheme == 'https': + self.writeln('Sorry, SSL not supported in this release') + return None + + self.writeln('--%s-- %s' % (time.strftime('%Y-%m-%d %H:%M:%S'), url)) + self.writeln('Connecting to %s:%d... connected.' % (host, port)) + self.write('HTTP request sent, awaiting response... ') + + factory = HTTPProgressDownloader( + self, fakeoutfile, url, outputfile, *args, **kwargs) + self.connection = reactor.connectTCP(host, port, factory) + return factory.deferred + + def ctrl_c(self): + self.writeln('^C') + self.connection.transport.loseConnection() + + def success(self, data): + self.exit() + + def error(self, error, url): + if hasattr(error, 'getErrorMessage'): # exceptions + error = error.getErrorMessage() + self.writeln(error) + # Real wget also adds this: + #self.writeln('%s ERROR 404: Not Found.' % \ + # time.strftime('%Y-%m-%d %T')) + self.exit() +commands['/usr/bin/wget'] = command_wget + +# from http://code.activestate.com/recipes/525493/ +class HTTPProgressDownloader(client.HTTPDownloader): + def __init__(self, wget, fakeoutfile, url, outfile, headers=None): + client.HTTPDownloader.__init__(self, url, outfile, headers=headers) + self.status = None + self.wget = wget + self.fakeoutfile = fakeoutfile + self.lastupdate = 0 + self.started = time.time() + self.proglen = 0 + + def noPage(self, reason): # called for non-200 responses + if self.status == '304': + client.HTTPDownloader.page(self, '') + else: + client.HTTPDownloader.noPage(self, reason) + + def gotHeaders(self, headers): + if self.status == '200': + self.wget.writeln('200 OK') + if headers.has_key('content-length'): + self.totallength = int(headers['content-length'][0]) + else: + self.totallength = 0 + if headers.has_key('content-type'): + self.contenttype = headers['content-type'][0] + else: + self.contenttype = 'text/whatever' + self.currentlength = 0.0 + + if self.totallength > 0: + self.wget.writeln('Length: %d (%s) [%s]' % \ + (self.totallength, + sizeof_fmt(self.totallength), + self.contenttype)) + else: + self.wget.writeln('Length: unspecified [%s]' % \ + (self.contenttype)) + self.wget.writeln('Saving to: `%s' % self.fakeoutfile) + self.wget.honeypot.terminal.nextLine() + + return client.HTTPDownloader.gotHeaders(self, headers) + + def pagePart(self, data): + if self.status == '200': + self.currentlength += len(data) + if (time.time() - self.lastupdate) < 0.5: + return client.HTTPDownloader.pagePart(self, data) + if self.totallength: + percent = (self.currentlength/self.totallength)*100 + spercent = "%i%%" % percent + else: + spercent = '%dK' % (self.currentlength/1000) + percent = 0 + self.speed = self.currentlength / (time.time() - self.started) + eta = (self.totallength - self.currentlength) / self.speed + s = '\r%s [%s] %s %dK/s eta %s' % \ + (spercent.rjust(3), + ('%s>' % (int(39.0 / 100.0 * percent) * '=')).ljust(39), + splitthousands(str(int(self.currentlength))).ljust(12), + self.speed / 1000, + tdiff(eta)) + self.wget.write(s.ljust(self.proglen)) + self.proglen = len(s) + self.lastupdate = time.time() + return client.HTTPDownloader.pagePart(self, data) + + def pageEnd(self): + if self.totallength != 0 and self.currentlength != self.totallength: + return client.HTTPDownloader.pageEnd(self) + self.wget.write('\r100%%[%s] %s %dK/s' % \ + ('%s>' % (38 * '='), + splitthousands(str(int(self.totallength))).ljust(12), + self.speed / 1000)) + self.wget.honeypot.terminal.nextLine() + self.wget.honeypot.terminal.nextLine() + self.wget.writeln( + '%s (%d KB/s) - `%s\' saved [%d/%d]' % \ + (time.strftime('%Y-%m-%d %H:%M:%S'), + self.speed / 1000, + self.fakeoutfile, self.currentlength, self.totallength)) + outfile = '%s/%s' % (self.wget.honeypot.cwd, self.fakeoutfile) + self.wget.fs.mkfile(outfile, 0, 0, self.totallength, 33188) + self.wget.fs.update_realfile( + self.wget.fs.getfile(outfile), + self.wget.safeoutfile) + return client.HTTPDownloader.pageEnd(self) + +# vim: set sw=4 et: diff --git a/kippo/core/__init__.py b/kippo/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/kippo/core/config.py b/kippo/core/config.py new file mode 100644 index 00000000..1b7bd07d --- /dev/null +++ b/kippo/core/config.py @@ -0,0 +1,14 @@ +# Copyright (c) 2009 Upi Tamminen +# See the COPYRIGHT file for more information + +import ConfigParser, os + +def config(): + cfg = ConfigParser.ConfigParser() + for f in ('kippo.cfg', '/etc/kippo/kippo.cfg', '/etc/kippo.cfg'): + if os.path.exists(f): + cfg.read('kippo.cfg') + return cfg + return None + +# vim: set sw=4 et: diff --git a/kippo/core/fs.py b/kippo/core/fs.py new file mode 100644 index 00000000..f8234994 --- /dev/null +++ b/kippo/core/fs.py @@ -0,0 +1,126 @@ +# Copyright (c) 2009 Upi Tamminen +# See the COPYRIGHT file for more information + +import os, time + +A_NAME, \ + A_TYPE, \ + A_UID, \ + A_GID, \ + A_SIZE, \ + A_MODE, \ + A_CTIME, \ + A_CONTENTS, \ + A_TARGET, \ + A_REALFILE = range(0, 10) +T_LINK, \ + T_DIR, \ + T_FILE, \ + T_BLK, \ + T_CHR, \ + T_SOCK, \ + T_FIFO = range(0, 7) + +class HoneyPotFilesystem(object): + def __init__(self, fs): + self.fs = fs + + def resolve_path(self, path, cwd): + pieces = path.rstrip('/').split('/') + + if path[0] == '/': + cwd = [] + else: + cwd = [x for x in cwd.split('/') if len(x) and x is not None] + + while 1: + if not len(pieces): + break + piece = pieces.pop(0) + if piece == '..': + if len(cwd): cwd.pop() + continue + if piece in ('.', ''): + continue + cwd.append(piece) + + return '/%s' % '/'.join(cwd) + + def get_path(self, path): + p = self.fs + for i in path.split('/'): + if not i: + continue + p = [x for x in p[A_CONTENTS] if x[A_NAME] == i][0] + return p[A_CONTENTS] + + def list_files(self, path): + return self.get_path(path) + + def exists(self, path): + f = self.getfile(path) + if f is not False: + return True + + def update_realfile(self, f, realfile): + if not f[A_REALFILE] and os.path.exists(realfile) and \ + not os.path.islink(realfile) and os.path.isfile(realfile) and \ + f[A_SIZE] < 25000000: + print 'Updating realfile to %s' % realfile + f[A_REALFILE] = realfile + + def realfile(self, f, path): + self.update_realfile(f, path) + if f[A_REALFILE]: + return f[A_REALFILE] + return None + + def getfile(self, path): + pieces = path.strip('/').split('/') + p = self.fs + while 1: + if not len(pieces): + break + piece = pieces.pop(0) + if piece not in [x[A_NAME] for x in p[A_CONTENTS]]: + return False + p = [x for x in p[A_CONTENTS] \ + if x[A_NAME] == piece][0] + return p + + def mkfile(self, path, uid, gid, size, mode, ctime = None): + if ctime is None: + ctime = time.time() + dir = self.get_path(os.path.dirname(path)) + outfile = os.path.basename(path) + if outfile in [x[A_NAME] for x in dir]: + dir.remove([x for x in dir if x[A_NAME] == outfile][0]) + dir.append([outfile, T_FILE, uid, gid, size, mode, ctime, [], + None, None]) + return True + + def mkdir(self, path, uid, gid, size, mode, ctime = None): + if ctime is None: + ctime = time.time() + if not len(path.strip('/')): + return False + try: + dir = self.get_path(os.path.dirname(path.strip('/'))) + except IndexError: + return False + dir.append([os.path.basename(path), T_DIR, uid, gid, size, mode, + ctime, [], None, None]) + return True + + def is_dir(self, path): + if path == '/': + return True + dir = self.get_path(os.path.dirname(path)) + l = [x for x in dir + if x[A_NAME] == os.path.basename(path) and + x[A_TYPE] == T_DIR] + if l: + return True + return False + +# vim: set sw=4 et: diff --git a/kippo/core/honeypot.py b/kippo/core/honeypot.py new file mode 100644 index 00000000..c76f7a47 --- /dev/null +++ b/kippo/core/honeypot.py @@ -0,0 +1,303 @@ +# Copyright (c) 2009 Upi Tamminen +# See the COPYRIGHT file for more information + +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.protocols.policies import TrafficLoggingFactory +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 + +from kippo.core import ttylog, fs +from kippo.core.config import config +import commands + +class HoneyPotCommand(object): + def __init__(self, honeypot, *args): + self.honeypot = honeypot + self.args = args + self.writeln = self.honeypot.writeln + self.write = self.honeypot.terminal.write + self.nextLine = self.honeypot.terminal.nextLine + self.fs = self.honeypot.fs + + def start(self): + self.call() + self.exit() + + def call(self): + self.honeypot.writeln('Hello World! [%s]' % repr(self.args)) + + def exit(self): + self.honeypot.cmdstack.pop() + self.honeypot.cmdstack[-1].resume() + + def ctrl_c(self): + print 'Received CTRL-C, exiting..' + self.writeln('^C') + self.exit() + + def lineReceived(self, line): + print 'INPUT: %s' % line + + def resume(self): + pass + +class HoneyPotShell(object): + def __init__(self, honeypot): + self.honeypot = honeypot + self.showPrompt() + + def lineReceived(self, line): + print 'CMD: %s' % line + if not len(line.strip()): + self.showPrompt() + return + try: + cmdAndArgs = shlex.split(line.strip()) + except: + self.honeypot.writeln( + '-bash: syntax error: unexpected end of file') + self.showPrompt() + return + cmd, args = cmdAndArgs[0], [] + if len(cmdAndArgs) > 1: + args = cmdAndArgs[1:] + cmdclass = self.honeypot.getCommand(cmd) + if cmdclass: + obj = cmdclass(self.honeypot, *args) + self.honeypot.cmdstack.append(obj) + self.honeypot.setTypeoverMode() + obj.start() + else: + if len(line.strip()): + self.honeypot.writeln('bash: %s: command not found' % cmd) + self.showPrompt() + + def resume(self): + self.honeypot.setInsertMode() + self.showPrompt() + + def showPrompt(self): + prompt = '%s:%%(path)s# ' % self.honeypot.hostname + path = self.honeypot.cwd + if path == '/root': + path = '~' + attrs = {'path': path} + self.honeypot.terminal.write(prompt % attrs) + + def ctrl_c(self): + self.honeypot.terminal.nextLine() + self.showPrompt() + +class HoneyPotProtocol(recvline.HistoricRecvLine): + def __init__(self, user, env): + self.user = user + self.env = env + self.cwd = '/root' + self.hostname = self.env.cfg.get('honeypot', 'hostname') + self.fs = fs.HoneyPotFilesystem(deepcopy(self.env.fs)) + # 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 connectionMade(self): + recvline.HistoricRecvLine.connectionMade(self) + self.cmdstack = [HoneyPotShell(self)] + + def connectionLost(self, reason): + recvline.HistoricRecvLine.connectionLost(self, reason) + # not sure why i need to do this: + del self.fs + del self.commands + + # Overriding to prevent terminal.reset() + def initializeScreen(self): + self.setInsertMode() + + def getCommand(self, cmd): + 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' % (x, cmd) for x in \ + '/bin', '/usr/bin', '/sbin', '/usr/sbin']: + if self.fs.exists(i): + path = i + break + 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 keystrokeReceived(self, keyID, modifier): + if type(keyID) == type(''): + ttylog.ttylog_write(self.terminal.ttylog_file, len(keyID), + ttylog.DIR_READ, time.time(), keyID) + if keyID == '\x03': + self.cmdstack[-1].ctrl_c() + 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 writeln(self, data): + self.terminal.write(data) + self.terminal.nextLine() + +class LoggingServerProtocol(insults.ServerProtocol): + def connectionMade(self): + self.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' % self.ttylog_file + ttylog.ttylog_open(self.ttylog_file, time.time()) + self.ttylog_open = True + insults.ServerProtocol.connectionMade(self) + + def write(self, bytes): + if self.ttylog_open: + ttylog.ttylog_write(self.ttylog_file, len(bytes), + ttylog.DIR_WRITE, time.time(), bytes) + insults.ServerProtocol.write(self, bytes) + + def connectionLost(self, reason): + if self.ttylog_open: + ttylog.ttylog_close(self.ttylog_file, time.time()) + self.ttylog_open = False + insults.ServerProtocol.connectionLost(self, reason) + +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':session.SSHSession}) + + def openShell(self, protocol): + serverProtocol = LoggingServerProtocol(HoneyPotProtocol, self, self.env) + serverProtocol.makeConnection(protocol) + protocol.makeConnection(session.wrapProtocol(serverProtocol)) + + def getPty(self, terminal, windowSize, attrs): + self.windowSize = windowSize + return None + + def execCommand(self, protocol, cmd): + raise NotImplementedError + + def closed(self): + pass + + def windowChanged(self, windowSize): + self.windowSize = windowSize + +class HoneyPotEnvironment(object): + def __init__(self): + self.cfg = config() + self.commands = {} + import kippo.commands + for c in kippo.commands.__all__: + module = __import__('kippo.commands.%s' % c, + globals(), locals(), ['commands']) + self.commands.update(module.commands) + self.fs = pickle.load(file( + self.cfg.get('honeypot', 'filesystem_file'))) + +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." + +# As implemented by Kojoney +class HoneyPotSSHFactory(factory.SSHFactory): + #publicKeys = {'ssh-rsa': keys.getPublicKeyString(data=publicKey)} + #privateKeys = {'ssh-rsa': keys.getPrivateKeyObject(data=privateKey)} + services = { + 'ssh-userauth': userauth.SSHUserAuthServer, + 'ssh-connection': connection.SSHConnection, + } + + def buildProtocol(self, addr): + # FIXME: try to mimic something real 100% + t = transport.SSHServerTransport() + 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,) + + def __init__(self, users): + self.users = users + + def requestAvatarId(self, credentials): + if (credentials.username, credentials.password) in self.users: + print 'login attempt [%s/%s] succeeded' % \ + (credentials.username, credentials.password) + return defer.succeed(credentials.username) + else: + print 'login attempt [%s/%s] failed' % \ + (credentials.username, credentials.password) + return defer.fail(error.UnauthorizedLogin()) + +def getRSAKeys(): + if not (os.path.exists('public.key') and os.path.exists('private.key')): + # generate a RSA keypair + print "Generating RSA keypair..." + from Crypto.PublicKey import RSA + KEY_LENGTH = 1024 + rsaKey = RSA.generate(KEY_LENGTH, common.entropy.get_bytes) + publicKeyString = keys.makePublicKeyString(rsaKey) + privateKeyString = keys.makePrivateKeyString(rsaKey) + # save keys for next time + file('public.key', 'w+b').write(publicKeyString) + file('private.key', 'w+b').write(privateKeyString) + print "done." + else: + publicKeyString = file('public.key').read() + privateKeyString = file('private.key').read() + return publicKeyString, privateKeyString + +# vim: set sw=4 et: diff --git a/kippo/core/ttylog.py b/kippo/core/ttylog.py new file mode 100644 index 00000000..61c398fd --- /dev/null +++ b/kippo/core/ttylog.py @@ -0,0 +1,30 @@ +# Copyright (c) 2009 Upi Tamminen +# See the COPYRIGHT file for more information + +# Should be compatible with user mode linux + +import struct, sys + +OP_OPEN, OP_CLOSE, OP_WRITE, OP_EXEC = 1, 2, 3, 4 +DIR_READ, DIR_WRITE = 1, 2 + +def ttylog_write(logfile, len, direction, stamp, data = None): + f = file(logfile, 'a') + sec, usec = int(stamp), int(1000000 * (stamp - int(stamp))) + f.write(struct.pack('iLiiLL', 3, 0, len, direction, sec, usec)) + f.write(data) + f.close() + +def ttylog_open(logfile, stamp): + f = file(logfile, 'a') + sec, usec = int(stamp), int(1000000 * (stamp - int(stamp))) + f.write(struct.pack('iLiiLL', 1, 0, 0, 0, sec, usec)) + f.close() + +def ttylog_close(logfile, stamp): + f = file(logfile, 'a') + sec, usec = int(stamp), int(1000000 * (stamp - int(stamp))) + f.write(struct.pack('iLiiLL', 2, 0, 0, 0, sec, usec)) + f.close() + +# vim: set sw=4 et: