Basic Telnet support implemented

A squash merge of GoSecure/cowrie telnet-poc branch:
https://github.com/GoSecure/cowrie/tree/telnet-poc

Rebased on current upstream master.

August 2016 update: Resolved several conflicts when rebasing
This commit is contained in:
Olivier Bilodeau 2016-08-14 00:08:47 -04:00 committed by Michel Oosterhof
parent bae58890f5
commit 640652207d
13 changed files with 461 additions and 38 deletions

View File

@ -1,6 +1,6 @@
# Cowrie
Cowrie is a medium interaction SSH honeypot designed to log brute force attacks and the shell interaction performed by the attacker.
Cowrie is a medium interaction SSH and Telnet honeypot designed to log brute force attacks and the shell interaction performed by the attacker.
[Cowrie](http://github.com/micheloosterhof/cowrie/) is developed by Michel Oosterhof and is based on [Kippo](http://github.com/desaster/kippo/) by Upi Tamminen (desaster).

View File

@ -16,7 +16,6 @@
# (default: not specified)
#sensor_name=myhostname
# Hostname for the honeypot. Displayed by the shell prompt of the virtual
# environment
#
@ -174,15 +173,15 @@ auth_class = UserDB
# IP addresses to listen for incoming SSH connections.
#
# (default: 0.0.0.0) = any IPv4 address
#listen_addr = 0.0.0.0
#listen_ssh_addr = 0.0.0.0
# (use :: for listen to all IPv6 and IPv4 addresses)
#listen_addr = ::
#listen_ssh_addr = ::
# Port to listen for incoming SSH connections.
#
# (default: 2222)
#listen_port = 2222
#listen_ssh_port = 2222
# SSH Version String
@ -245,6 +244,28 @@ forward_redirect_443 = 127.0.0.1:8443
forward_redirect_25 = 127.0.0.1:12525
forward_redirect_587 = 127.0.0.1:12525
# ============================================================================
# Telnet Specific Options
# ============================================================================
# IP addresses to listen for incoming Telnet connections.
#
# (default: 0.0.0.0) = any IPv4 address
#listen_telnet_addr = 0.0.0.0
# (use :: for listen to all IPv6 and IPv4 addresses)
#listen_telnet_addr = ::
# Port to listen for incoming Telnet connections.
#
# (default: 2223)
#listen_telnet_port = 2223
# Source Port to report in logs (useful if you use iptables to forward ports to Cowrie)
#reported_telnet_port = 23
# ============================================================================
# Database logging Specific Options
# ============================================================================

View File

@ -19,9 +19,11 @@ class DBLogger(object):
self.cfg = cfg
self.sessions = {}
self.ttylogs = {}
# FIXME figure out what needs to be done here regarding
# HoneyPotTransport renamed to HoneyPotSSHTransport
#:* Handles ipv6
self.re_sessionlog = re.compile(
r'.*HoneyPotTransport,([0-9]+),[:a-f0-9.]+$')
r'.*HoneyPotSSHTransport,([0-9]+),[:a-f0-9.]+$')
# cowrie.session.connect is special since it kicks off new logging session,
# and is not handled here

View File

@ -67,8 +67,10 @@ class Output(object):
self.cfg = cfg
self.sessions = {}
self.ips = {}
# FIXME figure out what needs to be done here regarding
# HoneyPotTransport renamed to HoneyPotSSHTransport
self.re_sessionlog = re.compile(
'.*HoneyPotTransport,([0-9]+),[0-9a-f:.]+$')
'.*HoneyPotSSHTransport,([0-9]+),[0-9a-f:.]+$')
try:
self.sensor = self.cfg.get('honeypot', 'sensor_name')
except:

View File

