From 519c8910e10229660b93620e80d8457b8f94eb14 Mon Sep 17 00:00:00 2001 From: John Hammond Date: Fri, 7 May 2021 22:29:32 -0400 Subject: [PATCH] Made enumerate.misc.writable_path functional. Added support to enumerate groups --- pwncat/db/user.py | 6 +- pwncat/manager.py | 13 +++- .../linux/enumerate/misc/writable_path.py | 17 ++++- .../modules/linux/enumerate/user/__init__.py | 2 +- pwncat/modules/linux/enumerate/user/group.py | 43 ++++++++++++ pwncat/platform/__init__.py | 10 +-- pwncat/platform/linux.py | 15 +++- pwncat/session.py | 68 ------------------- 8 files changed, 93 insertions(+), 81 deletions(-) create mode 100644 pwncat/modules/linux/enumerate/user/group.py delete mode 100644 pwncat/session.py diff --git a/pwncat/db/user.py b/pwncat/db/user.py index 73121c0..bcb6d12 100644 --- a/pwncat/db/user.py +++ b/pwncat/db/user.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 from typing import Optional +import rich.markup from persistent.list import PersistentList from pwncat.db.fact import Fact @@ -19,7 +20,10 @@ class Group(Fact): self.members: PersistentList = PersistentList(members) def __repr__(self): - return f"""Group(gid={self.id}, name={repr(self.name)}), members={repr(",".join(m for m in self.members))})""" + return f"""Group(gid={self.id}, name={repr(self.name)}, members={repr(self.members)})""" + + def __str__(self): + return f"""{rich.markup.escape(self.name)}, gid={self.id}, members={rich.markup.escape(",".join((m for m in self.members)))}""" class User(Fact): diff --git a/pwncat/manager.py b/pwncat/manager.py index 480ec33..3da1613 100644 --- a/pwncat/manager.py +++ b/pwncat/manager.py @@ -121,17 +121,26 @@ class Session: self.log("registered new host w/ db") def current_user(self) -> pwncat.db.User: - """ Retrieve the current user object """ + """Retrieve the current user object""" return self.find_user(uid=self.platform.getuid()) def find_user(self, uid=None, name=None): - """ Locate a user object by name or ID """ + """Locate a user object by name or ID""" for user in self.run("enumerate.gather", types=["user"]): if (uid is None or user.id == uid) and (name is None or user.name == name): return user + def find_group(self, gid=None, name=None): + """Locate a user object by name or ID""" + + for group in self.run("enumerate.gather", types=["group"]): + if (gid is None or group.id == gid) and ( + name is None or group.name == name + ): + return group + def run(self, module: str, **kwargs): """Run a module on this session""" diff --git a/pwncat/modules/linux/enumerate/misc/writable_path.py b/pwncat/modules/linux/enumerate/misc/writable_path.py index 20069e4..7493acc 100644 --- a/pwncat/modules/linux/enumerate/misc/writable_path.py +++ b/pwncat/modules/linux/enumerate/misc/writable_path.py @@ -2,12 +2,25 @@ import os import stat +import rich.markup + import pwncat +from pwncat.db import Fact from pwncat.util import Access from pwncat.platform.linux import Linux from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule +class WritablePath(Fact): + def __init__(self, source, path): + super().__init__(source=source, types=["misc.writable_path"]) + + self.path: str = path + + def __str__(self): + return f"""{rich.markup.escape(self.path)}""" + + class Module(EnumerateModule): """ Locate any components of the current PATH that are writable @@ -20,8 +33,6 @@ class Module(EnumerateModule): def enumerate(self, session): - user = session.platform.current_user() - for path in session.platform.getenv("PATH").split(":"): # Ignore empty components @@ -35,4 +46,4 @@ class Module(EnumerateModule): # See if we have write permission if path.is_dir() and path.writable(): - yield "misc.writable_path", str(path.resolve()) + yield WritablePath(self.name, str(path.resolve())) diff --git a/pwncat/modules/linux/enumerate/user/__init__.py b/pwncat/modules/linux/enumerate/user/__init__.py index 2dcbeb3..303114b 100644 --- a/pwncat/modules/linux/enumerate/user/__init__.py +++ b/pwncat/modules/linux/enumerate/user/__init__.py @@ -6,7 +6,7 @@ from pwncat.platform.linux import Linux, LinuxUser class Module(EnumerateModule): - """ Enumerate users from a linux target """ + """Enumerate users from a linux target""" PROVIDES = ["user"] PLATFORM = [Linux] diff --git a/pwncat/modules/linux/enumerate/user/group.py b/pwncat/modules/linux/enumerate/user/group.py new file mode 100644 index 0000000..b12457d --- /dev/null +++ b/pwncat/modules/linux/enumerate/user/group.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 + +from pwncat.modules import ModuleFailed, Status +from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule +from pwncat.platform.linux import Linux, LinuxGroup + + +class Module(EnumerateModule): + """Enumerate users from a linux target""" + + PROVIDES = ["group"] + PLATFORM = [Linux] + SCHEDULE = Schedule.ONCE + + def enumerate(self, session: "pwncat.manager.Session"): + + group_file = session.platform.Path("/etc/group") + + try: + with group_file.open("r") as filp: + for group_line in filp: + try: + # Extract the group fields + (group_name, hash, gid, members) = group_line.split(":") + + # Build a group object + group = LinuxGroup( + self.name, + group_name, + hash, + int(gid), + (m.strip() for m in members.split(",") if m.strip()), + ) + + yield group + + except Exception as exc: + raise ModuleFailed(f"something fucked {exc}") + # Bad group line + continue + + except (FileNotFoundError, PermissionError) as exc: + raise ModuleFailed(str(exc)) from exc diff --git a/pwncat/platform/__init__.py b/pwncat/platform/__init__.py index 48af62d..f7708e8 100644 --- a/pwncat/platform/__init__.py +++ b/pwncat/platform/__init__.py @@ -54,19 +54,19 @@ class Path: def writable(self) -> bool: """This is non-standard, but is useful""" - user = self._target.current_user() + user = self._target.session.current_user() + group = self._target.session.find_group(gid=user.gid) mode = self.stat().st_mode uid = self.stat().st_uid gid = self.stat().st_gid if uid == user.id and (mode & stat.S_IWUSR): return True - elif user.group.id == gid and (mode & stat.S_IWGRP): + elif group.id == gid and (mode & stat.S_IWGRP): return True else: - for group in user.groups: - if group.id == gid and (mode & stat.S_IWGRP): - return True + if group.id == gid and (mode & stat.S_IWGRP): + return True else: if mode & stat.S_IWOTH: return True diff --git a/pwncat/platform/linux.py b/pwncat/platform/linux.py index f385590..ff545c4 100644 --- a/pwncat/platform/linux.py +++ b/pwncat/platform/linux.py @@ -17,7 +17,7 @@ import pwncat.subprocess from pwncat import util from pwncat.gtfobins import GTFOBins, Capability, Stream, MissingBinary from pwncat.platform import Platform, PlatformError, Path -from pwncat.db import User +from pwncat.db import User, Group class PopenLinux(pwncat.subprocess.Popen): @@ -471,6 +471,19 @@ class LinuxUser(User): self.shell = shell +class LinuxGroup(Group): + def __init__(self, source, group_name, hash, gid, members, password=None): + + # We've never seen a group password hash, but those apparently exist???? + if hash == "x": + hash = None + + super().__init__(source, group_name, gid, members) + + self.hash = hash + self.password = password + + class Linux(Platform): """ Concrete platform class abstracting interaction with a GNU/Linux remote diff --git a/pwncat/session.py b/pwncat/session.py deleted file mode 100644 index c5dc934..0000000 --- a/pwncat/session.py +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env python3 -import threading -from typing import Optional, Union - -import pwncat -import pwncat.channel -import pwncat.platform - - -class Session: - """ - Combine a platform and a C2 channel to track an abstract session - with a victim. - """ - - def __init__( - self, - platform: Union[str, pwncat.platform.Platform], - channel: Optional[pwncat.channel.Channel] = None, - ): - - # Allow creation of a session from a platform identifier - # or from an already initialized platform object. - if not isinstance(platform, pwncat.platform.Platform): - self.platform: pwncat.platform.Platform = pwncat.platform.create( - platform, channel=channel - ) - else: - self.platform: pwncat.platform.Platform = platform - - # Find the host hash identifying this unique victim - host_hash = self.platform.get_host_hash() - - # Lookup the host hash in the database - self.host: pwncat.db.Host = pwncat.db.get_session().query( - pwncat.db.Host - ).filter_by(hash=host_hash).first() - - # A lock used to ensure that multiple actions aren't performed - # at the same time on one session. This is mainly implemented with - # the intension of implementing multi-session capabilities into - # pwncat in the future - self.lock: threading.Lock = threading.Lock() - - # Bootstrap the new host object - if self.host is None: - self._bootstrap_new_host(host_hash) - - def _bootstrap_new_host(self, host_hash): - """ - Utilize the enumerated host hash to build a new host object in the - database. This tracks all data related to an individual host. - """ - - def __enter__(self) -> "Session": - """ Acquire the session lock - - :return: the locked session object - :rtype: pwncat.session.Session - """ - - self.lock.acquire() - return self - - def __exit__(self): - """ Release the session lock """ - - self.lock.release()