diff --git a/pwncat/modules/linux/enumerate/system/services.py b/pwncat/modules/linux/enumerate/system/services.py index e5f21a1..94c0145 100644 --- a/pwncat/modules/linux/enumerate/system/services.py +++ b/pwncat/modules/linux/enumerate/system/services.py @@ -1,31 +1,34 @@ #!/usr/bin/env python3 -import dataclasses + +import rich.markup import pwncat +from pwncat.db import Fact from pwncat.util import Init from pwncat.platform.linux import Linux from pwncat.modules.enumerate import Schedule, EnumerateModule -@dataclasses.dataclass -class ServiceData: +class ServiceData(Fact): + def __init__(self, source, name, uid, state, pid): + super().__init__(source=source, types=["system.service"]) - 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 + self.name: str = name + """ The name of the service as given on the remote host """ + self.uid: int = uid + """ The user this service is running as """ + self.state: str = state + """ Whether the service is running """ + self.pid: int = pid - def __str__(self): + def title(self, session): if self.uid == 0: color = "red" else: color = "green" try: - user_name = pwncat.victim.find_user_by_id(self.uid).name + user_name = session.find_user(uid=self.uid).name except KeyError: user_name = f"{self.uid} (unknown user)" color = "yellow" @@ -42,18 +45,18 @@ class ServiceData: class Module(EnumerateModule): - """ Enumerate systemd services on the victim """ + """Enumerate systemd services on the victim""" PROVIDES = ["system.service"] PLATFORM = [Linux] SCHEDULE = Schedule.ONCE - def enumerate(self): + def enumerate(self, session): - for fact in pwncat.modules.run( + for fact in session.run( "enumerate.gather", types=["system.init"], progress=self.progress ): - if fact.data.init != Init.SYSTEMD: + if fact.init != Init.SYSTEMD: return break @@ -61,37 +64,28 @@ class Module(EnumerateModule): # 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 = session.platform.run( + "systemctl show --type=service --no-pager --all --value --property Id --property MainPID --property UID --property SubState \\*", + capture_output=True, + text=True, + check=True, ) - data = data.strip().decode("utf-8").split("\n") - for i in range(0, len(data), 5): - if i >= (len(data) - 4): - break - 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() + if data.stdout: + data = data.stdout.split("\n\n") - yield "system.service", ServiceData(name, uid, state, pid) + for segment in data: + section = segment.split("\n") + try: + pid = int(section[0]) + except ValueError as exc: + pwncat.console.log(repr(data), markup=False) + if section[1] == "[not set]": + uid = 0 + else: + uid = int(section[1]) + name = section[2].removesuffix(".service") + state = section[3] + + yield ServiceData(self.name, name, uid, state, pid) diff --git a/pwncat/modules/linux/enumerate/system/uname.py b/pwncat/modules/linux/enumerate/system/uname.py index bd7863b..817ec55 100644 --- a/pwncat/modules/linux/enumerate/system/uname.py +++ b/pwncat/modules/linux/enumerate/system/uname.py @@ -6,28 +6,45 @@ from typing import List, Optional import pkg_resources import pwncat +from pwncat.db import Fact from pwncat import util from pwncat.platform.linux import Linux from pwncat.modules.enumerate import Schedule, EnumerateModule -@dataclasses.dataclass -class ArchData: +class ArchData(Fact): """ Simply the architecture of the remote machine. This class wraps the architecture name in a nicely printable data class. """ - arch: str - """ The determined architecture. """ + def __init__(self, source, arch): + super().__init__(source=source, types=["system.arch"]) - def __str__(self): + self.arch: str = arch + """ The determined architecture. """ + + def title(self, session): return f"Running on a [cyan]{self.arch}[/cyan] processor" -@dataclasses.dataclass -class KernelVersionData: +class HostnameData(Fact): + """ + The hostname of this machine. + """ + + def __init__(self, source, hostname): + super().__init__(source=source, types=["system.hostname"]) + + self.hostname: str = hostname + """ The determined architecture. """ + + def title(self, session): + return f"Hostname [cyan]{self.hostname}[/cyan]" + + +class KernelVersionData(Fact): """ Represents a W.X.Y-Z kernel version where W is the major version, X is the minor version, Y is the patch, and Z is the ABI. @@ -36,12 +53,15 @@ class KernelVersionData: https://askubuntu.com/questions/843197/what-are-kernel-version-number-components-w-x-yy-zzz-called """ - major: int - minor: int - patch: int - abi: str + def __init__(self, source, major, minor, patch, abi): + super().__init__(source=source, types=["system.kernel.version"]) - def __str__(self): + self.major: int = major + self.minor: int = minor + self.patch: int = patch + self.abi: str = abi + + def title(self, session): return ( f"Running Linux Kernel [red]{self.major}[/red]." f"[green]{self.minor}[/green]." @@ -49,8 +69,7 @@ class KernelVersionData: ) -@dataclasses.dataclass -class KernelVulnerabilityData: +class KernelVulnerabilityData(Fact): """ Data describing a kernel vulnerability which appears to be exploitable on the remote host. This is **not** guaranteed to be exploitable, however @@ -59,21 +78,23 @@ class KernelVulnerabilityData: vulnerability. """ - name: str - versions: List[str] - link: Optional[str] - cve: Optional[str] - # All exploits are assumed working, but can be marked as not working - working: bool = True + def __init__(self, source, name, versions, link, cve): + super().__init__(source=source, types=["system.kernel.vuln"]) - def __str__(self): + self.name: str = name + self.versions: List[str] = versions + self.link: Optional[str] = link + self.cve: Optional[str] = cve + # All exploits are assumed working, but can be marked as not working + working: bool = True + + def title(self, title): line = f"[red]{self.name}[/red]" if self.cve is not None: line += f" ([cyan]CVE-{self.cve}[/cyan])" return line - @property - def description(self): + def description(self, title): line = f"Affected Versions: {repr(self.versions)}\n" if self.link: line += f"Details: {self.link}" @@ -102,12 +123,15 @@ class Module(EnumerateModule): PLATFORM = [Linux] SCHEDULE = Schedule.ONCE - def enumerate(self): - """ Run uname and organize information """ + def enumerate(self, session): + """Run uname and organize information""" # Grab the uname output - output = pwncat.victim.run("uname -s -n -r -m -o").decode("utf-8").strip() - fields = output.split(" ") + output = session.platform.run( + "uname -s -n -r -m -o", capture_output=True, text=True, check=True + ) + + fields = output.stdout.split(" ") # Grab the components # kernel_name = fields[0] if fields else None @@ -121,14 +145,14 @@ class Module(EnumerateModule): y_and_z = ".".join(y_and_z).split("-") y = y_and_z[0] z = "-".join(y_and_z[1:]) - version = KernelVersionData(int(w), int(x), int(y), z) - yield "system.kernel.version", version + version = KernelVersionData(self.name, int(w), int(x), int(y), z) + yield version # Handle arch - yield "system.arch", ArchData(machine_name) + yield ArchData(self.name, machine_name) # Handle Hostname - yield "system.hostname", hostname + yield HostnameData(self.name, hostname) # Handle Kernel vulnerabilities with open( @@ -140,6 +164,10 @@ class Module(EnumerateModule): for name, vuln in vulns.items(): if version_string not in vuln["vuln"]: continue - yield "system.kernel.vuln", KernelVulnerabilityData( - name, vuln["vuln"], vuln.get("mil", None), vuln.get("cve", None) + yield KernelVulnerabilityData( + self.name, + name, + vuln["vuln"], + vuln.get("mil", None), + vuln.get("cve", None), ) diff --git a/test.py b/test.py index dcf8ef9..18d0bbf 100755 --- a/test.py +++ b/test.py @@ -17,7 +17,17 @@ manager = pwncat.manager.Manager("data/pwncatrc") # Establish a session # session = manager.create_session("windows", host="192.168.56.10", port=4444) # session = manager.create_session("windows", host="192.168.122.11", port=4444) -session = manager.create_session("linux", host="127.0.0.1", port=4444) +session = manager.create_session("linux", host="127.0.0.1", port=9999) # session = manager.create_session("windows", host="0.0.0.0", port=4444) -print(session.current_user()) +for _ in range(30): + + data = session.platform.run( + "cat /tmp/dummy", + capture_output=True, + text=True, + check=True, + ) + + print(data.stdout.split("\n\n")[0]) + print("===================================================") diff --git a/test.zodb b/test.zodb new file mode 100644 index 0000000..379763d Binary files /dev/null and b/test.zodb differ diff --git a/test.zodb.index b/test.zodb.index new file mode 100644 index 0000000..6f3e499 Binary files /dev/null and b/test.zodb.index differ diff --git a/test.zodb.lock b/test.zodb.lock new file mode 100644 index 0000000..370befd --- /dev/null +++ b/test.zodb.lock @@ -0,0 +1 @@ + 2639 diff --git a/test.zodb.tmp b/test.zodb.tmp new file mode 100644 index 0000000..3001e46 Binary files /dev/null and b/test.zodb.tmp differ