Ejemplo n.º 1
0
    def run(self, args):

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

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

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

            # Find all binaries which are provided from busybox
            nprovides = (pwncat.victim.session.query(pwncat.db.Binary).filter(
                pwncat.db.Binary.path.contains(pwncat.victim.host.busybox),
                pwncat.db.Binary.host_id == pwncat.victim.host.id,
            ).with_entities(func.count()).scalar())
            console.log(f"busybox provides [green]{nprovides}[/green] applets")
Ejemplo n.º 2
0
    def run(self, manager: "pwncat.manager.Manager", args):
        if args.topic:
            for command in manager.parser.commands:
                if command.PROG == args.topic:
                    if command.parser is not None:
                        command.parser.print_help()
                    else:
                        console.print(textwrap.dedent(command.__doc__).strip())
                    break
        else:
            table = Table(
                Column("Command", style="green"),
                Column("Description", no_wrap=True),
                box=rich.box.SIMPLE,
            )

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

                table.add_row(command.PROG, doc)

            console.print(table)
Ejemplo n.º 3
0
    def run(self, manager: "pwncat.manager.Manager", args):

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

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

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

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

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

        console.print(table)
Ejemplo n.º 4
0
 def run(self, manager, args):
     if args.key is None:
         for key, binding in manager.config.bindings.items():
             console.print(f" [cyan]{key}[/cyan] = [yellow]{repr(binding)}[/yellow]")
     elif args.key is not None and args.script is None:
         if args.key in manager.config.bindings:
             del manager.config.bindings[args.key]
     else:
         manager.config.bindings[args.key] = args.script
Ejemplo n.º 5
0
    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)
Ejemplo n.º 6
0
 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:
                     console.print(textwrap.dedent(command.__doc__).strip())
                 break
     else:
         for command in pwncat.victim.command_parser.commands:
             console.print(f" - {command.PROG}")
Ejemplo n.º 7
0
 def run(self, manager, args):
     if args.alias is None:
         for name, command in manager.parser.aliases.items():
             console.print(
                 f" [cyan]{name}[/cyan] \u2192 [yellow]{command.PROG}[/yellow]"
             )
     elif args.command is not None:
         # This is safe because of "choices" in the argparser
         manager.parser.aliases[args.alias] = [
             c for c in manager.parser.commands if c.PROG == args.command
         ][0]
     else:
         del manager.parser.aliases[args.alias]
Ejemplo n.º 8
0
    def run(self, manager: "pwncat.manager.Manager", args):

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

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

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

            console.print(table)

            return

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

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

        session = manager.sessions[args.session_id]

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

        manager.target = session
        console.log(f"targeting session-{args.session_id} ({session.platform.channel})")
Ejemplo n.º 9
0
    def run(self, args):
        if args.password:
            if args.variable is None:
                found = False
                for name, user in pwncat.victim.users.items():
                    if user.password is not None:
                        console.print(
                            f" - [green]{user}[/green] -> [red]{repr(user.password)}[/red]"
                        )
                        found = True
                if not found:
                    console.log(
                        "[yellow]warning[/yellow]: no known user passwords")
            else:
                if args.variable not in pwncat.victim.users:
                    self.parser.error(f"{args.variable}: no such user")
                console.print(
                    f" - [green]{args.variable}[/green] -> [red]{repr(args.value)}[/red]"
                )
                pwncat.victim.users[args.variable].password = args.value
        else:
            if (args.variable is not None and args.variable == "state"
                    and args.value is not None):
                try:
                    pwncat.victim.state = State._member_map_[
                        args.value.upper()]
                except KeyError:
                    console.log(
                        f"[red]error[/red]: {args.value}: invalid state")
            elif args.variable is not None and args.value is not None:
                try:
                    pwncat.victim.config[args.variable] = args.value
                    if args.variable == "db":
                        # We handle this specially to ensure the database is available
                        # as soon as this config is set
                        pwncat.victim.engine = create_engine(
                            pwncat.victim.config["db"], echo=False)
                        pwncat.db.Base.metadata.create_all(
                            pwncat.victim.engine)

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

        if args.action == "revert":
            if args.all:
                tampers = list(pwncat.victim.tamper)
            else:
                try:
                    tampers = [pwncat.victim.tamper[args.tamper]]
                except KeyError:
                    console.log("[red]error[/red]: invalid tamper id")
                    return
            self.revert(tampers)
        else:
            for ident, tamper in enumerate(pwncat.victim.tamper):
                console.print(f" [cyan]{ident}[/cyan] - {tamper}")
Ejemplo n.º 11
0
 def run(self, manager, args):
     if args.password and manager.target is None:
         manager.log(
             "[red]error[/red]: active target is required for user interaction"
         )
         return
     elif args.password:
         if args.variable is None:
             found = False
             for user in manager.target.run("enumerate", types=["user"]):
                 if user.password is not None:
                     console.print(
                         f" - [green]{user.name}[/green] -> [red]{repr(user.password)}[/red]"
                     )
                     found = True
             if not found:
                 console.log(
                     "[yellow]warning[/yellow]: no known user passwords")
         else:
             user = manager.target.find_user(name=args.variable)
             if user is None:
                 manager.target.log(
                     "[red]error[/red]: {args.variable}: user not found")
                 return
             console.print(
                 f" - [green]{args.variable}[/green] -> [red]{repr(args.value)}[/red]"
             )
             user.password = args.value
             manager.target.db.transaction_manager.commit()
     else:
         if args.variable is not None and args.value is not None:
             try:
                 if manager.sessions and args.variable == "db":
                     raise ValueError(
                         "cannot change database with running session")
                 manager.config.set(args.variable, args.value,
                                    getattr(args, "global"))
                 if args.variable == "db":
                     # Ensure the database is re-opened, if it was already
                     manager.open_database()
             except ValueError as exc:
                 console.log(f"[red]error[/red]: {exc}")
         elif args.variable is not None:
             value = manager.config[args.variable]
             console.print(
                 f" [cyan]{args.variable}[/cyan] = [yellow]{repr(value)}[/yellow]"
             )
         else:
             for name in manager.config:
                 value = manager.config[name]
                 console.print(
                     f" [cyan]{name}[/cyan] = [yellow]{repr(value)}[/yellow]"
                 )
Ejemplo n.º 12
0
    def list_abilities(self, manager, args):
        """This is just a wrapper for `run enumerate types=escalate.*`, but
        it makes the workflow for escalation more apparent."""

        found = False

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

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

        if not found and args.user:
            console.log(
                f"[yellow]warning[/yellow]: no direct escalations for {args.user.name}"
            )
        elif not found:
            console.log("[yellow]warning[/yellow]: no direct escalations found")
Ejemplo n.º 13
0
    def show_facts(self, typ: str, provider: str, long: bool):
        """ Display known facts matching the criteria """

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

        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(textwrap.indent(fact.data.description, "    "))
Ejemplo n.º 14
0
    def run(self, args):

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

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

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

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

        console.print(table)
Ejemplo n.º 15
0
    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)
Ejemplo n.º 16
0
    def run(self, manager: "pwncat.manager.Manager", args):

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

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

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

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

        console.print(table)
Ejemplo n.º 17
0
def main():

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

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

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

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

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

            return

        if args.list:

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

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

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

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

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

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

            return

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        manager.interactive()

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

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

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

                progress.update(task, status="done!", completed=100)
Ejemplo n.º 18
0
    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]")