@ -53,24 +53,32 @@ class HoneyPotBaseProtocol(insults.TerminalProtocol, TimeoutMixin):
self.password_input = False
self.cmdstack = []
def getProtoTransport(self):
"""
Due to protocol nesting differences, we need provide how we grab
the proper transport to access underlying SSH information. Meant to be
overridden for other protocols.
"""
return self.terminal.transport.session.conn.transport
def logDispatch(self, *msg, **args):
"""
Send log directly to factory, avoiding normal log dispatch
"""
transport = self.terminal.transport.session.conn.transport
args['sessionno'] = transport.transport.sessionno
transport.factory.logDispatch(*msg, **args)
pt = self.getProtoTransport()
args['sessionno'] = pt.transport.sessionno
pt.factory.logDispatch(*msg, **args)
def connectionMade(self):
"""
"""
transport = self.terminal.transport.session.conn.transport
pt = self.getProtoTransport()
self.realClientIP = transport.transport.getPeer().host
self.realClientPort = transport.transport.getPeer().port
self.clientVersion = transport.otherVersionString
self.realClientIP = pt.transport.getPeer().host
self.realClientPort = pt.transport.getPeer().port
self.clientVersion = self.getClientVersion()
self.logintime = time.time()
self.setTimeout(1800)
@ -189,10 +197,15 @@ class HoneyPotBaseProtocol(insults.TerminalProtocol, TimeoutMixin):
"""
Uptime
"""
transport = self.terminal.transport.session.conn.transport
r = time.time() - transport.factory.starttime
pt = self.getProtoTransport()
r = time.time() - pt.factory.starttime
if reset:
pt.factory.starttime = reset
return r
def getClientVersion(self):
pt = self.getProtoTransport()
return pt.otherVersionString
class HoneyPotExecProtocol(HoneyPotBaseProtocol):
@ -235,8 +248,8 @@ class HoneyPotInteractiveProtocol(HoneyPotBaseProtocol, recvline.HistoricRecvLin
self.cmdstack = [honeypot.HoneyPotShell(self)]
transport = self.terminal.transport.session.conn.transport
transport.factory.sessions[transport.transport.sessionno] = self
pt = self.getProtoTransport()
pt.factory.sessions[pt.transport.sessionno] = self
self.keyHandlers.update({
'\x01': self.handle_HOME, # CTRL-A
@ -292,9 +305,9 @@ class HoneyPotInteractiveProtocol(HoneyPotBaseProtocol, recvline.HistoricRecvLin
def connectionLost(self, reason):
"""
"""
transport = self.terminal.transport.session.conn.transport
if transport.transport.sessionno in transport.factory.sessions:
del transport.factory.sessions[transport.transport.sessionno]
pt = self.getProtoTransport()
if pt.transport.sessionno in pt.factory.sessions:
del pt.factory.sessions[pt.transport.sessionno]
self.lastlogExit()
HoneyPotBaseProtocol.connectionLost(self, reason)
@ -396,3 +409,23 @@ class HoneyPotInteractiveProtocol(HoneyPotBaseProtocol, recvline.HistoricRecvLin
"""
pass
class HoneyPotInteractiveTelnetProtocol(HoneyPotInteractiveProtocol):
"""
Specialized HoneyPotInteractiveProtocol that provides Telnet specific
overrides.
"""
def __init__(self, avatar):
recvline.HistoricRecvLine.__init__(self)
HoneyPotInteractiveProtocol.__init__(self, avatar)
def getProtoTransport(self):
"""
Due to protocol nesting differences, we need to override how we grab
the proper transport to access underlying Telnet information.
"""
return self.terminal.transport.session.transport
def getClientVersion(self):
return 'Telnet'

View File

@ -38,12 +38,14 @@ import pickle
import twisted
from twisted.conch import interfaces as conchinterfaces
from twisted.conch.telnet import ITelnetProtocol
from twisted.python import log
from cowrie.core import protocol
from cowrie.core import server
from cowrie.core import avatar
from cowrie.core import fs
from cowrie.telnet import session
@implementer(twisted.cred.portal.IRealm)
@ -78,6 +80,11 @@ class HoneyPotRealm(object):
serv = server.CowrieServer(self)
user = avatar.CowrieUser(avatarId, serv)
return interfaces[0], user, user.logout
else:
raise Exception("No supported interfaces found.")
elif ITelnetProtocol in interfaces:
cs = server.CowrieServer(self)
av = session.HoneyPotTelnetSession(avatarId, cs)
return interfaces[0], av, lambda:None
log.msg('No supported interfaces found.')
# TODO: this exception doesn't raise for a reason I don't understand
raise NotImplementedError("No supported interfaces found.")

View File

@ -41,12 +41,15 @@ class LoggingServerProtocol(insults.ServerProtocol):
else:
self.type = 'i' # Interactive
def getSessionId(self):
transportId = self.transport.session.conn.transport.transportId
channelId = self.transport.session.id
return (transportId, channelId)
def connectionMade(self):
"""
"""
transportId = self.transport.session.conn.transport.transportId
channelId = self.transport.session.id
transportId, channelId = self.getSessionId()
self.startTime = time.time()
self.ttylogFile = '%s/tty/%s-%s-%s%s.log' % \
@ -176,3 +179,12 @@ class LoggingServerProtocol(insults.ServerProtocol):
insults.ServerProtocol.connectionLost(self, reason)
class LoggingTelnetServerProtocol(LoggingServerProtocol):
"""
Wrap LoggingServerProtocol with single method to fetch session id for Telnet
"""
def getSessionId(self):
transportId = self.transport.session.transportId
sn = self.transport.session.transport.transport.sessionno
return (transportId, sn)

View File

@ -26,7 +26,7 @@ from cowrie.core import keys as cowriekeys
class HoneyPotSSHFactory(factory.SSHFactory):
"""
This factory creates HoneyPotTransport instances
This factory creates HoneyPotSSHTransport instances
They listen directly to the TCP port
"""
@ -119,13 +119,13 @@ class HoneyPotSSHFactory(factory.SSHFactory):
@type addr: L{twisted.internet.interfaces.IAddress} provider
@param addr: The address at which the server will listen.
@rtype: L{cowrie.core.HoneyPotTransport}
@rtype: L{cowrie.ssh.transport.HoneyPotSSHTransport}
@return: The built transport.
"""
_modulis = '/etc/ssh/moduli', '/private/etc/moduli'
t = HoneyPotTransport()
t = HoneyPotSSHTransport()
try:
t.ourVersionString = self.cfg.get('honeypot', 'ssh_version_string')
@ -164,7 +164,7 @@ class HoneyPotSSHFactory(factory.SSHFactory):
class HoneyPotTransport(transport.SSHServerTransport, TimeoutMixin):
class HoneyPotSSHTransport(transport.SSHServerTransport, TimeoutMixin):
"""
"""

View File

154
cowrie/telnet/session.py Normal file
View File

@ -0,0 +1,154 @@
# Copyright (C) 2015, 2016 GoSecure Inc.
"""
Telnet User Session management for the Honeypot
@author: Olivier Bilodeau <obilodeau@gosecure.ca>
"""
from zope.interface import implementer
from twisted.internet import interfaces, protocol
from twisted.python import log
from twisted.conch.ssh import session
from twisted.conch.telnet import StatefulTelnetProtocol, TelnetBootstrapProtocol
from cowrie.core import pwd
from cowrie.core import protocol as cproto
from cowrie.insults import insults
class HoneyPotTelnetSession(TelnetBootstrapProtocol):
def __init__(self, username, server):
self.username = username
self.server = server
self.cfg = self.server.cfg
try:
pwentry = pwd.Passwd(self.cfg).getpwnam(self.username)
self.uid = pwentry["pw_uid"]
self.gid = pwentry["pw_gid"]
self.home = pwentry["pw_dir"]
except:
self.uid = 1001
self.gid = 1001
self.home = '/home'
self.environ = {
'LOGNAME': self.username,
'USER': self.username,
'HOME': self.home,
'TMOUT': '1800'}
if self.uid==0:
self.environ['PATH']='/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
else:
self.environ['PATH']='/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games'
# required because HoneyPotBaseProtocol relies on avatar.avatar.home
self.avatar = self
# to be populated by HoneyPotTelnetAuthTransport after auth
self.transportId = None
def connectionMade(self):
processprotocol = TelnetSessionProcessProtocol(self)
self.protocol = insults.LoggingTelnetServerProtocol(
cproto.HoneyPotInteractiveTelnetProtocol, self)
self.protocol.makeConnection(processprotocol)
processprotocol.makeConnection(session.wrapProtocol(self.protocol))
# TODO do I need to implement connectionLost?
# XXX verify if HoneyPotTelnetAuthTransport's connectionLost fires otherwise
# we'll have to reimplement some of the stuff here
#def connectionLost(self, reason):
# pt = self.transport
# if pt.transport.sessionno in pt.factory.sessions:
# del pt.factory.sessions[pt.transport.sessionno]
# pt.connectionLost(reason)
# def lineReceived(self, line):
# self.transport.write("I received %r from you\r\n" % (line,))
def logout(self):
"""
"""
log.msg('avatar {} logging out'.format(self.username))
# Taken and adapted from
# https://github.com/twisted/twisted/blob/26ad16ab41db5f0f6d2526a891e81bbd3e260247/twisted/conch/ssh/session.py#L186
@implementer(interfaces.ITransport)
class TelnetSessionProcessProtocol(protocol.ProcessProtocol):
"""I am both an L{IProcessProtocol} and an L{ITransport}.
I am a transport to the remote endpoint and a process protocol to the
local subsystem.
"""
def __init__(self, sess):
self.session = sess
self.lostOutOrErrFlag = False
# FIXME probably no such thing such as buffering in Telnet protocol
#def connectionMade(self):
# if self.session.buf:
# self.session.write(self.session.buf)
# self.session.buf = None
def outReceived(self, data):
self.session.write(data)
def errReceived(self, err):
self.session.writeExtended(connection.EXTENDED_DATA_STDERR, err)
def outConnectionLost(self):
"""
EOF should only be sent when both STDOUT and STDERR have been closed.
"""
if self.lostOutOrErrFlag:
self.session.conn.sendEOF(self.session)
else:
self.lostOutOrErrFlag = True
def errConnectionLost(self):
"""
See outConnectionLost().
"""
self.outConnectionLost()
def connectionLost(self, reason = None):
self.session.loseConnection()
# here SSH is doing signal handling, I don't think telnet supports that so
# I'm simply going to bail out
def processEnded(self, reason=None):
# TODO: log reason maybe?
log.msg("Process ended. Telnet Session disconnected")
self.session.loseConnection()
def getHost(self):
"""
Return the host from my session's transport.
"""
return self.session.transport.getHost()
def getPeer(self):
"""
Return the peer from my session's transport.
"""
return self.session.transport.getPeer()
def write(self, data):
self.session.write(data)
def writeSequence(self, seq):
self.session.write(''.join(seq))
def loseConnection(self):
self.session.loseConnection()

164
cowrie/telnet/transport.py Normal file
View File

@ -0,0 +1,164 @@
# Copyright (C) 2015, 2016 GoSecure Inc.
"""
Telnet Transport and Authentication for the Honeypot
@author: Olivier Bilodeau <obilodeau@gosecure.ca>
"""
import time
import uuid
from twisted.python import log
from twisted.internet import protocol
from twisted.conch.telnet import AuthenticatingTelnetProtocol, ECHO, \
ITelnetProtocol, ProtocolTransportMixin, \
TelnetTransport
from twisted.protocols.policies import TimeoutMixin
from cowrie.core.credentials import UsernamePasswordIP
class HoneyPotTelnetFactory(protocol.ServerFactory):
"""
This factory creates HoneyPotTelnetAuthTransport instances
They listen directly to the TCP port
"""
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):
"""
"""
# The banner to serve
honeyfs = self.portal.realm.cfg.get('honeypot', 'contents_path')
issuefile = honeyfs + "/etc/issue.net"
self.banner = file(issuefile).read()
# Interactive protocols 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 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))
# hook protocol
self.protocol = lambda: TelnetTransport(HoneyPotTelnetAuthTransport,
self.portal)
protocol.ServerFactory.startFactory(self)
def stopFactory(self):
"""
Stop output plugins
"""
for output in self.output_plugins:
output.stop()
protocol.ServerFactory.stopFactory(self)
class HoneyPotTelnetAuthTransport(AuthenticatingTelnetProtocol, ProtocolTransportMixin, TimeoutMixin):
"""
Telnet Transport that takes care of Authentication. Once authenticated this
transport is replaced with HoneyPotTelnetSession.
"""
def connectionMade(self):
self.transportId = uuid.uuid4().hex[:8]
sessionno = self.transport.transport.sessionno
self.factory.sessions[sessionno] = self.transportId
log.msg(eventid='cowrie.session.connect',
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,
session=self.transportId, sessionno=sessionno)
# p/Cisco telnetd/ d/router/ o/IOS/ cpe:/a:cisco:telnet/ cpe:/o:cisco:ios/a
# NB _write() is for raw data and write() handles telnet special bytes
self.transport._write("\xff\xfb\x01\xff\xfb\x03\xff\xfb\0\xff\xfd\0\xff\xfd\x1f\r\n")
self.transport.write(self.factory.banner)
self.transport._write("User Access Verification\r\n\r\nUsername: ")
self.setTimeout(120)
# FIXME TelnetTransport is throwing an exception when client disconnects
# Not sure if this is true anymore
def connectionLost(self, reason):
"""
This seems to be the only reliable place of catching lost connection
"""
self.setTimeout(None)
if self.transport.transport.sessionno in self.factory.sessions:
del self.factory.sessions[self.transport.transport.sessionno]
self.transport.connectionLost(reason)
self.transport = None
log.msg(eventid='cowrie.session.closed', format='Connection lost')
def telnet_Password(self, line):
username, password = self.username, line
del self.username
def login(ignored):
self.src_ip = self.transport.getPeer().host
creds = UsernamePasswordIP(username, password, self.src_ip)
d = self.portal.login(creds, self.src_ip, ITelnetProtocol)
d.addCallback(self._cbLogin)
d.addErrback(self._ebLogin)
self.transport.wont(ECHO).addCallback(login)
return 'Discard'
def _cbLogin(self, ial):
"""
"""
interface, protocol, logout = ial
self.protocol = protocol
self.logout = logout
self.state = 'Command'
# transfer important state info to new transport
protocol.transportId = self.transportId
# replace myself with avatar protocol
protocol.makeConnection(self.transport)
self.transport.protocol = protocol

2
honeyfs/etc/issue.net Normal file
View File

@ -0,0 +1,2 @@
Debian GNU/Linux 7

View File

@ -48,6 +48,7 @@ from cowrie import core
import cowrie.core.realm
import cowrie.core.checkers
import cowrie.telnet.transport
import cowrie.ssh.transport
class Options(usage.Options):
@ -99,28 +100,53 @@ class CowrieServiceMaker(object):
factory.portal.registerChecker(
core.checkers.HoneypotNoneChecker())
if cfg.has_option('honeypot', 'listen_addr'):
listenAddr = cfg.get('honeypot', 'listen_addr')
if cfg.has_option('honeypot', 'listen_ssh_addr'):
listen_ssh_addr = cfg.get('honeypot', 'listen_ssh_addr')
else:
listenAddr = '0.0.0.0'
listen_ssh_addr = '0.0.0.0'
# Preference: 1, option, 2, config, 3, default of 2222
if options['port'] != 0:
listenPort = int(options["port"])
elif cfg.has_option('honeypot', 'listen_port'):
listenPort = int(cfg.get('honeypot', 'listen_port'))
listen_ssh_port = int(options["port"])
elif cfg.has_option('honeypot', 'listen_ssh_port'):
listen_ssh_port = int(cfg.get('honeypot', 'listen_ssh_port'))
else:
listenPort = 2222
listen_ssh_port = 2222
for i in listenAddr.split():
svc = internet.TCPServer(listenPort, factory, interface=i)
for i in listen_ssh_addr.split():
svc = internet.TCPServer(listen_ssh_port, factory, interface=i)
# FIXME: Use addService on topService ?
svc.setServiceParent(topService)
# TODO deduplicate telnet and ssh into a generic loop for each service
if cfg.has_option('honeypot', 'listen_telnet_addr'):
listen_telnet_addr = cfg.get('honeypot', 'listen_telnet_addr')
else:
listen_telnet_addr = '0.0.0.0'
# Preference: 1, config, 2, default of 2223
if cfg.has_option('honeypot', 'listen_telnet_port'):
listen_telnet_port = int(cfg.get('honeypot', 'listen_telnet_port'))
else:
listen_telnet_port = 2223
f = cowrie.telnet.transport.HoneyPotTelnetFactory(cfg)
f.portal = portal.Portal(core.realm.HoneyPotRealm(cfg))
f.portal.registerChecker(core.checkers.HoneypotPasswordChecker(cfg))
if cfg.has_option('honeypot', 'auth_none_enabled') and \
cfg.get('honeypot', 'auth_none_enabled').lower() in \
('yes', 'true', 'on'):
f.portal.registerChecker(core.checkers.HoneypotNoneChecker())
for i in listen_telnet_addr.split():
tsvc = internet.TCPServer(listen_telnet_port, f, interface=i)
# FIXME: Use addService on topService ?
tsvc.setServiceParent(topService)
if cfg.has_option('honeypot', 'interact_enabled') and \
cfg.get('honeypot', 'interact_enabled').lower() in \
('yes', 'true', 'on'):
iport = int(cfg.get('honeypot', 'interact_port'))
# FIXME this doesn't support checking both Telnet and SSH sessions
from cowrie.core import interact
svc = internet.TCPServer(iport,
interact.makeInteractFactory(factory), interface='127.0.0.1')