diff --git a/pwncat/modules/linux/enumerate/software/cron.py b/pwncat/modules/linux/enumerate/software/cron.py index a0d6536..bd0001b 100644 --- a/pwncat/modules/linux/enumerate/software/cron.py +++ b/pwncat/modules/linux/enumerate/software/cron.py @@ -3,23 +3,27 @@ import dataclasses import os import re +import rich.markup + import pwncat +from pwncat.db import Fact from pwncat.platform.linux import Linux from pwncat.modules import Status from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule -@dataclasses.dataclass -class CronEntry: +class CronEntry(Fact): + def __init__(self, source, path, uid, command, datetime): + super().__init__(source=source, types=["software.cron.entry"]) - path: str - """ The path to the crontab where this was found """ - uid: int - """ The user ID this entry will run as """ - command: str - """ The command that will execute """ - datetime: str - """ The entire date/time specifier from the crontab entry """ + self.path: str = path + """ The path to the crontab where this was found """ + self.uid: int = uid + """ The user ID this entry will run as """ + self.command: str = command + """ The command that will execute """ + self.datetime: str = datetime + """ The entire date/time specifier from the crontab entry """ def __str__(self): return ( diff --git a/pwncat/modules/linux/enumerate/software/screen.py b/pwncat/modules/linux/enumerate/software/screen.py index 49c432f..e13f448 100644 --- a/pwncat/modules/linux/enumerate/software/screen.py +++ b/pwncat/modules/linux/enumerate/software/screen.py @@ -4,20 +4,31 @@ import os import re import shlex +import rich.markup + import pwncat -from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule +from pwncat.db import Fact from pwncat.platform.linux import Linux +from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule +from pwncat.subprocess import CalledProcessError -@dataclasses.dataclass -class ScreenVersion: +""" +TODO: This should realistically yield an ability (which can be used for +privilege escalation)... but we can implement that later. +""" - path: str - perms: int - vulnerable: bool = True + +class ScreenVersion(Fact): + def __init__(self, source, path, perms, vulnerable): + super().__init__(source=source, types=["software.screen.version"]) + + self.path: str = path + self.perms: int = perms + self.vulnerable: bool = vulnerable def __str__(self): - return f"[cyan]{self.path}[/cyan] (perms: [blue]{oct(self.perms)[2:]}[/blue])" + return f"[cyan]{rich.markup.escape(self.path)}[/cyan] (perms: [blue]{oct(self.perms)[2:]}[/blue]) [bold red]is vulnerable[/bold red]" class Module(EnumerateModule): @@ -31,14 +42,14 @@ class Module(EnumerateModule): PLATFORM = [Linux] SCHEDULE = Schedule.ONCE - def enumerate(self): + def enumerate(self, session): """ - Enumerate kernel/OS version information + Enumerate locations of vulnerable screen versions :return: """ # Grab current path plus other interesting paths - paths = set(pwncat.victim.getenv("PATH").split(":")) + paths = set(session.platform.getenv("PATH").split(":")) paths = paths | { "/bin", "/sbin", @@ -49,17 +60,56 @@ class Module(EnumerateModule): } # Look for matching binaries - with pwncat.victim.subprocess( - f"find {shlex.join(paths)} \\( -type f -or -type l \\) -executable \\( -name 'screen' -or -name 'screen-*' \\) -printf '%#m %p\\n' 2>/dev/null" - ) as pipe: - for line in pipe: - line = line.decode("utf-8").strip() - perms, *path = line.split(" ") - path = " ".join(path) - perms = int(perms, 8) + proc = session.platform.Popen( + f"find {shlex.join(paths)} \\( -type f -or -type l \\) -executable \\( -name 'screen' -or -name 'screen-*' \\) -printf '%#m %p\\n' 2>/dev/null", + shell=True, + text=True, + stdout=pwncat.subprocess.PIPE, + ) - # When the screen source code is on disk and marked as executable, this happens... - if os.path.splitext(path)[1] in [".c", ".o", ".h"]: + # First, collect all the paths to a `screen` binary we can find + screen_paths = [] + for line in proc.stdout: + line = line.strip() + perms, *path = line.split(" ") + path = " ".join(path) + perms = int(perms, 8) + + # When the screen source code is on disk and marked as executable, this happens... + if os.path.splitext(path)[1] in [".c", ".o", ".h"]: + continue + + if perms & 0o4000: + # if this is executable + screen_paths.append(path) + + # Now, check each screen version to determine if it is vulnerable + for screen_path in screen_paths: + version_output = session.platform.Popen( + f"{screen_path} --version", + shell=True, + text=True, + stdout=pwncat.subprocess.PIPE, + ) + for line in version_output.stdout: + # This process checks if it is a vulnerable version of screen + match = re.search(r"(\d+\.\d+\.\d+)", line) + if not match: continue - yield "software.screen.version", ScreenVersion(path, perms) + version_triplet = [int(x) for x in match.group().split(".")] + + if version_triplet[0] > 4: + continue + + if version_triplet[0] == 4 and version_triplet[1] > 5: + continue + + if ( + version_triplet[0] == 4 + and version_triplet[1] == 5 + and version_triplet[2] >= 1 + ): + continue + + yield ScreenVersion(self.name, path, perms, vulnerable=True) diff --git a/pwncat/modules/linux/enumerate/software/sudo/rules.py b/pwncat/modules/linux/enumerate/software/sudo/rules.py index 1297fbc..6d663fd 100644 --- a/pwncat/modules/linux/enumerate/software/sudo/rules.py +++ b/pwncat/modules/linux/enumerate/software/sudo/rules.py @@ -3,9 +3,12 @@ import dataclasses import re from typing import Generator, Optional, List +import rich.markup + import pwncat -from pwncat.platform.linux import Linux from pwncat import util +from pwncat.db import Fact +from pwncat.platform.linux import Linux from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule per_user = True @@ -19,30 +22,44 @@ sudo_pattern = re.compile( directives = ["Defaults", "User_Alias", "Runas_Alias", "Host_Alias", "Cmnd_Alias"] -@dataclasses.dataclass -class SudoSpec: +class SudoSpec(Fact): + def __init__( + self, + source, + line: str, + matched: bool = False, + user: Optional[str] = None, + group: Optional[str] = None, + host: Optional[str] = None, + runas_user: Optional[str] = None, + runas_group: Optional[str] = None, + options: List[str] = None, + hash: str = None, + commands: List[str] = None, + ): + super().__init__(source=source, types=["software.sudo.rule"]) - line: str - """ The full, unaltered line from the sudoers file """ - matched: bool = False - """ The regular expression match data. If this is None, all following fields - are invalid and should not be used. """ - user: Optional[str] = None - """ The user which this rule applies to. This is None if a group was specified """ - group: Optional[str] = None - """ The group this rule applies to. This is None if a user was specified. """ - host: Optional[str] = None - """ The host this rule applies to """ - runas_user: Optional[str] = None - """ The user we are allowed to run as """ - runas_group: Optional[str] = None - """ The GID we are allowed to run as (may be None)""" - options: List[str] = None - """ A list of options specified (e.g. NOPASSWD, SETENV, etc) """ - hash: str = None - """ A hash type and value which sudo will obey """ - commands: List[str] = None - """ The command specification """ + self.line: str + """ The full, unaltered line from the sudoers file """ + self.matched: bool = False + """ The regular expression match data. If this is None, all following fields + are invalid and should not be used. """ + self.user: Optional[str] = None + """ The user which this rule applies to. This is None if a group was specified """ + self.group: Optional[str] = None + """ The group this rule applies to. This is None if a user was specified. """ + self.host: Optional[str] = None + """ The host this rule applies to """ + self.runas_user: Optional[str] = None + """ The user we are allowed to run as """ + self.runas_group: Optional[str] = None + """ The GID we are allowed to run as (may be None)""" + self.options: List[str] = None + """ A list of options specified (e.g. NOPASSWD, SETENV, etc) """ + self.hash: str = None + """ A hash type and value which sudo will obey """ + self.commands: List[str] = None + """ The command specification """ def __str__(self): display = "" @@ -51,28 +68,32 @@ class SudoSpec: return self.line if self.user is not None: - display += f"User [blue]{self.user}[/blue]: " + display += f"User [blue]{rich.markup.escape(self.user)}[/blue]: " else: - display += f"Group [cyan]{self.group}[/cyan]: " + display += f"Group [cyan]{rich.markup.escape(self.group)}[/cyan]: " - display += f"[yellow]{'[/yellow], [yellow]'.join(self.commands)}[/yellow] as " + display += f"[yellow]{'[/yellow], [yellow]'.join((rich.markup.escape(x) for c in self.commands))}[/yellow] as " if self.runas_user == "root": display += f"[red]root[/red]" elif self.runas_user is not None: - display += f"[blue]{self.runas_user}[/blue]" + display += f"[blue]{rich.markup.escape(self.runas_user)}[/blue]" if self.runas_group == "root": display += f":[red]root[/red]" elif self.runas_group is not None: - display += f"[cyan]{self.runas_group}[/cyan]" + display += f"[cyan]{rich.markup.escape(self.runas_group)}[/cyan]" if self.host is not None: - display += f" on [magenta]{self.host}[/magenta]" + display += f" on [magenta]{rich.markup.escape(self.host)}[/magenta]" if self.options: display += ( - " (" + ",".join(f"[green]{x}[/green]" for x in self.options) + ")" + " (" + + ",".join( + f"[green]{rich.markup.escape(x)}[/green]" for x in self.options + ) + + ")" ) return display @@ -151,17 +172,17 @@ class Module(EnumerateModule): PLATFORM = [Linux] SCHEDULE = Schedule.PER_USER - def enumerate(self): + def enumerate(self, session): try: - with pwncat.victim.open("/etc/sudoers", "r") as filp: + with session.platform.open("/etc/sudoers", "r") as filp: for line in filp: line = line.strip() # Ignore comments and empty lines if line.startswith("#") or line == "": continue - yield "sudo", LineParser(line) + yield LineParser(line) # No need to parse `sudo -l`, since can read /etc/sudoers return @@ -170,10 +191,13 @@ class Module(EnumerateModule): # Check for our privileges try: - result = pwncat.victim.sudo("-nl", send_password=False).decode("utf-8") - if result.strip() == "sudo: a password is required": - result = pwncat.victim.sudo("-l").decode("utf-8") + + proc = session.platform.sudo(["sudo", "-nl"], as_is=True) + result = proc.stdout.read() + proc.wait() # ensure this closes properly + except PermissionError: + # if this asks for a password and we don't have one, bail return for line in result.split("\n"): @@ -191,6 +215,6 @@ class Module(EnumerateModule): continue # Build the beginning part of a normal spec - line = f"{pwncat.victim.current_user.name} local=" + line.strip() + line = f"{session.current_user()} local=" + line.strip() - yield "software.sudo.rule", LineParser(line) + yield LineParser(line) diff --git a/pwncat/modules/linux/enumerate/software/sudo/version.py b/pwncat/modules/linux/enumerate/software/sudo/version.py index 99dc19b..107079b 100644 --- a/pwncat/modules/linux/enumerate/software/sudo/version.py +++ b/pwncat/modules/linux/enumerate/software/sudo/version.py @@ -2,24 +2,30 @@ import dataclasses import re -from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule +import rich.markup + import pwncat +from pwncat.db import Fact from pwncat.platform.linux import Linux +from pwncat.subprocess import CalledProcessError +from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule -@dataclasses.dataclass -class SudoVersion: +class SudoVersion(Fact): """ Version of the installed sudo binary may be useful for exploitation """ - version: str - output: str - vulnerable: bool + def __init__(self, source, version, output, vulnerable): + super().__init__(source=source, types=["software.sudo.version"]) + + self.version: str = version + self.output: str = output + self.vulnerable: bool = vulnerable def __str__(self): - result = f"[yellow]sudo[/yellow] version [cyan]{self.version}[/cyan]" + result = f"[yellow]sudo[/yellow] version [cyan]{rich.markup.escape(self.version)}[/cyan]" if self.vulnerable: result += f" (may be [red]vulnerable[/red])" return result @@ -44,18 +50,23 @@ class Module(EnumerateModule): PLATFORM = [Linux] SCHEDULE = Schedule.ONCE - def enumerate(self): + def enumerate(self, session): """ - Enumerate kernel/OS version information + Enumerate the currently running version of sudo :return: """ try: # Check the sudo version number - result = pwncat.victim.env(["sudo", "--version"]).decode("utf-8").strip() - except FileNotFoundError: + result = session.platform.run( + ["sudo", "--version"], capture_output=True, check=True + ) + except CalledProcessError: + # Something went wrong with the sudo version return + version = result.stdout.decode("utf-8") + # Taken from here: # https://book.hacktricks.xyz/linux-unix/privilege-escalation#sudo-version known_vulnerable = [ @@ -76,7 +87,7 @@ class Module(EnumerateModule): # Can we match this output to a specific sudo version? match = re.search( - r"sudo version ([0-9]+\.[0-9]+\.[^\s]*)", result, re.IGNORECASE + r"sudo version ([0-9]+\.[0-9]+\.[^\s]*)", version, re.IGNORECASE ) if match is not None and match.group(1) is not None: vulnerable = False @@ -87,9 +98,9 @@ class Module(EnumerateModule): vulnerable = True break - yield "sudo.version", SudoVersion(match.group(1), result, vulnerable) + yield SudoVersion(self.name, match.group(1), version, vulnerable) return # We couldn't parse the version out, but at least give the full version # output in the long form/report of enumeration. - yield "software.sudo.version", SudoVersion("unknown", result, False) + yield SudoVersion(self.name, "unknown", version, False) diff --git a/pwncat/modules/linux/enumerate/user/group.py b/pwncat/modules/linux/enumerate/user/group.py index b12457d..6edcfe9 100644 --- a/pwncat/modules/linux/enumerate/user/group.py +++ b/pwncat/modules/linux/enumerate/user/group.py @@ -35,7 +35,6 @@ class Module(EnumerateModule): yield group except Exception as exc: - raise ModuleFailed(f"something fucked {exc}") # Bad group line continue diff --git a/pwncat/platform/linux.py b/pwncat/platform/linux.py index c58ac10..36e3d77 100644 --- a/pwncat/platform/linux.py +++ b/pwncat/platform/linux.py @@ -1217,7 +1217,7 @@ class Linux(Platform): popen_kwargs["env"] = None if password is None: - password = self.current_user.password + password = self.session.current_user().password # At this point, the command is a string if not as_is: