diff --git a/CHANGELOG.md b/CHANGELOG.md index a4d558a..47b49fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,6 @@ * fixes for behavior with non-existent files (cd /test, cat /test/nonexistent, etc) * fix for ability to ping/ssh non-existent IP address * always send ssh exit-status 0 on exec and shell -* add 1st version of 'netstat' * ls output is now alphabetically sorted * banner_file is deprecated. honeyfs/etc/issue.net is default * add 'dir' alias for 'ls' @@ -32,4 +31,5 @@ * add 'users' aliased to 'whoami' * add 'killall' and 'killall5' aliased to nop * add 'poweroff' 'halt' and 'reboot' aliases for shutdown -* add 'which' and environment passing to commands +* add environment passing to commands +* added 'which', 'netstat' and 'gcc' from kippo-extra diff --git a/README.md b/README.md index f2ecbcd..53736a4 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,6 @@ Kippo is a medium interaction SSH honeypot designed to log brute force attacks a Kippo is inspired, but not based on [Kojoney](http://kojoney.sourceforge.net/). -## Demo - -Some interesting logs from a live Kippo installation below (viewable within a web browser with the help of Ajaxterm). Note that some commands may have been improved since these logs were recorded. - - * [2009-11-22](http://kippo.rpg.fi/playlog/?l=20091122-075013-5055.log) - * [2009-11-23](http://kippo.rpg.fi/playlog/?l=20091123-003854-3359.log) - * [2009-11-23](http://kippo.rpg.fi/playlog/?l=20091123-012814-626.log) - * [2010-03-16](http://kippo.rpg.fi/playlog/?l=20100316-233121-1847.log) - ## Features Some interesting features: @@ -20,7 +11,6 @@ Some interesting features: * Possibility of adding fake file contents so the attacker can 'cat' files such as /etc/passwd. Only minimal file contents are included * Session logs stored in an [UML Compatible](http://user-mode-linux.sourceforge.net/) format for easy replay with original timings * Just like Kojoney, Kippo saves files downloaded with wget for later inspection -* Trickery; ssh pretends to connect somewhere, exit doesn't really exit, etc ## Requirements diff --git a/data/userdb.txt b/data/userdb.txt index decb76c..88fc15e 100644 --- a/data/userdb.txt +++ b/data/userdb.txt @@ -1 +1,8 @@ -root:0:123456 +# This file contains user authorizations. +# To allow all passwords, use '*' +# To deny specific passwords, user '!password' +# The file is processed linearly, denials need to happen before allows. +# Default config allows all root passwords except 'root' and '123456' +root:0:!root +root:0:!123456 +root:0:* diff --git a/kippo/commands/__init__.py b/kippo/commands/__init__.py index e2d7f54..f95d4ad 100644 --- a/kippo/commands/__init__.py +++ b/kippo/commands/__init__.py @@ -18,4 +18,6 @@ __all__ = [ 'malware', 'netstat', 'which', + 'gcc', + 'iptables' ] diff --git a/kippo/commands/gcc.py b/kippo/commands/gcc.py new file mode 100644 index 0000000..bb6b7f9 --- /dev/null +++ b/kippo/commands/gcc.py @@ -0,0 +1,274 @@ +# Copyright (c) 2013 Bas Stottelaar + +import time +import re +import getopt +import random + +from twisted.internet import reactor +from kippo.core.honeypot import HoneyPotCommand + +commands = {} + +class command_gcc(HoneyPotCommand): + # Name of program. Under OSX, you might consider i686-apple-darwin11-llvm-gcc-X.X + APP_NAME = "gcc" + + # GCC verson, used in help, version and the commandline name gcc-X.X + APP_VERSION = (4, 7, 2) + + # Random binary data, which looks awesome. You could change this to whatever you want, but this + # data will be put in the actual file and thus exposed to our hacker when he\she cats the file. + RANDOM_DATA = "\x6a\x00\x48\x89\xe5\x48\x83\xe4\xf0\x48\x8b\x7d\x08\x48\x8d\x75\x10\x89\xfa" \ + "\x83\xc2\x01\xc1\xe2\x03\x48\x01\xf2\x48\x89\xd1\xeb\x04\x48\x83\xc1\x08\x48" \ + "\x83\x39\x00\x75\xf6\x48\x83\xc1\x08\xe8\x0c\x00\x00\x00\x89\xc7\xe8\xb9\x00" \ + "\x00\x00\xf4\x90\x90\x90\x90\x55\x48\x89\xe5\x48\x83\xec\x40\x89\x7d\xfc\x48" \ + "\x89\x75\xf0\x48\x8b\x45\xf0\x48\x8b\x00\x48\x83\xf8\x00\x75\x0c\xb8\x00\x00" \ + "\x00\x00\x89\xc7\xe8\x8c\x00\x00\x00\x48\x8b\x45\xf0\x48\x8b\x40\x08\x30\xc9" \ + "\x48\x89\xc7\x88\xc8\xe8\x7e\x00\x00\x00\x89\xc1\x89\x4d\xdc\x48\x8d\x0d\xd8" \ + "\x01\x00\x00\x48\x89\xcf\x48\x89\x4d\xd0\xe8\x72\x00\x00\x00\x8b\x4d\xdc\x30" \ + "\xd2\x48\x8d\x3d\xa4\x00\x00\x00\x89\xce\x88\x55\xcf\x48\x89\xc2\x8a\x45\xcf" \ + "\xe8\x53\x00\x00\x00\x8b\x45\xdc\x88\x05\xc3\x01\x00\x00\x8b\x45\xdc\xc1\xe8" \ + "\x08\x88\x05\xb8\x01\x00\x00\x8b\x45\xdc\xc1\xe8\x10\x88\x05\xad\x01\x00\x00" \ + "\x8b\x45\xdc\xc1\xe8\x18\x88\x05\xa2\x01\x00\x00\x48\x8b\x45\xd0\x48\x89\x45" \ + "\xe0\x48\x8b\x45\xe0\xff\xd0\x8b\x45\xec\x48\x83\xc4\x40\x5d\xc3\xff\x25\x3e" \ + "\x01\x00\x00\xff\x25\x40\x01\x00\x00\xff\x25\x42\x01\x00\x00\xff\x25\x44\x01" \ + "\x00\x00\x4c\x8d\x1d\x1d\x01\x00\x00\x41\x53\xff\x25\x0d\x01\x00\x00\x90\x68" \ + "\x00\x00\x00\x00\xe9\xe6\xff\xff\xff\x68\x0c\x00\x00\x00\xe9\xdc\xff\xff\xff" \ + "\x68\x1d\x00\x00\x00\xe9\xd2\xff\xff\xff\x68\x2b\x00\x00\x00\xe9\xc8\xff\xff" \ + "\xff\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x00\x00" \ + "\x00\x00\x1c\x00\x00\x00\x02\x00\x00\x00\x00\x0e\x00\x00\x34\x00\x00\x00\x34" \ + "\x00\x00\x00\xf5\x0e\x00\x00\x00\x00\x00\x00\x34\x00\x00\x00\x03\x00\x00\x00" \ + "\x0c\x00\x02\x00\x14\x00\x02\x00\x00\x00\x00\x01\x40\x00\x00\x00\x00\x00\x00" \ + "\x01\x00\x00\x00" + + def start(self): + """ + Parse as much as possible from a GCC syntax and generate the output + that is requested. The file that is generated can be read (and will) + output garbage from an actual file, but when executed, it will generate + a segmentation fault. + + The input files are expected to exists, but can be empty. + + Verified syntaxes, including non-existing files: + * gcc test.c + * gcc test.c -o program + * gcc test1.c test2.c + * gcc test1.c test2.c -o program + * gcc test.c -o program -lm + * gcc -g test.c -o program -lm + * gcc test.c -DF_CPU=16000000 -I../etc -o program + * gcc test.c -O2 -o optimized_program + * gcc test.c -Wstrict-overflow=n -o overflowable_program + + Others: + * gcc + * gcc -h + * gcc -v + * gcc --help + * gcc --version + """ + + output_file = None + input_files = 0 + complete = True + + # Parse options or display no files + try: + opts, args = getopt.gnu_getopt(self.args, 'ESchvgo:x:l:I:W:D:X:O:', ['help', 'version', 'param']) + except getopt.GetoptError as err: + self.no_files() + return + + # Parse options + for o, a in opts: + if o in ("-v"): + self.version(short=False) + return + elif o in ("--version"): + self.version(short=True) + return + elif o in ("-h"): + self.arg_missing("-h") + return + elif o in ("--help"): + self.help() + return + elif o in ("-o"): + if len(a) == 0: + self.arg_missing("-o") + else: + output_file = a + + # Check for *.c or *.cpp files + for value in args: + if '.c' in value.lower(): + sourcefile = self.fs.resolve_path(value, self.honeypot.cwd) + + if self.fs.exists(sourcefile): + input_files = input_files + 1 + else: + self.writeln("%s: %s: No such file or directory" % (command_gcc.APP_NAME, value)) + complete = False + + # To generate, or not + if input_files > 0 and complete: + func = lambda: self.generate_file(output_file if output_file else 'a.out') + timeout = 0.1 + random.random() + + # Schedule call to make it more time consuming and real + self.scheduled = reactor.callLater(timeout, func) + else: + self.no_files() + + def ctrl_c(self): + """ Make sure the scheduled call will be canceled """ + + if getattr(self, 'scheduled', False): + self.scheduled.cancel() + + def no_files(self): + """ Notify user there are no input files, and exit """ + self.writeln( """gcc: fatal error: no input files +compilation terminated.""" ) + self.exit() + + + def version(self, short): + """ Print long or short version, and exit """ + + # Generate version number + version = '.'.join([ str(v) for v in command_gcc.APP_VERSION[:3] ]) + version_short = '.'.join([ str(v) for v in command_gcc.APP_VERSION[:2] ]) + + if short: + data = ( """%s (Debian %s-8) %s +Copyright (C) 2010 Free Software Foundation, Inc. +This is free software; see the source for copying conditions. There is NO +warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.""", (command_gcc.APP_NAME, version, version) ) + else: + data = ( """Using built-in specs. +COLLECT_GCC=gcc +COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/4.7/lto-wrapper +Target: x86_64-linux-gnu +Configured with: ../src/configure -v --with-pkgversion=\'Debian %s-5\' --with-bugurl=file:///usr/share/doc/gcc-%s/README.Bugs --enable-languages=c,c++,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-%s --enable-shared --enable-multiarch --enable-linker-build-id --with-system-zlib --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --with-gxx-include-dir=/usr/include/c++/%s --libdir=/usr/lib --enable-nls --enable-clocale=gnu --enable-libstdcxx-debug --enable-objc-gc --with-arch-32=i586 --with-tune=generic --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu +Thread model: posix +gcc version %s (Debian %s-5)""" % (version, version_short, version_short, version_short, version, version)) + + # Write + self.writeln(data) + self.exit() + + def generate_file(self, outfile): + data = "" + # TODO: make sure it is written to temp file, not downloads + 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]', '_', outfile)) + + # Data contains random garbage from an actual file, so when + # catting the file, you'll see some 'real' compiled data + for i in range(random.randint(3, 15)): + if random.randint(1, 3) == 1: + data = data + command_gcc.RANDOM_DATA[::-1] + else: + data = data + command_gcc.RANDOM_DATA + + # Write random data + with open(safeoutfile, 'wb') as f: f.write(data) + + # Output file + outfile = self.fs.resolve_path(outfile, self.honeypot.cwd) + + # Create file for the honeypot + self.fs.mkfile(outfile, 0, 0, len(data), 33188) + self.fs.update_realfile(self.fs.getfile(outfile), safeoutfile) + + # Segfault command + class segfault_command(HoneyPotCommand): + def call(self): + self.write("Segmentation fault\n") + + # Trick the 'new compiled file' as an segfault + self.honeypot.commands[outfile] = segfault_command + + # Done + self.exit() + + def arg_missing(self, arg): + """ Print missing argument message, and exit """ + self.writeln("%s: argument to '%s' is missing" % (command_gcc.APP_NAME, arg)) + self.exit() + + def help(self): + """ Print help info, and exit """ + + version = '.'.join([ str(v) for v in command_gcc.APP_VERSION[:2] ]) + + self.writeln( """Usage: gcc [options] file... +Options: + -pass-exit-codes Exit with highest error code from a phase + --help Display this information + --target-help Display target specific command line options + --help={common|optimizers|params|target|warnings|[^]{joined|separate|undocumented}}[,...] + Display specific types of command line options + (Use '-v --help' to display command line options of sub-processes) + --version Display compiler version information + -dumpspecs Display all of the built in spec strings + -dumpversion Display the version of the compiler + -dumpmachine Display the compiler's target processor + -print-search-dirs Display the directories in the compiler's search path + -print-libgcc-file-name Display the name of the compiler's companion library + -print-file-name= Display the full path to library + -print-prog-name= Display the full path to compiler component + -print-multiarch Display the target's normalized GNU triplet, used as + a component in the library path + -print-multi-directory Display the root directory for versions of libgcc + -print-multi-lib Display the mapping between command line options and + multiple library search directories + -print-multi-os-directory Display the relative path to OS libraries + -print-sysroot Display the target libraries directory + -print-sysroot-headers-suffix Display the sysroot suffix used to find headers + -Wa, Pass comma-separated on to the assembler + -Wp, Pass comma-separated on to the preprocessor + -Wl, Pass comma-separated on to the linker + -Xassembler Pass on to the assembler + -Xpreprocessor Pass on to the preprocessor + -Xlinker Pass on to the linker + -save-temps Do not delete intermediate files + -save-temps= Do not delete intermediate files + -no-canonical-prefixes Do not canonicalize paths when building relative + prefixes to other gcc components + -pipe Use pipes rather than intermediate files + -time Time the execution of each subprocess + -specs= Override built-in specs with the contents of + -std= Assume that the input sources are for + --sysroot= Use as the root directory for headers + and libraries + -B Add to the compiler's search paths + -v Display the programs invoked by the compiler + -### Like -v but options quoted and commands not executed + -E Preprocess only; do not compile, assemble or link + -S Compile only; do not assemble or link + -c Compile and assemble, but do not link + -o Place the output into + -pie Create a position independent executable + -shared Create a shared library + -x Specify the language of the following input files + Permissible languages include: c c++ assembler none + 'none' means revert to the default behavior of + guessing the language based on the file's extension + +Options starting with -g, -f, -m, -O, -W, or --param are automatically + passed on to the various sub-processes invoked by gcc. In order to pass + other options on to these processes the -W options must be used. + +For bug reporting instructions, please see: +.""") + self.exit() + +# Definitions +commands['/usr/bin/gcc'] = command_gcc +commands['/usr/bin/gcc-%s' % ('.'.join([ str(v) for v in command_gcc.APP_VERSION[:2] ]))] = command_gcc diff --git a/kippo/commands/iptables.py b/kippo/commands/iptables.py new file mode 100644 index 0000000..ace4e50 --- /dev/null +++ b/kippo/commands/iptables.py @@ -0,0 +1,414 @@ +# Copyright (c) 2013 Bas Stottelaar + +import optparse + +from twisted.internet import reactor +from kippo.core.honeypot import HoneyPotCommand + +commands = {} + +class OptionParsingError(RuntimeError): + def __init__(self, msg): + self.msg = msg + +class OptionParsingExit(Exception): + def __init__(self, status, msg): + self.msg = msg + self.status = status + +class ModifiedOptionParser(optparse.OptionParser): + def error(self, msg): + raise OptionParsingError(msg) + + def exit(self, status=0, msg=None): + raise OptionParsingExit(status, msg) + + +class command_iptables(HoneyPotCommand): + # Do not resolve args + resolve_args = False + + # iptables app name + APP_NAME = "iptables" + + # iptables app version, used in help messages etc. + APP_VERSION = "v1.4.14" + + # Default iptable table + DEFAULT_TABLE = "filter" + + def user_is_root(self): + return self.honeypot.user.username == 'root' + + def start(self): + """ + Emulate iptables commands, including permission checking. + + Verified examples: + * iptables -A OUTPUT -o eth0 -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT + * iptables -A INPUT -i eth0 -p tcp -s "127.0.0.1" -j DROP + + Others: + * iptables + * iptables [[-t | --table] ] [-h | --help] + * iptables [[-t | --table] ] [-v | --version] + * iptables [[-t | --table] ] [-F | --flush] + * iptables [[-t | --table] ] [-L | --list] + * iptables [[-t | --table] ] [-S | --list-rules] + * iptables --this-is-invalid + """ + + # In case of no arguments + if len(self.args) == 0: + self.no_command() + return + + # Utils + def optional_arg(arg_default): + def func(option,opt_str,value,parser): + if parser.rargs and not parser.rargs[0].startswith('-'): + val=parser.rargs[0] + parser.rargs.pop(0) + else: + val=arg_default + setattr(parser.values,option.dest,val) + return func + + # Initialize options + parser = ModifiedOptionParser(add_help_option=False) + parser.add_option("-h", "--help", dest="help", action="store_true") + parser.add_option("-V", "--version", dest="version", action="store_true") + parser.add_option("-v", "--verbose", dest="verbose", action="store_true") + parser.add_option("-x", "--exact", dest="exact", action="store_true") + parser.add_option("--line-numbers", dest="line_numbers", action="store_true") + parser.add_option("--modprobe", dest="modprobe", action="store") + + parser.add_option("-t", "--table", dest="table", action="store", default=command_iptables.DEFAULT_TABLE) + parser.add_option("-F", "--flush", dest="flush", action="callback", callback=optional_arg(True)) + parser.add_option("-Z", "--zero", dest="zero", action="callback", callback=optional_arg(True)) + parser.add_option("-S", "--list-rules", dest="list_rules", action="callback", callback=optional_arg(True)) + parser.add_option("-L", "--list", dest="list", action="callback", callback=optional_arg(True)) + parser.add_option("-A", "--append", dest="append", action="store") + parser.add_option("-D", "--delete", dest="delete", action="store") + parser.add_option("-I", "--insert", dest="insert", action="store") + parser.add_option("-R", "--replace", dest="replace", action="store") + parser.add_option("-N", "--new-chain", dest="new_chain", action="store") + parser.add_option("-X", "--delete-chain", dest="delete_chain", action="store") + parser.add_option("-P", "--policy", dest="policy", action="store") + parser.add_option("-E", "--rename-chain", dest="rename_chain", action="store") + + parser.add_option("-p", "--protocol", dest="protocol", action="store") + parser.add_option("-s", "--source", dest="source", action="store") + parser.add_option("-d", "--destination", dest="destination", action="store") + parser.add_option("-j", "--jump", dest="jump", action="store") + parser.add_option("-g", "--goto", dest="goto", action="store") + parser.add_option("-i", "--in-interface", dest="in_interface", action="store") + parser.add_option("-o", "--out-interface", dest="out_interface", action="store") + parser.add_option("-f", "--fragment", dest="fragment", action="store_true") + parser.add_option("-c", "--set-counters", dest="set_counters", action="store") + parser.add_option("-m", "--match", dest="match", action="store") + + parser.add_option("--sport", "--source-ports", dest="source_ports", action="store") + parser.add_option("--dport", "--destination-ports", dest="dest_ports", action="store") + parser.add_option("--ports", dest="ports", action="store") + parser.add_option("--state", dest="state", action="store") + + + # Parse options or display no files + try: + (opts, args) = parser.parse_args(list(self.args)) + except OptionParsingError, e: + self.bad_argument(self.args[0]) + return + except OptionParsingExit, e: + self.unknown_option(e) + return + + # Initialize table + if not self.setup_table(opts.table): + return + + # Parse options + if opts.help: + self.show_help() + return + elif opts.version: + self.show_version() + return + elif opts.flush: + self.flush("" if opts.flush == True else opts.flush) + return + elif opts.list: + self.list("" if opts.list == True else opts.list) + return + elif opts.list_rules: + self.list_rules("" if opts.list_rules == True else opts.list_rules) + return + + # Done + self.exit() + + def setup_table(self, table): + """ + Called during startup to make sure the current environment has some + fake rules in memory. + """ + + # Create fresh tables on start + if not hasattr(self.honeypot.env, 'iptables'): + setattr(self.honeypot.env, 'iptables', { + "raw": { + "PREROUTING": [], + "OUTPUT": [] + }, + "filter": { + "INPUT": [ + ('ACCEPT', 'tcp', '--', 'anywhere', 'anywhere', 'tcp', 'dpt:ssh'), + ('DROP', 'all', '--', 'anywhere', 'anywhere', '', '') + ], + "FORWARD": [], + "OUTPUT": [] + }, + "mangle": { + "PREROUTING": [], + "INPUT": [], + "FORWARD": [], + "OUTPUT": [], + "POSTROUTING": [] + }, + "nat": { + "PREROUTING": [], + "OUTPUT": [] + } + }) + + # Get the tables + self.tables = getattr(self.honeypot.env, 'iptables') + + # Verify selected table + if not self.is_valid_table(table): + return False + + # Set table + self.current_table = self.tables[table] + + # Done + return True + + def is_valid_table(self, table): + if self.user_is_root(): + # Verify table existence + if not table in self.tables.iterkeys(): + self.writeln( """%s: can\'t initialize iptables table \'%s\': Table does not exist (do you need to insmod?) +Perhaps iptables or your kernel needs to be upgraded.""" % (command_iptables.APP_NAME, table) ) + self.exit() + else: + # Exists + return True + else: + self.no_permission() + + # Failed + return False + + def is_valid_chain(self, chain): + # Verify chain existence. Requires valid table first + if not chain in self.current_table.iterkeys(): + self.writeln("%s: No chain/target/match by that name." % command_iptables.APP_NAME) + self.exit() + return False + + # Exists + return True + + def show_version(self): + """ Show version and exit """ + self.writeln('%s %s' % (command_iptables.APP_NAME, command_iptables.APP_VERSION)) + self.exit() + + def show_help(self): + """ Show help and exit """ + + self.writeln( """%s %s' + +Usage: iptables -[AD] chain rule-specification [options] + iptables -I chain [rulenum] rule-specification [options] + iptables -R chain rulenum rule-specification [options] + iptables -D chain rulenum [options] + iptables -[LS] [chain [rulenum]] [options] + iptables -[FZ] [chain] [options] + iptables -[NX] chain + iptables -E old-chain-name new-chain-name + iptables -P chain target [options] + iptables -h (print this help information) + +Commands: +Either long or short options are allowed. + --append -A chain Append to chain + --delete -D chain Delete matching rule from chain + --delete -D chain rulenum + Delete rule rulenum (1 = first) from chain + --insert -I chain [rulenum] + Insert in chain as rulenum (default 1=first) + --replace -R chain rulenum + Replace rule rulenum (1 = first) in chain + --list -L [chain [rulenum]] + List the rules in a chain or all chains + --list-rules -S [chain [rulenum]] + Print the rules in a chain or all chains + --flush -F [chain] Delete all rules in chain or all chains + --zero -Z [chain [rulenum]] + Zero counters in chain or all chains + --new -N chain Create a new user-defined chain + --delete-chain + -X [chain] Delete a user-defined chain + --policy -P chain target + Change policy on chain to target + --rename-chain + -E old-chain new-chain + Change chain name, (moving any references) +Options: +[!] --proto -p proto protocol: by number or name, eg. \'tcp\' +[!] --source -s address[/mask][...] + source specification +[!] --destination -d address[/mask][...] + destination specification +[!] --in-interface -i input name[+] + network interface name ([+] for wildcard) + --jump -j target + target for rule (may load target extension) + --goto -g chain + jump to chain with no return + --match -m match + extended match (may load extension) + --numeric -n numeric output of addresses and ports +[!] --out-interface -o output name[+] + network interface name ([+] for wildcard) + --table -t table table to manipulate (default: \'filter\') + --verbose -v verbose mode + --line-numbers print line numbers when listing + --exact -x expand numbers (display exact values) +[!] --fragment -f match second or further fragments only + --modprobe= try to insert modules using this command + --set-counters PKTS BYTES set the counter during insert/append +[!] --version -V print package version.""" % (command_iptables.APP_NAME, command_iptables.APP_VERSION)) + self.exit() + + def list_rules(self, chain): + """ List current rules as commands""" + + if self.user_is_root(): + if len(chain) > 0: + print chain + # Check chain + if not self.is_valid_chain(chain): + return + + chains = [chain] + else: + chains = self.current_table.iterkeys() + + # Output buffer + output = [] + + for chain in chains: + output.append("-P %s ACCEPT" % chain) + + # Done + self.writeln(output) + self.exit() + else: + self.no_permission() + + def list(self, chain): + """ List current rules """ + + if self.user_is_root(): + if len(chain) > 0: + print chain + # Check chain + if not self.is_valid_chain(chain): + return + + chains = [chain] + else: + chains = self.current_table.iterkeys() + + # Output buffer + output = [] + + for chain in chains: + # Chain table header + chain_output = [ + "Chain %s (policy ACCEPT)" % chain, + "target prot opt source destination", + ] + + # Format the rules + for rule in self.current_table[chain]: + chain_output.append( + "%-10s %-4s %-3s %-20s %-20s %s %s" % rule, + ) + + # Create one string + output.append("\n".join(chain_output)) + + # Done + self.writeln("\n\n".join(output)) + self.exit() + else: + self.no_permission() + + def flush(self, chain): + """ Mark rules as flushed """ + + if self.user_is_root(): + if len(chain) > 0: + # Check chain + if not self.is_valid_chain(chain): + return + + chains = [chain] + else: + chains = self.current_table.iterkeys() + + # Flush + for chain in chains: + self.current_table[chain] = [] + + self.exit() + else: + self.no_permission() + + def no_permission(self): + self.writeln( """%s %s: can\'t initialize iptables table \'filter\': Permission denied (you must be root) +Perhaps iptables or your kernel needs to be upgraded.""" + % (command_iptables.APP_NAME, command_iptables.APP_VERSION) ) + self.exit() + + def no_command(self): + """ Print no command message and exit """ + + self.writeln( """%s %s: no command specified' +Try `iptables -h\' or \'iptables --help\' for more information.""" + % (command_iptables.APP_NAME, command_iptables.APP_VERSION) ) + self.exit() + + def unknown_option(self, option): + """ Print unknown option message and exit """ + + self.writeln( """%s %s: unknown option \'%s\'' +Try `iptables -h\' or \'iptables --help\' for more information.""" + % (command_iptables.APP_NAME, command_iptables.APP_VERSION, option) ) + self.exit() + + def bad_argument(self, argument): + """ Print bad argument and exit """ + + self.writeln( """Bad argument \'%s\'' % argument, +Try `iptables -h\' or \'iptables --help\' for more information.""" + % argument ) + self.exit() + +# Definition +commands['/sbin/iptables'] = command_iptables diff --git a/kippo/core/auth.py b/kippo/core/auth.py index 5ad9186..e426e1f 100644 --- a/kippo/core/auth.py +++ b/kippo/core/auth.py @@ -41,6 +41,9 @@ class UserDB(object): if not line: continue + if line.startswith( '#' ): + continue + (login, uid_str, passwd) = line.split(':', 2) uid = 0 diff --git a/kippo/dblog/hpfeeds.py b/kippo/dblog/hpfeeds.py index 35ee117..af496b6 100644 --- a/kippo/dblog/hpfeeds.py +++ b/kippo/dblog/hpfeeds.py @@ -1,4 +1,4 @@ -/* From https://github.com/threatstream/kippo/blob/master/kippo/dblog/hpfeeds.py */ +## From https://github.com/threatstream/kippo/blob/master/kippo/dblog/hpfeeds.py from kippo.core import dblog from twisted.python import log diff --git a/utils/elk/README.md b/utils/elk/README.md new file mode 100644 index 0000000..0a65d8c --- /dev/null +++ b/utils/elk/README.md @@ -0,0 +1,67 @@ +# How to process Kippo output in an ELK stack + +(Note: work in progress, instructions are not verified) + + +## Prerequisites + +* Working Kippo installation +* Kippo JSON log file (enable database json in kippo.cfg) + +## Installation + +* Install logstash, elasticsearch and kibana + +``` +apt-get install logstash +apt-get install elasticsearch +```` + +* Install Kibana + +This may be different depending on your operating system. Kibana will need additional components such as a web server + + +## ElasticSearch Configuration + +TBD + +## Logstash Configuration + +* Download GeoIP data + +``` +wget http://geolite.maxmind.com/download/geoip/database/GeoLiteCity.dat.gz +wget http://download.maxmind.com/download/geoip/database/asnum/GeoIPASNum.dat.gz +``` + +* Place these somewhere in your filesystem. + +* Configure logstash + +``` +cp logstash-kippo.conf /etc/logstash/conf.d +``` + +* Make sure the configuration file is correct. Check the input section (path), filter (geoip databases) and output (elasticsearch hostname) + +``` +service logstash restart +``` + +* By default the logstash is creating debug logs in /tmp. + +* To test whether logstash is working correctly, check the file in /tmp + +``` +tail /tmp/kippo-logstash.log +``` + +* To test whether data is loaded into ElasticSearch, run the following query: + +``` +http://:9200/_search?q=kippo&size=5 +``` + +* If this gives output, your data is correctly loaded into ElasticSearch + diff --git a/utils/kibana-kippo.conf b/utils/elk/kibana-kippo.conf similarity index 85% rename from utils/kibana-kippo.conf rename to utils/elk/kibana-kippo.conf index e23ed52..8c49e38 100644 --- a/utils/kibana-kippo.conf +++ b/utils/elk/kibana-kippo.conf @@ -18,8 +18,31 @@ ] }, "filter": { - "list": {}, - "ids": [] + "list": { + "0": { + "type": "terms", + "field": "_type", + "value": "kippo", + "mandate": "must", + "active": true, + "alias": "", + "id": 0 + }, + "1": { + "type": "time", + "field": "@timestamp", + "from": "now-30d", + "to": "now", + "mandate": "must", + "active": true, + "alias": "", + "id": 1 + } + }, + "ids": [ + 0, + 1 + ] } }, "rows": [ @@ -30,6 +53,39 @@ "collapse": false, "collapsable": true, "panels": [ + { + "error": false, + "span": 3, + "editable": true, + "type": "terms", + "loadingEditor": false, + "field": "sensor", + "exclude": [], + "missing": false, + "other": false, + "size": 5, + "order": "count", + "style": { + "font-size": "10pt" + }, + "donut": false, + "tilt": false, + "labels": true, + "arrangement": "horizontal", + "chart": "table", + "counter_pos": "above", + "spyable": true, + "queries": { + "mode": "all", + "ids": [ + 0 + ] + }, + "tmode": "terms", + "tstat": "total", + "valuefield": "", + "title": "Sensors" + }, { "error": false, "span": 3, @@ -97,39 +153,6 @@ "tstat": "total", "valuefield": "", "title": "Successes" - }, - { - "error": false, - "span": 3, - "editable": true, - "type": "terms", - "loadingEditor": false, - "field": "sensor", - "exclude": [], - "missing": false, - "other": false, - "size": 5, - "order": "count", - "style": { - "font-size": "10pt" - }, - "donut": false, - "tilt": false, - "labels": true, - "arrangement": "horizontal", - "chart": "table", - "counter_pos": "above", - "spyable": true, - "queries": { - "mode": "all", - "ids": [ - 0 - ] - }, - "tmode": "terms", - "tstat": "total", - "valuefield": "", - "title": "Sensors" } ], "notice": false @@ -231,7 +254,7 @@ "editable": true, "type": "terms", "loadingEditor": false, - "field": "username", + "field": "username.raw", "exclude": [], "missing": false, "other": false, @@ -264,7 +287,7 @@ "editable": true, "type": "terms", "loadingEditor": false, - "field": "username", + "field": "username.raw", "exclude": [], "missing": false, "other": false, @@ -307,7 +330,7 @@ "editable": true, "type": "terms", "loadingEditor": false, - "field": "password", + "field": "password.raw", "exclude": [], "missing": false, "other": false, @@ -340,7 +363,7 @@ "editable": true, "type": "terms", "loadingEditor": false, - "field": "password", + "field": "password.raw", "exclude": [], "missing": false, "other": false, @@ -383,7 +406,7 @@ "editable": true, "type": "terms", "loadingEditor": false, - "field": "client", + "field": "client.raw", "exclude": [], "missing": false, "other": false, @@ -416,7 +439,7 @@ "editable": true, "type": "terms", "loadingEditor": false, - "field": "client", + "field": "client.raw", "exclude": [], "missing": false, "other": false, @@ -480,15 +503,23 @@ "error": false, "span": 4, "editable": true, - "type": "map", + "type": "terms", "loadingEditor": false, - "map": "europe", - "colors": [ - "#A0E2E2", - "#265656" - ], - "size": 100, + "field": "geoip.country_name.raw", "exclude": [], + "missing": false, + "other": true, + "size": 20, + "order": "count", + "style": { + "font-size": "10pt" + }, + "donut": false, + "tilt": false, + "labels": true, + "arrangement": "horizontal", + "chart": "table", + "counter_pos": "above", "spyable": true, "queries": { "mode": "all", @@ -496,8 +527,10 @@ 0 ] }, - "title": "Attack map (Europe)", - "field": "country_code2" + "tmode": "terms", + "tstat": "count", + "valuefield": "", + "title": "Countries" } ], "notice": false @@ -506,7 +539,7 @@ "title": "Events", "height": "650px", "editable": true, - "collapse": false, + "collapse": true, "collapsable": true, "panels": [ { @@ -551,11 +584,54 @@ } ], "notice": false + }, + { + "title": "ASN", + "height": "150px", + "editable": true, + "collapse": false, + "collapsable": true, + "panels": [ + { + "error": false, + "span": 4, + "editable": true, + "type": "terms", + "loadingEditor": false, + "field": "geoip.asn.raw", + "exclude": [], + "missing": true, + "other": true, + "size": 20, + "order": "count", + "style": { + "font-size": "10pt" + }, + "donut": false, + "tilt": false, + "labels": true, + "arrangement": "horizontal", + "chart": "table", + "counter_pos": "above", + "spyable": true, + "queries": { + "mode": "all", + "ids": [ + 0 + ] + }, + "tmode": "terms", + "tstat": "total", + "valuefield": "", + "title": "ASN" + } + ], + "notice": false } ], "editable": true, "index": { - "interval": "none", + "interval": "day", "pattern": "[logstash-]YYYY.MM.DD", "default": "_all", "warm_fields": false @@ -624,8 +700,10 @@ "2h", "1d" ], - "timefield": "timestamp", - "enable": true + "timefield": "@timestamp", + "enable": true, + "now": true, + "filter_id": 1 } ], "refresh": false diff --git a/utils/logstash-kippo.conf b/utils/elk/logstash-kippo.conf similarity index 72% rename from utils/logstash-kippo.conf rename to utils/elk/logstash-kippo.conf index 7b67ade..5533ac3 100644 --- a/utils/logstash-kippo.conf +++ b/utils/elk/logstash-kippo.conf @@ -1,9 +1,13 @@ - input { + # this is the actual live log file to monitor file { - path => ["/home/michel/src/kippo-git/log/kippo.json", "/home/kippo/kippo-git/log/kippo.json"] -# path => ["/home/michel/src/kippo-git/log/kippo.json"] - codec => json + path => ["/home/kippo/kippo-git/log/kippo.json"] + codec => json_lines + type => "kippo" + } + # this is to send old logs to for reprocessing + tcp { + port => 3333 type => "kippo" } } @@ -11,11 +15,13 @@ input { filter { if [type] == "kippo" { - date { - match => [ "timestamp", "ISO8601" ] - locale => "en" + json { + source => message } + date { + match => [ "timestamp", "ISO8601" ] + } if [src_ip] { @@ -46,7 +52,10 @@ filter { output { if [type] == "kippo" { - elasticsearch { host => helium } + elasticsearch { + host => helium + protocol => http + } file { path => "/tmp/kippo-logstash.log" codec => json