proxy.py 5.01 KB
Newer Older
Michal Horejsek's avatar
Michal Horejsek committed
1
"""
2
Implementation of SSH proxy using Twisted.
Michal Horejsek's avatar
Michal Horejsek committed
3 4 5 6 7 8 9 10 11 12
"""

import fcntl
import json
import os
import pwd
import struct
import tty

from twisted import cred
13
from twisted.application import service
Michal Horejsek's avatar
Michal Horejsek committed
14 15 16 17 18 19
from twisted.conch.avatar import ConchUser
from twisted.conch.ssh import factory, keys, userauth, connection, session
from twisted.conch.unix import SSHSessionForUnixConchUser
from twisted.internet import reactor, defer
from twisted.python import components

20
from haas_proxy.utils import force_text
Michal Horejsek's avatar
Michal Horejsek committed
21

22 23

class ProxyService(service.Service):
Michal Horejsek's avatar
Michal Horejsek committed
24
    """
25
    Service to be able to run it daemon with ``twistd`` command.
Michal Horejsek's avatar
Michal Horejsek committed
26
    """
27 28 29 30 31 32 33 34 35 36 37

    def __init__(self, args):
        self.args = args
        self._port = None

    def startService(self):
        # pylint: disable=no-member
        self._port = reactor.listenTCP(self.args.port, ProxySSHFactory(self.args))

    def stopService(self):
        return self._port.stopListening()
Michal Horejsek's avatar
Michal Horejsek committed
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141


# pylint: disable=abstract-method
class ProxySSHFactory(factory.SSHFactory):
    """
    Factory putting together all pieces of SSH proxy to honeypot together.
    """

    def __init__(self, cmd_args):
        self.publicKeys = {b'ssh-rsa': keys.Key.fromString(data=cmd_args.public_key)}
        self.privateKeys = {b'ssh-rsa': keys.Key.fromString(data=cmd_args.private_key)}
        self.services = {
            b'ssh-userauth': userauth.SSHUserAuthServer,
            b'ssh-connection': connection.SSHConnection,
        }
        self.portal = cred.portal.Portal(ProxySSHRealm(), checkers=[ProxyPasswordChecker()])
        ProxySSHSession.cmd_args = cmd_args
        components.registerAdapter(ProxySSHSession, ProxySSHUser, session.ISession)


class ProxyPasswordChecker:
    """
    Simple object checking credentials. For this SSH proxy we allow only passwords
    because we need to pass some information to session and the easiest way is to
    send it mangled in password.
    """

    credentialInterfaces = (cred.credentials.IUsernamePassword,)

    # pylint: disable=invalid-name
    def requestAvatarId(self, credentials):
        """
        Proxy allows any password. Honeypot decide what will accept later.
        """
        return defer.succeed(credentials)


class ProxySSHRealm:
    """
    Simple object to implement getting avatar used in :py:any:`portal.Portal`
    after checking credentials.
    """

    # pylint: disable=invalid-name,unused-argument
    def requestAvatar(self, avatarId, mind, *interfaces):
        """
        Normaly :py:any:`ProxyPasswordChecker` should return only username but
        we need also password so we unwrap it here.
        """
        avatar = ProxySSHUser(avatarId.username, avatarId.password)
        return interfaces[0], avatar, lambda: None


class ProxySSHUser(ConchUser):
    """
    Avatar returned by :py:any:`ProxySSHRealm`. It stores username and password
    for later usage in :py:any:`ProxySSHSession`.
    """

    def __init__(self, username, password):
        ConchUser.__init__(self)
        self.username = username
        self.password = password
        self.channelLookup.update({b'session': session.SSHSession})


class ProxySSHSession(SSHSessionForUnixConchUser):
    """
    Main function of SSH proxy - connects to honeypot and change password
    to JSON with more information needed to tag activity with user's account.
    """

    cmd_args = None  # Will inject ProxySSHFactory.

    # pylint: disable=invalid-name
    def openShell(self, proto):
        """
        Custom implementation of shell - proxy to real SSH to honeypot.
        """
        user = pwd.getpwuid(os.getuid())
        # pylint: disable=no-member
        self.pty = reactor.spawnProcess(
            proto,
            executable='/usr/bin/sshpass',
            args=self.honeypot_ssh_arguments,
            env=self.environ,
            path='/',
            uid=user.pw_uid,
            gid=user.pw_gid,
            usePTY=self.ptyTuple,
        )
        fcntl.ioctl(self.pty.fileno(), tty.TIOCSWINSZ, struct.pack('4H', *self.winSize))
        self.avatar.conn.transport.transport.setTcpNoDelay(1)

    @property
    def honeypot_ssh_arguments(self):
        """
        Command line arguments to call SSH to honeypot. Uses sshpass to be able
        pass password from command line.
        """
        return [
            'sshpass',
            '-p', self.mangled_password,
            'ssh',
Michal Horejsek's avatar
Michal Horejsek committed
142 143
            '-o', 'UserKnownHostsFile=/dev/null',
            '-o', 'StrictHostKeyChecking=no',
Michal Horejsek's avatar
Michal Horejsek committed
144 145 146 147 148 149 150 151 152 153 154 155
            '-p', str(self.cmd_args.honeypot_port),
            '{}@{}'.format(force_text(self.avatar.username), self.cmd_args.honeypot_host),
        ]

    @property
    def mangled_password(self):
        """
        Password as JSON string containing more information needed to
        tag activity with user's account.
        """
        peer = self.avatar.conn.transport.transport.getPeer()
        password_data = {
156 157
            'pass': force_text(self.avatar.password),
            'device_token': self.cmd_args.device_token,
Michal Horejsek's avatar
Michal Horejsek committed
158 159 160 161
            'remote': peer.host,
            'remote_port': peer.port,
        }
        return json.dumps(password_data)