From 34f846473217636f592f0fc8e13f448ceeb3871f Mon Sep 17 00:00:00 2001 From: Guilherme Borges Date: Mon, 1 Jul 2019 18:41:03 +0100 Subject: [PATCH] Add a Telnet proxy for Cowrie (#1159) * add telnet proxy --- etc/cowrie.cfg.dist | 36 ++- src/cowrie/ssh_proxy/server_transport.py | 2 +- src/cowrie/telnet/factory.py | 62 +++++ src/cowrie/telnet/transport.py | 169 +----------- src/cowrie/telnet/userauth.py | 142 ++++++++++ src/cowrie/telnet_proxy/README.md | 14 + src/cowrie/telnet_proxy/__init__.py | 0 src/cowrie/telnet_proxy/client_transport.py | 68 +++++ src/cowrie/telnet_proxy/handler.py | 283 ++++++++++++++++++++ src/cowrie/telnet_proxy/server_transport.py | 123 +++++++++ src/twisted/plugins/cowrie_plugin.py | 4 +- 11 files changed, 730 insertions(+), 173 deletions(-) create mode 100644 src/cowrie/telnet/factory.py create mode 100644 src/cowrie/telnet/userauth.py create mode 100644 src/cowrie/telnet_proxy/README.md create mode 100644 src/cowrie/telnet_proxy/__init__.py create mode 100644 src/cowrie/telnet_proxy/client_transport.py create mode 100644 src/cowrie/telnet_proxy/handler.py create mode 100644 src/cowrie/telnet_proxy/server_transport.py diff --git a/etc/cowrie.cfg.dist b/etc/cowrie.cfg.dist index 079cba3d..aa399008 100644 --- a/etc/cowrie.cfg.dist +++ b/etc/cowrie.cfg.dist @@ -199,9 +199,43 @@ auth_class = UserDB backend_ssh_host = localhost backend_ssh_port = 2022 +backend_telnet_host = localhost +backend_telnet_port = 2023 + backend_user = root backend_pass = root +# Telnet Spoofing # +# To spoof telnet authentication we need to capture login and password prompts, and spoof data to the backend in order +# to successfully authenticate. If disabled, attackers can only use the real user credentials of the backend. + +telnet_spoof_authentication = true + +# These regex were made using Ubuntu 18.04; you have to adapt these for the prompts +# from your backend. You can enable raw logging above to analyse data passing through +# and identify the format of the prompts you need. +# You should generally include ".*" at the beginning and end of prompts, since Telnet messages can contain +# more data than the prompt. + +# For login it is usually login: +telnet_username_prompt_regex = (\n|^)46438fdf5ebc login: .* + +# Password prompt is usually only the word Password +telnet_password_prompt_regex = .*Password: .* + +# This data is sent by clients at the beginning of negotiation (before the password prompt), and contains the username +# that is trying to log in. We replace that username with the one in "backend_user" to allow the chance of a successful +# login after the first password prompt. We are only able to check if credentials are allowed after the password is +# inserted. If they are, then a correct username was already sent and authentication succeeds; if not, we send a fake +# password to force authentication to fail. +telnet_username_in_negotiation_regex = (.*\xff\xfa.*USER\x01)(.*?)(\xff.*) + + +# Other Configs # +# log raw TCP packets in SSh and Telnet +log_raw = false + + # ============================================================================ # Shell Options # Options around Cowrie's Shell Emulation @@ -474,8 +508,6 @@ forward_tunnel = false # Configure keyboard-interactive login auth_keyboard_interactive_enabled = false - - # ============================================================================ # Telnet Specific Options # ============================================================================ diff --git a/src/cowrie/ssh_proxy/server_transport.py b/src/cowrie/ssh_proxy/server_transport.py index c2b35f52..58623087 100644 --- a/src/cowrie/ssh_proxy/server_transport.py +++ b/src/cowrie/ssh_proxy/server_transport.py @@ -117,7 +117,7 @@ class FrontendSSHTransport(transport.SSHServerTransport, TimeoutMixin): d.addErrback(self.pool_connection_error) else: # simply a proxy, no pool - backend_ip = CowrieConfig().get('proxy', 'backend_ip') + backend_ip = CowrieConfig().get('proxy', 'backend_ssh_host') backend_port = CowrieConfig().getint('proxy', 'backend_ssh_port') self.connect_to_backend(backend_ip, backend_port) diff --git a/src/cowrie/telnet/factory.py b/src/cowrie/telnet/factory.py new file mode 100644 index 00000000..77e7d2ae --- /dev/null +++ b/src/cowrie/telnet/factory.py @@ -0,0 +1,62 @@ +# Copyright (C) 2015, 2016 GoSecure Inc. +""" +Telnet Transport and Authentication for the Honeypot + +@author: Olivier Bilodeau +""" + +from __future__ import absolute_import, division + +import time + + +from twisted.internet import protocol +from twisted.python import log + +from cowrie.core.config import CowrieConfig +from cowrie.telnet.transport import CowrieTelnetTransport +from cowrie.telnet.userauth import HoneyPotTelnetAuthProtocol +from cowrie.telnet_proxy.server_transport import FrontendTelnetTransport + + +class HoneyPotTelnetFactory(protocol.ServerFactory): + """ + This factory creates HoneyPotTelnetAuthProtocol instances + They listen directly to the TCP port + """ + tac = None + + # 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{0}'.format(str(args['sessionno'])) + for output in self.tac.output_plugins: + output.logDispatch(*msg, **args) + + def startFactory(self): + try: + honeyfs = CowrieConfig().get('honeypot', 'contents_path') + issuefile = honeyfs + "/etc/issue.net" + self.banner = open(issuefile, 'rb').read() + except IOError: + self.banner = b"" + + # For use by the uptime command + self.starttime = time.time() + + # hook protocol + if CowrieConfig().get('honeypot', 'backend', fallback='shell') == 'proxy': + self.protocol = lambda: FrontendTelnetTransport() + else: + 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) diff --git a/src/cowrie/telnet/transport.py b/src/cowrie/telnet/transport.py index 8ee2f0cb..a8fa45c5 100644 --- a/src/cowrie/telnet/transport.py +++ b/src/cowrie/telnet/transport.py @@ -7,181 +7,14 @@ Telnet Transport and Authentication for the Honeypot from __future__ import absolute_import, division -import struct import time import uuid -from twisted.conch.telnet import AlreadyNegotiating, AuthenticatingTelnetProtocol, ITelnetProtocol, TelnetTransport -from twisted.conch.telnet import ECHO, LINEMODE, NAWS, SGA -from twisted.internet import protocol +from twisted.conch.telnet import AlreadyNegotiating, TelnetTransport from twisted.protocols.policies import TimeoutMixin from twisted.python import log from cowrie.core.config import CowrieConfig -from cowrie.core.credentials import UsernamePasswordIP - - -class HoneyPotTelnetFactory(protocol.ServerFactory): - """ - This factory creates HoneyPotTelnetAuthProtocol instances - They listen directly to the TCP port - """ - tac = None - - # 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{0}'.format(str(args['sessionno'])) - for output in self.tac.output_plugins: - output.logDispatch(*msg, **args) - - def startFactory(self): - try: - honeyfs = CowrieConfig().get('honeypot', 'contents_path') - issuefile = honeyfs + "/etc/issue.net" - self.banner = open(issuefile, 'rb').read() - except IOError: - self.banner = b"" - - # 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 = b'login: ' - passwordPrompt = b'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(b'\n', b'\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 # .decode() - # only send ECHO option if we are chatting with a real Telnet client - 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 # .decode() - 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 + b'\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' - - self.transport.write(b'\n') - - # Remove the short timeout of the login prompt. - self.transport.setTimeout(CowrieConfig().getint('honeypot', 'interactive_timeout', fallback=300)) - - # 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(b"\nLogin incorrect\n") - self.transport.write(self.loginPrompt) - self.state = "User" - - def telnet_NAWS(self, data): - """ - From TelnetBootstrapProtocol in twisted/conch/telnet.py - """ - 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 - # TODO: check if twisted now supports SGA (see git commit c58056b0) - elif opt == SGA: - return False - else: - return False - - def enableRemote(self, opt): - # TODO: check if twisted now supports LINEMODE (see git commit c58056b0) - if opt == LINEMODE: - return False - elif opt == NAWS: - return True - elif opt == SGA: - return True - else: - return False class CowrieTelnetTransport(TelnetTransport, TimeoutMixin): diff --git a/src/cowrie/telnet/userauth.py b/src/cowrie/telnet/userauth.py new file mode 100644 index 00000000..762b8945 --- /dev/null +++ b/src/cowrie/telnet/userauth.py @@ -0,0 +1,142 @@ +# Copyright (C) 2015, 2016 GoSecure Inc. +""" +Telnet Transport and Authentication for the Honeypot + +@author: Olivier Bilodeau +""" + +from __future__ import absolute_import, division + +import struct + + +from twisted.conch.telnet import AuthenticatingTelnetProtocol, ITelnetProtocol +from twisted.conch.telnet import ECHO, LINEMODE, NAWS, SGA +from twisted.python import log + +from cowrie.core.config import CowrieConfig +from cowrie.core.credentials import UsernamePasswordIP + + +class HoneyPotTelnetAuthProtocol(AuthenticatingTelnetProtocol): + """ + TelnetAuthProtocol that takes care of Authentication. Once authenticated this + protocol is replaced with HoneyPotTelnetSession. + """ + + loginPrompt = b'login: ' + passwordPrompt = b'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(b'\n', b'\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 # .decode() + # only send ECHO option if we are chatting with a real Telnet client + 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 # .decode() + 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 + b'\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' + + self.transport.write(b'\n') + + # Remove the short timeout of the login prompt. + self.transport.setTimeout(CowrieConfig().getint('honeypot', 'interactive_timeout', fallback=300)) + + # 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(b"\nLogin incorrect\n") + self.transport.write(self.loginPrompt) + self.state = "User" + + def telnet_NAWS(self, data): + """ + From TelnetBootstrapProtocol in twisted/conch/telnet.py + """ + 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 + # TODO: check if twisted now supports SGA (see git commit c58056b0) + elif opt == SGA: + return False + else: + return False + + def enableRemote(self, opt): + # TODO: check if twisted now supports LINEMODE (see git commit c58056b0) + if opt == LINEMODE: + return False + elif opt == NAWS: + return True + elif opt == SGA: + return True + else: + return False diff --git a/src/cowrie/telnet_proxy/README.md b/src/cowrie/telnet_proxy/README.md new file mode 100644 index 00000000..c6c3579e --- /dev/null +++ b/src/cowrie/telnet_proxy/README.md @@ -0,0 +1,14 @@ +All username credentials, when sent to backend, have the configured username that is known to succeed (i.e. exist in +the backend). When we spoof the password we decide whether the login is valid or not, and in the second case we send +an invalid password, thus causing auth to fail. + + +# Caveats in the protocol: + +* When username is being input (and chars are being sent to the backend), the **client expects to receive their echo**. +In our proxy, we do the echo locally, since **we don't want the backend to see our authentication** (in the end we send +to the backend what we want it to see). When we send the username in the end, the **backend then sends the full echo** +of the username, which look for and **ignore** in the proxy. + +* Backspaces in authentication are sent as **0x7F from the frontend**, but it expects to **receive "0x08 0x08" +as echo**, so we also have to look for that in the proxy's handler. diff --git a/src/cowrie/telnet_proxy/__init__.py b/src/cowrie/telnet_proxy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/cowrie/telnet_proxy/client_transport.py b/src/cowrie/telnet_proxy/client_transport.py new file mode 100644 index 00000000..c3ca6e7f --- /dev/null +++ b/src/cowrie/telnet_proxy/client_transport.py @@ -0,0 +1,68 @@ +# Copyright (c) 2019 Guilherme Borges +# All rights reserved. + +from twisted.conch.telnet import TelnetTransport +from twisted.internet import protocol +from twisted.protocols.policies import TimeoutMixin +from twisted.python import log + + +class BackendTelnetTransport(TelnetTransport, TimeoutMixin): + def __init__(self): + # self.delayedPacketsToFrontend = [] + self.backendConnected = False + self.telnetHandler = None + super(BackendTelnetTransport, self).__init__() + + def connectionMade(self): + log.msg('Connected to Telnet backend at {0}'.format(self.transport.getPeer().host)) + self.telnetHandler = self.factory.server.telnetHandler + self.telnetHandler.setClient(self) + + self.backendConnected = True + self.factory.server.client = self + + for packet in self.factory.server.delayedPacketsToBackend: + self.transport.write(packet) + self.factory.server.delayedPacketsToBackend = [] + + super().connectionMade() + # TODO timeout if no backend available + + def connectionLost(self, reason): + # close transport on frontend + self.factory.server.loseConnection() + + # signal that we're closing to the handler + self.telnetHandler.close() + + def timeoutConnection(self): + """ + Make sure all sessions time out eventually. + Timeout is reset when authentication succeeds. + """ + log.msg('Timeout reached in BackendTelnetTransport') + + # close transports on both sides + self.transport.loseConnection() + self.factory.server.transport.loseConnection() + + # signal that we're closing to the handler + self.telnetHandler.close() + + def dataReceived(self, data): + self.telnetHandler.addPacket('backend', data) + + def write(self, data): + self.transport.write(data) + + def packet_buffer(self, payload): + """ + We can only proceed if authentication has been performed between client and proxy. + Meanwhile we hold packets in here. + """ + self.factory.server.transport.write(payload) + + +class BackendTelnetFactory(protocol.ClientFactory): + protocol = BackendTelnetTransport diff --git a/src/cowrie/telnet_proxy/handler.py b/src/cowrie/telnet_proxy/handler.py new file mode 100644 index 00000000..3b37713f --- /dev/null +++ b/src/cowrie/telnet_proxy/handler.py @@ -0,0 +1,283 @@ +import os +import re +import time + +from twisted.python import log + +from cowrie.core import ttylog +from cowrie.core.checkers import HoneypotPasswordChecker +from cowrie.core.config import CowrieConfig + + +def process_backspaces(s): + """ + Takes a user-input string that might have backspaces in it (represented as 0x7F), + and actually performs the 'backspace operation' to return a clean string. + """ + n = b'' + for i in range(len(s)): + char = chr(s[i]).encode() + if char == b'\x7f': + n = n[:-1] + else: + n += char + return n + + +def remove_all(original_string, remove_list): + """ + Removes all substrings in the list remove_list from string original_string. + """ + n = original_string + for substring in remove_list: + n = n.replace(substring, b'') + return n + + +class TelnetHandler: + def __init__(self, server): + # holds packet data; useful to manipulate it across functions as needed + self.currentData = None + self.sendData = True + + # front and backend references + self.server = server + self.client = None + + # definitions from config + self.spoofAuthenticationData = CowrieConfig().getboolean('proxy', 'telnet_spoof_authentication') + + self.backendLogin = CowrieConfig().get('proxy', 'backend_user').encode() + self.backendPassword = CowrieConfig().get('proxy', 'backend_pass').encode() + + self.usernameInNegotiationRegex = CowrieConfig().get('proxy', 'telnet_username_in_negotiation_regex', + raw=True).encode() + self.usernamePromptRegex = CowrieConfig().get('proxy', 'telnet_username_prompt_regex', raw=True).encode() + self.passwordPromptRegex = CowrieConfig().get('proxy', 'telnet_password_prompt_regex', raw=True).encode() + + # telnet state + self.currentCommand = b'' + + # auth state + self.authStarted = False + self.authDone = False + + self.usernameState = b'' # TODO clear on end + self.inputingLogin = False + + self.passwordState = b'' # TODO clear on end + self.inputingPassword = False + + self.waitingLoginEcho = False + + # some data is sent by the backend right before the password prompt, we want to capture that + # and the respective frontend response and send it before starting to intercept auth data + self.prePasswordData = False + + # tty logging + self.startTime = time.time() + self.ttylogPath = CowrieConfig().get('honeypot', 'ttylog_path') + self.ttylogEnabled = CowrieConfig().getboolean('honeypot', 'ttylog', fallback=True) + self.ttylogSize = 0 + + if self.ttylogEnabled: + self.ttylogFile = '{0}/telnet-{1}.log'.format(self.ttylogPath, time.strftime('%Y%m%d-%H%M%S')) + ttylog.ttylog_open(self.ttylogFile, self.startTime) + + def setClient(self, client): + self.client = client + + def close(self): + if self.ttylogEnabled: + ttylog.ttylog_close(self.ttylogFile, time.time()) + shasum = ttylog.ttylog_inputhash(self.ttylogFile) + shasumfile = os.path.join(self.ttylogPath, shasum) + + if os.path.exists(shasumfile): + duplicate = True + os.remove(self.ttylogFile) + else: + duplicate = False + os.rename(self.ttylogFile, shasumfile) + umask = os.umask(0) + os.umask(umask) + os.chmod(shasumfile, 0o666 & ~umask) + + self.ttylogEnabled = False # do not close again if function called after closing + + log.msg(eventid='cowrie.log.closed', + format='Closing TTY Log: %(ttylog)s after %(duration)d seconds', + ttylog=shasumfile, + size=self.ttylogSize, + shasum=shasum, + duplicate=duplicate, + duration=time.time() - self.startTime) + + def sendBackend(self, data): + self.client.transport.write(data) + + # log raw packets if user sets so + if CowrieConfig().getboolean('proxy', 'log_raw', fallback=False): + log.msg(b'to_backend - ' + data) + + if self.ttylogEnabled and self.authStarted: + cleanData = data.replace(b'\x00', b'\n') # some frontends send 0xFF instead of newline + + ttylog.ttylog_write(self.ttylogFile, len(data), ttylog.TYPE_INPUT, time.time(), cleanData) + self.ttylogSize += len(data) + + def sendFrontend(self, data): + self.server.transport.write(data) + + # log raw packets if user sets so + if CowrieConfig().getboolean('proxy', 'log_raw', fallback=False): + log.msg(b'to_frontend - ' + data) + + if self.ttylogEnabled and self.authStarted: + ttylog.ttylog_write(self.ttylogFile, len(data), ttylog.TYPE_OUTPUT, time.time(), data) + # self.ttylogSize += len(data) + + def addPacket(self, parent, data): + self.currentData = data + self.sendData = True + + if self.spoofAuthenticationData and not self.authDone: + # detect prompts from backend + if parent == 'backend': + self.setProcessingStateBackend() + + # detect patterns from frontend + if parent == 'frontend': + self.setProcessingStateFrontend() + + # save user inputs from frontend + if parent == 'frontend': + if self.inputingPassword: + self.processPasswordInput() + + if self.inputingLogin: + self.processUsernameInput() + + # capture username echo from backend + if self.waitingLoginEcho and parent == 'backend': + self.currentData = self.currentData.replace(self.backendLogin + b'\r\n', b'') + self.waitingLoginEcho = False + + # log user commands + if parent == 'frontend' and self.authDone: + self.currentCommand += data.replace(b'\r\x00', b'').replace(b'\r\n', b'') + + # check if a command has terminated + if b'\r' in data: + if len(self.currentCommand) > 0: + log.msg('CMD: {0}'.format(self.currentCommand.decode())) + self.currentCommand = b'' + + # send data after processing (also check if processing did not reduce it to an empty string) + if self.sendData and len(self.currentData): + if parent == 'frontend': + self.sendBackend(self.currentData) + else: + self.sendFrontend(self.currentData) + + def processUsernameInput(self): + self.sendData = False # withold data until input is complete + + # remove control characters + control_chars = [b'\r', b'\x00', b'\n'] + self.usernameState += remove_all(self.currentData, control_chars) + + # backend echoes data back to user to show on terminal prompt + # - NULL char is replaced by NEWLINE by backend + # - 0x7F (backspace) is replaced by two 0x08 separated by a blankspace + self.sendFrontend(self.currentData.replace(b'\x7f', b'\x08 \x08').replace(b'\x00', b'\n')) + + # check if done inputing + if b'\r' in self.currentData: + terminatingChar = chr(self.currentData[self.currentData.index(b'\r') + 1]).encode() # usually \n or \x00 + + # cleanup + self.usernameState = process_backspaces(self.usernameState) + + log.msg('User input login: {0}'.format(self.usernameState)) + self.inputingLogin = False + + # actually send to backend + self.currentData = self.backendLogin + b'\r' + terminatingChar + self.sendData = True + + # we now have to ignore the username echo from the backend in the next packet + self.waitingLoginEcho = True + + def processPasswordInput(self): + self.sendData = False # withold data until input is complete + + if self.prePasswordData: + self.sendBackend(self.currentData[:3]) + self.prePasswordData = False + + # remove control characters + control_chars = [b'\xff', b'\xfd', b'\x01', b'\r', b'\x00', b'\n'] + self.passwordState += remove_all(self.currentData, control_chars) + + # check if done inputing + if b'\r' in self.currentData: + terminatingChar = chr(self.currentData[self.currentData.index(b'\r') + 1]).encode() # usually \n or \x00 + + # cleanup + self.passwordState = process_backspaces(self.passwordState) + + log.msg('User input password: {0}'.format(self.passwordState)) + self.inputingPassword = False + + # having the password (and the username, either empy or set before), we can check the login + # on the database, and if valid authenticate or else, if invalid send a fake password to get + # the login failed prompt + src_ip = self.server.transport.getPeer().host + if HoneypotPasswordChecker().checkUserPass(self.usernameState, self.passwordState, src_ip): + passwordToSend = self.backendPassword + self.authDone = True + else: + log.msg('Sending invalid auth to backend') + passwordToSend = self.backendPassword + b'fake' + + # actually send to backend + self.currentData = passwordToSend + b'\r' + terminatingChar + self.sendData = True + + def setProcessingStateBackend(self): + """ + This function analyses a data packet and sets the processing state of the handler accordingly. + It looks for authentication phases (password input and username input), as well as data that + may need to be processed specially. + """ + hasPassword = re.search(self.passwordPromptRegex, self.currentData) + if hasPassword: + log.msg('Password prompt from backend') + self.authStarted = True + self.inputingPassword = True + self.passwordState = b'' + + hasLogin = re.search(self.usernamePromptRegex, self.currentData) + if hasLogin: + log.msg('Login prompt from backend') + self.authStarted = True + self.inputingLogin = True + self.usernameState = b'' + + self.prePasswordData = b'\xff\xfb\x01' in self.currentData + + def setProcessingStateFrontend(self): + """ + Same for the frontend. + """ + # login username is sent in channel negotiation to match the client's username + negotiationLoginPattern = re.compile(self.usernameInNegotiationRegex) + hasNegotiationLogin = negotiationLoginPattern.search(self.currentData) + if hasNegotiationLogin: + self.usernameState = hasNegotiationLogin.group(2) + log.msg('Detected username {0} in negotiation, spoofing for backend...'.format(self.usernameState.decode())) + + # spoof username in data sent + # username is always sent correct, password is the one sent wrong if we don't want to authenticate + self.currentData = negotiationLoginPattern.sub(br'\1' + self.backendLogin + br'\3', self.currentData) diff --git a/src/cowrie/telnet_proxy/server_transport.py b/src/cowrie/telnet_proxy/server_transport.py new file mode 100644 index 00000000..5270729b --- /dev/null +++ b/src/cowrie/telnet_proxy/server_transport.py @@ -0,0 +1,123 @@ +# Copyright (C) 2015, 2016 GoSecure Inc. +""" +Telnet Transport and Authentication for the Honeypot + +@author: Olivier Bilodeau +""" + +from __future__ import absolute_import, division + +import time +import uuid + +from twisted.conch.telnet import TelnetTransport +from twisted.internet import reactor +from twisted.protocols.policies import TimeoutMixin +from twisted.python import log + +from cowrie.core.config import CowrieConfig +from cowrie.telnet_proxy import client_transport +from cowrie.telnet_proxy.handler import TelnetHandler + + +class FrontendTelnetTransport(TelnetTransport, TimeoutMixin): + def __init__(self): + super().__init__() + + self.peer_ip = None + self.peer_port = 0 + self.local_ip = None + self.local_port = 0 + + self.honey_ip = CowrieConfig().get('proxy', 'backend_telnet_host') + self.honey_port = CowrieConfig().getint('proxy', 'backend_telnet_port') + + self.client = None + self.frontendAuthenticated = False + self.delayedPacketsToBackend = [] + + self.telnetHandler = TelnetHandler(self) + + def connectionMade(self): + self.transportId = uuid.uuid4().hex[:12] + sessionno = self.transport.sessionno + + self.startTime = time.time() + self.setTimeout(CowrieConfig().getint('honeypot', 'authentication_timeout', fallback=120)) + + self.peer_ip = self.transport.getPeer().host + self.peer_port = self.transport.getPeer().port + 1 + self.local_ip = self.transport.getHost().host + self.local_port = self.transport.getHost().port + + # connection to the backend starts here + client_factory = client_transport.BackendTelnetFactory() + client_factory.server = self + + reactor.connectTCP(self.honey_ip, self.honey_port, client_factory, + bindAddress=('0.0.0.0', 0), + timeout=10) + + log.msg(eventid='cowrie.session.connect', + format='New connection: %(src_ip)s:%(src_port)s (%(dst_ip)s:%(dst_port)s) [session: %(session)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{0}'.format(str(sessionno)), + protocol='telnet') + TelnetTransport.connectionMade(self) + + def dataReceived(self, data): + self.telnetHandler.addPacket('frontend', data) + + def write(self, data): + self.transport.write(data) + + def timeoutConnection(self): + """ + Make sure all sessions time out eventually. + Timeout is reset when authentication succeeds. + """ + log.msg('Timeout reached in FrontendTelnetTransport') + + # close transports on both sides + self.transport.loseConnection() + self.client.transport.loseConnection() + + # signal that we're closing to the handler + self.telnetHandler.close() + + def connectionLost(self, reason): + """ + Fires on pre-authentication disconnects + """ + self.setTimeout(None) + TelnetTransport.connectionLost(self, reason) + + # close transport on backend + self.client.transport.loseConnection() + + # signal that we're closing to the handler + self.telnetHandler.close() + + duration = time.time() - self.startTime + log.msg(eventid='cowrie.session.closed', + format='Connection lost after %(duration)d seconds', + duration=duration) + + def packet_buffer(self, payload): + """ + We have to wait until we have a connection to the backend ready. Meanwhile, we hold packets from client + to server in here. + """ + if not self.client.backendConnected: + # wait till backend connects to send packets to them + log.msg('Connection to backend not ready, buffering packet from frontend') + self.delayedPacketsToBackend.append(payload) + else: + if len(self.delayedPacketsToBackend) > 0: + self.delayedPacketsToBackend.append(payload) + else: + self.client.transport.write(payload) diff --git a/src/twisted/plugins/cowrie_plugin.py b/src/twisted/plugins/cowrie_plugin.py index 10974474..480e695a 100644 --- a/src/twisted/plugins/cowrie_plugin.py +++ b/src/twisted/plugins/cowrie_plugin.py @@ -45,7 +45,7 @@ from zope.interface import implementer, provider import cowrie.core.checkers import cowrie.core.realm import cowrie.ssh.factory -import cowrie.telnet.transport +import cowrie.telnet.factory from cowrie import core from cowrie.core.config import CowrieConfig from cowrie.core.utils import create_endpoint_services, get_endpoints_from_section @@ -165,7 +165,7 @@ Makes a Cowrie SSH/Telnet honeypot. create_endpoint_services(reactor, topService, listen_endpoints, factory) if enableTelnet: - f = cowrie.telnet.transport.HoneyPotTelnetFactory() + f = cowrie.telnet.factory.HoneyPotTelnetFactory() f.tac = self f.portal = portal.Portal(core.realm.HoneyPotRealm()) f.portal.registerChecker(core.checkers.HoneypotPasswordChecker())