# Copyright (C) 2015, 2016 GoSecure Inc.
"""
Telnet Transport and Authentication for the Honeypot

@author: Olivier Bilodeau <obilodeau@gosecure.ca>
"""

import struct
import time
import uuid
import inspect
import random

from twisted.python import log
from twisted.internet import protocol
from twisted.conch.telnet import AuthenticatingTelnetProtocol, ECHO, TRAPSIG, \
                                 ITelnetProtocol, ProtocolTransportMixin, \
                                 SGA, NAWS, MODE, LINEMODE, TelnetTransport, AlreadyNegotiating
from twisted.protocols.policies import TimeoutMixin

from cowrie.core.credentials import UsernamePasswordIP

class HoneyPotTelnetFactory(protocol.ServerFactory):
    """
    This factory creates HoneyPotTelnetAuthProtocol instances
    They listen directly to the TCP port
    """
    tac = None # gets set later

    def __init__(self, cfg):
        self.cfg = cfg


    # TODO logging clarity can be improved: see what SSH does
    def logDispatch(self, *msg, **args):
        """
        Special delivery to the loggers to avoid scope problems
        """
        args['sessionno'] = 'T'+str(args['sessionno'])
        for dblog in self.tac.dbloggers:
            dblog.logDispatch(*msg, **args)
        for output in self.tac.output_plugins:
            output.logDispatch(*msg, **args)


    def startFactory(self):
        """
        """
        try:
            honeyfs = self.portal.realm.cfg.get('honeypot', 'contents_path')
            issuefile = honeyfs + "/etc/issue.net"
            self.banner = open(issuefile).read()
        except IOError:
            self.banner = ""

        # For use by the uptime command
        self.starttime = time.time()

        # hook protocol
        self.protocol = lambda: CowrieTelnetTransport(HoneyPotTelnetAuthProtocol,
                                         self.portal)
        protocol.ServerFactory.startFactory(self)
        log.msg("Ready to accept Telnet connections")


    def stopFactory(self):
        """
        Stop output plugins
        """
        protocol.ServerFactory.stopFactory(self)


class HoneyPotTelnetAuthProtocol(AuthenticatingTelnetProtocol):
    """
    TelnetAuthProtocol that takes care of Authentication. Once authenticated this
    protocol is replaced with HoneyPotTelnetSession.
    """

    loginPrompt = 'login: '
    passwordPrompt = 'Password: '
    windowSize = [40, 80]

    def connectionMade(self):
        """
        """
        self.transport.negotiationMap[NAWS] = self.telnet_NAWS
        # Initial option negotation. Want something at least for Mirai
        for opt in (NAWS,):
            self.transport.doChain(opt).addErrback(log.err)

        # I need to doubly escape here since my underlying
        # CowrieTelnetTransport hack would remove it and leave just \n
        self.transport.write(self.factory.banner.replace('\n', '\r\r\n'))
        self.transport.write(self.loginPrompt)


    def connectionLost(self, reason):
        """
        Fires on pre-authentication disconnects
        """
        AuthenticatingTelnetProtocol.connectionLost(self, reason)


    def telnet_User(self, line):
        """
        Overridden to conditionally kill 'WILL ECHO' which confuses clients
        that don't implement a proper Telnet protocol (most malware)
        """
        self.username = line
        # only send ECHO option if we are chatting with a real Telnet client
        #if self.transport.options: <-- doesn't work
        self.transport.willChain(ECHO)
        # FIXME: this should be configurable or provided via filesystem
        self.transport.write(self.passwordPrompt)
        return 'Password'


    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)

        # are we dealing with a real Telnet client?
        if self.transport.options:
            # stop ECHO
            # even if ECHO negotiation fails we still want to attempt a login
            # this allows us to support dumb clients which is common in malware
            # thus the addBoth: on success and on exception (AlreadyNegotiating)
            self.transport.wontChain(ECHO).addBoth(login)
        else:
            # process login
            login('')

        return 'Discard'

    def _cbLogin(self, ial):
        """
        Fired on a successful login
        """
        interface, protocol, logout = ial
        protocol.windowSize = self.windowSize
        self.protocol = protocol
        self.logout = logout
        self.state = 'Command'

        # Remove the short timeout of the login prompt. Timeout will be
        # provided later by the HoneyPotBaseProtocol class.
        self.transport.setTimeout(None)

        # replace myself with avatar protocol
        protocol.makeConnection(self.transport)
        self.transport.protocol = protocol


    def _ebLogin(self, failure):
    # TODO: provide a way to have user configurable strings for wrong password
        self.transport.wontChain(ECHO)
        self.transport.write("\nLogin incorrect\n")
        self.transport.write(self.loginPrompt)
        self.state = "User"

    # From TelnetBootstrapProtocol in twisted/conch/telnet.py
    def telnet_NAWS(self, data):
        if len(data) == 4:
            width, height = struct.unpack('!HH', b''.join(data))
            self.windowSize = [height, width]
        else:
            log.msg("Wrong number of NAWS bytes")

    def enableLocal(self, opt):
        if opt == ECHO:
            return True
        elif opt == SGA:
            return True
        else:
            return False


    def enableRemote(self, opt):
        if opt == LINEMODE:
            self.transport.requestNegotiation(LINEMODE, MODE + chr(TRAPSIG))
            return True
        elif opt == NAWS:
            return True
        elif opt == SGA:
            return True
        else:
            return False



