mirror of https://github.com/desaster/kippo.git
restructuring and cleanup
This commit is contained in:
parent
72a6118849
commit
c3c09adb02
11
.gitignore
11
.gitignore
|
@ -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/*
|
|
|
@ -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/
|
15
kippo.tac
15
kippo.tac
|
@ -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),
|
||||||
|
|
|
@ -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 = {}
|
||||||
|
|
|
@ -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:
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
|
@ -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:
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue