From 1821dcbb27be2dabc5246357c18c4574765905cd Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Tue, 8 Jul 2014 22:29:00 -0700 Subject: [PATCH 01/13] Create hpfeeds.py --- kippo/dblog/hpfeeds.py | 242 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 kippo/dblog/hpfeeds.py diff --git a/kippo/dblog/hpfeeds.py b/kippo/dblog/hpfeeds.py new file mode 100644 index 0000000..5615e54 --- /dev/null +++ b/kippo/dblog/hpfeeds.py @@ -0,0 +1,242 @@ +from kippo.core import dblog +from twisted.python import log + +import os +import struct +import hashlib +import json +import socket +import uuid + +BUFSIZ = 16384 + +OP_ERROR = 0 +OP_INFO = 1 +OP_AUTH = 2 +OP_PUBLISH = 3 +OP_SUBSCRIBE = 4 + +MAXBUF = 1024**2 +SIZES = { + OP_ERROR: 5+MAXBUF, + OP_INFO: 5+256+20, + OP_AUTH: 5+256+20, + OP_PUBLISH: 5+MAXBUF, + OP_SUBSCRIBE: 5+256*2, +} + +KIPPOCHAN = 'kippo.sessions' + +class BadClient(Exception): + pass + +# packs a string with 1 byte length field +def strpack8(x): + if isinstance(x, str): x = x.encode('latin1') + return struct.pack('!B', len(x)) + x + +# unpacks a string with 1 byte length field +def strunpack8(x): + l = x[0] + return x[1:1+l], x[1+l:] + +def msghdr(op, data): + return struct.pack('!iB', 5+len(data), op) + data +def msgpublish(ident, chan, data): + return msghdr(OP_PUBLISH, strpack8(ident) + strpack8(chan) + data) +def msgsubscribe(ident, chan): + if isinstance(chan, str): chan = chan.encode('latin1') + return msghdr(OP_SUBSCRIBE, strpack8(ident) + chan) +def msgauth(rand, ident, secret): + hash = hashlib.sha1(bytes(rand)+secret).digest() + return msghdr(OP_AUTH, strpack8(ident) + hash) + +class FeedUnpack(object): + def __init__(self): + self.buf = bytearray() + def __iter__(self): + return self + def next(self): + return self.unpack() + def feed(self, data): + self.buf.extend(data) + def unpack(self): + if len(self.buf) < 5: + raise StopIteration('No message.') + + ml, opcode = struct.unpack('!iB', buffer(self.buf,0,5)) + if ml > SIZES.get(opcode, MAXBUF): + raise BadClient('Not respecting MAXBUF.') + + if len(self.buf) < ml: + raise StopIteration('No message.') + + data = bytearray(buffer(self.buf, 5, ml-5)) + del self.buf[:ml] + return opcode, data + +class hpclient(object): + def __init__(self, server, port, ident, secret, debug): + print 'hpfeeds client init broker {0}:{1}, identifier {2}'.format(server, port, ident) + self.server, self.port = server, int(port) + self.ident, self.secret = ident.encode('latin1'), secret.encode('latin1') + self.debug = debug + self.unpacker = FeedUnpack() + self.state = 'INIT' + + self.connect() + self.sendfiles = [] + self.filehandle = None + + def connect(self): + self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.s.settimeout(3) + try: self.s.connect((self.server, self.port)) + except: + print 'hpfeeds client could not connect to broker.' + self.s = None + else: + self.s.settimeout(None) + self.handle_established() + + def send(self, data): + if not self.s: return + self.s.send(data) + + def close(self): + self.s.close() + self.s = None + + def handle_established(self): + if self.debug: print 'hpclient established' + while self.state != 'GOTINFO': + self.read() + + #quickly try to see if there was an error message + self.s.settimeout(0.5) + self.read() + self.s.settimeout(None) + + def read(self): + if not self.s: return + try: d = self.s.recv(BUFSIZ) + except socket.timeout: + return + + if not d: + if self.debug: log.msg('hpclient connection closed?') + self.close() + return + + self.unpacker.feed(d) + try: + for opcode, data in self.unpacker: + if self.debug: log.msg('hpclient msg opcode {0} data {1}'.format(opcode, data)) + if opcode == OP_INFO: + name, rand = strunpack8(data) + if self.debug: log.msg('hpclient server name {0} rand {1}'.format(name, rand)) + self.send(msgauth(rand, self.ident, self.secret)) + self.state = 'GOTINFO' + + elif opcode == OP_PUBLISH: + ident, data = strunpack8(data) + chan, data = strunpack8(data) + if self.debug: log.msg('publish to {0} by {1}: {2}'.format(chan, ident, data)) + + elif opcode == OP_ERROR: + log.err('errormessage from server: {0}'.format(data)) + else: + log.err('unknown opcode message: {0}'.format(opcode)) + except BadClient: + log.err('unpacker error, disconnecting.') + self.close() + + def publish(self, channel, **kwargs): + try: + self.send(msgpublish(self.ident, channel, json.dumps(kwargs).encode('latin1'))) + except Exception, e: + log.err('connection to hpfriends lost: {0}'.format(e)) + log.err('connecting') + self.connect() + self.send(msgpublish(self.ident, channel, json.dumps(kwargs).encode('latin1'))) + + def sendfile(self, filepath): + # does not read complete binary into memory, read and send chunks + if not self.filehandle: + self.sendfileheader(i.file) + self.sendfiledata() + else: self.sendfiles.append(filepath) + + def sendfileheader(self, filepath): + self.filehandle = open(filepath, 'rb') + fsize = os.stat(filepath).st_size + headc = strpack8(self.ident) + strpack8(UNIQUECHAN) + headh = struct.pack('!iB', 5+len(headc)+fsize, OP_PUBLISH) + self.send(headh + headc) + + def sendfiledata(self): + tmp = self.filehandle.read(BUFSIZ) + if not tmp: + if self.sendfiles: + fp = self.sendfiles.pop(0) + self.sendfileheader(fp) + else: + self.filehandle = None + self.handle_io_in(b'') + else: + self.send(tmp) + + +class DBLogger(dblog.DBLogger): + def start(self, cfg): + print 'hpfeeds DBLogger start' + + server = cfg.get('database_hpfeeds', 'server') + port = cfg.get('database_hpfeeds', 'port') + ident = cfg.get('database_hpfeeds', 'identifier') + secret = cfg.get('database_hpfeeds', 'secret') + debug = cfg.get('database_hpfeeds', 'debug') + + self.client = hpclient(server, port, ident, secret, debug) + self.meta = {} + + # We have to return an unique ID + def createSession(self, peerIP, peerPort, hostIP, hostPort): + session = uuid.uuid4().hex + self.meta[session] = {'peerIP': peerIP, 'peerPort': peerPort, + 'hostIP': hostIP, 'hostPort': hostPort, 'loggedin': None, + 'credentials':[], 'version': None, 'ttylog': None } + return session + + def handleConnectionLost(self, session, args): + log.msg('publishing metadata to hpfeeds') + meta = self.meta[session] + ttylog = self.ttylog(session) + if ttylog: meta['ttylog'] = ttylog.encode('hex') + self.client.publish(KIPPOCHAN, **meta) + + def handleLoginFailed(self, session, args): + u, p = args['username'], args['password'] + self.meta[session]['credentials'].append((u,p)) + + def handleLoginSucceeded(self, session, args): + u, p = args['username'], args['password'] + self.meta[session]['loggedin'] = (u,p) + + def handleCommand(self, session, args): + pass + + def handleUnknownCommand(self, session, args): + pass + + def handleInput(self, session, args): + pass + + def handleTerminalSize(self, session, args): + pass + + def handleClientVersion(self, session, args): + v = args['version'] + self.meta[session]['version'] = v + +# vim: set sw=4 et: From f185494a82fbf9bb77ab64976fdb3e1a519b098f Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Tue, 8 Jul 2014 22:30:45 -0700 Subject: [PATCH 02/13] adding config template for hpfeeds --- kippo.cfg.dist | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/kippo.cfg.dist b/kippo.cfg.dist index 73cee22..138a37e 100644 --- a/kippo.cfg.dist +++ b/kippo.cfg.dist @@ -193,3 +193,10 @@ interact_port = 5123 #[database_textlog] #logfile = kippo-textlog.log + +#[database_hpfeeds] +#server = hpfeeds.mysite.org +#port = 10000 +#identifier = abc123 +#secret = secret +#debug=false From 6a4ee90dc0160d3618aa841826dad057b2a859e3 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Tue, 8 Jul 2014 22:40:16 -0700 Subject: [PATCH 03/13] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 07a7a68..ff682e9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Kippo +With added HPfeeds support for MHN compatibility. + Kippo is a medium interaction SSH honeypot designed to log brute force attacks and, most importantly, the entire shell interaction performed by the attacker. Kippo is inspired, but not based on [Kojoney](http://kojoney.sourceforge.net/). From 38693444254666370ea4a8e4cfebd6cdeb792644 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Tue, 8 Jul 2014 22:40:45 -0700 Subject: [PATCH 04/13] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ff682e9..2835bb1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Kippo -With added HPfeeds support for MHN compatibility. +With added HPfeeds support for MHN compatibility. 07-08-2014 @threatstream Kippo is a medium interaction SSH honeypot designed to log brute force attacks and, most importantly, the entire shell interaction performed by the attacker. From 015ffaef05b89a58316bf0796aa384a6a3ec390e Mon Sep 17 00:00:00 2001 From: Jason Trost Date: Thu, 8 Jan 2015 05:38:30 -0800 Subject: [PATCH 05/13] Update wget.py --- kippo/commands/wget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kippo/commands/wget.py b/kippo/commands/wget.py index ce18937..752ce6e 100644 --- a/kippo/commands/wget.py +++ b/kippo/commands/wget.py @@ -84,7 +84,7 @@ def download(self, url, fakeoutfile, outputfile, *args, **kwargs): host = parsed.hostname port = parsed.port or (443 if scheme == 'https' else 80) path = parsed.path or '/' - if scheme == 'https' or port != 80: + if scheme == 'https' or port == 443 or port == 8443: self.writeln('Sorry, SSL not supported in this release') self.exit() return None From e0afed332433b79d63eb26d4ce1b60e6c8598f20 Mon Sep 17 00:00:00 2001 From: aabed Date: Tue, 20 Jan 2015 17:30:31 +0000 Subject: [PATCH 06/13] adds implemntation for logging commands,unknown command and urls --- kippo/dblog/hpfeeds.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/kippo/dblog/hpfeeds.py b/kippo/dblog/hpfeeds.py index 5615e54..e34e030 100644 --- a/kippo/dblog/hpfeeds.py +++ b/kippo/dblog/hpfeeds.py @@ -203,9 +203,9 @@ def start(self, cfg): # We have to return an unique ID def createSession(self, peerIP, peerPort, hostIP, hostPort): session = uuid.uuid4().hex - self.meta[session] = {'peerIP': peerIP, 'peerPort': peerPort, - 'hostIP': hostIP, 'hostPort': hostPort, 'loggedin': None, - 'credentials':[], 'version': None, 'ttylog': None } + self.meta[session] = {'session':session,'peerIP': peerIP, 'peerPort': peerPort, + 'hostIP': hostIP, 'hostPort': hostPort, 'loggedin': None, + 'credentials':[], 'commands':[],"unknownCommands":[],'urls':[],'version': None, 'ttylog': None } return session def handleConnectionLost(self, session, args): @@ -224,19 +224,26 @@ def handleLoginSucceeded(self, session, args): self.meta[session]['loggedin'] = (u,p) def handleCommand(self, session, args): - pass - + c = args['input'] + self.meta[session]['commands'].append(c) + def handleUnknownCommand(self, session, args): - pass + uc = args['input'] + self.meta[session]['unknownCommands'].append(uc) def handleInput(self, session, args): pass - + def handleTerminalSize(self, session, args): - pass + pass def handleClientVersion(self, session, args): v = args['version'] self.meta[session]['version'] = v + def handleFileDownload(self,session,args): + url = args['url'] + self.meta[session]['urls'].append(url) + + # vim: set sw=4 et: From c93d85b840860d4392f7c1eea1a0c01fa3fd00fd Mon Sep 17 00:00:00 2001 From: aabed Date: Sun, 25 Jan 2015 20:41:54 +0000 Subject: [PATCH 07/13] Fixing indentation --- kippo/dblog/hpfeeds.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kippo/dblog/hpfeeds.py b/kippo/dblog/hpfeeds.py index e34e030..295277a 100644 --- a/kippo/dblog/hpfeeds.py +++ b/kippo/dblog/hpfeeds.py @@ -204,8 +204,8 @@ def start(self, cfg): def createSession(self, peerIP, peerPort, hostIP, hostPort): session = uuid.uuid4().hex self.meta[session] = {'session':session,'peerIP': peerIP, 'peerPort': peerPort, - 'hostIP': hostIP, 'hostPort': hostPort, 'loggedin': None, - 'credentials':[], 'commands':[],"unknownCommands":[],'urls':[],'version': None, 'ttylog': None } + 'hostIP': hostIP, 'hostPort': hostPort, 'loggedin': None, + 'credentials':[], 'commands':[],"unknownCommands":[],'urls':[],'version': None, 'ttylog': None } return session def handleConnectionLost(self, session, args): From 1d4dace835c30f4eea5e5f32d9136d4c59001ea4 Mon Sep 17 00:00:00 2001 From: aabed Date: Mon, 26 Jan 2015 23:15:57 +0000 Subject: [PATCH 08/13] Adds startTime and endTime to the payload --- kippo/dblog/hpfeeds.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/kippo/dblog/hpfeeds.py b/kippo/dblog/hpfeeds.py index 295277a..892af52 100644 --- a/kippo/dblog/hpfeeds.py +++ b/kippo/dblog/hpfeeds.py @@ -1,5 +1,6 @@ from kippo.core import dblog from twisted.python import log +from datetime import datetime import os import struct @@ -203,7 +204,8 @@ def start(self, cfg): # We have to return an unique ID def createSession(self, peerIP, peerPort, hostIP, hostPort): session = uuid.uuid4().hex - self.meta[session] = {'session':session,'peerIP': peerIP, 'peerPort': peerPort, + startTime=datetime.now().isoformat() + self.meta[session] = {'session':session,'startTime':startTime,'endTime':'','peerIP': peerIP, 'peerPort': peerPort, 'hostIP': hostIP, 'hostPort': hostPort, 'loggedin': None, 'credentials':[], 'commands':[],"unknownCommands":[],'urls':[],'version': None, 'ttylog': None } return session @@ -211,6 +213,7 @@ def createSession(self, peerIP, peerPort, hostIP, hostPort): def handleConnectionLost(self, session, args): log.msg('publishing metadata to hpfeeds') meta = self.meta[session] + self.meta[session]['endTime']=datetime.now().isoformat() ttylog = self.ttylog(session) if ttylog: meta['ttylog'] = ttylog.encode('hex') self.client.publish(KIPPOCHAN, **meta) From 8db3c70029fe0808816d150aec75b6efb338a3ac Mon Sep 17 00:00:00 2001 From: Jason Trost Date: Sat, 7 Feb 2015 12:11:20 -0500 Subject: [PATCH 09/13] fix for hydra and libssh --- kippo/core/honeypot.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/kippo/core/honeypot.py b/kippo/core/honeypot.py index 6cd07ad..e6be215 100644 --- a/kippo/core/honeypot.py +++ b/kippo/core/honeypot.py @@ -531,9 +531,12 @@ def sendKexInit(self): transport.SSHServerTransport.sendKexInit(self) def dataReceived(self, data): + # Workaround libssh not working with Twisted Hydra not working (by mercolino) + isLibssh = data.find('libssh', data.find('SSH-')) != -1 + transport.SSHServerTransport.dataReceived(self, data) # later versions seem to call sendKexInit again on their own - if twisted.version.major < 11 and \ + if (twisted.version.major < 11 or isLibssh) and \ not self.hadVersion and self.gotVersion: self.sendKexInit() self.hadVersion = True From ac2d7c0e907a75c1ee9d677d7a0badd925c6339a Mon Sep 17 00:00:00 2001 From: Jason Trost Date: Mon, 29 Jun 2015 07:06:34 -0400 Subject: [PATCH 10/13] added use of reported_ssh_port --- kippo.cfg.dist | 3 +++ kippo/core/dblog.py | 17 +++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/kippo.cfg.dist b/kippo.cfg.dist index 138a37e..47a5bcc 100644 --- a/kippo.cfg.dist +++ b/kippo.cfg.dist @@ -14,6 +14,9 @@ # (default: 2222) ssh_port = 2222 +# Source Port to report in logs (useful if you use iptables to forward ports to kippo) +reported_ssh_port = 22 + # Hostname for the honeypot. Displayed by the shell prompt of the virtual # environment. # diff --git a/kippo/core/dblog.py b/kippo/core/dblog.py index 59fba1b..59076c4 100644 --- a/kippo/core/dblog.py +++ b/kippo/core/dblog.py @@ -39,6 +39,11 @@ def __init__(self, cfg): ('^Remote SSH version: (?P.*)$', self.handleClientVersion), )] + + self.reported_ssh_port = None + if self.cfg.has_option('honeypot', 'reported_ssh_port'): + self.reported_ssh_port = int(cfg.get('honeypot', 'reported_ssh_port')) + self.start(cfg) def logDispatch(self, sessionid, msg): @@ -68,10 +73,14 @@ def emit(self, ev): match = self.re_connected.match(ev['message'][0]) if match: sessionid = int(match.groups()[4]) - self.sessions[sessionid] = \ - self.createSession( - match.groups()[0], int(match.groups()[1]), - match.groups()[2], int(match.groups()[3])) + peerIP, peerPort = match.groups()[0], int(match.groups()[1]) + hostIP, hostPort = match.groups()[2], int(match.groups()[3]) + if self.reported_ssh_port: + hostPort = self.reported_ssh_port + + self.sessions[sessionid] = self.createSession( + peerIP, peerPort, hostIP, hostPort + ) return match = self.re_sessionlog.match(ev['system']) if not match: From 634d08bad8b1e9ae984487b8108a3d06f018043d Mon Sep 17 00:00:00 2001 From: Jason Trost Date: Mon, 29 Jun 2015 08:06:29 -0400 Subject: [PATCH 11/13] added ability to log the public IP (useful if listening on 127.0.0.1) --- kippo.cfg.dist | 2 ++ kippo/core/dblog.py | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/kippo.cfg.dist b/kippo.cfg.dist index 47a5bcc..3d80dad 100644 --- a/kippo.cfg.dist +++ b/kippo.cfg.dist @@ -17,6 +17,8 @@ ssh_port = 2222 # Source Port to report in logs (useful if you use iptables to forward ports to kippo) reported_ssh_port = 22 +report_public_ip = true + # Hostname for the honeypot. Displayed by the shell prompt of the virtual # environment. # diff --git a/kippo/core/dblog.py b/kippo/core/dblog.py index 59076c4..ea3bc86 100644 --- a/kippo/core/dblog.py +++ b/kippo/core/dblog.py @@ -44,6 +44,13 @@ def __init__(self, cfg): if self.cfg.has_option('honeypot', 'reported_ssh_port'): self.reported_ssh_port = int(cfg.get('honeypot', 'reported_ssh_port')) + self.report_public_ip = False + if self.cfg.has_option('honeypot', 'report_public_ip'): + if cfg.get('honeypot', 'report_public_ip') == "true" or cfg.get('honeypot', 'report_public_ip') == "1": + self.report_public_ip = True + import urllib + self.public_ip = urllib.urlopen('http://myip.threatstream.com').readline() + self.start(cfg) def logDispatch(self, sessionid, msg): @@ -77,6 +84,8 @@ def emit(self, ev): hostIP, hostPort = match.groups()[2], int(match.groups()[3]) if self.reported_ssh_port: hostPort = self.reported_ssh_port + if self.report_public_ip: + hostIP = self.public_ip self.sessions[sessionid] = self.createSession( peerIP, peerPort, hostIP, hostPort From e7dec5620100257fa56cec2a1858cbb4864a12a5 Mon Sep 17 00:00:00 2001 From: Martin Forssen Date: Wed, 14 Oct 2015 13:28:04 +0200 Subject: [PATCH 12/13] Try to reconnect to the hpfeeds server if the connection is down when we are trying to send data. This way we recover from temporary network problems. --- kippo/dblog/hpfeeds.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/kippo/dblog/hpfeeds.py b/kippo/dblog/hpfeeds.py index 892af52..53134cc 100644 --- a/kippo/dblog/hpfeeds.py +++ b/kippo/dblog/hpfeeds.py @@ -101,6 +101,9 @@ def connect(self): self.handle_established() def send(self, data): + if not self.s: + self.connect() + if not self.s: return self.s.send(data) From d6d7a52e66d9b5b430e8a540b5f0e45adc9d5e7b Mon Sep 17 00:00:00 2001 From: Sean Maloney Date: Mon, 30 Sep 2019 15:49:37 -0700 Subject: [PATCH 13/13] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2835bb1..11e8a68 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Kippo -With added HPfeeds support for MHN compatibility. 07-08-2014 @threatstream +With added HPfeeds support for MHN compatibility. 07-08-2014 Kippo is a medium interaction SSH honeypot designed to log brute force attacks and, most importantly, the entire shell interaction performed by the attacker.