Ejemplo n.º 19
0
    def run(self, args):

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

                # Read the data from the pipe
                shutil.copyfileobj(read_pipe, sys.stdout.buffer)
                read_pipe.close()

                # Unwrap in case we had to privesc to get here
                pwncat.victim.privesc.unwrap(chain)

            except privesc.PrivescError as exc:
                console.log(f"file write [red]failed[/red]")
        elif args.action == "write":
            # Make sure the correct arguments are present
            if not args.path:
                self.parser.error("missing required argument: --path")
            if not args.data:
                self.parser.error("missing required argument: --data")

            # Read in the data file
            try:
                with open(args.data, "rb") as f:
                    data = f.read()
            except (PermissionError, FileNotFoundError):
                console.log(f"{args.data}: no such file or directory")

            try:
                # Attempt to write the data to the remote file
                chain = pwncat.victim.privesc.write_file(
                    args.path, data, target_user=args.user, depth=args.max_depth,
                )
                pwncat.victim.privesc.unwrap(chain)
                console.log("file write [green]succeeded[/green]")
            except privesc.PrivescError as exc:
                console.log(f"file write [red]failed[/red]: {exc}")
        elif args.action == "escalate":
            try:
                chain = pwncat.victim.privesc.escalate(
                    args.user, depth=args.max_depth, exclude=args.exclude
                )

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

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

                pwncat.victim.reset()
                pwncat.victim.state = State.RAW
            except privesc.PrivescError as exc:
                console.log(f"privilege escalation [red]failed[/red]: {exc}")
Ejemplo n.º 20
0
    def run(self, manager: "pwncat.manager.Manager", args):

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

        if args.list:

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

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

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

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

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

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

            return

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        if used_implant is not None:
            manager.target.log(f"connected via {used_implant.title(manager.target)}")
        else:
            try:
                manager.create_session(
                    platform=args.platform,
                    protocol=protocol,
                    user=user,
                    password=password,
                    host=host,
                    port=port,
                    identity=args.identity,
                )
            except (ChannelError, PlatformError) as exc:
                manager.log(f"connection failed: {exc}")
Ejemplo n.º 21
0
    def run(self, session, remove, escalate):
        """Perform the requested action"""

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

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

        try:
            session._progress.stop()

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

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

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

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

        finally:
            session._progress.start()

        nremoved = 0

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

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

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

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

        # Save database modifications
        session.db.transaction_manager.commit()
Ejemplo n.º 22
0
    def display_item(self, manager, title, results):
        """Display a possibly complex item"""

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

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

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

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

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

        # Show long-form results in their own sections
        if longform:
            for result in longform:
                if result.category(manager.target) is None:
                    console.print(
                        f"[bold]{result.title(manager.target)}[/bold]")
                else:
                    console.print(
                        f"[bold]{result.category(manager.target)} - {result.title(manager.target)}[/bold]"
                    )
                console.print(
                    textwrap.indent(result.description(manager.target), "  "))
Ejemplo n.º 23
0
    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")
Ejemplo n.º 24
0
    def print(self, *args, **kwargs):

        if self.target is not None and self.target._progress is not None:
            self.target._progress.print(*args, **kwargs)
        else:
            console.print(*args, **kwargs)
Ejemplo n.º 25
0
    def run(self, args):

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

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

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

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

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

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

            return

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            if port is None:
                port = 22

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

            if not (password or args.identity):
                password = prompt("Password: "******"[red]error[/red]: {str(exc)}")
                return

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

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

                # Attempt authentication
                try:
                    t.auth_publickey(user, key)
                except paramiko.ssh_exception.AuthenticationException as exc:
                    console.log(
                        f"[red]error[/red]: authentication failed: {exc}")
            else:
                try:
                    t.auth_password(user, password)
                except paramiko.ssh_exception.AuthenticationException as exc:
                    console.log(
                        f"[red]error[/red]: authentication failed: {exc}")

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

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

            # Initialize the session!
            pwncat.victim.connect(chan)

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

        else:
            console.log(f"[red]error[/red]: {args.action}: invalid action")
Ejemplo n.º 26
0
    def run(self, session: "pwncat.manager.Session", output, template, fmt,
            custom):
        """Perform enumeration and optionally write report"""

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

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

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

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

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

        try:
            if output != "terminal":
                with open(output, "w") as filp:
                    template.stream(context).dump(filp)
            else:
                markdown = Markdown(template.render(context), hyperlinks=False)
                console.print(markdown)
        except jinja2.TemplateError as exc:
            raise ModuleFailed(str(exc)) from exc