Add an SSH proxy for Cowrie (#1154)

* add SSH proxy
This commit is contained in:
Guilherme Borges
2019-06-29 10:20:26 +01:00
committed by Michel Oosterhof
parent 8b7415cb1b
commit cc7d65adc0
21 changed files with 1725 additions and 1471 deletions

View File

@ -191,7 +191,16 @@ auth_class = UserDB
# Source Port to report in logs (useful if you use iptables to forward ports to Cowrie)
#reported_ssh_port = 22
# ============================================================================
# Proxy Options
# ============================================================================
[proxy]
backend_ssh_host = localhost
backend_ssh_port = 2022
backend_user = root
backend_pass = root
# ============================================================================
# Shell Options

View File

@ -39,11 +39,11 @@ class EnvironmentConfigParser(configparser.ConfigParser):
return True
return super(EnvironmentConfigParser, self).has_option(section, option)
def get(self, section, option, **kwargs):
def get(self, section, option, raw=False, **kwargs):
key = to_environ_key('_'.join((section, option)))
if key in environ:
return environ[key]
return super(EnvironmentConfigParser, self).get(section, option, **kwargs)
return super(EnvironmentConfigParser, self).get(section, option, raw=raw, **kwargs)
def readConfigFile(cfgfile):

View File

@ -36,8 +36,6 @@ from twisted.python import log
from zope.interface import implementer
from cowrie.core.config import CowrieConfig
from cowrie.proxy import avatar as proxyavatar
from cowrie.proxy import server as proxyserver
from cowrie.shell import avatar as shellavatar
from cowrie.shell import server as shellserver
from cowrie.telnet import session
@ -61,15 +59,10 @@ class HoneyPotRealm(object):
serv = shellserver.CowrieServer(self)
user = session.HoneyPotTelnetSession(avatarId, serv)
return interfaces[0], user, user.logout
raise NotImplementedError("No supported interfaces found.")
elif backend == 'proxy':
if conchinterfaces.IConchUser in interfaces:
serv = proxyserver.CowrieServer(self)
user = proxyavatar.CowrieUser(avatarId, serv)
return interfaces[0], user, user.logout
elif ITelnetProtocol in interfaces:
raise NotImplementedError("Telnet not yet supported for proxy mode.")
log.msg('No supported interfaces found.')
raise NotImplementedError("No supported interfaces found.")
elif backend == 'proxy':
# not used in proxies, as they operate on a lower level
pass
else:
raise NotImplementedError("No supported backend found.")

View File

@ -1,43 +0,0 @@
# Copyright (c) 2009-2014 Upi Tamminen <desaster@gmail.com>
# See the COPYRIGHT file for more information
"""
This module contains ...
"""
from __future__ import absolute_import, division
from twisted.conch import avatar
from twisted.conch.interfaces import IConchUser, ISession
from twisted.python import components, log
from zope.interface import implementer
from cowrie.core.config import CowrieConfig
from cowrie.proxy import session as proxysession
from cowrie.shell import session as shellsession
from cowrie.ssh import forwarding
@implementer(IConchUser)
class CowrieUser(avatar.ConchUser):
def __init__(self, username, server):
avatar.ConchUser.__init__(self)
self.username = username
self.server = server
self.channelLookup[b'session'] = proxysession.ProxySSHSession
# TODO: Is SFTP still supported? Check git commit 30949e0 for cleaned up code
# SSH forwarding disabled only when option is explicitly set
self.channelLookup[b'direct-tcpip'] = forwarding.cowrieOpenConnectForwardingClient
if CowrieConfig().getboolean('ssh', 'forwarding', fallback=False) is False:
del self.channelLookup[b'direct-tcpip']
def logout(self):
log.msg('avatar {} logging out'.format(self.username))
components.registerAdapter(shellsession.SSHSessionForCowrieUser, CowrieUser, ISession)

File diff suppressed because it is too large Load Diff

View File

@ -1,260 +0,0 @@
# Copyright (c) 2017 Michel Oosterhof <michel@oosterhof.net>
# See the COPYRIGHT file for more information
from __future__ import absolute_import, division
from configparser import NoOptionError
from twisted.conch.client.knownhosts import KnownHostsFile
from twisted.conch.ssh import common, keys, session
from twisted.conch.ssh.common import getNS
from twisted.internet import protocol, reactor
from twisted.python import log
from cowrie.core.config import CowrieConfig
from cowrie.proxy import endpoints
from cowrie.ssh import channel
class _ProtocolFactory():
"""
Factory to return the (existing) ssh session to pass to ssh command endpoint
It does not actually function as a factory
"""
def __init__(self, protocol):
self.protocol = protocol
def buildProtocol(self, addr):
return self.protocol
class ProxyClient(object):
"""
Dummy object because SSHSession expects a .client with an attached transport
TODO: Forward ssh-exit-status
"""
transport = None
session = None
def __init__(self, session):
self.session = session
self.transport = InBetween()
self.transport.client = self.session
class InBetween(protocol.Protocol):
"""
This is the glue between the SSH server one one side and the
SSH client on the other side
"""
transport = None # Transport is the back-end the ssh-server
client = None # Client is the front-end, the ssh-client
buf = b"" # buffer to send to back-end
def makeConnection(self, transport):
protocol.Protocol.makeConnection(self, transport)
def connectionMade(self):
log.msg("IB: connection Made")
if len(self.buf) and self.transport is not None:
self.transport.write(self.buf)
self.buf = None
def write(self, bytes):
"""
This is data going from the end-user to the back-end
"""
if not self.transport:
self.buf += bytes
return
log.msg("IB: write: {0} to transport {1}".format(repr(bytes), repr(self.transport)))
self.transport.write(bytes)
def dataReceived(self, data):
"""
This is data going from the back-end to the end-user
"""
log.msg("IB: dataReceived: {0}".format(repr(data)))
self.client.write(data)
def closed(self):
log.msg("IB: closed")
def closeReceived(self):
log.msg("IB: closeRecieved")
def loseConnection(self):
"""
Frontend disconnected
"""
log.msg("IB: loseConnection")
def connectionLost(self, reason):
"""
Backend has disconnected
"""
log.msg("IB: ConnectionLost")
self.client.loseConnection()
def eofReceived(self):
log.msg("IB: eofReceived")
class ProxySSHSession(channel.CowrieSSHChannel):
"""
For SSH sessions that are proxied to a back-end, this is the
SSHSession object that speaks to the client. It is responsible
for forwarding incoming requests to the backend.
"""
name = b'proxy-frontend-session'
buf = b""
keys = []
host = ""
port = 22
user = ""
password = ""
knownHosts = None
def __init__(self, *args, **kw):
channel.CowrieSSHChannel.__init__(self, *args, **kw)
keyPath = CowrieConfig().get('proxy', 'private_key')
self.keys.append(keys.Key.fromFile(keyPath))
try:
keyPath = CowrieConfig().get('proxy', 'private_key')
self.keys.append(keys.Key.fromFile(keyPath))
except NoOptionError:
self.keys = None
knownHostsPath = CowrieConfig().get('proxy', 'known_hosts')
self.knownHosts = KnownHostsFile.fromPath(knownHostsPath)
self.host = CowrieConfig().get('proxy', 'host')
self.port = CowrieConfig().getint('proxy', 'port')
self.user = CowrieConfig().get('proxy', 'user')
try:
self.password = CowrieConfig().get('proxy', 'password')
except NoOptionError:
self.password = None
log.msg("knownHosts = {0}".format(repr(self.knownHosts)))
log.msg("host = {0}".format(self.host))
log.msg("port = {0}".format(self.port))
log.msg("user = {0}".format(self.user))
self.client = ProxyClient(self)
def channelOpen(self, specificData):
"""
Once we open the frontend-session, also start connecting to back end
"""
channel.CowrieSSHChannel.channelOpen(self, specificData)
return
log.msg("channelOpen")
helper = endpoints._NewConnectionHelper(reactor, self.host, self.port, self.user, self.keys,
self.password, None, self.knownHosts, None)
log.msg("helper = {0}".format(repr(helper)))
d = helper.secureConnection()
d.addCallback(self._cbConnect)
d.addErrback(self._ebConnect)
log.msg("d = {0}".format(repr(d)))
return d
def _ebConnect(self):
log.msg("ERROR CONNECTED TO BACKEND")
self._state = b'ERROR'
def _cbConnect(self):
log.msg("CONNECTED TO BACKEND")
self._state = b'CONNECTED'
def request_env(self, data):
name, rest = getNS(data)
value, rest = getNS(rest)
if rest:
raise ValueError("Bad data given in env request")
log.msg(eventid='cowrie.client.var', format='request_env: %(name)s=%(value)s', name=name, value=value)
# FIXME: This only works for shell, not for exec command
# if self.session:
# self.session.environ[name] = value
return 0
def request_pty_req(self, data):
term, windowSize, modes = session.parseRequest_pty_req(data)
log.msg('pty request: %r %r' % (term, windowSize))
return 1
def request_window_change(self, data):
return 1
def request_subsystem(self, data):
subsystem, _ = common.getNS(data)
log.msg('asking for subsystem "{}"'.format(subsystem))
return 0
def request_exec(self, data):
cmd, data = common.getNS(data)
log.msg('request_exec "{}"'.format(cmd))
pf = _ProtocolFactory(self.client.transport)
endpoints.SSHCommandClientEndpoint.newConnection(reactor, cmd, self.user, self.host,
port=self.port, password=self.password).connect(pf)
return 1
def request_shell(self, data):
log.msg('request_shell')
pf = _ProtocolFactory(self.client.transport)
endpoints.SSHShellClientEndpoint.newConnection(reactor, self.user, self.host,
port=self.port, password=self.password).connect(pf)
return 1
def extReceived(self, dataType, data):
log.msg('weird extended data: {}'.format(dataType))
def request_agent(self, data):
log.msg('request_agent: {}'.format(repr(data), ))
return 0
def request_x11_req(self, data):
log.msg('request_x11: %s' % (repr(data),))
return 0
def sendClose(self):
"""
Utility function to request to send close for this session
"""
self.conn.sendClose(self)
def closed(self):
"""
This is reliably called on session close/disconnect and calls the avatar
"""
channel.CowrieSSHChannel.closed(self)
self.client = None
def channelClosed(self):
log.msg("Called channelClosed in SSHSession")
def closeReceived(self):
log.msg("closeReceived")
def sendEOF(self):
"""
Utility function to request to send EOF for this session
"""
self.conn.sendEOF(self)
def dataReceived(self, data):
if not self.client:
self.buf += data
return
self.client.transport.write(data)
def eofReceived(self):
log.msg("RECEIVED EOF")
return
if self.client:
self.conn.sendClose(self)

View File

@ -18,8 +18,10 @@ from twisted.python import log
from cowrie.core.config import CowrieConfig
from cowrie.ssh import connection
from cowrie.ssh import keys as cowriekeys
from cowrie.ssh import transport
from cowrie.ssh import userauth
from cowrie.ssh import transport as shellTransport
from cowrie.ssh.userauth import HoneyPotSSHUserAuthServer
from cowrie.ssh_proxy import server_transport as proxyTransport
from cowrie.ssh_proxy.userauth import ProxySSHAuthServer
class CowrieSSHFactory(factory.SSHFactory):
@ -29,7 +31,8 @@ class CowrieSSHFactory(factory.SSHFactory):
"""
services = {
b'ssh-userauth': userauth.HoneyPotSSHUserAuthServer,
b'ssh-userauth': ProxySSHAuthServer if CowrieConfig().get('honeypot', 'backend') == 'proxy'
else HoneyPotSSHUserAuthServer,
b'ssh-connection': connection.CowrieSSHConnection,
}
starttime = None
@ -72,6 +75,9 @@ class CowrieSSHFactory(factory.SSHFactory):
except IOError:
pass
# this can come from backend in the future, check HonSSH's slim client
self.ourVersionString = CowrieConfig().get('ssh', 'version', fallback='SSH-2.0-OpenSSH_6.0p1 Debian-4+deb7u2')
factory.SSHFactory.startFactory(self)
log.msg("Ready to accept SSH connections")
@ -88,8 +94,10 @@ class CowrieSSHFactory(factory.SSHFactory):
@rtype: L{cowrie.ssh.transport.HoneyPotSSHTransport}
@return: The built transport.
"""
t = transport.HoneyPotSSHTransport()
if CowrieConfig().get('honeypot', 'backend', fallback='shell') == 'proxy':
t = proxyTransport.FrontendSSHTransport()
else:
t = shellTransport.HoneyPotSSHTransport()
t.ourVersionString = self.ourVersionString
t.supportedPublicKeys = list(self.privateKeys.keys())

View File

@ -123,6 +123,9 @@ class HoneyPotSSHTransport(transport.SSHServerTransport, TimeoutMixin):
self.dispatchMessage(messageNum, packet[1:])
packet = self.getPacket()
def dispatchMessage(self, message_num, payload):
transport.SSHServerTransport.dispatchMessage(self, message_num, payload)
def sendPacket(self, messageType, payload):
"""
Override because OpenSSH pads with 0 on KEXINIT

View File

@ -0,0 +1,44 @@
The SSH proxy is divided into a server module (frontend) and a client module (backend).
When clients (the attackers) connect to Cowrie, we start a connection (using Twisted Conch's client) to a specified server.
The proxy's structure is:
```
+------------------------------------------------------+
| |
| |
+----------+ | +----------+ +-----------+ +---------+ | +------------+
| | | | | | | | | | | |
| ATTACKER +<-->-<->+ FRONTEND +<---->+ HANDLER +<---->+ BACKEND +<->+<-->+ SSH SERVER |
| | | | | | | | | | | |
+----------+ | +----------+ +-----------+ +---------+ | +------------+
| |
| COWRIE'S PROXY |
+------------------------------------------------------+
```
Frontend is serverTransport.py, handler is ssh.py, and backend is clientTransport.py.
When an attacker connects, authentication is performed between them and the frontend, and between backend and server. The frontend part is handled by Cowrie and Twisted's own service. The backend part is a simple password authentication specified in the config - we assume backends are in a secure network anyway.
After authentication all SSH transport data is forwarded from attacker to server. Our proxy intercepts all messages and handles them as needed in ssh.py. We support exec, direct-tcpip and sftp, all of which have their own specification in the protocols directory. If a service is disabled in config, the handler does not forward the request to the server, instead creating and returning a default error message immediately.
## Authentication
Authentication leverages the same mechanism found in Cowrie's shell: it's set as a service provided by Twisted, and the same configurations apply when using both shell and proxy backends.
## Managing the VM pool
The flow to get VMs from the pool might be a little complicated because of the amount of back-and-forth needed when using deferreds.
The Pool interface is started in the Cowrie plugin (if enabled by configuration, of course), and the reference to it passed to both SSH and Telnet factories.
When the pool handler is started by the plugin, it immediately establishes a connection to the pool server, setting values from configuration into that server. If the connection fails, or these values are not set correclty, Cowrie is aborted, since it wouldn't be possible to continue without a correctly configured pool.
### SSH
When a new SSH connection is received in the frontend (serverTransport.py), the later has two tasks: perform userauth and connect to a backend. We start this in the connectionMade function. A pool connection is requested, and if that connection is successful we call pool_connection_success.
We then request a backend via send_vm_request. If any of these operations fail, we disconnect the frontend. Else we have a function called when data is received from the pool, **received_pool_data**.
When we receive a response to a VM request operation, we known we can start connecting to the backend, which we do. After that connection is established we can glue the two transports - frontend and backend - when the backendConnected variable is set to true in the backend.

View File

@ -0,0 +1,166 @@
# Copyright (c) 2019 Guilherme Borges <guilhermerosasborges@gmail.com>
# All rights reserved.
from twisted.conch.ssh import transport
from twisted.internet import defer, protocol
from twisted.protocols.policies import TimeoutMixin
from twisted.python import log
from cowrie.core.config import CowrieConfig
from cowrie.ssh_proxy.util import bin_string_to_hex, string_to_hex
def get_int(data, length=4):
return int.from_bytes(data[:length], byteorder='big')
def get_bool(data):
return bool(get_int(data, length=1))
def get_string(data):
length = get_int(data, 4)
value = data[4:length+4]
return length+4, value
class BackendSSHTransport(transport.SSHClientTransport, TimeoutMixin):
"""
This class represents the transport layer from Cowrie's proxy to the backend SSH server. It is responsible for
authentication to that server, and sending messages it gets to the handler.
"""
def __init__(self, factory):
self.delayedPackets = []
self.factory = factory
self.canAuth = False
# keep these from when frontend authenticates
self.frontendTriedUsername = None
self.frontendTriedPassword = None
def connectionMade(self):
log.msg('Connected to SSH backend at {0}'.format(self.transport.getPeer().host))
self.factory.server.client = self
self.factory.server.sshParse.set_client(self)
transport.SSHClientTransport.connectionMade(self)
def verifyHostKey(self, pub_key, fingerprint):
return defer.succeed(True)
def connectionSecure(self):
log.msg('Backend Connection Secured')
self.canAuth = True
self.authenticateBackend()
def authenticateBackend(self, tried_username=None, tried_password=None):
"""
This is called when the frontend is authenticated, so as to give us the option to authenticate with the
username and password given by the attacker.
"""
# we keep these here in case frontend has authenticated and backend hasn't established the secure channel yet;
# in that case, tried credentials are stored to be used whenever usearauth with backend can be performed
if tried_username and tried_password:
self.frontendTriedUsername = tried_username
self.frontendTriedPassword = tried_password
# do nothing if frontend is not authenticated, or backend has not established a secure channel
if not self.factory.server.frontendAuthenticated or not self.canAuth:
return
# we authenticate with the backend using the credentials provided
# TODO create the account in the backend before (contact the pool of VMs for example)
# so these credentials from the config may not be needed after all
username = CowrieConfig().get('proxy', 'backend_user').encode()
password = CowrieConfig().get('proxy', 'backend_pass').encode()
log.msg('Will auth with backend: {0}/{1}'.format(username, password))
self.sendPacket(5, bin_string_to_hex(b'ssh-userauth'))
payload = bin_string_to_hex(username) + \
string_to_hex('ssh-connection') + \
string_to_hex('password') + \
b'\x00' + \
bin_string_to_hex(password)
self.sendPacket(50, payload)
self.factory.server.backendConnected = True
# send packets from the frontend that were waiting to go to the backend
for packet in self.factory.server.delayedPackets:
self.factory.server.sshParse.parse_packet('[SERVER]', packet[0], packet[1])
self.factory.server.delayedPackets = []
def connectionLost(self, reason):
if self.factory.server.pool_interface:
log.msg(
eventid='cowrie.proxy.client_disconnect',
format="Lost connection with the pool backend: id %(vm_id)s",
vm_id=self.factory.server.pool_interface.vm_id,
protocol='ssh'
)
else:
log.msg(
eventid='cowrie.proxy.client_disconnect',
format="Lost connection with the proxy's backend: %(honey_ip)s:%(honey_port)s",
honey_ip=self.factory.server.backend_ip,
honey_port=self.factory.server.backend_port,
protocol='ssh'
)
self.transport.connectionLost(reason)
self.transport = None
# if connection from frontend is not closed, do it here
if self.factory.server.transport:
self.factory.server.transport.loseConnection()
def timeoutConnection(self):
"""
Make sure all sessions time out eventually.
Timeout is reset when authentication succeeds.
"""
log.msg('Timeout reached in BackendSSHTransport')
self.transport.loseConnection()
self.factory.server.transport.loseConnection()
def dispatchMessage(self, message_num, payload):
if message_num in [6, 52]:
return # TODO consume these in connectionSecure
if message_num == 98:
# looking for RFC 4254 - 6.10. Returning Exit Status
pointer = 4 # ignore recipient_channel
leng, message = get_string(payload[pointer:])
if message == b'exit-status':
pointer += leng + 1 # also boolean ignored
exit_status = get_int(payload[pointer:])
log.msg('exitCode: {0}'.format(exit_status))
if transport.SSHClientTransport.isEncrypted(self, "both"):
self.packet_buffer(message_num, payload)
else:
transport.SSHClientTransport.dispatchMessage(self, message_num, payload)
def packet_buffer(self, message_num, payload):
"""
We can only proceed if authentication has been performed between client and proxy. Meanwhile we hold packets
from the backend to the frontend in here.
"""
if not self.factory.server.frontendAuthenticated:
# wait till frontend connects and authenticates to send packets to them
log.msg('Connection to client not ready, buffering packet from backend')
self.delayedPackets.append([message_num, payload])
else:
if len(self.delayedPackets) > 0:
self.delayedPackets.append([message_num, payload])
for packet in self.delayedPackets:
self.factory.server.sshParse.parse_packet('[CLIENT]', packet[0], packet[1])
self.delayedPackets = []
else:
self.factory.server.sshParse.parse_packet('[CLIENT]', message_num, payload)
class BackendSSHFactory(protocol.ClientFactory):
def buildProtocol(self, addr):
return BackendSSHTransport(self)

View File

@ -0,0 +1,84 @@
# Copyright (c) 2016 Thomas Nicholson <tnnich@googlemail.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# 3. The names of the author(s) may not be used to endorse or promote
# products derived from this software without specific prior written
# permission.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
class BaseProtocol(object):
data = ''
packetSize = 0
name = ''
uuid = ''
ttylog_file = None
def __init__(self, uuid=None, name=None, ssh=None):
if uuid is not None:
self.uuid = uuid
if name is not None:
self.name = name
if ssh is not None:
self.ssh = ssh
def parse_packet(self, parent, data):
# log.msg(parent + ' ' + repr(data))
# log.msg(parent + ' ' + '\'\\x' + "\\x".join("{:02x}".format(ord(c)) for c in self.data) + '\'')
pass
def channel_closed(self):
pass
def extract_int(self, length):
value = int.from_bytes(self.data[:length], byteorder='big')
self.packetSize = self.packetSize - length
self.data = self.data[length:]
return value
def put_int(self, number):
return number.to_bytes(4, byteorder='big')
def extract_string(self):
length = self.extract_int(4)
value = self.data[:length]
self.packetSize -= length
self.data = self.data[length:]
return value
def extract_bool(self):
value = self.extract_int(1)
return bool(value)
def extract_data(self):
length = self.extract_int(4)
self.packetSize = length
value = self.data
self.packetSize -= len(value)
self.data = ''
return value
def __deepcopy__(self, memo):
return None

View File

@ -0,0 +1,87 @@
# Copyright (c) 2016 Thomas Nicholson <tnnich@googlemail.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# 3. The names of the author(s) may not be used to endorse or promote
# products derived from this software without specific prior written
# permission.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
import os
import time
from twisted.python import log
from cowrie.core import ttylog
from cowrie.core.config import CowrieConfig
from cowrie.ssh_proxy.protocols import base_protocol
class ExecTerm(base_protocol.BaseProtocol):
def __init__(self, uuid, channelName, ssh, channelId, command):
super(ExecTerm, self).__init__(uuid, channelName, ssh)
log.msg(eventid='cowrie.command.input',
input=command.decode('ascii'),
format='CMD: %(input)s')
self.transportId = ssh.server.transportId
self.channelId = channelId
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}/{1}-{2}-{3}e.log'.format(
self.ttylogPath, time.strftime('%Y%m%d-%H%M%S'), self.transportId, self.channelId)
ttylog.ttylog_open(self.ttylogFile, self.startTime)
def parse_packet(self, parent, payload):
if self.ttylogEnabled:
ttylog.ttylog_write(self.ttylogFile, len(payload), ttylog.TYPE_OUTPUT, time.time(), payload)
self.ttylogSize += len(payload)
def channel_closed(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)
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)

View File

@ -1,4 +1,4 @@
# Copyright (c) 2015 Michel Oosterhof <michel@oosterhof.net>
# Copyright (c) 2016 Thomas Nicholson <tnnich@googlemail.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
@ -26,21 +26,15 @@
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
from __future__ import absolute_import, division
# Inspiration and code snippets used from:
# http://www.codeproject.com/Tips/612847/Generate-a-quick-and-easy-custom-pcap-file-using-P
from cowrie.core.config import CowrieConfig
from cowrie.ssh_proxy.protocols import base_protocol
class CowrieServer(object):
"""
In traditional Kippo each connection gets its own simulated machine.
This is not always ideal, sometimes two connections come from the same
source IP address. we want to give them the same environment as well.
So files uploaded through SFTP are visible in the SSH session.
This class represents a 'virtual server' that can be shared between
multiple Cowrie connections
"""
class PortForward(base_protocol.BaseProtocol):
def __init__(self, uuid, chan_name, ssh):
super(PortForward, self).__init__(uuid, chan_name, ssh)
def __init__(self, realm):
self.avatars = []
self.hostname = CowrieConfig().get('honeypot', 'hostname')
def parse_packet(self, parent, payload):
pass

View File

@ -0,0 +1,312 @@
# Copyright (c) 2016 Thomas Nicholson <tnnich@googlemail.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# 3. The names of the author(s) may not be used to endorse or promote
# products derived from this software without specific prior written
# permission.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
from twisted.python import log
from cowrie.ssh_proxy.protocols import base_protocol
class SFTP(base_protocol.BaseProtocol):
prevID = ''
ID = ''
handle = ''
path = ''
command = ''
payloadSize = 0
payloadOffset = 0
theFile = ''
packetLayout = {
1: 'SSH_FXP_INIT',
# ['uint32', 'version'], [['string', 'extension_name'], ['string', 'extension_data']]]
2: 'SSH_FXP_VERSION',
# [['uint32', 'version'], [['string', 'extension_name'], ['string', 'extension_data']]]
3: 'SSH_FXP_OPEN',
# [['uint32', 'id'], ['string', 'filename'], ['uint32', 'pflags'], ['ATTRS', 'attrs']]
4: 'SSH_FXP_CLOSE', # [['uint32', 'id'], ['string', 'handle']]
5: 'SSH_FXP_READ', # [['uint32', 'id'], ['string', 'handle'], ['uint64', 'offset'], ['uint32', 'len']]
6: 'SSH_FXP_WRITE',
# [['uint32', 'id'], ['string', 'handle'], ['uint64', 'offset'], ['string', 'data']]
7: 'SSH_FXP_LSTAT', # [['uint32', 'id'], ['string', 'path']]
8: 'SSH_FXP_FSTAT', # [['uint32', 'id'], ['string', 'handle']]
9: 'SSH_FXP_SETSTAT', # [['uint32', 'id'], ['string', 'path'], ['ATTRS', 'attrs']]
10: 'SSH_FXP_FSETSTAT', # [['uint32', 'id'], ['string', 'handle'], ['ATTRS', 'attrs']]
11: 'SSH_FXP_OPENDIR', # [['uint32', 'id'], ['string', 'path']]
12: 'SSH_FXP_READDIR', # [['uint32', 'id'], ['string', 'handle']]
13: 'SSH_FXP_REMOVE', # [['uint32', 'id'], ['string', 'filename']]
14: 'SSH_FXP_MKDIR', # [['uint32', 'id'], ['string', 'path'], ['ATTRS', 'attrs']]
15: 'SSH_FXP_RMDIR', # [['uint32', 'id'], ['string', 'path']]
16: 'SSH_FXP_REALPATH', # [['uint32', 'id'], ['string', 'path']]
17: 'SSH_FXP_STAT', # [['uint32', 'id'], ['string', 'path']]
18: 'SSH_FXP_RENAME', # [['uint32', 'id'], ['string', 'oldpath'], ['string', 'newpath']]
19: 'SSH_FXP_READLINK', # [['uint32', 'id'], ['string', 'path']]
20: 'SSH_FXP_SYMLINK', # [['uint32', 'id'], ['string', 'linkpath'], ['string', 'targetpath']]
101: 'SSH_FXP_STATUS',
# [['uint32', 'id'], ['uint32', 'error_code'], ['string', 'error_message'], ['string', 'language']]
102: 'SSH_FXP_HANDLE', # [['uint32', 'id'], ['string', 'handle']]
103: 'SSH_FXP_DATA', # [['uint32', 'id'], ['string', 'data']]
104: 'SSH_FXP_NAME',
# [['uint32', 'id'], ['uint32', 'count'], [['string', 'filename'], ['string', 'longname'], ['ATTRS', 'attrs']]]
105: 'SSH_FXP_ATTRS', # [['uint32', 'id'], ['ATTRS', 'attrs']]
200: 'SSH_FXP_EXTENDED', # []
201: 'SSH_FXP_EXTENDED_REPLY' # []
}
def __init__(self, uuid, chan_name, ssh):
super(SFTP, self).__init__(uuid, chan_name, ssh)
self.clientPacket = base_protocol.BaseProtocol()
self.serverPacket = base_protocol.BaseProtocol()
self.parent = None
self.parentPacket = None
self.offset = 0
def parse_packet(self, parent, payload):
self.parent = parent
if parent == '[SERVER]':
self.parentPacket = self.serverPacket
elif parent == '[CLIENT]':
self.parentPacket = self.clientPacket
if self.parentPacket.packetSize == 0:
self.parentPacket.packetSize = int(payload[:4].hex(), 16) - len(payload[4:])
payload = payload[4:]
self.parentPacket.data = payload
payload = ''
else:
if len(payload) > self.parentPacket.packetSize:
self.parentPacket.data = self.parentPacket.data + payload[:self.parentPacket.packetSize]
payload = payload[self.parentPacket.packetSize:]
self.parentPacket.packetSize = 0
else:
self.parentPacket.packetSize -= len(payload)
self.parentPacket.data = self.parentPacket.data + payload
payload = ''
if self.parentPacket.packetSize == 0:
self.handle_packet(parent)
if len(payload) != 0:
self.parse_packet(parent, payload)
def handle_packet(self, parent):
self.packetSize = self.parentPacket.packetSize
self.data = self.parentPacket.data
sftp_num = self.extract_int(1)
packet = self.packetLayout[sftp_num]
self.prevID = self.ID
self.ID = self.extract_int(4)
if packet == 'SSH_FXP_OPENDIR':
self.path = self.extract_string()
elif packet == 'SSH_FXP_REALPATH':
self.path = self.extract_string()
self.command = b'cd ' + self.path
log.msg(parent + '[SFTP] Entered Command: ' + self.command.decode())
elif packet == 'SSH_FXP_OPEN':
self.path = self.extract_string()
pflags = '{0:08b}'.format(self.extract_int(4))
if pflags[6] == '1':
self.command = b'put ' + self.path
self.theFile = ''
# self.out.download_started(self.uuid, self.path)
elif pflags[7] == '1':
self.command = b'get ' + self.path
else:
# Unknown PFlag
log.msg(parent + '[SFTP] New SFTP pflag detected: %s %s' %
(pflags, self.data))
log.msg(parent + '[SFTP] Entered Command: ' + self.command)
elif packet == 'SSH_FXP_READ':
pass
elif packet == 'SSH_FXP_WRITE':
if self.handle == self.extract_string():
self.offset = self.extract_int(8)
self.theFile = self.theFile[:self.offset] + self.extract_data()
elif packet == 'SSH_FXP_HANDLE':
if self.ID == self.prevID:
self.handle = self.extract_string()
elif packet == 'SSH_FXP_READDIR':
if self.handle == self.extract_string():
self.command = b'ls ' + self.path
elif packet == 'SSH_FXP_SETSTAT':
self.path = self.extract_string()
self.command = self.extract_attrs() + ' ' + self.path
elif packet == 'SSH_FXP_EXTENDED':
cmd = self.extract_string()
self.path = self.extract_string()
if cmd == 'statvfs@openssh.com':
self.command = b'df ' + self.path
elif cmd == 'hardlink@openssh.com':
self.command = b'ln ' + self.path + b' ' + self.extract_string()
elif cmd == 'posix-rename@openssh.com':
self.command = b'mv ' + self.path + b' ' + self.extract_string()
else:
# UNKNOWN COMMAND
log.msg(parent + '[SFTP] New SFTP Extended Command detected: %s %s' %
(cmd, self.data))
elif packet == 'SSH_FXP_EXTENDED_REPLY':
log.msg(parent + '[SFTP] Entered Command: ' + self.command)
# self.out.command_entered(self.uuid, self.command)
elif packet == 'SSH_FXP_CLOSE':
if self.handle == self.extract_string():
if b'get' in self.command:
log.msg(parent + '[SFTP] Finished Downloading: ' + self.path)
elif b'put' in self.command:
log.msg(parent + '[SFTP] Finished Uploading: ' + self.path)
# if self.out.cfg.getboolean(['download', 'passive']):
# # self.out.make_downloads_folder()
# outfile = self.out.downloadFolder + datetime.datetime.now().strftime("%Y%m%d_%H%M%S_%f")\
# + "-" + self.path.split('/')[-1]
# f = open(outfile, 'wb')
# f.write(self.theFile)
# f.close()
# #self.out.file_downloaded((self.uuid, True, self.path, outfile, None))
elif packet == 'SSH_FXP_SYMLINK':
self.command = b'ln -s ' + self.extract_string() + ' ' + self.extract_string()
elif packet == 'SSH_FXP_MKDIR':
self.command = b'mkdir ' + self.extract_string()
elif packet == 'SSH_FXP_REMOVE':
self.command = b'rm ' + self.extract_string()
elif packet == 'SSH_FXP_RMDIR':
self.command = b'rmdir ' + self.extract_string()
elif packet == 'SSH_FXP_STATUS':
if self.ID == self.prevID:
code = self.extract_int(4)
if code in [0, 1]:
if b'get' not in self.command and b'put' not in self.command:
log.msg(parent + '[SFTP] Entered Command: ' + self.command.decode())
else:
message = self.extract_string()
log.msg(parent + '[SFTP] Failed Command: ' + self.command.decode() + ' Reason: ' + message)
def extract_attrs(self):
cmd = ''
flags = '{0:08b}'.format(self.extract_int(4))
if flags[5] == '1':
perms = '{0:09b}'.format(self.extract_int(4))
# log.msg(log.LPURPLE, self.parent + '[SFTP]', 'PERMS:' + perms)
chmod = str(int(perms[:3], 2)) + str(int(perms[3:6], 2)) + str(int(perms[6:], 2))
cmd = 'chmod ' + chmod
elif flags[6] == '1':
user = str(self.extract_int(4))
group = str(self.extract_int(4))
cmd = 'chown ' + user + ':' + group
else:
pass
# Unknown attribute
# log.msg(log.LRED, self.parent + '[SFTP]',
# 'New SFTP Attribute detected - Please raise a HonSSH issue on github with the details: %s %s' %
# (flags, self.data))
return cmd
'''
CLIENT SERVER
SSH_FXP_INIT -->
<-- SSH_FXP_VERSION
SSH_FXP_OPEN -->
<-- SSH_FXP_HANDLE (or SSH_FXP_STATUS if fail)
SSH_FXP_READ -->
<-- SSH_FXP_DATA (or SSH_FXP_STATUS if fail)
SSH_FXP_WRITE -->
<-- SSH_FXP_STATUS
SSH_FXP_REMOVE -->
<-- SSH_FXP_STATUS
SSH_FXP_RENAME -->
<-- SSH_FXP_STATUS
SSH_FXP_MKDIR -->
<-- SSH_FXP_STATUS
SSH_FXP_RMDIR -->
<-- SSH_FXP_STATUS
SSH_FXP_OPENDIR -->
<-- SSH_FXP_HANDLE (or SSH_FXP_STATUS if fail)
SSH_FXP_READDIR -->
<-- SSH_FXP_NAME (or SSH_FXP_STATUS if fail)
SSH_FXP_STAT --> //Follows symlinks
<-- SSH_FXP_ATTRS (or SSH_FXP_STATUS if fail)
SSH_FXP_LSTAT --> //Does not follow symlinks
<-- SSH_FXP_ATTRS (or SSH_FXP_STATUS if fail)
SSH_FXP_FSTAT --> //Works on an open file/handle not a file path like (L)STAT
<-- SSH_FXP_ATTRS (or SSH_FXP_STATUS if fail)
SSH_FXP_SETSTAT --> //Sets file attributes on path
<-- SSH_FXP_STATUS
SSH_FXP_FSETSTAT--> //Sets file attributes on a handle
<-- SSH_FXP_STATUS
SSH_FXP_READLINK --> //Used to find the target of a symlink
<-- SSH_FXP_NAME (or SSH_FXP_STATUS if fail)
SSH_FXP_SYMLINK --> //Used to create a symlink
<-- SSH_FXP_NAME (or SSH_FXP_STATUS if fail)
SSH_FXP_REALPATH --> //Relative path
<-- SSH_FXP_NAME (or SSH_FXP_STATUS if fail)
SSH_FXP_CLOSE --> //Closes handle not session
<-- SSH_FXP_STATUS
'''

View File

@ -0,0 +1,362 @@
# Copyright (c) 2016 Thomas Nicholson <tnnich@googlemail.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# 3. The names of the author(s) may not be used to endorse or promote
# products derived from this software without specific prior written
# permission.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
import uuid
from twisted.python import log
from cowrie.core.config import CowrieConfig
from cowrie.ssh_proxy.protocols import base_protocol, exec_term, port_forward, sftp, term
from cowrie.ssh_proxy.util import int_to_hex, string_to_hex
class SSH(base_protocol.BaseProtocol):
packetLayout = {
1: 'SSH_MSG_DISCONNECT', # ['uint32', 'reason_code'], ['string', 'reason'], ['string', 'language_tag']
2: 'SSH_MSG_IGNORE', # ['string', 'data']
3: 'SSH_MSG_UNIMPLEMENTED', # ['uint32', 'seq_no']
4: 'SSH_MSG_DEBUG', # ['boolean', 'always_display']
5: 'SSH_MSG_SERVICE_REQUEST', # ['string', 'service_name']
6: 'SSH_MSG_SERVICE_ACCEPT', # ['string', 'service_name']
20: 'SSH_MSG_KEXINIT', # ['string', 'service_name']
21: 'SSH_MSG_NEWKEYS',
50: 'SSH_MSG_USERAUTH_REQUEST', # ['string', 'username'], ['string', 'service_name'], ['string', 'method_name']
51: 'SSH_MSG_USERAUTH_FAILURE', # ['name-list', 'authentications'], ['boolean', 'partial_success']
52: 'SSH_MSG_USERAUTH_SUCCESS', #
53: 'SSH_MSG_USERAUTH_BANNER', # ['string', 'message'], ['string', 'language_tag']
60: 'SSH_MSG_USERAUTH_INFO_REQUEST', # ['string', 'name'], ['string', 'instruction'],
# ['string', 'language_tag'], ['uint32', 'num-prompts'],
# ['string', 'prompt[x]'], ['boolean', 'echo[x]']
61: 'SSH_MSG_USERAUTH_INFO_RESPONSE', # ['uint32', 'num-responses'], ['string', 'response[x]']
80: 'SSH_MSG_GLOBAL_REQUEST', # ['string', 'request_name'], ['boolean', 'want_reply'] #tcpip-forward
81: 'SSH_MSG_REQUEST_SUCCESS',
82: 'SSH_MSG_REQUEST_FAILURE',
90: 'SSH_MSG_CHANNEL_OPEN', # ['string', 'channel_type'], ['uint32', 'sender_channel'],
# ['uint32', 'initial_window_size'], ['uint32', 'maximum_packet_size'],
91: 'SSH_MSG_CHANNEL_OPEN_CONFIRMATION', # ['uint32', 'recipient_channel'], ['uint32', 'sender_channel'],
# ['uint32', 'initial_window_size'], ['uint32', 'maximum_packet_size']
92: 'SSH_MSG_CHANNEL_OPEN_FAILURE', # ['uint32', 'recipient_channel'], ['uint32', 'reason_code'],
# ['string', 'reason'], ['string', 'language_tag']
93: 'SSH_MSG_CHANNEL_WINDOW_ADJUST', # ['uint32', 'recipient_channel'], ['uint32', 'additional_bytes']
94: 'SSH_MSG_CHANNEL_DATA', # ['uint32', 'recipient_channel'], ['string', 'data']
95: 'SSH_MSG_CHANNEL_EXTENDED_DATA', # ['uint32', 'recipient_channel'],
# ['uint32', 'data_type_code'], ['string', 'data']
96: 'SSH_MSG_CHANNEL_EOF', # ['uint32', 'recipient_channel']
97: 'SSH_MSG_CHANNEL_CLOSE', # ['uint32', 'recipient_channel']
98: 'SSH_MSG_CHANNEL_REQUEST', # ['uint32', 'recipient_channel'], ['string', 'request_type'],
# ['boolean', 'want_reply']
99: 'SSH_MSG_CHANNEL_SUCCESS',
100: 'SSH_MSG_CHANNEL_FAILURE'
}
def __init__(self, server):
super(SSH, self).__init__()
self.channels = []
self.username = ''
self.password = ''
self.auth_type = ''
self.sendOn = False
self.expect_password = 0
self.server = server
self.channels = []
self.client = None
def set_client(self, client):
self.client = client
def parse_packet(self, parent, message_num, payload):
self.data = payload
self.packetSize = len(payload)
self.sendOn = True
if message_num in self.packetLayout:
packet = self.packetLayout[message_num]
else:
packet = 'UNKNOWN_{0}'.format(message_num)
if parent == '[SERVER]':
direction = 'PROXY -> BACKEND'
else:
direction = 'BACKEND -> PROXY'
# log raw packets if user sets so
if CowrieConfig().getboolean('proxy', 'log_raw', fallback=False):
log.msg(
eventid='cowrie.proxy.ssh',
format="%(direction)s - %(packet)s - %(payload)s",
direction=direction,
packet=packet.ljust(37),
payload=repr(payload),
protocol='ssh'
)
if packet == 'SSH_MSG_SERVICE_REQUEST':
service = self.extract_string()
if service == b'ssh-userauth':
self.sendOn = False
# - UserAuth
if packet == 'SSH_MSG_USERAUTH_REQUEST':
self.sendOn = False
self.username = self.extract_string()
self.extract_string() # service
self.auth_type = self.extract_string()
if self.auth_type == b'password':
self.extract_bool()
self.password = self.extract_string()
# self.server.sendPacket(52, b'')
elif self.auth_type == b'publickey':
self.sendOn = False
self.server.sendPacket(51, string_to_hex('password') + chr(0).encode())
elif packet == 'SSH_MSG_USERAUTH_FAILURE':
self.sendOn = False
auth_list = self.extract_string()
if b'publickey' in auth_list:
log.msg('[SSH] Detected Public Key Auth - Disabling!')
payload = string_to_hex('password') + chr(0).encode()
elif packet == 'SSH_MSG_USERAUTH_SUCCESS':
self.sendOn = False
if len(self.username) > 0 and len(self.password) > 0:
# self.server.login_successful(self.username, self.password)
pass
elif packet == 'SSH_MSG_USERAUTH_INFO_REQUEST':
self.sendOn = False
self.auth_type = b'keyboard-interactive'
self.extract_string()
self.extract_string()
self.extract_string()
num_prompts = self.extract_int(4)
for i in range(0, num_prompts):
request = self.extract_string()
self.extract_bool()
if 'password' in request.lower():
self.expect_password = i
elif packet == 'SSH_MSG_USERAUTH_INFO_RESPONSE':
self.sendOn = False
num_responses = self.extract_int(4)
for i in range(0, num_responses):
response = self.extract_string()
if i == self.expect_password:
self.password = response
# - End UserAuth
# - Channels
elif packet == 'SSH_MSG_CHANNEL_OPEN':
channel_type = self.extract_string()
channel_id = self.extract_int(4)
log.msg('got channel {} request'.format(channel_type))
if channel_type == b'session':
self.create_channel(parent, channel_id, channel_type)
elif channel_type == b'direct-tcpip' or channel_type == b'forwarded-tcpip':
self.extract_int(4)
self.extract_int(4)
dst_ip = self.extract_string()
dst_port = self.extract_int(4)
src_ip = self.extract_string()
src_port = self.extract_int(4)
if CowrieConfig().getboolean('ssh', 'forwarding'):
log.msg(eventid='cowrie.direct-tcpip.request',
format='direct-tcp connection request to %(dst_ip)s:%(dst_port)s '
'from %(src_ip)s:%(src_port)s',
dst_ip=dst_ip, dst_port=dst_port,
src_ip=src_ip, src_port=src_port)
the_uuid = uuid.uuid4().hex
self.create_channel(parent, channel_id, channel_type)
if parent == '[SERVER]':
other_parent = '[CLIENT]'
the_name = '[LPRTF' + str(channel_id) + ']'
else:
other_parent = '[SERVER]'
the_name = '[RPRTF' + str(channel_id) + ']'
channel = self.get_channel(channel_id, other_parent)
channel['name'] = the_name
channel['session'] = port_forward.PortForward(the_uuid, channel['name'], self)
else:
log.msg('[SSH] Detected Port Forwarding Channel - Disabling!')
log.msg(eventid='cowrie.direct-tcpip.data',
format='discarded direct-tcp forward request %(id)s to %(dst_ip)s:%(dst_port)s ',
dst_ip=dst_ip, dst_port=dst_port)
self.sendOn = False
self.send_back(parent, 92, int_to_hex(channel_id) + int_to_hex(1) +
string_to_hex('open failed') + int_to_hex(0))
else:
# UNKNOWN CHANNEL TYPE
if channel_type not in [b'exit-status']:
log.msg('[SSH Unknown Channel Type Detected - {0}'.format(channel_type))
elif packet == 'SSH_MSG_CHANNEL_OPEN_CONFIRMATION':
channel = self.get_channel(self.extract_int(4), parent)
# SENDER
sender_id = self.extract_int(4)
if parent == '[SERVER]':
channel['serverID'] = sender_id
elif parent == '[CLIENT]':
channel['clientID'] = sender_id
# CHANNEL OPENED
elif packet == 'SSH_MSG_CHANNEL_OPEN_FAILURE':
channel = self.get_channel(self.extract_int(4), parent)
self.channels.remove(channel)
# CHANNEL FAILED TO OPEN
elif packet == 'SSH_MSG_CHANNEL_REQUEST':
channel = self.get_channel(self.extract_int(4), parent)
channel_type = self.extract_string()
the_uuid = uuid.uuid4().hex
if channel_type == b'shell':
channel['name'] = '[TERM' + str(channel['serverID']) + ']'
channel['session'] = term.Term(the_uuid, channel['name'], self, channel['clientID'])
elif channel_type == b'exec':
channel['name'] = '[EXEC' + str(channel['serverID']) + ']'
self.extract_bool()
command = self.extract_string()
channel['session'] = exec_term.ExecTerm(the_uuid, channel['name'], self, channel['serverID'], command)
elif channel_type == b'subsystem':
self.extract_bool()
subsystem = self.extract_string()
if subsystem == b'sftp':
if CowrieConfig().getboolean('ssh', 'sftp_enabled'):
channel['name'] = '[SFTP' + str(channel['serverID']) + ']'
# self.out.channel_opened(the_uuid, channel['name'])
channel['session'] = sftp.SFTP(the_uuid, channel['name'], self)
else:
# log.msg(log.LPURPLE, '[SSH]', 'Detected SFTP Channel Request - Disabling!')
self.sendOn = False
self.send_back(parent, 100, int_to_hex(channel['serverID']))
else:
# UNKNOWN SUBSYSTEM
log.msg('[SSH] Unknown Subsystem Type Detected - ' + subsystem.decode())
else:
# UNKNOWN CHANNEL REQUEST TYPE
if channel_type not in [b'window-change', b'env', b'pty-req', b'exit-status', b'exit-signal']:
log.msg('[SSH] Unknown Channel Request Type Detected - {0}'.format(channel_type.decode()))
elif packet == 'SSH_MSG_CHANNEL_FAILURE':
pass
elif packet == 'SSH_MSG_CHANNEL_CLOSE':
channel = self.get_channel(self.extract_int(4), parent)
# Is this needed?!
channel[parent] = True
if '[SERVER]' in channel and '[CLIENT]' in channel:
# CHANNEL CLOSED
if channel['session'] is not None:
log.msg('remote close')
channel['session'].channel_closed()
self.channels.remove(channel)
# - END Channels
# - ChannelData
elif packet == 'SSH_MSG_CHANNEL_DATA':
channel = self.get_channel(self.extract_int(4), parent)
channel['session'].parse_packet(parent, self.extract_string())
elif packet == 'SSH_MSG_CHANNEL_EXTENDED_DATA':
channel = self.get_channel(self.extract_int(4), parent)
self.extract_int(4)
channel['session'].parse_packet(parent, self.extract_string())
# - END ChannelData
elif packet == 'SSH_MSG_GLOBAL_REQUEST':
channel_type = self.extract_string()
if channel_type == b'tcpip-forward':
if not CowrieConfig().getboolean(['ssh', 'forwarding']):
self.sendOn = False
self.send_back(parent, 82, '')
if self.sendOn:
if parent == '[SERVER]':
self.client.sendPacket(message_num, payload)
else:
self.server.sendPacket(message_num, payload)
def send_back(self, parent, message_num, payload):
packet = self.packetLayout[message_num]
if parent == '[SERVER]':
direction = 'PROXY -> FRONTEND'
else:
direction = 'PROXY -> BACKEND'
log.msg(
eventid='cowrie.proxy.ssh',
format="%(direction)s - %(packet)s - %(payload)s",
direction=direction,
packet=packet.ljust(37),
payload=repr(payload),
protocol='ssh'
)
if parent == '[SERVER]':
self.server.sendPacket(message_num, payload)
elif parent == '[CLIENT]':
self.client.sendPacket(message_num, payload)
def create_channel(self, parent, channel_id, channel_type, session=None):
if parent == '[SERVER]':
self.channels.append({'serverID': channel_id, 'type': channel_type, 'session': session})
elif parent == '[CLIENT]':
self.channels.append({'clientID': channel_id, 'type': channel_type, 'session': session})
def get_channel(self, channel_num, parent):
the_channel = None
for channel in self.channels:
if parent == '[CLIENT]':
search = 'serverID'
else:
search = 'clientID'
if channel[search] == channel_num:
the_channel = channel
break
return the_channel

View File

@ -0,0 +1,184 @@
# Copyright (c) 2016 Thomas Nicholson <tnnich@googlemail.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# 3. The names of the author(s) may not be used to endorse or promote
# products derived from this software without specific prior written
# permission.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
import os
import time
from twisted.python import log
from cowrie.core import ttylog
from cowrie.core.config import CowrieConfig
from cowrie.ssh_proxy.protocols import base_protocol
class Term(base_protocol.BaseProtocol):
def __init__(self, uuid, chan_name, ssh, channelId):
super(Term, self).__init__(uuid, chan_name, ssh)
self.command = b''
self.pointer = 0
self.tabPress = False
self.upArrow = False
self.transportId = ssh.server.transportId
self.channelId = channelId
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}/{1}-{2}-{3}i.log'.format(self.ttylogPath, time.strftime('%Y%m%d-%H%M%S'), uuid, self.channelId)
ttylog.ttylog_open(self.ttylogFile, self.startTime)
def channel_closed(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)
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 parse_packet(self, parent, payload):
self.data = payload
if parent == '[SERVER]':
while len(self.data) > 0:
# If Tab Pressed
if self.data[:1] == b'\x09':
self.tabPress = True
self.data = self.data[1:]
# If Backspace Pressed
elif self.data[:1] == b'\x7f' or self.data[:1] == b'\x08':
if self.pointer > 0:
self.command = self.command[:self.pointer - 1] + self.command[self.pointer:]
self.pointer -= 1
self.data = self.data[1:]
# If enter or ctrl+c or newline
elif self.data[:1] == b'\x0d' or self.data[:1] == b'\x03' or self.data[:1] == b'\x0a':
if self.data[:1] == b'\x03':
self.command += b'^C'
self.data = self.data[1:]
if self.command != b'':
log.msg(eventid='cowrie.command.input',
input=self.command.decode('ascii'),
format='CMD: %(input)s')
self.command = b''
self.pointer = 0
# If Home Pressed
elif self.data[:3] == b'\x1b\x4f\x48':
self.pointer = 0
self.data = self.data[3:]
# If End Pressed
elif self.data[:3] == b'\x1b\x4f\x46':
self.pointer = len(self.command)
self.data = self.data[3:]
# If Right Pressed
elif self.data[:3] == b'\x1b\x5b\x43':
if self.pointer != len(self.command):
self.pointer += 1
self.data = self.data[3:]
# If Left Pressed
elif self.data[:3] == b'\x1b\x5b\x44':
if self.pointer != 0:
self.pointer -= 1
self.data = self.data[3:]
# If up or down arrow
elif self.data[:3] == b'\x1b\x5b\x41' or self.data[:3] == b'\x1b\x5b\x42':
self.upArrow = True
self.data = self.data[3:]
else:
self.command = self.command[:self.pointer] + self.data[:1] + self.command[self.pointer:]
self.pointer += 1
self.data = self.data[1:]
if self.ttylogEnabled:
self.ttylogSize += len(payload)
ttylog.ttylog_write(self.ttylogFile, len(payload), ttylog.TYPE_OUTPUT, time.time(), payload)
elif parent == '[CLIENT]':
if self.tabPress:
if not self.data.startswith(b'\x0d'):
if self.data != b'\x07':
self.command = self.command + self.data
self.tabPress = False
if self.upArrow:
while len(self.data) != 0:
# Backspace
if self.data[:1] == b'\x08':
self.command = self.command[:-1]
self.pointer -= 1
self.data = self.data[1:]
# ESC[K - Clear Line
elif self.data[:3] == b'\x1b\x5b\x4b':
self.command = self.command[:self.pointer]
self.data = self.data[3:]
elif self.data[:1] == b'\x0d':
self.pointer = 0
self.data = self.data[1:]
# Right Arrow
elif self.data[:3] == b'\x1b\x5b\x43':
self.pointer += 1
self.data = self.data[3:]
elif self.data[:2] == b'\x1b\x5b' and self.data[3] == b'\x50':
self.data = self.data[4:]
# Needed?!
elif self.data[:1] != b'\x07' and self.data[:1] != b'\x0d':
self.command = self.command[:self.pointer] + self.data[:1] + self.command[self.pointer:]
self.pointer += 1
self.data = self.data[1:]
else:
self.pointer += 1
self.data = self.data[1:]
self.upArrow = False
if self.ttylogEnabled:
self.ttylogSize += len(payload)
ttylog.ttylog_write(self.ttylogFile, len(payload), ttylog.TYPE_INPUT, time.time(), payload)

View File

@ -0,0 +1,397 @@
# Copyright (c) 2016 Thomas Nicholson <tnnich@googlemail.com>, 2019 Guilherme Borges <guilhermerosasborges@gmail.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# 3. The names of the author(s) may not be used to endorse or promote
# products derived from this software without specific prior written
# permission.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
import re
import struct
import time
import uuid
import zlib
from hashlib import md5
from twisted.conch.ssh import transport
from twisted.conch.ssh.common import getNS
from twisted.internet import reactor
from twisted.internet.endpoints import TCP4ClientEndpoint
from twisted.protocols.policies import TimeoutMixin
from twisted.python import log, randbytes
from cowrie.core.config import CowrieConfig
from cowrie.ssh_proxy import client_transport
from cowrie.ssh_proxy.protocols import ssh
class FrontendSSHTransport(transport.SSHServerTransport, TimeoutMixin):
"""
Represents a connection from the frontend (a client or attacker).
When such connection is received, start the connection to the backend (the VM that will provide the session);
at the same time, perform the userauth service via ProxySSHAuthServer (built-in Cowrie's mechanism).
After both sides are authenticated, forward all things from one side to another.
"""
# TODO merge this with HoneyPotSSHTransport(transport.SSHServerTransport, TimeoutMixin)
# maybe create a parent class with common methods for the two
def __init__(self):
self.timeoutCount = 0
self.sshParse = None
self.disconnected = False # what was this used for
self.peer_ip = None
self.peer_port = 0
self.local_ip = None
self.local_port = 0
self.startTime = None
self.transportId = None
self.pool_interface = None
self.backendConnected = False
self.frontendAuthenticated = False
self.delayedPackets = []
# only used when simple proxy (no pool) set
self.backend_ip = None
self.backend_port = None
def connectionMade(self):
"""
Called when the connection is made to the other side. We sent our
version and the MSG_KEXINIT packet.
"""
self.sshParse = ssh.SSH(self)
self.transportId = uuid.uuid4().hex[:12]
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
self.transport.write('{0}\r\n'.format(self.ourVersionString).encode())
self.currentEncryptions = transport.SSHCiphers(b'none', b'none', b'none', b'none')
self.currentEncryptions.setKeys(b'', b'', b'', b'', b'', b'')
self.otherVersionString = 'Unknown'
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.peer_ip,
src_port=self.transport.getPeer().port,
dst_ip=self.local_ip,
dst_port=self.transport.getHost().port,
session=self.transportId,
sessionno='S{0}'.format(self.transport.sessionno),
protocol='ssh'
)
# if we have a pool connect to it and later request a backend, else just connect to a simple backend
# when pool is set we can just test self.pool_interface to the same effect of getting the config
if CowrieConfig().getboolean('proxy', 'enable_pool', fallback=False):
# request a backend
d = self.factory.pool_handler.request_interface()
d.addCallback(self.pool_connection_success)
d.addErrback(self.pool_connection_error)
else:
# simply a proxy, no pool
backend_ip = CowrieConfig().get('proxy', 'backend_ip')
backend_port = CowrieConfig().getint('proxy', 'backend_ssh_port')
self.connect_to_backend(backend_ip, backend_port)
def pool_connection_error(self, reason):
log.msg('Conenction to backend pool refused: {0}. Disconnecting frontend...'.format(reason.value))
self.transport.loseConnection()
def pool_connection_success(self, pool_interface):
log.msg("Connected to backend pool")
self.pool_interface = pool_interface
self.pool_interface.set_parent(self)
# now request a backend
self.pool_interface.send_vm_request(self.peer_ip)
def received_pool_data(self, operation, status, *data):
if operation == b'r':
log.msg('Got backend data from pool')
honey_ip = data[0]
ssh_port = data[1]
self.connect_to_backend(honey_ip, ssh_port)
def backend_connection_error(self, reason):
log.msg('Conenction to honeypot backend refused: {0}. Disconnecting frontend...'.format(reason.value))
self.transport.loseConnection()
def backend_connection_success(self, backendTransport):
log.msg("Connected to honeypot backend")
self.startTime = time.time()
self.setTimeout(CowrieConfig().getint('honeypot', 'authentication_timeout', fallback=120))
def connect_to_backend(self, ip, port):
# connection to the backend starts here
client_factory = client_transport.BackendSSHFactory()
client_factory.server = self
point = TCP4ClientEndpoint(reactor, ip, port, timeout=10)
d = point.connect(client_factory)
d.addCallback(self.backend_connection_success)
d.addErrback(self.backend_connection_error)
def sendKexInit(self):
"""
Don't send key exchange prematurely
"""
if not self.gotVersion:
return
transport.SSHServerTransport.sendKexInit(self)
def _unsupportedVersionReceived(self, remoteVersion):
"""
Change message to be like OpenSSH
"""
self.transport.write(b'Protocol major versions differ.\n')
self.transport.loseConnection()
def dataReceived(self, data):
"""
First, check for the version string (SSH-2.0-*). After that has been
received, this method adds data to the buffer, and pulls out any
packets.
@type data: C{str}
"""
self.buf += data
# get version from start of communication; check if valid and supported by Twisted
if not self.gotVersion:
if b'\n' not in self.buf:
return
self.otherVersionString = self.buf.split(b'\n')[0].strip()
log.msg(eventid='cowrie.client.version', version=repr(self.otherVersionString),
format="Remote SSH version: %(version)s")
m = re.match(br'SSH-(\d+.\d+)-(.*)', self.otherVersionString)
if m is None:
log.msg("Bad protocol version identification: {}".format(repr(self.otherVersionString)))
self.transport.write(b'Protocol mismatch.\n')
self.transport.loseConnection()
return
else:
self.gotVersion = True
remote_version = m.group(1)
if remote_version not in self.supportedVersions:
self._unsupportedVersionReceived(None)
return
i = self.buf.index(b'\n')
self.buf = self.buf[i + 1:]
self.sendKexInit()
packet = self.getPacket()
while packet:
message_num = ord(packet[0:1])
self.dispatchMessage(message_num, packet[1:])
packet = self.getPacket()
def dispatchMessage(self, message_num, payload):
# overriden dispatchMessage sets services, we do that here too then
# we're particularly interested in userauth, since Twisted does most of that for us
if message_num == 5:
self.ssh_SERVICE_REQUEST(payload)
elif 50 <= message_num <= 79: # userauth numbers
self.frontendAuthenticated = False
transport.SSHServerTransport.dispatchMessage(self, message_num, payload) # let userauth deal with it
# TODO delay userauth until backend is connected?
elif transport.SSHServerTransport.isEncrypted(self, "both"):
self.packet_buffer(message_num, payload)
else:
transport.SSHServerTransport.dispatchMessage(self, message_num, payload)
def sendPacket(self, messageType, payload):
"""
Override because OpenSSH pads with 0 on KEXINIT
"""
if self._keyExchangeState != self._KEY_EXCHANGE_NONE:
if not self._allowedKeyExchangeMessageType(messageType):
self._blockedByKeyExchange.append((messageType, payload))
return
payload = chr(messageType).encode() + payload
if self.outgoingCompression:
payload = (self.outgoingCompression.compress(payload)
+ self.outgoingCompression.flush(2))
bs = self.currentEncryptions.encBlockSize
# 4 for the packet length and 1 for the padding length
totalSize = 5 + len(payload)
lenPad = bs - (totalSize % bs)
if lenPad < 4:
lenPad = lenPad + bs
if messageType == transport.MSG_KEXINIT:
padding = b'\0' * lenPad
else:
padding = randbytes.secureRandom(lenPad)
packet = (struct.pack(b'!LB',
totalSize + lenPad - 4, lenPad) +
payload + padding)
encPacket = (self.currentEncryptions.encrypt(packet) +
self.currentEncryptions.makeMAC(
self.outgoingPacketSequence, packet))
self.transport.write(encPacket)
self.outgoingPacketSequence += 1
def ssh_KEXINIT(self, packet):
k = getNS(packet[16:], 10)
strings, _ = k[:-1], k[-1]
(kexAlgs, keyAlgs, encCS, _, macCS, _, compCS, _, langCS,
_) = [s.split(b',') for s in strings]
# hassh SSH client fingerprint
# https://github.com/salesforce/hassh
ckexAlgs = ','.join([alg.decode('utf-8') for alg in kexAlgs])
cencCS = ','.join([alg.decode('utf-8') for alg in encCS])
cmacCS = ','.join([alg.decode('utf-8') for alg in macCS])
ccompCS = ','.join([alg.decode('utf-8') for alg in compCS])
hasshAlgorithms = "{kex};{enc};{mac};{cmp}".format(
kex=ckexAlgs,
enc=cencCS,
mac=cmacCS,
cmp=ccompCS)
hassh = md5(hasshAlgorithms.encode('utf-8')).hexdigest()
log.msg(eventid='cowrie.client.kex',
format="SSH client hassh fingerprint: %(hassh)s",
hassh=hassh,
hasshAlgorithms=hasshAlgorithms,
kexAlgs=kexAlgs, keyAlgs=keyAlgs, encCS=encCS, macCS=macCS,
compCS=compCS, langCS=langCS)
return transport.SSHServerTransport.ssh_KEXINIT(self, packet)
def timeoutConnection(self):
"""
Make sure all sessions time out eventually.
Timeout is reset when authentication succeeds.
"""
log.msg('Timeout reached in FrontendSSHTransport')
self.transport.loseConnection()
self.sshParse.client.transport.loseConnection()
def setService(self, service):
"""
Remove login grace timeout, set zlib compression after auth
"""
# Reset timeout. Not everyone opens shell so need timeout at transport level
if service.name == b'ssh-connection':
self.setTimeout(CowrieConfig().getint('honeypot', 'interactive_timeout', fallback=300))
# when auth is successful we enable compression
# this is called right after MSG_USERAUTH_SUCCESS
if service.name == 'ssh-connection':
if self.outgoingCompressionType == 'zlib@openssh.com':
self.outgoingCompression = zlib.compressobj(6)
if self.incomingCompressionType == 'zlib@openssh.com':
self.incomingCompression = zlib.decompressobj()
transport.SSHServerTransport.setService(self, service)
def connectionLost(self, reason):
"""
This seems to be the only reliable place of catching lost connection
"""
self.setTimeout(None)
try:
self.client.loseConnection()
except Exception:
pass
transport.SSHServerTransport.connectionLost(self, reason)
self.transport.connectionLost(reason)
self.transport = None
# if connection from backend is not closed, do it here
if self.sshParse.client and self.sshParse.client.transport:
self.sshParse.client.transport.loseConnection()
# free connection
if self.pool_interface:
self.pool_interface.send_vm_free()
if self.startTime is not None: # startTime is not set when auth fails
duration = time.time() - self.startTime
log.msg(eventid='cowrie.session.closed',
format='Connection lost after %(duration)d seconds',
duration=duration)
def sendDisconnect(self, reason, desc):
"""
http://kbyte.snowpenguin.org/portal/2013/04/30/kippo-protocol-mismatch-workaround/
Workaround for the "bad packet length" error message.
@param reason: the reason for the disconnect. Should be one of the
DISCONNECT_* values.
@type reason: C{int}
@param desc: a description of the reason for the disconnection.
@type desc: C{str}
"""
if b'bad packet length' not in desc:
# With python >= 3 we can use super?
transport.SSHServerTransport.sendDisconnect(self, reason, desc)
else:
self.transport.write(b'Packet corrupt\n')
log.msg('Disconnecting with error, code {0}\nreason: {1}'.format(reason, desc))
self.transport.loseConnection()
def receiveError(self, reasonCode, description):
"""
Called when we receive a disconnect error message from the other
side.
@param reasonCode: the reason for the disconnect, one of the
DISCONNECT_ values.
@type reasonCode: L{int}
@param description: a human-readable description of the
disconnection.
@type description: L{str}
"""
log.msg('Got remote error, code {0} reason: {1}'.format(reasonCode, description))
def packet_buffer(self, message_num, payload):
"""
We have to wait until we have a connection to the backend is ready. Meanwhile, we hold packets from client
to server in here.
"""
if not self.backendConnected:
# wait till backend connects to send packets to them
log.msg('Connection to backend not ready, buffering packet from frontend')
self.delayedPackets.append([message_num, payload])
else:
if len(self.delayedPackets) > 0:
self.delayedPackets.append([message_num, payload])
else:
self.sshParse.parse_packet('[SERVER]', message_num, payload)

View File

@ -0,0 +1,34 @@
# Copyright (c) 2009-2014 Upi Tamminen <desaster@gmail.com>
# See the COPYRIGHT file for more information
from __future__ import absolute_import, division
from twisted.conch.ssh.common import getNS
from cowrie.ssh import userauth
class ProxySSHAuthServer(userauth.HoneyPotSSHUserAuthServer):
def __init__(self):
super().__init__()
self.triedPassword = None
def auth_password(self, packet):
"""
Overridden to get password
"""
self.triedPassword = getNS(packet[1:])[0]
return super().auth_password(packet)
def _cbFinishedAuth(self, result):
"""
We only want to return a success to the user, no service needs to be set.
Those will be proxied back to the backend.
"""
self.transport.sendPacket(52, b'')
self.transport.frontendAuthenticated = True
# TODO store this somewhere else, and do not call from here
if self.transport.sshParse.client:
self.transport.sshParse.client.authenticateBackend(self.user, self.triedPassword)

View File

@ -0,0 +1,16 @@
import struct
def string_to_hex(message):
b = message.encode('utf-8')
size = struct.pack('>L', len(b))
return size + b
def bin_string_to_hex(message):
size = struct.pack('>L', len(message))
return size + message
def int_to_hex(value):
return struct.pack('>L', value)