split ssh.py in multiple files to manage easier

This commit is contained in:
Michel Oosterhof
2015-12-10 13:13:54 +00:00
parent 411a76171e
commit 0ed569b2e6
4 changed files with 358 additions and 332 deletions

64
cowrie/core/keys.py Normal file
View File

@@ -0,0 +1,64 @@
# Copyright (c) 2009-2014 Upi Tamminen <desaster@gmail.com>
# See the COPYRIGHT file for more information
"""
This module contains ...
"""
import os
from twisted.conch.ssh import keys
from twisted.python import log
def getRSAKeys(cfg):
"""
"""
publicKeyFile = cfg.get('honeypot', 'rsa_public_key')
privateKeyFile = cfg.get('honeypot', 'rsa_private_key')
if not (os.path.exists(publicKeyFile) and os.path.exists(privateKeyFile)):
log.msg("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 open(publicKeyFile, 'w+b') as f:
f.write(publicKeyString)
with open(privateKeyFile, 'w+b') as f:
f.write(privateKeyString)
else:
with open(publicKeyFile, 'r') as f:
publicKeyString = f.read()
with open(privateKeyFile, 'r') as f:
privateKeyString = f.read()
return publicKeyString, privateKeyString
def getDSAKeys(cfg):
"""
"""
publicKeyFile = cfg.get('honeypot', 'dsa_public_key')
privateKeyFile = cfg.get('honeypot', 'dsa_private_key')
if not (os.path.exists(publicKeyFile) and os.path.exists(privateKeyFile)):
log.msg("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 open(publicKeyFile, 'w+b') as f:
f.write(publicKeyString)
with open(privateKeyFile, 'w+b') as f:
f.write(privateKeyString)
else:
with open(publicKeyFile, 'r') as f:
publicKeyString = f.read()
with open(privateKeyFile, 'r') as f:
privateKeyString = f.read()
return publicKeyString, privateKeyString

View File

@@ -6,298 +6,21 @@ This module contains ...
"""
import os
import time
import uuid
from zope.interface import implementer
import twisted
from twisted.conch import avatar, interfaces as conchinterfaces
from twisted.conch.ssh import factory
from twisted.conch.ssh import keys
from twisted.conch.ssh import session
from twisted.conch.ssh import transport
from twisted.conch.ssh import filetransfer
from twisted.conch.ssh import forwarding
from twisted.conch.ssh.filetransfer import FXF_READ, FXF_WRITE, FXF_APPEND, FXF_CREAT, FXF_TRUNC, FXF_EXCL
import twisted.conch.ls
from twisted.python import log, components
from twisted.conch.openssh_compat import primes
from twisted.conch.ssh.common import NS, getNS
from twisted.internet import defer
from twisted.protocols.policies import TimeoutMixin
from twisted.conch.ssh.common import getNS
from cowrie.core import credentials
from cowrie.core import auth
from cowrie.core import pwd
from cowrie.core import connection
from cowrie.core import honeypot
from cowrie.core import protocol
from cowrie.core import server
from cowrie.core import userauth
class HoneyPotSSHFactory(factory.SSHFactory):
"""
"""
services = {
'ssh-userauth': userauth.HoneyPotSSHUserAuthServer,
'ssh-connection': connection.CowrieSSHConnection,
}
def __init__(self, cfg):
self.cfg = cfg
def logDispatch(self, *msg, **args):
"""
Special delivery to the loggers to avoid scope problems
"""
for dblog in self.dbloggers:
dblog.logDispatch(*msg, **args)
for output in self.output_plugins:
output.logDispatch(*msg, **args)
def startFactory(self):
"""
"""
# Interactive protocols are kept here for the interact feature
self.sessions = {}
# For use by the uptime command
self.starttime = time.time()
# Load/create keys
rsaPubKeyString, rsaPrivKeyString = getRSAKeys(self.cfg)
dsaPubKeyString, dsaPrivKeyString = getDSAKeys(self.cfg)
self.publicKeys = {'ssh-rsa': keys.Key.fromString(data=rsaPubKeyString),
'ssh-dss': keys.Key.fromString(data=dsaPubKeyString)}
self.privateKeys = {'ssh-rsa': keys.Key.fromString(data=rsaPrivKeyString),
'ssh-dss': keys.Key.fromString(data=dsaPrivKeyString)}
# Load db loggers
self.dbloggers = []
for x in self.cfg.sections():
if not x.startswith('database_'):
continue
engine = x.split('_')[1]
try:
dblogger = __import__( 'cowrie.dblog.{}'.format(engine),
globals(), locals(), ['dblog']).DBLogger(self.cfg)
log.addObserver(dblogger.emit)
self.dbloggers.append(dblogger)
log.msg("Loaded dblog engine: {}".format(engine))
except:
log.err()
log.msg("Failed to load dblog engine: {}".format(engine))
# Load output modules
self.output_plugins = []
for x in self.cfg.sections():
if not x.startswith('output_'):
continue
engine = x.split('_')[1]
try:
output = __import__( 'cowrie.output.{}'.format(engine),
globals(), locals(), ['output']).Output(self.cfg)
log.addObserver(output.emit)
self.output_plugins.append(output)
log.msg("Loaded output engine: {}".format(engine))
except:
log.err()
log.msg("Failed to load output engine: {}".format(engine))
factory.SSHFactory.startFactory(self)
def stopFactory(self):
"""
"""
factory.SSHFactory.stopFactory(self)
def buildProtocol(self, addr):
"""
Create an instance of the server side of the SSH protocol.
@type addr: L{twisted.internet.interfaces.IAddress} provider
@param addr: The address at which the server will listen.
@rtype: L{cowrie.core.HoneyPotTransport}
@return: The built transport.
"""
_modulis = '/etc/ssh/moduli', '/private/etc/moduli'
t = HoneyPotTransport()
try:
t.ourVersionString = self.cfg.get('honeypot', 'ssh_version_string')
except:
t.ourVersionString = "SSH-2.0-OpenSSH_6.0p1 Debian-4+deb7u2"
t.supportedPublicKeys = list(self.privateKeys.keys())
for _moduli in _modulis:
try:
self.primes = primes.parseModuliFile(_moduli)
break
except IOError as err:
pass
if not self.primes:
log.msg("Moduli not found, disabling diffie-hellman-group-exchange-sha1")
ske = t.supportedKeyExchanges[:]
ske.remove('diffie-hellman-group-exchange-sha1')
t.supportedKeyExchanges = ske
# Reorder supported ciphers to resemble current openssh more
t.supportedCiphers = ['aes128-ctr', 'aes192-ctr', 'aes256-ctr', 'aes128-cbc', '3des-cbc', 'blowfish-cbc', 'cast128-cbc', 'aes192-cbc', 'aes256-cbc']
t.supportedPublicKeys = ['ssh-rsa', 'ssh-dss']
t.supportedMACs = ['hmac-md5', 'hmac-sha1']
t.factory = self
return t
class HoneyPotTransport(transport.SSHServerTransport, TimeoutMixin):
"""
"""
def connectionMade(self):
"""
Called when the connection is made from the other side.
We send our version, but wait with sending KEXINIT
"""
self.transportId = uuid.uuid4().hex[:8]
log.msg(eventid='KIPP0001',
format='New connection: %(src_ip)s:%(src_port)s (%(dst_ip)s:%(dst_port)s) [session: %(sessionno)s]',
src_ip=self.transport.getPeer().host, src_port=self.transport.getPeer().port,
dst_ip=self.transport.getHost().host, dst_port=self.transport.getHost().port,
id=self.transportId, sessionno=self.transport.sessionno)
self.transport.write('{}\r\n'.format(self.ourVersionString))
self.currentEncryptions = transport.SSHCiphers('none', 'none', 'none', 'none')
self.currentEncryptions.setKeys('', '', '', '', '', '')
self.setTimeout(120)
def sendKexInit(self):
"""
Don't send key exchange prematurely
"""
if not self.gotVersion:
return
transport.SSHServerTransport.sendKexInit(self)
def dataReceived(self, data):
"""
First, check for the version string (SSH-2.0-*). After that has been
received, this method adds data to the buffer, and pulls out any
packets.
@type data: C{str}
"""
self.buf = self.buf + data
if not self.gotVersion:
if not '\n' in self.buf:
return
self.otherVersionString = self.buf.split('\n')[0].strip()
if self.buf.startswith('SSH-'):
self.gotVersion = True
remoteVersion = self.buf.split('-')[1]
if remoteVersion not in self.supportedVersions:
self._unsupportedVersionReceived(remoteVersion)
return
i = self.buf.index('\n')
self.buf = self.buf[i+1:]
self.sendKexInit()
else:
self.transport.write('Protocol mismatch.\n')
log.msg('Bad protocol version identification: %s' % (self.otherVersionString,))
self.transport.loseConnection()
return
packet = self.getPacket()
while packet:
messageNum = ord(packet[0])
self.dispatchMessage(messageNum, packet[1:])
packet = self.getPacket()
# 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):
"""
"""
k = getNS(packet[16:], 10)
strings, rest = k[:-1], k[-1]
(kexAlgs, keyAlgs, encCS, encSC, macCS, macSC, compCS, compSC, langCS,
langSC) = [s.split(',') for s in strings]
log.msg(eventid='KIPP0009', version=self.otherVersionString,
kexAlgs=kexAlgs, keyAlgs=keyAlgs, encCS=encCS, macCS=macCS,
compCS=compCS, format='Remote SSH version: %(version)s')
return transport.SSHServerTransport.ssh_KEXINIT(self, packet)
def timeoutConnection(self):
"""
"""
log.msg( "Authentication Timeout reached" )
self.transport.loseConnection()
def setService(self, service):
"""
Remove login grace timeout
"""
if service.name == "ssh-connection":
self.setTimeout(None)
transport.SSHServerTransport.setService(self, service)
def connectionLost(self, reason):
"""
This seems to be the only reliable place of catching lost connection
"""
self.setTimeout(None)
if self.transport.sessionno in self.factory.sessions:
del self.factory.sessions[self.transport.sessionno]
transport.SSHServerTransport.connectionLost(self, reason)
self.transport.connectionLost(reason)
self.transport = None
log.msg(eventid='KIPP0011', format='Connection lost')
def sendDisconnect(self, reason, desc):
"""
http://kbyte.snowpenguin.org/portal/2013/04/30/kippo-protocol-mismatch-workaround/
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:
transport.SSHServerTransport.sendDisconnect(self, reason, desc)
else:
self.transport.write('Packet corrupt\n')
log.msg('[SERVER] - Disconnecting with error, code %s\nreason: %s'
% (reason, desc))
self.transport.loseConnection()
@@ -480,58 +203,6 @@ class SSHSessionForCowrieUser:
def getRSAKeys(cfg):
"""
"""
publicKeyFile = cfg.get('honeypot', 'rsa_public_key')
privateKeyFile = cfg.get('honeypot', 'rsa_private_key')
if not (os.path.exists(publicKeyFile) and os.path.exists(privateKeyFile)):
log.msg("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 open(publicKeyFile, 'w+b') as f:
f.write(publicKeyString)
with open(privateKeyFile, 'w+b') as f:
f.write(privateKeyString)
else:
with open(publicKeyFile, 'r') as f:
publicKeyString = f.read()
with open(privateKeyFile, 'r') as f:
privateKeyString = f.read()
return publicKeyString, privateKeyString
def getDSAKeys(cfg):
"""
"""
publicKeyFile = cfg.get('honeypot', 'dsa_public_key')
privateKeyFile = cfg.get('honeypot', 'dsa_private_key')
if not (os.path.exists(publicKeyFile) and os.path.exists(privateKeyFile)):
log.msg("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 open(publicKeyFile, 'w+b') as f:
f.write(publicKeyString)
with open(privateKeyFile, 'w+b') as f:
f.write(privateKeyString)
else:
with open(publicKeyFile, 'r') as f:
publicKeyString = f.read()
with open(privateKeyFile, 'r') as f:
privateKeyString = f.read()
return publicKeyString, privateKeyString
@implementer(conchinterfaces.ISFTPFile)
class CowrieSFTPFile:
"""

291
cowrie/core/transport.py Normal file
View File

@@ -0,0 +1,291 @@
# Copyright (c) 2009-2014 Upi Tamminen <desaster@gmail.com>
# See the COPYRIGHT file for more information
"""
This module contains ...
"""
import time
import uuid
import twisted
from twisted.conch.ssh import factory
from twisted.conch.ssh import keys
from twisted.conch.ssh import transport
from twisted.python import log
from twisted.conch.openssh_compat import primes
from twisted.conch.ssh.common import getNS
from twisted.protocols.policies import TimeoutMixin
from cowrie.core import connection
from cowrie.core import userauth
from cowrie.core import keys as cowriekeys
class HoneyPotSSHFactory(factory.SSHFactory):
"""
This factory creates HoneyPotTransport instances
"""
services = {
'ssh-userauth': userauth.HoneyPotSSHUserAuthServer,
'ssh-connection': connection.CowrieSSHConnection,
}
def __init__(self, cfg):
self.cfg = cfg
def logDispatch(self, *msg, **args):
"""
Special delivery to the loggers to avoid scope problems
"""
for dblog in self.dbloggers:
dblog.logDispatch(*msg, **args)
for output in self.output_plugins:
output.logDispatch(*msg, **args)
def startFactory(self):
"""
"""
# Interactive protocols are kept here for the interact feature
self.sessions = {}
# For use by the uptime command
self.starttime = time.time()
# Load/create keys
rsaPubKeyString, rsaPrivKeyString = cowriekeys.getRSAKeys(self.cfg)
dsaPubKeyString, dsaPrivKeyString = cowriekeys.getDSAKeys(self.cfg)
self.publicKeys = {
'ssh-rsa': keys.Key.fromString(data=rsaPubKeyString),
'ssh-dss': keys.Key.fromString(data=dsaPubKeyString)}
self.privateKeys = {
'ssh-rsa': keys.Key.fromString(data=rsaPrivKeyString),
'ssh-dss': keys.Key.fromString(data=dsaPrivKeyString)}
# Load db loggers
self.dbloggers = []
for x in self.cfg.sections():
if not x.startswith('database_'):
continue
engine = x.split('_')[1]
try:
dblogger = __import__( 'cowrie.dblog.{}'.format(engine),
globals(), locals(), ['dblog']).DBLogger(self.cfg)
log.addObserver(dblogger.emit)
self.dbloggers.append(dblogger)
log.msg("Loaded dblog engine: {}".format(engine))
except:
log.err()
log.msg("Failed to load dblog engine: {}".format(engine))
# Load output modules
self.output_plugins = []
for x in self.cfg.sections():
if not x.startswith('output_'):
continue
engine = x.split('_')[1]
try:
output = __import__( 'cowrie.output.{}'.format(engine),
globals(), locals(), ['output']).Output(self.cfg)
log.addObserver(output.emit)
self.output_plugins.append(output)
log.msg("Loaded output engine: {}".format(engine))
except:
log.err()
log.msg("Failed to load output engine: {}".format(engine))
factory.SSHFactory.startFactory(self)
def stopFactory(self):
"""
"""
factory.SSHFactory.stopFactory(self)
def buildProtocol(self, addr):
"""
Create an instance of the server side of the SSH protocol.
@type addr: L{twisted.internet.interfaces.IAddress} provider
@param addr: The address at which the server will listen.
@rtype: L{cowrie.core.HoneyPotTransport}
@return: The built transport.
"""
_modulis = '/etc/ssh/moduli', '/private/etc/moduli'
t = HoneyPotTransport()
try:
t.ourVersionString = self.cfg.get('honeypot', 'ssh_version_string')
except:
t.ourVersionString = "SSH-2.0-OpenSSH_6.0p1 Debian-4+deb7u2"
t.supportedPublicKeys = list(self.privateKeys.keys())
for _moduli in _modulis:
try:
self.primes = primes.parseModuliFile(_moduli)
break
except IOError as err:
pass
if not self.primes:
ske = t.supportedKeyExchanges[:]
ske.remove('diffie-hellman-group-exchange-sha1')
t.supportedKeyExchanges = ske
log.msg("No moduli, disabled diffie-hellman-group-exchange-sha1")
# Reorder supported ciphers to resemble current openssh more
t.supportedCiphers = ['aes128-ctr', 'aes192-ctr', 'aes256-ctr',
'aes128-cbc', '3des-cbc', 'blowfish-cbc', 'cast128-cbc',
'aes192-cbc', 'aes256-cbc']
t.supportedPublicKeys = ['ssh-rsa', 'ssh-dss']
t.supportedMACs = ['hmac-md5', 'hmac-sha1']
t.factory = self
return t
class HoneyPotTransport(transport.SSHServerTransport, TimeoutMixin):
"""
"""
def connectionMade(self):
"""
Called when the connection is made from the other side.
We send our version, but wait with sending KEXINIT
"""
self.transportId = uuid.uuid4().hex[:8]
log.msg(eventid='KIPP0001',
format='New connection: %(src_ip)s:%(src_port)s (%(dst_ip)s:%(dst_port)s) [session: %(sessionno)s]',
src_ip=self.transport.getPeer().host, src_port=self.transport.getPeer().port,
dst_ip=self.transport.getHost().host, dst_port=self.transport.getHost().port,
id=self.transportId, sessionno=self.transport.sessionno)
self.transport.write('{}\r\n'.format(self.ourVersionString))
self.currentEncryptions = transport.SSHCiphers('none', 'none', 'none', 'none')
self.currentEncryptions.setKeys('', '', '', '', '', '')
self.setTimeout(120)
def sendKexInit(self):
"""
Don't send key exchange prematurely
"""
if not self.gotVersion:
return
transport.SSHServerTransport.sendKexInit(self)
def dataReceived(self, data):
"""
First, check for the version string (SSH-2.0-*). After that has been
received, this method adds data to the buffer, and pulls out any
packets.
@type data: C{str}
"""
self.buf = self.buf + data
if not self.gotVersion:
if not '\n' in self.buf:
return
self.otherVersionString = self.buf.split('\n')[0].strip()
if self.buf.startswith('SSH-'):
self.gotVersion = True
remoteVersion = self.buf.split('-')[1]
if remoteVersion not in self.supportedVersions:
self._unsupportedVersionReceived(remoteVersion)
return
i = self.buf.index('\n')
self.buf = self.buf[i+1:]
self.sendKexInit()
else:
self.transport.write('Protocol mismatch.\n')
log.msg('Bad protocol version identification: %s' % (self.otherVersionString,))
self.transport.loseConnection()
return
packet = self.getPacket()
while packet:
messageNum = ord(packet[0])
self.dispatchMessage(messageNum, packet[1:])
packet = self.getPacket()
# 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):
"""
"""
k = getNS(packet[16:], 10)
strings, rest = k[:-1], k[-1]
(kexAlgs, keyAlgs, encCS, encSC, macCS, macSC, compCS, compSC, langCS,
langSC) = [s.split(',') for s in strings]
log.msg(eventid='KIPP0009', version=self.otherVersionString,
kexAlgs=kexAlgs, keyAlgs=keyAlgs, encCS=encCS, macCS=macCS,
compCS=compCS, format='Remote SSH version: %(version)s')
return transport.SSHServerTransport.ssh_KEXINIT(self, packet)
def timeoutConnection(self):
"""
"""
log.msg( "Authentication Timeout reached" )
self.transport.loseConnection()
def setService(self, service):
"""
Remove login grace timeout
"""
if service.name == "ssh-connection":
self.setTimeout(None)
transport.SSHServerTransport.setService(self, service)
def connectionLost(self, reason):
"""
This seems to be the only reliable place of catching lost connection
"""
self.setTimeout(None)
if self.transport.sessionno in self.factory.sessions:
del self.factory.sessions[self.transport.sessionno]
transport.SSHServerTransport.connectionLost(self, reason)
self.transport.connectionLost(reason)
self.transport = None
log.msg(eventid='KIPP0011', format='Connection lost')
def sendDisconnect(self, reason, desc):
"""
http://kbyte.snowpenguin.org/portal/2013/04/30/kippo-protocol-mismatch-workaround/
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:
transport.SSHServerTransport.sendDisconnect(self, reason, desc)
else:
self.transport.write('Packet corrupt\n')
log.msg('[SERVER] - Disconnecting with error, code %s\nreason: %s'
% (reason, desc))
self.transport.loseConnection()

View File

@@ -45,7 +45,7 @@ from twisted.cred import portal
from cowrie.core.config import readConfigFile
from cowrie import core
import cowrie.core.ssh
import cowrie.core.transport
import cowrie.core.realm
import cowrie.core.checkers
@@ -93,7 +93,7 @@ class CowrieServiceMaker(object):
else:
listen_port = 2222
factory = core.ssh.HoneyPotSSHFactory(cfg)
factory = core.transport.HoneyPotSSHFactory(cfg)
factory.portal = portal.Portal(core.realm.HoneyPotRealm(cfg))
factory.portal.registerChecker(
core.checkers.HoneypotPublicKeyChecker())