From 691503a27073e1dcb2ed892d68688de9eceb9b1d Mon Sep 17 00:00:00 2001 From: Caleb Stewart Date: Sat, 22 May 2021 14:46:07 -0400 Subject: [PATCH] Fixed enumeration modules Some modules weren't cleaning up their Popen objects. All modules at least execute now. Their results need to be fact-checked, though. --- pwncat/commands/escalate.py | 5 ++ pwncat/commands/leave.py | 2 + pwncat/facts/__init__.py | 3 +- pwncat/modules/agnostic/enumerate/gather.py | 2 +- pwncat/modules/agnostic/implant.py | 8 +-- pwncat/modules/linux/enumerate/creds/pam.py | 59 ++++++++----------- .../linux/enumerate/creds/private_key.py | 8 ++- pwncat/modules/linux/enumerate/file/caps.py | 5 +- .../linux/enumerate/software/screen.py | 9 ++- .../linux/enumerate/software/sudo/rules.py | 57 ++++++++++-------- pwncat/platform/linux.py | 23 ++++++-- 11 files changed, 104 insertions(+), 77 deletions(-) diff --git a/pwncat/commands/escalate.py b/pwncat/commands/escalate.py index e6fbfb2..495cc7e 100644 --- a/pwncat/commands/escalate.py +++ b/pwncat/commands/escalate.py @@ -38,6 +38,7 @@ class Link: if self.escalation.type == "escalate.replace": # Exit out of the subshell self.old_session.layers.pop()(self.old_session) + self.old_session.platform.refresh_uid() def __str__(self): return self.escalation.title(self.old_session) @@ -155,6 +156,7 @@ class Command(CommandDefinition): try: # This direction failed. Go back up and try again. chain.pop().leave() + continue except IndexError: manager.target.log( @@ -170,6 +172,8 @@ class Command(CommandDefinition): ) result = escalation.escalate(manager.target) + manager.target.platform.refresh_uid() + # Construct the escalation link link = Link(manager.target, escalation, result) @@ -205,6 +209,7 @@ class Command(CommandDefinition): task, status=f"attempting {escalation.title(manager.target)}" ) result = escalation.escalate(manager.target) + manager.target.platform.refresh_uid() link = Link(manager.target, escalation, result) if escalation.type == "escalate.replace": diff --git a/pwncat/commands/leave.py b/pwncat/commands/leave.py index f675b29..1b57b0c 100644 --- a/pwncat/commands/leave.py +++ b/pwncat/commands/leave.py @@ -33,5 +33,7 @@ class Command(CommandDefinition): for i in range(args.count): manager.target.layers.pop()(manager.target) + + manager.target.platform.refresh_uid() except IndexError: manager.target.log("[yellow]warning[/yellow]: no more layers to leave") diff --git a/pwncat/facts/__init__.py b/pwncat/facts/__init__.py index 25c7755..7ba1e34 100644 --- a/pwncat/facts/__init__.py +++ b/pwncat/facts/__init__.py @@ -105,8 +105,7 @@ class PrivateKey(Fact): color = "green" return f"Potential private key for [{color}]{self.uid}[/{color}] at [cyan]{rich.markup.escape(self.path)}[/cyan]" - @property - def description(self) -> str: + def description(self, session) -> str: return self.content diff --git a/pwncat/modules/agnostic/enumerate/gather.py b/pwncat/modules/agnostic/enumerate/gather.py index 2ebef69..7264e4f 100644 --- a/pwncat/modules/agnostic/enumerate/gather.py +++ b/pwncat/modules/agnostic/enumerate/gather.py @@ -89,7 +89,7 @@ class Module(pwncat.modules.BaseModule): if clear: for module in modules: yield pwncat.modules.Status(module.name) - module.run(clear=True) + module.run(session, clear=True) return # Enumerate all facts diff --git a/pwncat/modules/agnostic/implant.py b/pwncat/modules/agnostic/implant.py index 6e54b94..702546b 100644 --- a/pwncat/modules/agnostic/implant.py +++ b/pwncat/modules/agnostic/implant.py @@ -1,10 +1,9 @@ #!/usr/bin/env python3 -from rich.prompt import Prompt - -from pwncat.modules import BaseModule, Argument, Status, Bool, ModuleFailed -from pwncat.facts import Implant from pwncat.util import console +from rich.prompt import Prompt +from pwncat.facts import Implant +from pwncat.modules import Bool, Status, Argument, BaseModule, ModuleFailed class Module(BaseModule): @@ -92,6 +91,7 @@ class Module(BaseModule): else: # Track the new shell layer in the current session session.layers.append(result) + session.platform.refresh_uid() session.log( f"escalation [green]succeeded[/green] with: {implant.title(session)}" diff --git a/pwncat/modules/linux/enumerate/creds/pam.py b/pwncat/modules/linux/enumerate/creds/pam.py index 12d9384..c5932ec 100644 --- a/pwncat/modules/linux/enumerate/creds/pam.py +++ b/pwncat/modules/linux/enumerate/creds/pam.py @@ -28,43 +28,36 @@ class Module(EnumerateModule): def enumerate(self, session): - pam: InstalledModule = None - # Check if we previously had PAM persistence... this isn't re-implemented yet - # for module in session.run( - # "persist.gather", progress=self.progress, module="persist.pam_backdoor" - # ): - # pam = module - # break + # Ensure the user database is already retrieved + session.find_user(uid=0) - if pam is None: - # The pam persistence module isn't installed. - return + for implant in session.run("enumerate", types=["implant.*"]): + if implant.source != "linux.implant.pam": + continue - # Grab the log path - log_path = pam.persist.args["log"] - # Just in case we have multiple of the same password logged - observed = [] + # Just in case we have multiple of the same password logged + observed = [] - try: - with session.platform.open(log_path, "r") as filp: - for line in filp: - line = line.rstrip("\n") - if line in observed: - continue + try: + with session.platform.open(implant.log, "r") as filp: + for lineno, line in enumerate(filp): + line = line.rstrip("\n") + if line in observed: + continue - user, *password = line.split(":") - password = ":".join(password) + user, *password = line.split(":") + password = ":".join(password) - try: - # Check for valid user name - session.platform.find_user(name=user) - except KeyError: - continue + try: + # Check for valid user name + user_info = session.find_user(name=user) + except KeyError: + continue - observed.append(line) + observed.append(line) - yield "creds.password", PotentialPassword( - password, log_path, None, uid=pwncat.victim.users[user].id - ) - except (FileNotFoundError, PermissionError): - pass + yield PotentialPassword( + self.name, password, implant.log, lineno, user_info.id + ) + except (FileNotFoundError, PermissionError): + pass diff --git a/pwncat/modules/linux/enumerate/creds/private_key.py b/pwncat/modules/linux/enumerate/creds/private_key.py index 2181339..b839a36 100644 --- a/pwncat/modules/linux/enumerate/creds/private_key.py +++ b/pwncat/modules/linux/enumerate/creds/private_key.py @@ -1,12 +1,11 @@ #!/usr/bin/env python3 import time -import rich.markup -from Crypto.PublicKey import RSA - import pwncat +import rich.markup from pwncat.facts import PrivateKey from pwncat.modules import Status +from Crypto.PublicKey import RSA from pwncat.platform.linux import Linux from pwncat.modules.enumerate import Schedule, EnumerateModule @@ -48,6 +47,9 @@ class Module(EnumerateModule): yield Status(f"found [cyan]{rich.markup.escape(path)}[/cyan]") facts.append(PrivateKey(self.name, path, uid, None, False)) + # Ensure proc is cleaned up + proc.wait() + for fact in facts: try: yield Status(f"reading [cyan]{rich.markup.escape(fact.path)}[/cyan]") diff --git a/pwncat/modules/linux/enumerate/file/caps.py b/pwncat/modules/linux/enumerate/file/caps.py index 915acf4..849cfea 100644 --- a/pwncat/modules/linux/enumerate/file/caps.py +++ b/pwncat/modules/linux/enumerate/file/caps.py @@ -2,9 +2,8 @@ import dataclasses from typing import List -import rich.markup - import pwncat +import rich.markup from pwncat import util from pwncat.db import Fact from pwncat.platform.linux import Linux @@ -67,3 +66,5 @@ class Module(EnumerateModule): fact = FileCapabilityData(self.name, path, caps) yield fact + + proc.wait() diff --git a/pwncat/modules/linux/enumerate/software/screen.py b/pwncat/modules/linux/enumerate/software/screen.py index e6f322f..117eeba 100644 --- a/pwncat/modules/linux/enumerate/software/screen.py +++ b/pwncat/modules/linux/enumerate/software/screen.py @@ -4,9 +4,8 @@ import re import shlex import dataclasses -import rich.markup - import pwncat +import rich.markup from pwncat.db import Fact from pwncat.subprocess import CalledProcessError from pwncat.platform.linux import Linux @@ -82,6 +81,9 @@ class Module(EnumerateModule): # if this is executable screen_paths.append(path) + # Clean up the search + proc.wait() + # Now, check each screen version to determine if it is vulnerable for screen_path in screen_paths: version_output = session.platform.Popen( @@ -112,3 +114,6 @@ class Module(EnumerateModule): continue yield ScreenVersion(self.name, path, perms, vulnerable=True) + + # Clean up process + version_output.wait() diff --git a/pwncat/modules/linux/enumerate/software/sudo/rules.py b/pwncat/modules/linux/enumerate/software/sudo/rules.py index b3f4d9d..3b1a468 100644 --- a/pwncat/modules/linux/enumerate/software/sudo/rules.py +++ b/pwncat/modules/linux/enumerate/software/sudo/rules.py @@ -195,6 +195,7 @@ class Module(EnumerateModule): try: etc_sudoers = session.platform.Path("/etc/sudoers") if etc_sudoers.readable(): + rules = [] with etc_sudoers.open() as filp: for line in filp: line = line.strip() @@ -207,37 +208,41 @@ class Module(EnumerateModule): # Yield the sudo rule yield rule - # We can't handle abilities which we didn't parse properly - if not rule.matched: - continue + rules.append(rule) - user_name = rule.runas_user - if user_name == "ALL": - user_name = "root" + for rule in rules: - # Grab the user by name so we can get the UID - runas_user = session.find_user(name=user_name) - if runas_user is None: - # Not a valid user? :/ - continue + # We can't handle abilities which we didn't parse properly + if not rule.matched: + continue - user = session.find_user(name=rule.user) - if user is None: - continue + user_name = rule.runas_user + if user_name == "ALL": + user_name = "root" - # Yield escalation abilities - for spec in rule.commands: - yield from ( - build_gtfo_ability( - self.name, - runas_user.id, - method, - spec=spec, - source_uid=user.id, - user=runas_user.name, - ) - for method in session.platform.gtfo.iter_sudo(spec) + # Grab the user by name so we can get the UID + runas_user = session.find_user(name=user_name) + if runas_user is None: + # Not a valid user? :/ + continue + + user = session.find_user(name=rule.user) + if user is None: + continue + + # Yield escalation abilities + for spec in rule.commands: + yield from ( + build_gtfo_ability( + self.name, + runas_user.id, + method, + spec=spec, + source_uid=user.id, + user=runas_user.name, ) + for method in session.platform.gtfo.iter_sudo(spec) + ) return except (FileNotFoundError, PermissionError): diff --git a/pwncat/platform/linux.py b/pwncat/platform/linux.py index c19393b..355d174 100644 --- a/pwncat/platform/linux.py +++ b/pwncat/platform/linux.py @@ -95,7 +95,7 @@ class PopenLinux(pwncat.subprocess.Popen): if self.stdin is not None: self.stdin.close() if self.stdout_raw is not None: - self.stdout_raw.close() + self.stdout_raw.close # Hope they know what they're doing... self.platform.command_running = None @@ -477,6 +477,8 @@ class Linux(Platform): self.name = "linux" self.command_running = None + self._uid = None + # This causes an stty to be sent. # If we aren't in a pty, it doesn't matter. # if we are, we need this stty to properly handle process IO @@ -521,6 +523,8 @@ class Linux(Platform): else: self.has_pty = False + self.refresh_uid() + def disable_history(self): """Disable shell history""" @@ -690,18 +694,29 @@ class Linux(Platform): except CalledProcessError: return None - def getuid(self): + def refresh_uid(self): """Retrieve the current user ID""" try: # NOTE: this is probably not great... but sometimes it fails when transitioning # states, and I can't pin down why. The second time normally succeeds, and I've # never observed it hanging for any significant amount of time. - proc = self.run(["id", "-ru"], capture_output=True, text=True, check=True) - return int(proc.stdout.rstrip("\n")) + while True: + try: + proc = self.run( + ["id", "-ru"], capture_output=True, text=True, check=True + ) + self._uid = int(proc.stdout.rstrip("\n")) + return self._uid + except ValueError: + continue except CalledProcessError as exc: raise PlatformError(str(exc)) from exc + def getuid(self): + """ Retrieve the current cached uid """ + return self._uid + def getenv(self, name: str): try: