Ejemplo n.º 1
0
class Command(CommandDefinition):
    """Alias an existing command with a new name. Specifying no alias or command
    will list all aliases. Specifying an alias with no command will remove the
    alias if it exists."""
    def get_command_names(self):
        return [c.PROG for c in self.manager.parser.commands]

    PROG = "alias"
    ARGS = {
        "alias":
        Parameter(Complete.NONE, help="name for the new alias", nargs="?"),
        "command":
        Parameter(
            Complete.CHOICES,
            metavar="COMMAND",
            choices=get_command_names,
            help="the command the new alias will use",
            nargs="?",
        ),
    }
    LOCAL = True

    def run(self, manager, args):
        if args.alias is None:
            for name, command in manager.parser.aliases.items():
                console.print(
                    f" [cyan]{name}[/cyan] \u2192 [yellow]{command.PROG}[/yellow]"
                )
        elif args.command is not None:
            # This is safe because of "choices" in the argparser
            manager.parser.aliases[args.alias] = [
                c for c in manager.parser.commands if c.PROG == args.command
            ][0]
        else:
            del manager.parser.aliases[args.alias]
Ejemplo n.º 2
0
class Command(CommandDefinition):
    """Create key aliases for when in raw mode. This only works from platforms
    which provide a raw interaction (such as linux)."""

    PROG = "bind"
    ARGS = {
        "key": Parameter(
            Complete.NONE,
            metavar="KEY",
            type=KeyType,
            help="The key to map after your prefix",
            nargs="?",
        ),
        "script": Parameter(
            Complete.NONE,
            help="The script to run when the key is pressed",
            nargs="?",
        ),
    }
    LOCAL = True

    def run(self, manager, args):
        if args.key is None:
            for key, binding in manager.config.bindings.items():
                console.print(f" [cyan]{key}[/cyan] = [yellow]{repr(binding)}[/yellow]")
        elif args.key is not None and args.script is None:
            if args.key in manager.config.bindings:
                del manager.config.bindings[args.key]
        else:
            manager.config.bindings[args.key] = args.script
Ejemplo n.º 3
0
class Command(CommandDefinition):
    """Download a file from the remote host to the local host"""

    PROG = "download"
    ARGS = {
        "source": Parameter(Complete.REMOTE_FILE),
        "destination": Parameter(Complete.LOCAL_FILE, nargs="?"),
    }

    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 = os.path.basename(args.source)
        elif os.path.isdir(args.destination):
            args.destination = os.path.join(
                args.destination, os.path.basename(args.source)
            )

        try:
            path = manager.target.platform.Path(args.source)
            length = path.stat().st_size
            started = time.time()
            with progress:
                task_id = progress.add_task(
                    "download", filename=args.source, total=length, start=False
                )
                with open(args.destination, "wb") as destination:
                    with path.open("rb") as source:
                        progress.start_task(task_id)
                        util.copyfileobj(
                            source,
                            destination,
                            lambda count: progress.update(task_id, advance=count),
                        )
                elapsed = time.time() - started

            console.log(
                f"downloaded [cyan]{util.human_readable_size(length)}[/cyan] "
                f"in [green]{util.human_readable_delta(elapsed)}[/green]"
            )
        except (FileNotFoundError, PermissionError, IsADirectoryError) as exc:
            self.parser.error(str(exc))
Ejemplo n.º 4
0
class Command(CommandDefinition):
    """
    Load modules from the specified directory. This does not remove
    currently loaded modules, but may replace modules which were already
    loaded. Also, prior to loading any specified modules, the standard
    modules are loaded. This normally happens only when modules are first
    utilized. This ensures that a standard module does not shadow a custom
    module. In fact, the opposite may happen in a custom module is defined
    with the same name as a standard module.
    """

    PROG = "load"
    ARGS = {
        "path":
        Parameter(
            Complete.LOCAL_FILE,
            help="Path to a python package directory to load modules from",
            nargs="+",
        )
    }
    DEFAULTS = {}
    LOCAL = True

    def run(self, manager: "pwncat.manager.Manager", args):

        manager.load_modules(args.path)
Ejemplo n.º 5
0
class Command(CommandDefinition):

    PROG = "shortcut"
    ARGS = {
        "prefix":
        Parameter(Complete.NONE,
                  help="the prefix character used for the shortcut"),
        "command":
        Parameter(Complete.NONE, help="the command to execute"),
    }
    LOCAL = True

    def run(self, manager, args):

        for command in manager.parser.commands:
            if command.PROG == args.command:
                manager.parser.shortcuts[args.prefix] = command
                return

        self.parser.error(f"{args.command}: no such command")
Ejemplo n.º 6
0
class Command(CommandDefinition):
    """View info about a module"""

    PROG = "info"
    ARGS = {
        "module": Parameter(
            Complete.CHOICES,
            choices=get_module_choices,
            metavar="MODULE",
            help="The module to view information on",
            nargs="?",
        )
    }

    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)
Ejemplo n.º 7
0
class Command(CommandDefinition):
    """
    Leave a layer of execution from this session. Layers are normally added
    as sub-shells from escalation modules.
    """

    PROG = "leave"
    ARGS = {
        "count":
        Parameter(
            Complete.NONE,
            type=int,
            default=1,
            nargs="?",
            help="number of layers to remove (default: 1)",
        ),
        "--all,-a":
        Parameter(
            Complete.NONE,
            action="store_true",
            help="leave all active layers",
        ),
    }

    def run(self, manager: "pwncat.manager.Manager", args):

        try:
            if args.all:
                args.count = len(manager.target.layers)

            for i in range(args.count):
                manager.target.layers.pop()(manager.target)

            manager.target.platform.refresh_uid()
        except IndexError:
            manager.target.log(
                "[yellow]warning[/yellow]: no more layers to leave")
Ejemplo n.º 8
0
class Command(CommandDefinition):
    """View info about a module"""

    PROG = "search"
    ARGS = {
        "module": Parameter(
            Complete.NONE,
            help="glob pattern",
        )
    }

    def run(self, manager: "pwncat.manager.Manager", args):

        modules = list(manager.target.find_module(f"*{args.module}*"))
        min_width = max(
            len(module.name.removeprefix("agnostic.")) for module in modules)

        table = Table(
            Column(header="Name", style="cyan", min_width=min_width),
            Column(header="Description"),
            title="Results",
            box=box.MINIMAL_DOUBLE_HEAD,
            expand=True,
        )

        for module in modules:
            # Rich will ellipsize the column, but we need to squeze
            # white space and remove newlines. `textwrap.shorten` is
            # the easiest way to do that, so we use a large size for
            # width.
            description = module.__doc__ if module.__doc__ is not None else ""
            module_name = module.name.removeprefix("agnostic.")

            if self.manager.target is not None:
                module_name = module_name.removeprefix(
                    self.manager.target.platform.name + ".")

            table.add_row(
                f"[cyan]{module_name}[/cyan]",
                textwrap.shorten(description.replace("\n", " "),
                                 width=80,
                                 placeholder="..."),
            )

        console.print(table)
Ejemplo n.º 9
0
class Command(CommandDefinition):
    """List known commands and print their associated help documentation."""
    def get_command_names(self):
        try:
            # Because we are initialized prior to `manager.parser`,
            # we have to wrap this in a try-except block.
            yield from [cmd.PROG for cmd in self.manager.parser.commands]
        except AttributeError:
            return

    PROG = "help"
    ARGS = {
        "topic": Parameter(Complete.CHOICES,
                           choices=get_command_names,
                           nargs="?")
    }
    LOCAL = True

    def run(self, manager: "pwncat.manager.Manager", args):
        if args.topic:
            for command in manager.parser.commands:
                if command.PROG == args.topic:
                    if command.parser is not None:
                        command.parser.print_help()
                    else:
                        console.print(textwrap.dedent(command.__doc__).strip())
                    break
        else:
            table = Table(
                Column("Command", style="green"),
                Column("Description", no_wrap=True),
                box=rich.box.SIMPLE,
            )

            for command in manager.parser.commands:
                doc = command.__doc__
                if doc is None:
                    doc = ""
                else:
                    doc = textwrap.shorten(
                        textwrap.dedent(doc).strip().replace("\n", ""), 60)

                table.add_row(command.PROG, doc)

            console.print(table)
Ejemplo n.º 10
0
class Command(CommandDefinition):
    """Set the currently used module in the config handler"""

    PROG = "use"
    ARGS = {
        "module": Parameter(
            Complete.CHOICES,
            choices=get_module_choices,
            metavar="MODULE",
            help="the module to use",
        )
    }
    LOCAL = False

    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)
Ejemplo n.º 11
0
class Command(CommandDefinition):
    """Set variable runtime variable parameters for pwncat"""
    def get_config_variables(self):
        options = ["state"] + list(self.manager.config.values)

        if self.manager.target is not None:
            options.extend(user.name
                           for user in self.manager.target.iter_users())

        if self.manager.config.module:
            options.extend(self.manager.config.module.ARGUMENTS.keys())

        return options

    PROG = "set"
    ARGS = {
        "--password,-p":
        Parameter(
            Complete.NONE,
            action="store_true",
            help="set a user password",
        ),
        "--global,-g":
        Parameter(
            Complete.NONE,
            action="store_true",
            help="Set a global configuration",
            default=False,
        ),
        "variable":
        Parameter(
            Complete.CHOICES,
            nargs="?",
            choices=get_config_variables,
            metavar="VARIABLE",
            help="the variable name to modify",
        ),
        "value":
        Parameter(Complete.LOCAL_FILE,
                  nargs="?",
                  help="the value for the given variable"),
    }
    LOCAL = True

    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]"
                    )
Ejemplo n.º 12
0
class Command(CommandDefinition):
    """
    Run a module. If no module is specified, use the module in the
    current context. You can enter a module context with the `use`
    command.

    Module arguments can be appended to the run command with `variable=value`
    syntax. Arguments are type-checked prior to executing, and the results
    are displayed to the terminal.

    To locate available modules, you can use the `search` command. To
    find documentation on individual modules including expected
    arguments, you can use the `info` command.
    """

    PROG = "run"
    ARGS = {
        "--raw,-r":
        Parameter(Complete.NONE,
                  action="store_true",
                  help="Display raw results unformatted"),
        "--traceback,-t":
        Parameter(Complete.NONE,
                  action="store_true",
                  help="Show traceback for module errors"),
        "module":
        Parameter(
            Complete.CHOICES,
            nargs="?",
            metavar="MODULE",
            choices=get_module_choices,
            help="The module name to execute",
        ),
        "args":
        Parameter(Complete.NONE, nargs="*", help="Module arguments"),
    }

    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)

    def display_item(self, manager, title, results):
        """Display a possibly complex item"""

        console.print(
            f"[bold underline]Module '{title}' Results[/bold underline]")

        # Uncategorized or raw results
        categories = {}
        uncategorized = []
        longform = []

        # Organize results by category
        for result in results:
            if isinstance(result,
                          pwncat.modules.Result) and result.is_long_form(
                              manager.target):
                longform.append(result)
            elif (not isinstance(result, pwncat.modules.Result)
                  or result.category(manager.target) is None):
                uncategorized.append(result)
            elif result.category(manager.target) not in categories:
                categories[result.category(manager.target)] = [result]
            else:
                categories[result.category(manager.target)].append(result)

        # Show uncategorized results first
        if uncategorized:
            console.print("[bold]Uncategorized Results[/bold]")
            for result in uncategorized:
                console.print("- " + result.title(manager.target))

        # Show all other categories
        if categories:
            for category, results in categories.items():
                console.print(f"[bold]{category}[/bold]")
                for result in results:
                    console.print(f"  - {result.title(manager.target)}")

        # Show long-form results in their own sections
        if longform:
            for result in longform:
                if result.category(manager.target) is None:
                    console.print(
                        f"[bold]{result.title(manager.target)}[/bold]")
                else:
                    console.print(
                        f"[bold]{result.category(manager.target)} - {result.title(manager.target)}[/bold]"
                    )
                console.print(
                    textwrap.indent(result.description(manager.target), "  "))
Ejemplo n.º 13
0
class Command(CommandDefinition):
    """
    Attempt privilege escalation in the current session. This command
    may initiate new sessions along the way to attain the privileges of
    the requested user.

    Escalation can happen either directly or recursively. In either case,
    each escalation may result in either replacing the user in the active
    session or spawning a new session. In the case of a new session, you
    should have configurations such as `lhost` and `lport` set prior to
    executing the escalation in case a reverse connection is needed.

    After escalation, if multiple stages were executed within an active
    session, you can use the `leave` command to back out of the users.
    This is useful for situations where escalation was achieved through
    peculiar ways (such as executing a command from `vim`).

    The list command is simply a wrapper around enumerating "escalation.*".
    This makes the escalation workflow more straightforward, but is not
    required."""

    PROG = "escalate"
    ARGS = {
        "command": Parameter(
            Complete.CHOICES,
            metavar="COMMAND",
            choices=["list", "run"],
            help="The action to take (list/run)",
        ),
        "--user,-u": Parameter(
            Complete.CHOICES,
            metavar="USERNAME",
            choices=get_user_choices,
            help="The target user for escalation.",
        ),
        "--recursive,-r": Parameter(
            Complete.NONE,
            action="store_true",
            help="Attempt recursive escalation through multiple users",
        ),
    }

    def run(self, manager: "pwncat.manager.Manager", args):

        if args.command == "help":
            self.parser.print_usage()
            return

        if args.command == "list":
            self.list_abilities(manager, args)
        elif args.command == "run":

            if args.user:
                args.user = manager.target.find_user(name=args.user)
            else:
                # NOTE: this should find admin regardless of platform
                args.user = manager.target.find_user(name="root")

            with manager.target.task(
                f"escalating to [cyan]{args.user.name}[/cyan]"
            ) as task:
                self.do_escalate(manager, task, args.user, args)

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

    def do_escalate(self, manager: "pwncat.manager.Manager", task, user, args):
        """Execute escalations until we find one that works"""

        attempted = []
        chain = []
        manager.target.current_user()
        original_session = manager.target
        failed = []

        while True:

            # Grab the current user in the active session
            current_user = manager.target.current_user()

            # Find escalations for users that weren't attempted already
            escalations = [
                e
                for e in list(manager.target.run("enumerate", types=["escalate.*"]))
                if (e.source_uid is None or e.source_uid == current_user.id)
                and e not in failed
                and e.uid not in attempted
            ]

            if not escalations:
                try:
                    # This direction failed. Go back up and try again.
                    chain.pop().leave()

                    continue
                except IndexError:
                    manager.target.log(
                        f"[red]error[/red]: no working escalation paths found for {user.name}"
                    )
                    break

            # Attempt escalation directly to the target user if possible
            for escalation in (e for e in escalations if e.uid == user.id):
                try:
                    original_session.update_task(
                        task, status=f"attempting {escalation.title(manager.target)}"
                    )
                    result = escalation.escalate(manager.target)

                    time.sleep(0.1)

                    manager.target.platform.refresh_uid()

                    # Construct the escalation link
                    link = Link(manager.target, escalation, result)

                    # Track the result object either as a new session or a subshell
                    if escalation.type == "escalate.replace":
                        manager.target.layers.append(result)
                    else:
                        manager.target = result

                    # Add our link to the chain
                    chain.append(link)

                    manager.log(
                        f"escalation to {user.name} [green]successful[/green] using:"
                    )
                    for link in chain:
                        manager.print(f" - {link}")

                    return result
                except ModuleFailed:
                    failed.append(escalation)

            if not args.recursive:
                manager.target.log(
                    f"[red]error[/red]: no working direct escalations to {user.name}"
                )
                return

            # Attempt escalation to a different user and recurse
            for escalation in (e for e in escalations if e.uid != user.id):
                try:
                    original_session.update_task(
                        task, status=f"attempting {escalation.title(manager.target)}"
                    )
                    result = escalation.escalate(manager.target)

                    time.sleep(0.1)

                    manager.target.platform.refresh_uid()
                    link = Link(manager.target, escalation, result)

                    if escalation.type == "escalate.replace":
                        manager.target.layers.append(result)
                    else:
                        manager.target = result

                    chain.append(link)
                    attempted.append(escalation.uid)
                    break
                except ModuleFailed:
                    failed.append(escalation)
Ejemplo n.º 14
0
class Command(CommandDefinition):
    """
    Interact and control active remote sessions. This command can be used
    to change context between sessions or kill active sessions which were
    established with the `connect` command.
    """

    PROG = "sessions"
    ARGS = {
        "--list,-l": Parameter(
            Complete.NONE,
            action="store_true",
            help="List active connections",
        ),
        "--kill,-k": Parameter(
            Complete.NONE,
            action="store_true",
            help="Kill an active session",
        ),
        "session_id": Parameter(
            Complete.NONE,
            type=int,
            help="Interact with the given session",
            nargs="?",
        ),
    }
    LOCAL = True

    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})")
Ejemplo n.º 15
0
class Command(CommandDefinition):
    """
    Connect to a remote victim. This command is only valid prior to an established
    connection. This command attempts to act similar to common tools such as netcat
    and ssh simultaneosly. Connection strings come in two forms. Firstly, pwncat
    can act like netcat. Using `connect [host] [port]` will connect to a bind shell,
    while `connect -l [port]` will listen for a reverse shell on the specified port.

    The second form is more explicit. A connection string can be used of the form
    `[protocol://][user[:password]@][host][:port]`. If a user is specified, the
    default protocol is `ssh`. If no user is specified, the default protocol is
    `connect` (connect to bind shell). If no host is specified or `host` is "0.0.0.0"
    then the `bind` protocol is used (listen for reverse shell). The currently available
    protocols are:

    - ssh
    - connect
    - bind

    The `--identity/-i` argument is ignored unless the `ssh` protocol is used.
    """

    PROG = "connect"
    ARGS = {
        "--identity,-i": Parameter(
            Complete.LOCAL_FILE,
            help="The private key for authentication for SSH connections",
        ),
        "--listen,-l": Parameter(
            Complete.NONE,
            action="store_true",
            help="Enable the `bind` protocol (supports netcat-like syntax)",
        ),
        "--platform,-m": Parameter(
            Complete.NONE,
            help="Name of the platform to use (default: linux)",
            default="linux",
        ),
        "--port,-p": Parameter(
            Complete.NONE,
            help="Alternative port number argument supporting netcat-like syntax",
        ),
        "--list": Parameter(
            Complete.NONE,
            action="store_true",
            help="List installed implants with remote connection capability",
        ),
        "connection_string": Parameter(
            Complete.NONE,
            metavar="[protocol://][user[:password]@][host][:port]",
            help="Connection string describing the victim to connect to",
            nargs="?",
        ),
        "pos_port": Parameter(
            Complete.NONE,
            nargs="?",
            metavar="port",
            help="Alternative port number argument supporting netcat-like syntax",
        ),
    }
    LOCAL = True
    CONNECTION_PATTERN = re.compile(
        r"""^(?P<protocol>[-a-zA-Z0-9_]*://)?((?P<user>[^:@]*)?(?P<password>:(\\@|[^@])*)?@)?(?P<host>[^:]*)?(?P<port>:[0-9]*)?$"""
    )

    def run(self, manager: "pwncat.manager.Manager", args):

        protocol = None
        user = None
        password = None
        host = None
        port = None
        used_implant = None

        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

        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:
            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 (ChannelError, PlatformError, ModuleFailed):
                        continue

        if used_implant 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}")