mirror of
https://github.com/cowrie/cowrie.git
synced 2025-07-01 18:07:27 -04:00
committed by
Michel Oosterhof
parent
cc7d65adc0
commit
34f8464732
@ -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 <hostname> 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
|
||||
# ============================================================================
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
62
src/cowrie/telnet/factory.py
Normal file
62
src/cowrie/telnet/factory.py
Normal file
@ -0,0 +1,62 @@
|
||||
# Copyright (C) 2015, 2016 GoSecure Inc.
|
||||
"""
|
||||
Telnet Transport and Authentication for the Honeypot
|
||||
|
||||
@author: Olivier Bilodeau <obilodeau@gosecure.ca>
|
||||
"""
|
||||
|
||||
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)
|
||||
@ -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):
|
||||
|
||||
142
src/cowrie/telnet/userauth.py
Normal file
142
src/cowrie/telnet/userauth.py
Normal file
@ -0,0 +1,142 @@
|
||||
# Copyright (C) 2015, 2016 GoSecure Inc.
|
||||
"""
|
||||
Telnet Transport and Authentication for the Honeypot
|
||||
|
||||
@author: Olivier Bilodeau <obilodeau@gosecure.ca>
|
||||
"""
|
||||
|
||||
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
|
||||
14
src/cowrie/telnet_proxy/README.md
Normal file
14
src/cowrie/telnet_proxy/README.md
Normal file
@ -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.
|
||||
0
src/cowrie/telnet_proxy/__init__.py
Normal file
0
src/cowrie/telnet_proxy/__init__.py
Normal file
68
src/cowrie/telnet_proxy/client_transport.py
Normal file
68
src/cowrie/telnet_proxy/client_transport.py
Normal file
@ -0,0 +1,68 @@
|
||||
# Copyright (c) 2019 Guilherme Borges <guilhermerosasborges@gmail.com>
|
||||
# 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
|
||||
283
src/cowrie/telnet_proxy/handler.py
Normal file
283
src/cowrie/telnet_proxy/handler.py
Normal file
@ -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)
|
||||
123
src/cowrie/telnet_proxy/server_transport.py
Normal file
123
src/cowrie/telnet_proxy/server_transport.py
Normal file
@ -0,0 +1,123 @@
|
||||
# Copyright (C) 2015, 2016 GoSecure Inc.
|
||||
"""
|
||||
Telnet Transport and Authentication for the Honeypot
|
||||
|
||||
@author: Olivier Bilodeau <obilodeau@gosecure.ca>
|
||||
"""
|
||||
|
||||
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)
|
||||
@ -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())
|
||||
|
||||
Reference in New Issue
Block a user