Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Kippo

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.

Kippo is inspired, but not based on [Kojoney](http://kojoney.sourceforge.net/).
Expand Down
12 changes: 12 additions & 0 deletions kippo.cfg.dist
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
# (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

report_public_ip = true

# Hostname for the honeypot. Displayed by the shell prompt of the virtual
# environment.
#
Expand Down Expand Up @@ -193,3 +198,10 @@ interact_port = 5123

#[database_textlog]
#logfile = kippo-textlog.log

#[database_hpfeeds]
#server = hpfeeds.mysite.org
#port = 10000
#identifier = abc123
#secret = secret
#debug=false
2 changes: 1 addition & 1 deletion kippo/commands/wget.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 22 additions & 4 deletions kippo/core/dblog.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ def __init__(self, cfg):
('^Remote SSH version: (?P<version>.*)$',
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.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):
Expand Down Expand Up @@ -68,10 +80,16 @@ 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
if self.report_public_ip:
hostIP = self.public_ip

self.sessions[sessionid] = self.createSession(
peerIP, peerPort, hostIP, hostPort
)
return
match = self.re_sessionlog.match(ev['system'])
if not match:
Expand Down
5 changes: 4 additions & 1 deletion kippo/core/honeypot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
255 changes: 255 additions & 0 deletions kippo/dblog/hpfeeds.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
from kippo.core import dblog
from twisted.python import log
from datetime import datetime

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:
self.connect()

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
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

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)

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):
c = args['input']
self.meta[session]['commands'].append(c)

def handleUnknownCommand(self, session, args):
uc = args['input']
self.meta[session]['unknownCommands'].append(uc)

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

def handleFileDownload(self,session,args):
url = args['url']
self.meta[session]['urls'].append(url)


# vim: set sw=4 et: