From 6ac5afdee652318ab5eb8a64983243278bfdef37 Mon Sep 17 00:00:00 2001 From: Chocapikk Date: Sat, 5 Apr 2025 20:25:29 +0200 Subject: [PATCH 1/2] fix: add compatibility with Python 3.13 (fix distutils, find_module, crypt) - Replaced deprecated `find_module` usage in `CommandParser` and `Manager` by `importlib.util.module_from_spec` and `exec_module` (fixes AttributeError: 'FileFinder' object has no attribute 'find_module') - Replaced `crypt.crypt(...)` with `passlib.hash.sha512_crypt` for password hashing (fixes ModuleNotFoundError: No module named 'crypt') - Added `passlib` as a dependency in `pyproject.toml` under [tool.poetry.dependencies] (fixes ModuleNotFoundError: No module named 'passlib') --- pwncat/commands/__init__.py | 14 +++++++++----- pwncat/manager.py | 8 ++++++-- .../linux/enumerate/escalate/append_passwd.py | 7 ++++--- pwncat/modules/linux/implant/passwd.py | 5 +++-- pyproject.toml | 5 +++-- 5 files changed, 25 insertions(+), 14 deletions(-) diff --git a/pwncat/commands/__init__.py b/pwncat/commands/__init__.py index 17fc7cdc..85eee70d 100644 --- a/pwncat/commands/__init__.py +++ b/pwncat/commands/__init__.py @@ -43,6 +43,7 @@ def run(self, manager: "pwncat.manager.Manager", args: "argparse.Namespace"): import pkgutil import termios import argparse +import importlib.util from io import TextIOWrapper from enum import Enum, auto from typing import Dict, List, Type, Callable, Iterable @@ -432,11 +433,14 @@ def __init__(self, manager: "pwncat.manager.Manager"): for loader, module_name, is_pkg in pkgutil.walk_packages(__path__): if module_name == "base": continue - self.commands.append( - loader.find_module(module_name) - .load_module(module_name) - .Command(manager) - ) + + spec = importlib.util.find_spec(f"{__name__}.{module_name}") + if spec is None: + raise ImportError(f"Could not find spec for module {module_name}") + + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + self.commands.append(module.Command(manager)) self.prompt: PromptSession = None self.toolbar: PromptSession = None diff --git a/pwncat/manager.py b/pwncat/manager.py index 393cb012..374e9ce1 100644 --- a/pwncat/manager.py +++ b/pwncat/manager.py @@ -35,6 +35,7 @@ import tempfile import threading import contextlib +import importlib.util from io import TextIOWrapper from enum import Enum, auto from typing import Dict, List, Tuple, Union, Callable, Optional, Generator @@ -932,9 +933,12 @@ def load_modules(self, *paths): paths, prefix="pwncat.modules." ): - # Why is this check *not* part of pkgutil??????? D:< if module_name not in sys.modules: - module = loader.find_module(module_name).load_module(module_name) + spec = importlib.util.find_spec(module_name) + if spec is None: + continue + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) else: module = sys.modules[module_name] diff --git a/pwncat/modules/linux/enumerate/escalate/append_passwd.py b/pwncat/modules/linux/enumerate/escalate/append_passwd.py index 7443b812..18021dd2 100644 --- a/pwncat/modules/linux/enumerate/escalate/append_passwd.py +++ b/pwncat/modules/linux/enumerate/escalate/append_passwd.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 -import crypt + +from passlib.hash import sha512_crypt import pwncat from pwncat.util import console @@ -31,7 +32,7 @@ def escalate(self, session: "pwncat.manager.Session"): shell = session.platform.getenv("SHELL") # Hash the backdoor password - backdoor_hash = crypt.crypt(backdoor_pass, crypt.METHOD_SHA512) + backdoor_hash = sha512_crypt.using(salt_size=16).hash(backdoor_pass) if not any(line.startswith(f"{backdoor_user}:") for line in passwd_contents): @@ -85,4 +86,4 @@ def enumerate(self, session): if ability.uid != 0: continue - yield AppendPasswd(self.name, ability) + yield AppendPasswd(self.name, ability) \ No newline at end of file diff --git a/pwncat/modules/linux/implant/passwd.py b/pwncat/modules/linux/implant/passwd.py index 8935e9c2..57db6133 100644 --- a/pwncat/modules/linux/implant/passwd.py +++ b/pwncat/modules/linux/implant/passwd.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 -import crypt + +from passlib.hash import sha512_crypt import pwncat from pwncat.facts import Implant, ImplantType @@ -93,7 +94,7 @@ def install( # Hash the password yield Status("hashing password") - backdoor_hash = crypt.crypt(backdoor_pass, crypt.METHOD_SHA512) + backdoor_hash = sha512_crypt.using(salt_size=16).hash(backdoor_pass) # Store the new line we are adding new_line = f"""{backdoor_user}:{backdoor_hash}:0:0::/root:{shell}\n""" diff --git a/pyproject.toml b/pyproject.toml index dc9e5eaf..834238df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ addopts = "-v" [tool.poetry] name = "pwncat-cs" -version = "0.5.4" +version = "0.5.5" description = "Reverse and bind shell automation framework" authors = ["Caleb Stewart ", "John Hammond"] readme = "README.md" @@ -32,7 +32,8 @@ pwncat-cs = "pwncat.__main__:main" [tool.poetry.dependencies] python = "^3.9" netifaces = "^0.11.0" -packaging = "^20.9" +packaging = ">=23.0" +passlib = "^1.7.4" prompt-toolkit = "^3.0.19" pycryptodome = "^3.10.1" requests = "^2.25.1" From a6b4b5a5773f3a95e6098bd4a6b6229997a5c790 Mon Sep 17 00:00:00 2001 From: Chocapikk Date: Sat, 5 Apr 2025 22:09:08 +0200 Subject: [PATCH 2/2] fix(enumerate): fix CVE-2019-14287 detection and restore pickle compatibility Fixed CVE-2019-14287 detection logic. Also reverted a previous accidental change that broke pickle serialization, preventing persistent storage from working properly. --- pwncat/manager.py | 35 ++++++++++--------- pwncat/modules/linux/enumerate/file/suid.py | 2 +- .../enumerate/software/sudo/cve_2019_14287.py | 10 ++++-- pwncat/platform/__init__.py | 11 +++--- pwncat/platform/linux.py | 12 +++---- 5 files changed, 37 insertions(+), 33 deletions(-) diff --git a/pwncat/manager.py b/pwncat/manager.py index 374e9ce1..25ed3bc3 100644 --- a/pwncat/manager.py +++ b/pwncat/manager.py @@ -923,34 +923,37 @@ def create_db_session(self): return self.db.open() def load_modules(self, *paths): - """Dynamically load modules from the specified paths + """ + Dynamically load modules from the specified paths. If a module has the same name as an already loaded module, it will - take it's place in the module list. This includes built-in modules. + take its place in the module list. This includes built-in modules. """ - for loader, module_name, _ in pkgutil.walk_packages( - paths, prefix="pwncat.modules." - ): + for loader, module_name, is_pkg in pkgutil.walk_packages(paths, prefix="pwncat.modules."): + # Locate the module spec + spec = importlib.util.find_spec(module_name) + if spec is None: + continue - if module_name not in sys.modules: - spec = importlib.util.find_spec(module_name) - if spec is None: - continue + # Always check `sys.modules` under `spec.name`. + # If it's already loaded, reuse that module; if not, load anew. + if spec.name in sys.modules: + module = sys.modules[spec.name] + else: module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module spec.loader.exec_module(module) - else: - module = sys.modules[module_name] - if getattr(module, "Module", None) is None: + # We only care about modules that actually define `Module` + if not hasattr(module, "Module"): continue # Create an instance of this module - module_name = module_name.split("pwncat.modules.")[1] - self.modules[module_name] = module.Module() + short_name = module_name.split("pwncat.modules.", 1)[1] + self.modules[short_name] = module.Module() + self.modules[short_name].name = short_name - # Store it's name so we know it later - setattr(self.modules[module_name], "name", module_name) def log(self, *args, **kwargs): """Output a log entry""" diff --git a/pwncat/modules/linux/enumerate/file/suid.py b/pwncat/modules/linux/enumerate/file/suid.py index 3b3d4ac5..d3538dde 100644 --- a/pwncat/modules/linux/enumerate/file/suid.py +++ b/pwncat/modules/linux/enumerate/file/suid.py @@ -76,4 +76,4 @@ def enumerate(self, session: "pwncat.manager.Session"): ) ) finally: - proc.wait() + proc.wait() \ No newline at end of file diff --git a/pwncat/modules/linux/enumerate/software/sudo/cve_2019_14287.py b/pwncat/modules/linux/enumerate/software/sudo/cve_2019_14287.py index 700e227c..30651e94 100644 --- a/pwncat/modules/linux/enumerate/software/sudo/cve_2019_14287.py +++ b/pwncat/modules/linux/enumerate/software/sudo/cve_2019_14287.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -from packaging import version +from packaging.version import parse, InvalidVersion import pwncat from pwncat.facts import build_gtfo_ability @@ -26,7 +26,11 @@ def enumerate(self, session: "pwncat.manager.Session"): return # This vulnerability was patched in 1.8.28 - if version.parse(sudo_info.version) >= version.parse("1.8.28"): + try: + parsed_version = parse(sudo_info.version) + if parsed_version >= parse("1.8.28"): + return + except InvalidVersion: return # Grab the current user/group @@ -74,4 +78,4 @@ def enumerate(self, session: "pwncat.manager.Session"): source_uid=current_user.id, user="\\#-1", spec=command, - ) + ) \ No newline at end of file diff --git a/pwncat/platform/__init__.py b/pwncat/platform/__init__.py index 13c7e042..a71d7670 100644 --- a/pwncat/platform/__init__.py +++ b/pwncat/platform/__init__.py @@ -515,12 +515,11 @@ def __init__( target = self class RemotePath(base_path, Path): - - _target = target - _stat = None - - def __init__(self, *args): - base_path.__init__(*args) + def __new__(cls, *args, **kwargs): + obj = super().__new__(cls, *args, **kwargs) + obj._target = target + obj._stat = None + return obj self.Path = RemotePath """ A concrete Path object for this platform conforming to pathlib.Path """ diff --git a/pwncat/platform/linux.py b/pwncat/platform/linux.py index 26c81519..5148842b 100644 --- a/pwncat/platform/linux.py +++ b/pwncat/platform/linux.py @@ -1635,14 +1635,12 @@ def context_changed(self): # Update self.shell just in case the user changed shells try: - # Get the PID of the running shell - pid = self.getenv("$") - # Grab the path to the executable representing the shell - self.shell = self.Path("/proc", pid, "exe").readlink() - except (FileNotFoundError, PermissionError, OSError): - # Fall back to SHELL even though it's not really trustworthy + pid = self.getenv("$").strip() + proc_exe_path = f"/proc/{pid}/exe" + self.shell = self.readlink(proc_exe_path) + except (FileNotFoundError, PermissionError, OSError, AttributeError): self.shell = self.getenv("SHELL") - if self.shell is None or self.shell == "": + if not self.shell: self.shell = "/bin/sh" # Refresh the currently tracked user and group IDs