class CowrieTelnetTransport(TelnetTransport, TimeoutMixin):
    """
    """
    def connectionMade(self):
        self.transportId = uuid.uuid4().hex[:8]
        sessionno = self.transport.sessionno
        self.startTime = time.time()
        self.setTimeout(300)

        log.msg(eventid='cowrie.session.connect',
           format='New connection: %(src_ip)s:%(src_port)s (%(dst_ip)s:%(dst_port)s) [session: T%(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='T'+str(sessionno))
        TelnetTransport.connectionMade(self)


    def write(self, bytes):
        """
        Because of the presence of two ProtocolTransportMixin in the protocol
        stack once authenticated, I need to override write() and remove a \r
        otherwise we end up with \r\r\n on the wire.

        It is kind of a hack. I asked for a better solution here:
        http://stackoverflow.com/questions/35087250/twisted-telnet-server-how-to-avoid-nested-crlf
        """
        self.transport.write(bytes.replace('\r\n', '\n'))


    def connectionLost(self, reason):
        """
        Fires on pre-authentication disconnects
        """
        self.setTimeout(None)
        TelnetTransport.connectionLost(self, reason)
        duration = time.time() - self.startTime
        log.msg(eventid='cowrie.session.closed',
            format='Connection lost after %(duration)d seconds',
            duration=duration)

    def willChain(self, option):
        return self._chainNegotiation(None, self.will, option)

    def wontChain(self, option):
        return self._chainNegotiation(None, self.wont, option)

    def doChain(self, option):
        return self._chainNegotiation(None, self.do, option)

    def dontChain(self, option):
        return self._chainNegotiation(None, self.dont, option)

    def _handleNegotiationError(self, f, func, option):
        if f.type is AlreadyNegotiating:
            s = self.getOptionState(option)
            if func in (self.do, self.dont):
                s.him.onResult.addCallback(self._chainNegotiation, func, option)
                s.him.onResult.addErrback(self._handleNegotiationError, func, option)
            if func in (self.will, self.wont):
                s.us.onResult.addCallback(self._chainNegotiation, func, option)
                s.us.onResult.addErrback(self._handleNegotiationError, func, option)
        # We only care about AlreadyNegotiating, everything else can be ignored
        # Possible other types include OptionRefused, AlreadyDisabled, AlreadyEnabled, ConnectionDone, ConnectionLost
        elif f.type is AssertionError:
            log.err('Client tried to illegally refuse to disable an option; ignoring, but undefined behavior may result')
            # TODO: Is ignoring this violation of the protocol the proper behavior?
            # Should the connection be terminated instead?
            # The telnetd package on Ubuntu (netkit-telnet) does all negotiation before sending the login prompt,
            # but does handle client-initiated negotiation at any time.
        return None  # This Failure has been handled, no need to continue processing errbacks

    def _chainNegotiation(self, res, func, option):
        return func(option).addErrback(self._handleNegotiationError, func, option)

cowrie/telnet/transport.py
# Copyright (C) 2015, 2016 GoSecure Inc.
"""
Telnet Transport and Authentication for the Honeypot

@author: Olivier Bilodeau <obilodeau@gosecure.ca>
"""

import struct
import time
import uuid
import inspect
import random

from twisted.python import log
from twisted.internet import protocol
from twisted.conch.telnet import AuthenticatingTelnetProtocol, ECHO, TRAPSIG, \
                                 ITelnetProtocol, ProtocolTransportMixin, \
                                 SGA, NAWS, MODE, LINEMODE, TelnetTransport, AlreadyNegotiating
from twisted.protocols.policies import TimeoutMixin

from cowrie.core.credentials import UsernamePasswordIP

class HoneyPotTelnetFactory(protocol.ServerFactory):
    """
    This factory creates HoneyPotTelnetAuthProtocol instances
    They listen directly to the TCP port
    """
    tac = None # gets set later

    def __init__(self, cfg):
        self.cfg = cfg


    # TODO logging clarity can be improved: see what SSH does
    def logDispatch(self, *msg, **args):
        """
        Special delivery to the loggers to avoid scope problems
        """
        args['sessionno'] = 'T'+str(args['sessionno'])
        for dblog in self.tac.dbloggers:
            dblog.logDispatch(*msg, **args)
        for output in self.tac.output_plugins:
            output.logDispatch(*msg, **args)


    def startFactory(self):
        """
        """
        try:
            honeyfs = self.portal.realm.cfg.get('honeypot', 'contents_path')
            issuefile = honeyfs + "/etc/issue.net"
            self.banner = open(issuefile).read()
        except IOError:
            self.banner = ""

        # For use by the uptime command
        self.starttime = time.time()

        # hook protocol
        self.protocol = lambda: CowrieTelnetTransport(HoneyPotTelnetAuthProtocol,
                                         self.portal)
        protocol.ServerFactory.startFactory(self)
        log.msg("Ready to accept Telnet connections")


    def stopFactory(self):
        """
        Stop output plugins
        """
        protocol.ServerFactory.stopFactory(self)


class HoneyPotTelnetAuthProtocol(AuthenticatingTelnetProtocol):
    """
    TelnetAuthProtocol that takes care of Authentication. Once authenticated this
    protocol is replaced with HoneyPotTelnetSession.
    """

    loginPrompt = 'login: '
    passwordPrompt = 'Password: '
    windowSize = [40, 80]

    def connectionMade(self):
        """
        """
        self.transport.negotiationMap[NAWS] = self.telnet_NAWS
        # Initial option negotation. Want something at least for Mirai
        for opt in (NAWS,):
            self.transport.doChain(opt).addErrback(log.err)

        # I need to doubly escape here since my underlying
        # CowrieTelnetTransport hack would remove it and leave just \n
        self.transport.write(self.factory.banner.replace('\n', '\r\r\n'))
        self.transport.write(self.loginPrompt)


    def connectionLost(self, reason):
        """
        Fires on pre-authentication disconnects
        """
        AuthenticatingTelnetProtocol.connectionLost(self, reason)


    def telnet_User(self, line):
        """
        Overridden to conditionally kill 'WILL ECHO' which confuses clients
        that don't implement a proper Telnet protocol (most malware)
        """
        self.username = line
        # only send ECHO option if we are chatting with a real Telnet client
        #if self.transport.options: <-- doesn't work
        self.transport.willChain(ECHO)
        # FIXME: this should be configurable or provided via filesystem
        self.transport.write(self.passwordPrompt)
        return 'Password'


    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)

        # are we dealing with a real Telnet client?
        if self.transport.options:
            # stop ECHO
            # even if ECHO negotiation fails we still want to attempt a login
            # this allows us to support dumb clients which is common in malware
            # thus the addBoth: on success and on exception (AlreadyNegotiating)
            self.transport.wontChain(ECHO).addBoth(login)
        else:
            # process login
            login('')

        return 'Discard'

    def telnet_Command(self, command):
        self.transport.protocol.dataReceived(command+'\r')
        return "Command"

    def _cbLogin(self, ial):
        """
        Fired on a successful login
        """
        interface, protocol, logout = ial
        protocol.windowSize = self.windowSize
        self.protocol = protocol
        self.logout = logout
        self.state = 'Command'

        # Remove the short timeout of the login prompt. Timeout will be
        # provided later by the HoneyPotBaseProtocol class.
        self.transport.setTimeout(None)

        # replace myself with avatar protocol
        protocol.makeConnection(self.transport)
        self.transport.protocol = protocol


    def _ebLogin(self, failure):
    # TODO: provide a way to have user configurable strings for wrong password
        self.transport.wontChain(ECHO)
        self.transport.write("\nLogin incorrect\n")
        self.transport.write(self.loginPrompt)
        self.state = "User"

    # From TelnetBootstrapProtocol in twisted/conch/telnet.py
    def telnet_NAWS(self, data):
        if len(data) == 4:
            width, height = struct.unpack('!HH', b''.join(data))
            self.windowSize = [height, width]
        else:
            log.msg("Wrong number of NAWS bytes")

    def enableLocal(self, opt):
        if opt == ECHO:
            return True
        elif opt == SGA:
            return True
        else:
            return False


    def enableRemote(self, opt):
        if opt == LINEMODE:
            self.transport.requestNegotiation(LINEMODE, MODE + chr(TRAPSIG))
            return True
        elif opt == NAWS:
            return True
        elif opt == SGA:
            return True
        else:
            return False



