restructuring and cleanup

This commit is contained in:
Upi Tamminen 2014-08-09 23:48:34 +03:00
parent 72a6118849
commit c3c09adb02
15 changed files with 702 additions and 615 deletions

View File

@ -1,11 +0,0 @@
*.pyc
kippo.cfg
kippo.pid
data/lastlog.txt
data/ssh_host_dsa_key
data/ssh_host_dsa_key.pub
data/ssh_host_rsa_key
data/ssh_host_rsa_key.pub
dl/*
log/kippo.log
log/tty/*

66
.gitignore vendored Normal file
View File

@ -0,0 +1,66 @@
kippo.cfg
kippo.pid
data/lastlog.txt
data/ssh_host_dsa_key
data/ssh_host_dsa_key.pub
data/ssh_host_rsa_key
data/ssh_host_rsa_key.pub
dl/*
log/kippo.log
log/tty/*
private.key
public.key
# Created by .gitignore support plugin (hsz.mobi)
### Python template
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.cache
nosetests.xml
coverage.xml
# Translations
*.mo
*.pot
# Sphinx documentation
docs/_build/
# PyBuilder
target/

View File

@ -22,15 +22,18 @@ if not os.path.exists('kippo.cfg'):
print 'ERROR: kippo.cfg is missing!' print 'ERROR: kippo.cfg is missing!'
sys.exit(1) sys.exit(1)
from kippo.core import honeypot
from kippo.core.config import config from kippo.core.config import config
import kippo.core.auth
import kippo.core.honeypot
import kippo.core.ssh
from kippo import core
factory = honeypot.HoneyPotSSHFactory() factory = core.ssh.HoneyPotSSHFactory()
factory.portal = portal.Portal(honeypot.HoneyPotRealm()) factory.portal = portal.Portal(core.ssh.HoneyPotRealm())
rsa_pubKeyString, rsa_privKeyString = honeypot.getRSAKeys() rsa_pubKeyString, rsa_privKeyString = core.ssh.getRSAKeys()
dsa_pubKeyString, dsa_privKeyString = honeypot.getDSAKeys() dsa_pubKeyString, dsa_privKeyString = core.ssh.getDSAKeys()
factory.portal.registerChecker(honeypot.HoneypotPasswordChecker()) factory.portal.registerChecker(core.auth.HoneypotPasswordChecker())
factory.publicKeys = {'ssh-rsa': keys.Key.fromString(data=rsa_pubKeyString), factory.publicKeys = {'ssh-rsa': keys.Key.fromString(data=rsa_pubKeyString),
'ssh-dss': keys.Key.fromString(data=dsa_pubKeyString)} 'ssh-dss': keys.Key.fromString(data=dsa_pubKeyString)}
factory.privateKeys = {'ssh-rsa': keys.Key.fromString(data=rsa_privKeyString), factory.privateKeys = {'ssh-rsa': keys.Key.fromString(data=rsa_privKeyString),

View File

@ -5,7 +5,7 @@ import os, time, anydbm, datetime
from kippo.core.honeypot import HoneyPotCommand from kippo.core.honeypot import HoneyPotCommand
from twisted.internet import reactor from twisted.internet import reactor
from kippo.core.config import config from kippo.core.config import config
from kippo.core.userdb import UserDB from kippo.core.auth import UserDB
from kippo.core import utils from kippo.core import utils
commands = {} commands = {}

View File

@ -1,15 +1,18 @@
# # Copyright (c) 2009-2014 Upi Tamminen <desaster@gmail.com>
# userdb.py for kippo # See the COPYRIGHT file for more information
# by Walter de Jong <walter@sara.nl>
#
# adopted and further modified by Upi Tamminen <desaster@gmail.com>
#
from kippo.core.config import config
import os
import string import string
class UserDB: import twisted
from twisted.cred import checkers, credentials, error
from twisted.internet import defer
from zope.interface import implements
from kippo.core.config import config
# by Walter de Jong <walter@sara.nl>
class UserDB(object):
def __init__(self): def __init__(self):
self.userdb = [] self.userdb = []
self.load() self.load()
@ -96,4 +99,39 @@ class UserDB:
self.userdb.append((login, uid, passwd)) self.userdb.append((login, uid, passwd))
self.save() self.save()
class HoneypotPasswordChecker:
implements(checkers.ICredentialsChecker)
credentialInterfaces = (credentials.IUsernamePassword,
credentials.IPluggableAuthenticationModules)
def requestAvatarId(self, credentials):
if hasattr(credentials, 'password'):
if self.checkUserPass(credentials.username, credentials.password):
return defer.succeed(credentials.username)
else:
return defer.fail(error.UnauthorizedLogin())
elif hasattr(credentials, 'pamConversion'):
return self.checkPamUser(credentials.username,
credentials.pamConversion)
return defer.fail(error.UnhandledCredentials())
def checkPamUser(self, username, pamConversion):
r = pamConversion((('Password:', 1),))
return r.addCallback(self.cbCheckPamUser, username)
def cbCheckPamUser(self, responses, username):
for response, zero in responses:
if self.checkUserPass(username, response):
return defer.succeed(username)
return defer.fail(error.UnauthorizedLogin())
def checkUserPass(self, username, password):
if UserDB().checklogin(username, password):
print 'login attempt [%s/%s] succeeded' % (username, password)
return True
else:
print 'login attempt [%s/%s] failed' % (username, password)
return False
# vim: set sw=4 et: # vim: set sw=4 et:

View File

@ -1,4 +1,4 @@
# Copyright (c) 2009 Upi Tamminen <desaster@gmail.com> # Copyright (c) 2009-2014 Upi Tamminen <desaster@gmail.com>
# See the COPYRIGHT file for more information # See the COPYRIGHT file for more information
import ConfigParser, os import ConfigParser, os

View File

@ -1,4 +1,4 @@
# Copyright (c) 2009 Upi Tamminen <desaster@gmail.com> # Copyright (c) 2009-2014 Upi Tamminen <desaster@gmail.com>
# See the COPYRIGHT file for more information # See the COPYRIGHT file for more information
import re, time, socket import re, time, socket

View File

@ -1,3 +1,8 @@
# Copyright (c) 2009-2014 Upi Tamminen <desaster@gmail.com>
# See the COPYRIGHT file for more information
class NotEnabledException(Exception): class NotEnabledException(Exception):
""" Feature not enabled """ Feature not enabled
""" """
# vim: set sw=4 et:

View File

@ -1,4 +1,4 @@
# Copyright (c) 2009 Upi Tamminen <desaster@gmail.com> # Copyright (c) 2009-2014 Upi Tamminen <desaster@gmail.com>
# See the COPYRIGHT file for more information # See the COPYRIGHT file for more information
import os, time, fnmatch import os, time, fnmatch

View File

@ -1,26 +1,16 @@
# Copyright (c) 2009 Upi Tamminen <desaster@gmail.com> # Copyright (c) 2009-2014 Upi Tamminen <desaster@gmail.com>
# See the COPYRIGHT file for more information # See the COPYRIGHT file for more information
import twisted import twisted
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.internet import reactor, protocol, defer
from twisted.python import failure, log
from zope.interface import implements
from copy import deepcopy, copy from copy import deepcopy, copy
import sys, os, random, pickle, time, stat, shlex, anydbm, struct import shlex
from kippo.core import ttylog, fs, utils from kippo.core import fs
from kippo.core.userdb import UserDB
from kippo.core.config import config from kippo.core.config import config
from kippo.core import exceptions import kippo.core.exceptions
from kippo import core
import commands import pickle
import ConfigParser
class HoneyPotCommand(object): class HoneyPotCommand(object):
def __init__(self, honeypot, *args): def __init__(self, honeypot, *args):
@ -253,296 +243,6 @@ class HoneyPotShell(object):
self.honeypot.lineBufferIndex = len(self.honeypot.lineBuffer) self.honeypot.lineBufferIndex = len(self.honeypot.lineBuffer)
self.honeypot.terminal.write(newbuf) self.honeypot.terminal.write(newbuf)
class HoneyPotBaseProtocol(insults.TerminalProtocol):
def __init__(self, user, env):
self.user = user
self.env = env
self.hostname = self.env.cfg.get('honeypot', 'hostname')
self.fs = fs.HoneyPotFilesystem(deepcopy(self.env.fs))
if self.fs.exists(user.home):
self.cwd = user.home
else:
self.cwd = '/'
# 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 logDispatch(self, msg):
transport = self.terminal.transport.session.conn.transport
msg = ':dispatch: ' + msg
transport.factory.logDispatch(transport.transport.sessionno, msg)
def connectionMade(self):
self.displayMOTD()
transport = self.terminal.transport.session.conn.transport
#transport = self.transport.transport.session.conn.transport
self.realClientIP = transport.getPeer().address.host
self.clientVersion = transport.otherVersionString
self.logintime = transport.logintime
self.ttylog_file = transport.ttylog_file
# source IP of client in user visible reports (can be fake or real)
cfg = config()
if cfg.has_option('honeypot', 'fake_addr'):
self.clientIP = cfg.get('honeypot', 'fake_addr')
else:
self.clientIP = self.realClientIP
def displayMOTD(self):
try:
self.writeln(self.fs.file_contents('/etc/motd'))
except:
pass
# this doesn't seem to be called upon disconnect, so please use
# HoneyPotTransport.connectionLost instead
def connectionLost(self, reason):
pass
# not sure why i need to do this:
# scratch that, these don't seem to be necessary anymore:
#del self.fs
#del self.commands
def txtcmd(self, txt):
class command_txtcmd(HoneyPotCommand):
def call(self):
print 'Reading txtcmd from "%s"' % txt
f = file(txt, 'r')
self.write(f.read())
f.close()
return command_txtcmd
def getCommand(self, cmd, paths):
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' % (self.fs.resolve_path(x, self.cwd), cmd) \
for x in paths]:
if self.fs.exists(i):
path = i
break
txt = os.path.abspath('%s/%s' % \
(self.env.cfg.get('honeypot', 'txtcmds_path'), path))
if os.path.exists(txt) and os.path.isfile(txt):
return self.txtcmd(txt)
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 writeln(self, data):
self.terminal.write(data)
self.terminal.nextLine()
def call_command(self, cmd, *args):
obj = cmd(self, *args)
self.cmdstack.append(obj)
obj.start()
def addInteractor(self, interactor):
transport = self.terminal.transport.session.conn.transport
transport.interactors.append(interactor)
def delInteractor(self, interactor):
transport = self.terminal.transport.session.conn.transport
transport.interactors.remove(interactor)
def uptime(self, reset = None):
transport = self.terminal.transport.session.conn.transport
r = time.time() - transport.factory.starttime
if reset:
transport.factory.starttime = reset
return r
class HoneyPotInteractiveProtocol(HoneyPotBaseProtocol, recvline.HistoricRecvLine):
def __init__(self, user, env):
recvline.HistoricRecvLine.__init__(self)
HoneyPotBaseProtocol.__init__(self, user, env)
def connectionMade(self):
HoneyPotBaseProtocol.connectionMade(self)
recvline.HistoricRecvLine.connectionMade(self)
self.cmdstack = [HoneyPotShell(self)]
transport = self.terminal.transport.session.conn.transport
transport.factory.sessions[transport.transport.sessionno] = self
self.keyHandlers.update({
'\x04': self.handle_CTRL_D,
'\x15': self.handle_CTRL_U,
'\x03': self.handle_CTRL_C,
'\x09': self.handle_TAB,
})
# this doesn't seem to be called upon disconnect, so please use
# HoneyPotTransport.connectionLost instead
def connectionLost(self, reason):
HoneyPotBaseProtocol.connectionLost(self, reason)
recvline.HistoricRecvLine.connectionLost(self, reason)
# Overriding to prevent terminal.reset()
def initializeScreen(self):
self.setInsertMode()
def call_command(self, cmd, *args):
self.setTypeoverMode()
HoneyPotBaseProtocol.call_command(self, cmd, *args)
def keystrokeReceived(self, keyID, modifier):
transport = self.terminal.transport.session.conn.transport
if type(keyID) == type(''):
ttylog.ttylog_write(transport.ttylog_file, len(keyID),
ttylog.TYPE_INPUT, time.time(), keyID)
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 handle_RETURN(self):
if len(self.cmdstack) == 1:
if self.lineBuffer:
self.historyLines.append(''.join(self.lineBuffer))
self.historyPosition = len(self.historyLines)
return recvline.RecvLine.handle_RETURN(self)
def handle_CTRL_C(self):
self.cmdstack[-1].ctrl_c()
def handle_CTRL_U(self):
for i in range(self.lineBufferIndex):
self.terminal.cursorBackward()
self.terminal.deleteCharacter()
self.lineBuffer = self.lineBuffer[self.lineBufferIndex:]
self.lineBufferIndex = 0
def handle_CTRL_D(self):
self.call_command(self.commands['exit'])
def handle_TAB(self):
self.cmdstack[-1].handle_TAB()
class HoneyPotExecProtocol(HoneyPotBaseProtocol):
def __init__(self, user, env, execcmd):
self.execcmd = execcmd
HoneyPotBaseProtocol.__init__(self, user, env)
def connectionMade(self):
HoneyPotBaseProtocol.connectionMade(self)
self.cmdstack = [HoneyPotShell(self, interactive=False)]
print 'Running exec command "%s"' % self.execcmd
self.cmdstack[0].lineReceived(self.execcmd)
class LoggingServerProtocol(insults.ServerProtocol):
def connectionMade(self):
transport = self.transport.session.conn.transport
transport.ttylog_file = '%s/tty/%s-%s.log' % \
(config().get('honeypot', 'log_path'),
time.strftime('%Y%m%d-%H%M%S'),
int(random.random() * 10000))
print 'Opening TTY log: %s' % transport.ttylog_file
ttylog.ttylog_open(transport.ttylog_file, time.time())
transport.ttylog_open = True
insults.ServerProtocol.connectionMade(self)
def write(self, bytes, noLog = False):
transport = self.transport.session.conn.transport
for i in transport.interactors:
i.sessionWrite(bytes)
if transport.ttylog_open and not noLog:
ttylog.ttylog_write(transport.ttylog_file, len(bytes),
ttylog.TYPE_OUTPUT, time.time(), bytes)
insults.ServerProtocol.write(self, bytes)
# this doesn't seem to be called upon disconnect, so please use
# HoneyPotTransport.connectionLost instead
def connectionLost(self, reason):
insults.ServerProtocol.connectionLost(self, reason)
class HoneyPotSSHSession(session.SSHSession):
def request_env(self, data):
print 'request_env: %s' % (repr(data))
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': HoneyPotSSHSession})
userdb = UserDB()
self.uid = self.gid = userdb.getUID(self.username)
if not self.uid:
self.home = '/root'
else:
self.home = '/home/' + username
def openShell(self, protocol):
serverProtocol = LoggingServerProtocol(
HoneyPotInteractiveProtocol, self, self.env)
serverProtocol.makeConnection(protocol)
protocol.makeConnection(session.wrapProtocol(serverProtocol))
def getPty(self, terminal, windowSize, attrs):
print 'Terminal size: %s %s' % windowSize[0:2]
self.windowSize = windowSize
return None
def execCommand(self, protocol, cmd):
cfg = config()
if not cfg.has_option('honeypot', 'exec_enabled') or \
cfg.get('honeypot', 'exec_enabled').lower() not in \
('yes', 'true', 'on'):
print 'Exec disabled. Not executing command: "%s"' % cmd
raise exceptions.NotEnabledException, \
'exce_enabled not enabled in configuration file!'
return
print 'exec command: "%s"' % cmd
serverProtocol = LoggingServerProtocol(
HoneyPotExecProtocol, self, self.env, cmd)
serverProtocol.makeConnection(protocol)
protocol.makeConnection(session.wrapProtocol(serverProtocol))
def closed(self):
pass
def eofReceived(self):
pass
def windowChanged(self, windowSize):
self.windowSize = windowSize
class HoneyPotEnvironment(object): class HoneyPotEnvironment(object):
def __init__(self): def __init__(self):
self.cfg = config() self.cfg = config()
@ -555,281 +255,4 @@ class HoneyPotEnvironment(object):
self.fs = pickle.load(file( self.fs = pickle.load(file(
self.cfg.get('honeypot', 'filesystem_file'), 'rb')) self.cfg.get('honeypot', 'filesystem_file'), 'rb'))
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."
class HoneyPotTransport(transport.SSHServerTransport):
hadVersion = False
def connectionMade(self):
print 'New connection: %s:%s (%s:%s) [session: %d]' % \
(self.transport.getPeer().host, self.transport.getPeer().port,
self.transport.getHost().host, self.transport.getHost().port,
self.transport.sessionno)
self.interactors = []
self.logintime = time.time()
self.ttylog_open = False
transport.SSHServerTransport.connectionMade(self)
def sendKexInit(self):
# Don't send key exchange prematurely
if not self.gotVersion:
return
transport.SSHServerTransport.sendKexInit(self)
def dataReceived(self, data):
transport.SSHServerTransport.dataReceived(self, data)
# later versions seem to call sendKexInit again on their own
if twisted.version.major < 11 and \
not self.hadVersion and self.gotVersion:
self.sendKexInit()
self.hadVersion = True
def ssh_KEXINIT(self, packet):
print 'Remote SSH version: %s' % (self.otherVersionString,)
return transport.SSHServerTransport.ssh_KEXINIT(self, packet)
def lastlogExit(self):
starttime = time.strftime('%a %b %d %H:%M',
time.localtime(self.logintime))
endtime = time.strftime('%H:%M',
time.localtime(time.time()))
duration = utils.durationHuman(time.time() - self.logintime)
clientIP = self.transport.getPeer().host
utils.addToLastlog('root\tpts/0\t%s\t%s - %s (%s)' % \
(clientIP, starttime, endtime, duration))
# this seems to be the only reliable place of catching lost connection
def connectionLost(self, reason):
for i in self.interactors:
i.sessionClosed()
if self.transport.sessionno in self.factory.sessions:
del self.factory.sessions[self.transport.sessionno]
self.lastlogExit()
if self.ttylog_open:
ttylog.ttylog_close(self.ttylog_file, time.time())
self.ttylog_open = False
transport.SSHServerTransport.connectionLost(self, reason)
def sendDisconnect(self, reason, desc):
"""
Workaround for the "bad packet length" error message.
@param reason: the reason for the disconnect. Should be one of the
DISCONNECT_* values.
@type reason: C{int}
@param desc: a descrption of the reason for the disconnection.
@type desc: C{str}
"""
if not 'bad packet length' in desc:
# With python >= 3 we can use super?
transport.SSHServerTransport.sendDisconnect(self, reason, desc)
else:
self.transport.write('Protocol mismatch.\n')
log.msg('Disconnecting with error, code %s\nreason: %s' % (reason, desc))
self.transport.loseConnection()
from twisted.conch.ssh.common import NS, getNS
class HoneyPotSSHUserAuthServer(userauth.SSHUserAuthServer):
def serviceStarted(self):
userauth.SSHUserAuthServer.serviceStarted(self)
self.bannerSent = False
def sendBanner(self):
if self.bannerSent:
return
cfg = config()
if not cfg.has_option('honeypot', 'banner_file'):
return
try:
data = file(cfg.get('honeypot', 'banner_file')).read()
except IOError:
print 'Banner file %s does not exist!' % \
cfg.get('honeypot', 'banner_file')
return
if not data or not len(data.strip()):
return
data = '\r\n'.join(data.splitlines() + [''])
self.transport.sendPacket(
userauth.MSG_USERAUTH_BANNER, NS(data) + NS('en'))
self.bannerSent = True
def ssh_USERAUTH_REQUEST(self, packet):
self.sendBanner()
return userauth.SSHUserAuthServer.ssh_USERAUTH_REQUEST(self, packet)
# As implemented by Kojoney
class HoneyPotSSHFactory(factory.SSHFactory):
services = {
'ssh-userauth': HoneyPotSSHUserAuthServer,
'ssh-connection': connection.SSHConnection,
}
# Special delivery to the loggers to avoid scope problems
def logDispatch(self, sessionid, msg):
for dblog in self.dbloggers:
dblog.logDispatch(sessionid, msg)
def __init__(self):
cfg = config()
# protocol^Wwhatever instances are kept here for the interact feature
self.sessions = {}
# for use by the uptime command
self.starttime = time.time()
# convert old pass.db root passwords
passdb_file = '%s/pass.db' % (cfg.get('honeypot', 'data_path'),)
if os.path.exists(passdb_file):
userdb = UserDB()
print 'pass.db deprecated - copying passwords over to userdb.txt'
if os.path.exists('%s.bak' % (passdb_file,)):
print 'ERROR: %s.bak already exists, skipping conversion!' % \
(passdb_file,)
else:
passdb = anydbm.open(passdb_file, 'c')
for p in passdb:
userdb.adduser('root', 0, p)
passdb.close()
os.rename(passdb_file, '%s.bak' % (passdb_file,))
print 'pass.db backed up to %s.bak' % (passdb_file,)
# load db loggers
self.dbloggers = []
for x in cfg.sections():
if not x.startswith('database_'):
continue
engine = x.split('_')[1]
dbengine = 'database_' + engine
lcfg = ConfigParser.ConfigParser()
lcfg.add_section(dbengine)
for i in cfg.options(x):
lcfg.set(dbengine, i, cfg.get(x, i))
lcfg.add_section('honeypot')
for i in cfg.options('honeypot'):
lcfg.set('honeypot', i, cfg.get('honeypot', i))
print 'Loading dblog engine: %s' % (engine,)
dblogger = __import__(
'kippo.dblog.%s' % (engine,),
globals(), locals(), ['dblog']).DBLogger(lcfg)
log.startLoggingWithObserver(dblogger.emit, setStdout=False)
self.dbloggers.append(dblogger)
def buildProtocol(self, addr):
cfg = config()
# FIXME: try to mimic something real 100%
t = HoneyPotTransport()
if cfg.has_option('honeypot', 'ssh_version_string'):
t.ourVersionString = cfg.get('honeypot','ssh_version_string')
else:
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,
credentials.IPluggableAuthenticationModules)
def requestAvatarId(self, credentials):
if hasattr(credentials, 'password'):
if self.checkUserPass(credentials.username, credentials.password):
return defer.succeed(credentials.username)
else:
return defer.fail(error.UnauthorizedLogin())
elif hasattr(credentials, 'pamConversion'):
return self.checkPamUser(credentials.username,
credentials.pamConversion)
return defer.fail(error.UnhandledCredentials())
def checkPamUser(self, username, pamConversion):
r = pamConversion((('Password:', 1),))
return r.addCallback(self.cbCheckPamUser, username)
def cbCheckPamUser(self, responses, username):
for response, zero in responses:
if self.checkUserPass(username, response):
return defer.succeed(username)
return defer.fail(error.UnauthorizedLogin())
def checkUserPass(self, username, password):
if UserDB().checklogin(username, password):
print 'login attempt [%s/%s] succeeded' % (username, password)
return True
else:
print 'login attempt [%s/%s] failed' % (username, password)
return False
def getRSAKeys():
cfg = config()
public_key = cfg.get('honeypot', 'rsa_public_key')
private_key = cfg.get('honeypot', 'rsa_private_key')
if not (os.path.exists(public_key) and os.path.exists(private_key)):
print "Generating new RSA keypair..."
from Crypto.PublicKey import RSA
from twisted.python import randbytes
KEY_LENGTH = 2048
rsaKey = RSA.generate(KEY_LENGTH, randbytes.secureRandom)
publicKeyString = keys.Key(rsaKey).public().toString('openssh')
privateKeyString = keys.Key(rsaKey).toString('openssh')
with file(public_key, 'w+b') as f:
f.write(publicKeyString)
with file(private_key, 'w+b') as f:
f.write(privateKeyString)
print "Done."
else:
with file(public_key) as f:
publicKeyString = f.read()
with file(private_key) as f:
privateKeyString = f.read()
return publicKeyString, privateKeyString
def getDSAKeys():
cfg = config()
public_key = cfg.get('honeypot', 'dsa_public_key')
private_key = cfg.get('honeypot', 'dsa_private_key')
if not (os.path.exists(public_key) and os.path.exists(private_key)):
print "Generating new DSA keypair..."
from Crypto.PublicKey import DSA
from twisted.python import randbytes
KEY_LENGTH = 1024
dsaKey = DSA.generate(KEY_LENGTH, randbytes.secureRandom)
publicKeyString = keys.Key(dsaKey).public().toString('openssh')
privateKeyString = keys.Key(dsaKey).toString('openssh')
with file(public_key, 'w+b') as f:
f.write(publicKeyString)
with file(private_key, 'w+b') as f:
f.write(privateKeyString)
print "Done."
else:
with file(public_key) as f:
publicKeyString = f.read()
with file(private_key) as f:
privateKeyString = f.read()
return publicKeyString, privateKeyString
# vim: set sw=4 et: # vim: set sw=4 et:

View File

@ -1,3 +1,6 @@
# Copyright (c) 2009-2014 Upi Tamminen <desaster@gmail.com>
# See the COPYRIGHT file for more information
from twisted.internet import protocol from twisted.internet import protocol
from twisted.conch import telnet, recvline from twisted.conch import telnet, recvline
from kippo.core import ttylog from kippo.core import ttylog

253
kippo/core/protocol.py Normal file
View File

@ -0,0 +1,253 @@
# Copyright (c) 2009-2014 Upi Tamminen <desaster@gmail.com>
# See the COPYRIGHT file for more information
import os
import random
import time
import struct
from twisted.conch import recvline
from twisted.conch.ssh import transport
from twisted.conch.insults import insults
from twisted.internet import protocol
from copy import deepcopy, copy
from kippo.core import ttylog, fs
from kippo.core.config import config
from kippo.core import exceptions
from kippo import core
class HoneyPotBaseProtocol(insults.TerminalProtocol):
def __init__(self, user, env):
self.user = user
self.env = env
self.hostname = self.env.cfg.get('honeypot', 'hostname')
self.fs = fs.HoneyPotFilesystem(deepcopy(self.env.fs))
if self.fs.exists(user.home):
self.cwd = user.home
else:
self.cwd = '/'
# 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 logDispatch(self, msg):
transport = self.terminal.transport.session.conn.transport
msg = ':dispatch: ' + msg
transport.factory.logDispatch(transport.transport.sessionno, msg)
def connectionMade(self):
self.displayMOTD()
transport = self.terminal.transport.session.conn.transport
#transport = self.transport.transport.session.conn.transport
self.realClientIP = transport.getPeer().address.host
self.clientVersion = transport.otherVersionString
self.logintime = transport.logintime
self.ttylog_file = transport.ttylog_file
# source IP of client in user visible reports (can be fake or real)
cfg = config()
if cfg.has_option('honeypot', 'fake_addr'):
self.clientIP = cfg.get('honeypot', 'fake_addr')
else:
self.clientIP = self.realClientIP
def displayMOTD(self):
try:
self.writeln(self.fs.file_contents('/etc/motd'))
except:
pass
# this doesn't seem to be called upon disconnect, so please use
# HoneyPotTransport.connectionLost instead
def connectionLost(self, reason):
pass
# not sure why i need to do this:
# scratch that, these don't seem to be necessary anymore:
#del self.fs
#del self.commands
def txtcmd(self, txt):
class command_txtcmd(HoneyPotCommand):
def call(self):
print 'Reading txtcmd from "%s"' % txt
f = file(txt, 'r')
self.write(f.read())
f.close()
return command_txtcmd
def getCommand(self, cmd, paths):
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' % (self.fs.resolve_path(x, self.cwd), cmd) \
for x in paths]:
if self.fs.exists(i):
path = i
break
txt = os.path.abspath('%s/%s' % \
(self.env.cfg.get('honeypot', 'txtcmds_path'), path))
if os.path.exists(txt) and os.path.isfile(txt):
return self.txtcmd(txt)
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 writeln(self, data):
self.terminal.write(data)
self.terminal.nextLine()
def call_command(self, cmd, *args):
obj = cmd(self, *args)
self.cmdstack.append(obj)
obj.start()
def addInteractor(self, interactor):
transport = self.terminal.transport.session.conn.transport
transport.interactors.append(interactor)
def delInteractor(self, interactor):
transport = self.terminal.transport.session.conn.transport
transport.interactors.remove(interactor)
def uptime(self, reset = None):
transport = self.terminal.transport.session.conn.transport
r = time.time() - transport.factory.starttime
if reset:
transport.factory.starttime = reset
return r
class HoneyPotExecProtocol(HoneyPotBaseProtocol):
def __init__(self, user, env, execcmd):
self.execcmd = execcmd
HoneyPotBaseProtocol.__init__(self, user, env)
def connectionMade(self):
HoneyPotBaseProtocol.connectionMade(self)
self.cmdstack = [core.honeypot.HoneyPotShell(self, interactive=False)]
print 'Running exec command "%s"' % self.execcmd
self.cmdstack[0].lineReceived(self.execcmd)
class HoneyPotInteractiveProtocol(HoneyPotBaseProtocol, recvline.HistoricRecvLine):
def __init__(self, user, env):
recvline.HistoricRecvLine.__init__(self)
HoneyPotBaseProtocol.__init__(self, user, env)
def connectionMade(self):
HoneyPotBaseProtocol.connectionMade(self)
recvline.HistoricRecvLine.connectionMade(self)
self.cmdstack = [core.honeypot.HoneyPotShell(self)]
transport = self.terminal.transport.session.conn.transport
transport.factory.sessions[transport.transport.sessionno] = self
self.keyHandlers.update({
'\x04': self.handle_CTRL_D,
'\x15': self.handle_CTRL_U,
'\x03': self.handle_CTRL_C,
'\x09': self.handle_TAB,
})
# this doesn't seem to be called upon disconnect, so please use
# HoneyPotTransport.connectionLost instead
def connectionLost(self, reason):
HoneyPotBaseProtocol.connectionLost(self, reason)
recvline.HistoricRecvLine.connectionLost(self, reason)
# Overriding to prevent terminal.reset()
def initializeScreen(self):
self.setInsertMode()
def call_command(self, cmd, *args):
self.setTypeoverMode()
HoneyPotBaseProtocol.call_command(self, cmd, *args)
def keystrokeReceived(self, keyID, modifier):
transport = self.terminal.transport.session.conn.transport
if type(keyID) == type(''):
ttylog.ttylog_write(transport.ttylog_file, len(keyID),
ttylog.TYPE_INPUT, time.time(), keyID)
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 handle_RETURN(self):
if len(self.cmdstack) == 1:
if self.lineBuffer:
self.historyLines.append(''.join(self.lineBuffer))
self.historyPosition = len(self.historyLines)
return recvline.RecvLine.handle_RETURN(self)
def handle_CTRL_C(self):
self.cmdstack[-1].ctrl_c()
def handle_CTRL_U(self):
for i in range(self.lineBufferIndex):
self.terminal.cursorBackward()
self.terminal.deleteCharacter()
self.lineBuffer = self.lineBuffer[self.lineBufferIndex:]
self.lineBufferIndex = 0
def handle_CTRL_D(self):
self.call_command(self.commands['exit'])
def handle_TAB(self):
self.cmdstack[-1].handle_TAB()
class LoggingServerProtocol(insults.ServerProtocol):
def connectionMade(self):
transport = self.transport.session.conn.transport
transport.ttylog_file = '%s/tty/%s-%s.log' % \
(config().get('honeypot', 'log_path'),
time.strftime('%Y%m%d-%H%M%S'),
int(random.random() * 10000))
print 'Opening TTY log: %s' % transport.ttylog_file
ttylog.ttylog_open(transport.ttylog_file, time.time())
transport.ttylog_open = True
insults.ServerProtocol.connectionMade(self)
def write(self, bytes, noLog = False):
transport = self.transport.session.conn.transport
for i in transport.interactors:
i.sessionWrite(bytes)
if transport.ttylog_open and not noLog:
ttylog.ttylog_write(transport.ttylog_file, len(bytes),
ttylog.TYPE_OUTPUT, time.time(), bytes)
insults.ServerProtocol.write(self, bytes)
# this doesn't seem to be called upon disconnect, so please use
# HoneyPotTransport.connectionLost instead
def connectionLost(self, reason):
insults.ServerProtocol.connectionLost(self, reason)
# vim: set sw=4 et:

307
kippo/core/ssh.py Normal file
View File

@ -0,0 +1,307 @@
# Copyright (c) 2009-2014 Upi Tamminen <desaster@gmail.com>
# See the COPYRIGHT file for more information
import twisted
from twisted.cred import portal
from twisted.conch import avatar, interfaces as conchinterfaces
from twisted.conch.ssh import factory, userauth, connection, keys, session, transport
from twisted.python import log
from zope.interface import implements
import os
import time
import ConfigParser
from kippo.core import ttylog, utils
from kippo.core.config import config
import kippo.core.auth
import kippo.core.honeypot
import kippo.core.ssh
import kippo.core.protocol
from kippo import core
from twisted.conch.ssh.common import NS, getNS
class HoneyPotSSHUserAuthServer(userauth.SSHUserAuthServer):
def serviceStarted(self):
userauth.SSHUserAuthServer.serviceStarted(self)
self.bannerSent = False
def sendBanner(self):
if self.bannerSent:
return
cfg = config()
if not cfg.has_option('honeypot', 'banner_file'):
return
try:
data = file(cfg.get('honeypot', 'banner_file')).read()
except IOError:
print 'Banner file %s does not exist!' % \
cfg.get('honeypot', 'banner_file')
return
if not data or not len(data.strip()):
return
data = '\r\n'.join(data.splitlines() + [''])
self.transport.sendPacket(
userauth.MSG_USERAUTH_BANNER, NS(data) + NS('en'))
self.bannerSent = True
def ssh_USERAUTH_REQUEST(self, packet):
self.sendBanner()
return userauth.SSHUserAuthServer.ssh_USERAUTH_REQUEST(self, packet)
# As implemented by Kojoney
class HoneyPotSSHFactory(factory.SSHFactory):
services = {
'ssh-userauth': HoneyPotSSHUserAuthServer,
'ssh-connection': connection.SSHConnection,
}
# Special delivery to the loggers to avoid scope problems
def logDispatch(self, sessionid, msg):
for dblog in self.dbloggers:
dblog.logDispatch(sessionid, msg)
def __init__(self):
cfg = config()
# protocol^Wwhatever instances are kept here for the interact feature
self.sessions = {}
# for use by the uptime command
self.starttime = time.time()
# load db loggers
self.dbloggers = []
for x in cfg.sections():
if not x.startswith('database_'):
continue
engine = x.split('_')[1]
dbengine = 'database_' + engine
lcfg = ConfigParser.ConfigParser()
lcfg.add_section(dbengine)
for i in cfg.options(x):
lcfg.set(dbengine, i, cfg.get(x, i))
lcfg.add_section('honeypot')
for i in cfg.options('honeypot'):
lcfg.set('honeypot', i, cfg.get('honeypot', i))
print 'Loading dblog engine: %s' % (engine,)
dblogger = __import__(
'kippo.dblog.%s' % (engine,),
globals(), locals(), ['dblog']).DBLogger(lcfg)
log.startLoggingWithObserver(dblogger.emit, setStdout=False)
self.dbloggers.append(dblogger)
def buildProtocol(self, addr):
cfg = config()
# FIXME: try to mimic something real 100%
t = HoneyPotTransport()
if cfg.has_option('honeypot', 'ssh_version_string'):
t.ourVersionString = cfg.get('honeypot','ssh_version_string')
else:
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 HoneyPotRealm:
implements(portal.IRealm)
def __init__(self):
# I don't know if i'm supposed to keep static stuff here
self.env = core.honeypot.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."
class HoneyPotTransport(transport.SSHServerTransport):
hadVersion = False
def connectionMade(self):
print 'New connection: %s:%s (%s:%s) [session: %d]' % \
(self.transport.getPeer().host, self.transport.getPeer().port,
self.transport.getHost().host, self.transport.getHost().port,
self.transport.sessionno)
self.interactors = []
self.logintime = time.time()
self.ttylog_open = False
transport.SSHServerTransport.connectionMade(self)
def sendKexInit(self):
# Don't send key exchange prematurely
if not self.gotVersion:
return
transport.SSHServerTransport.sendKexInit(self)
def dataReceived(self, data):
transport.SSHServerTransport.dataReceived(self, data)
# later versions seem to call sendKexInit again on their own
if twisted.version.major < 11 and \
not self.hadVersion and self.gotVersion:
self.sendKexInit()
self.hadVersion = True
def ssh_KEXINIT(self, packet):
print 'Remote SSH version: %s' % (self.otherVersionString,)
return transport.SSHServerTransport.ssh_KEXINIT(self, packet)
def lastlogExit(self):
starttime = time.strftime('%a %b %d %H:%M',
time.localtime(self.logintime))
endtime = time.strftime('%H:%M',
time.localtime(time.time()))
duration = utils.durationHuman(time.time() - self.logintime)
clientIP = self.transport.getPeer().host
utils.addToLastlog('root\tpts/0\t%s\t%s - %s (%s)' % \
(clientIP, starttime, endtime, duration))
# this seems to be the only reliable place of catching lost connection
def connectionLost(self, reason):
for i in self.interactors:
i.sessionClosed()
if self.transport.sessionno in self.factory.sessions:
del self.factory.sessions[self.transport.sessionno]
self.lastlogExit()
if self.ttylog_open:
ttylog.ttylog_close(self.ttylog_file, time.time())
self.ttylog_open = False
transport.SSHServerTransport.connectionLost(self, reason)
def sendDisconnect(self, reason, desc):
"""
Workaround for the "bad packet length" error message.
@param reason: the reason for the disconnect. Should be one of the
DISCONNECT_* values.
@type reason: C{int}
@param desc: a descrption of the reason for the disconnection.
@type desc: C{str}
"""
if not 'bad packet length' in desc:
# With python >= 3 we can use super?
transport.SSHServerTransport.sendDisconnect(self, reason, desc)
else:
self.transport.write('Protocol mismatch.\n')
log.msg('Disconnecting with error, code %s\nreason: %s' % \
(reason, desc))
self.transport.loseConnection()
class HoneyPotSSHSession(session.SSHSession):
def request_env(self, data):
print 'request_env: %s' % (repr(data))
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': HoneyPotSSHSession})
userdb = core.auth.UserDB()
self.uid = self.gid = userdb.getUID(self.username)
if not self.uid:
self.home = '/root'
else:
self.home = '/home/' + username
def openShell(self, protocol):
serverProtocol = core.protocol.LoggingServerProtocol(
core.protocol.HoneyPotInteractiveProtocol, self, self.env)
serverProtocol.makeConnection(protocol)
protocol.makeConnection(session.wrapProtocol(serverProtocol))
def getPty(self, terminal, windowSize, attrs):
print 'Terminal size: %s %s' % windowSize[0:2]
self.windowSize = windowSize
return None
def execCommand(self, protocol, cmd):
cfg = config()
if not cfg.has_option('honeypot', 'exec_enabled') or \
cfg.get('honeypot', 'exec_enabled').lower() not in \
('yes', 'true', 'on'):
print 'Exec disabled. Not executing command: "%s"' % cmd
raise core.exceptions.NotEnabledException, \
'exec_enabled not enabled in configuration file!'
return
print 'exec command: "%s"' % cmd
serverProtocol = kippo.core.protocol.LoggingServerProtocol(
kippo.core.protocol.HoneyPotExecProtocol, self, self.env, cmd)
serverProtocol.makeConnection(protocol)
protocol.makeConnection(session.wrapProtocol(serverProtocol))
def closed(self):
pass
def eofReceived(self):
pass
def windowChanged(self, windowSize):
self.windowSize = windowSize
def getRSAKeys():
cfg = config()
public_key = cfg.get('honeypot', 'rsa_public_key')
private_key = cfg.get('honeypot', 'rsa_private_key')
if not (os.path.exists(public_key) and os.path.exists(private_key)):
print "Generating new RSA keypair..."
from Crypto.PublicKey import RSA
from twisted.python import randbytes
KEY_LENGTH = 2048
rsaKey = RSA.generate(KEY_LENGTH, randbytes.secureRandom)
publicKeyString = keys.Key(rsaKey).public().toString('openssh')
privateKeyString = keys.Key(rsaKey).toString('openssh')
with file(public_key, 'w+b') as f:
f.write(publicKeyString)
with file(private_key, 'w+b') as f:
f.write(privateKeyString)
print "Done."
else:
with file(public_key) as f:
publicKeyString = f.read()
with file(private_key) as f:
privateKeyString = f.read()
return publicKeyString, privateKeyString
def getDSAKeys():
cfg = config()
public_key = cfg.get('honeypot', 'dsa_public_key')
private_key = cfg.get('honeypot', 'dsa_private_key')
if not (os.path.exists(public_key) and os.path.exists(private_key)):
print "Generating new DSA keypair..."
from Crypto.PublicKey import DSA
from twisted.python import randbytes
KEY_LENGTH = 1024
dsaKey = DSA.generate(KEY_LENGTH, randbytes.secureRandom)
publicKeyString = keys.Key(dsaKey).public().toString('openssh')
privateKeyString = keys.Key(dsaKey).toString('openssh')
with file(public_key, 'w+b') as f:
f.write(publicKeyString)
with file(private_key, 'w+b') as f:
f.write(privateKeyString)
print "Done."
else:
with file(public_key) as f:
publicKeyString = f.read()
with file(private_key) as f:
privateKeyString = f.read()
return publicKeyString, privateKeyString
# vim: set et sw=4 et:

View File

@ -1,4 +1,4 @@
# Copyright (c) 2009 Upi Tamminen <desaster@gmail.com> # Copyright (c) 2009-2014 Upi Tamminen <desaster@gmail.com>
# See the COPYRIGHT file for more information # See the COPYRIGHT file for more information
# Should be compatible with user mode linux # Should be compatible with user mode linux

View File

@ -1,4 +1,4 @@
# Copyright (c) 2010 Upi Tamminen <desaster@gmail.com> # Copyright (c) 2010-2014 Upi Tamminen <desaster@gmail.com>
# See the COPYRIGHT file for more information # See the COPYRIGHT file for more information
import time, anydbm import time, anydbm