class CowrieTelnetTransport(TelnetTransport, TimeoutMixin):
    """
    """
    def connectionMade(self):
        self.transportId = uuid.uuid4().hex[:8]
        sessionno = self.transport.sessionno
        self.startTime = time.time()
        self.setTimeout(300)

        log.msg(eventid='cowrie.session.connect',
           format='New connection: %(src_ip)s:%(src_port)s (%(dst_ip)s:%(dst_port)s) [session: T%(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='T'+str(sessionno))
        TelnetTransport.connectionMade(self)


    def write(self, bytes):
        """
        Because of the presence of two ProtocolTransportMixin in the protocol
        stack once authenticated, I need to override write() and remove a \r
        otherwise we end up with \r\r\n on the wire.

        It is kind of a hack. I asked for a better solution here:
        http://stackoverflow.com/questions/35087250/twisted-telnet-server-how-to-avoid-nested-crlf
        """
        self.transport.write(bytes.replace('\r\n', '\n'))


    def connectionLost(self, reason):
        """
        Fires on pre-authentication disconnects
        """
        self.setTimeout(None)
        TelnetTransport.connectionLost(self, reason)
        duration = time.time() - self.startTime
        log.msg(eventid='cowrie.session.closed',
            format='Connection lost after %(duration)d seconds',
            duration=duration)

    def willChain(self, option):
        return self._chainNegotiation(None, self.will, option)

    def wontChain(self, option):
        return self._chainNegotiation(None, self.wont, option)

    def doChain(self, option):
        return self._chainNegotiation(None, self.do, option)

    def dontChain(self, option):
        return self._chainNegotiation(None, self.dont, option)

    def _handleNegotiationError(self, f, func, option):
        if f.type is AlreadyNegotiating:
            s = self.getOptionState(option)
            if func in (self.do, self.dont):
                s.him.onResult.addCallback(self._chainNegotiation, func, option)
                s.him.onResult.addErrback(self._handleNegotiationError, func, option)
            if func in (self.will, self.wont):
                s.us.onResult.addCallback(self._chainNegotiation, func, option)
                s.us.onResult.addErrback(self._handleNegotiationError, func, option)
        # We only care about AlreadyNegotiating, everything else can be ignored
        # Possible other types include OptionRefused, AlreadyDisabled, AlreadyEnabled, ConnectionDone, ConnectionLost
        elif f.type is AssertionError:
            log.err('Client tried to illegally refuse to disable an option; ignoring, but undefined behavior may result')
            # TODO: Is ignoring this violation of the protocol the proper behavior?
            # Should the connection be terminated instead?
            # The telnetd package on Ubuntu (netkit-telnet) does all negotiation before sending the login prompt,
            # but does handle client-initiated negotiation at any time.
        return None  # This Failure has been handled, no need to continue processing errbacks

    def _chainNegotiation(self, res, func, option):
        return func(option).addErrback(self._handleNegotiationError, func, option)

cowrie/telnet/transport.py
diff --git a/cowrie/telnet/transport.py b/cowrie/telnet/transport.py
index 7bffe7646f..a2f0a96838 100644
--- a/cowrie/telnet/transport.py
+++ b/cowrie/telnet/transport.py
@@ -138,6 +138,10 @@ def login(ignored):
 
         return 'Discard'
 
+    def telnet_Command(self, command):
+        self.transport.protocol.dataReceived(command+'\r')
+        return "Command"
+
     def _cbLogin(self, ial):
         """
         Fired on a successful login

HoneyPotTelnetAuthProtocol instance has no attribute 'telnet_Command'
Environment: Python 2.7.12, twistd 16.0.0

Log:

> 2016-12-21 13:30:48-0800 [cowrie.telnet.transport.HoneyPotTelnetFactory] New connection: xx.xxx.x.xxx:xxxxx (xxx.xx.x.x:2223) [session: TT130]
2016-12-21 13:30:48-0800 [CowrieTelnetTransport,130,xx.xxx.x.xxx] login attempt [/888888] failed
2016-12-21 13:30:48-0800 [CowrieTelnetTransport,130,xx.xxx.x.xxx] Warning: state changed and new state returned
2016-12-21 13:30:48-0800 [CowrieTelnetTransport,130,xx.xxx.x.xxx] login attempt [888888/shell] succeeded
2016-12-21 13:30:48-0800 [CowrieTelnetTransport,130,xx.xxx.x.xxx] Opening TTY Log: /opt/share/events/tty/20161221-133048-None-130i.log
2016-12-21 13:30:48-0800 [CowrieTelnetTransport,130,xx.xxx.x.xxx] Warning: state changed and new state returned
2016-12-21 13:30:48-0800 [CowrieTelnetTransport,130,xx.xxx.x.xxx] Unhandled Error
        Traceback (most recent call last):
          File "/usr/lib/python2.7/dist-packages/twisted/python/log.py", line 101, in callWithLogger
            return callWithContext({"system": lp}, func, *args, **kw)
          File "/usr/lib/python2.7/dist-packages/twisted/python/log.py", line 84, in callWithContext
            return context.call({ILogContext: newCtx}, func, *args, **kw)
          File "/usr/lib/python2.7/dist-packages/twisted/python/context.py", line 118, in callWithContext
            return self.currentContext().callWithContext(ctx, func, *args, **kw)
          File "/usr/lib/python2.7/dist-packages/twisted/python/context.py", line 81, in callWithContext
            return func(*args,**kw)
        --- <exception caught here> ---
          File "/usr/lib/python2.7/dist-packages/twisted/internet/posixbase.py", line 597, in _doReadOrWrite
            why = selectable.doRead()
          File "/usr/lib/python2.7/dist-packages/twisted/internet/tcp.py", line 209, in doRead
            return self._dataReceived(data)
          File "/usr/lib/python2.7/dist-packages/twisted/internet/tcp.py", line 215, in _dataReceived
            rval = self.protocol.dataReceived(data)
          File "/usr/lib/python2.7/dist-packages/twisted/conch/telnet.py", line 589, in dataReceived
            self.applicationDataReceived(''.join(appDataBuffer))
          File "/usr/lib/python2.7/dist-packages/twisted/conch/telnet.py", line 898, in applicationDataReceived
            self.protocol.dataReceived(bytes)
          File "/usr/lib/python2.7/dist-packages/twisted/protocols/basic.py", line 571, in dataReceived
            why = self.lineReceived(line)
          File "/usr/lib/python2.7/dist-packages/twisted/conch/telnet.py", line 1000, in lineReceived
            newState = getattr(self, "telnet_" + oldState)(line)
        exceptions.AttributeError: HoneyPotTelnetAuthProtocol instance has no attribute 'telnet_Command'
2016-12-21 13:30:48-0800 [CowrieTelnetTransport,130,xx.xxx.x.xxx] Closing TTY Log: /opt/share/events/tty/20161221-133048-None-130i.log after 0 seconds
2016-12-21 13:30:48-0800 [CowrieTelnetTransport,130,xx.xxx.x.xxx] honeypot terminal protocol connection lost [Failure instance: Traceback (failure with no frames): <type 'excepti
ons.AttributeError'>: HoneyPotTelnetAuthProtocol instance has no attribute 'telnet_Command'
        ]
2016-12-21 13:30:48-0800 [CowrieTelnetTransport,130,xx.xxx.x.xxx] Connection lost after 0 seconds
