From ea5cbf5c81c1df3bd9c9f0600a74d16b728ed6c6 Mon Sep 17 00:00:00 2001 From: Caleb Stewart Date: Sun, 2 May 2021 14:28:41 -0400 Subject: [PATCH 1/2] Added history back to database --- data/pwncatrc | 2 +- pwncat/commands/__init__.py | 18 ++---- pwncat/db/__init__.py | 1 - pwncat/db/history.py | 13 ---- pwncat/manager.py | 20 +++++- pwncat/platform/__init__.py | 126 ++---------------------------------- pwncat/platform/linux.py | 9 +++ test.py | 16 +---- 8 files changed, 38 insertions(+), 167 deletions(-) delete mode 100644 pwncat/db/history.py diff --git a/data/pwncatrc b/data/pwncatrc index cc65d6d..e07a3ad 100644 --- a/data/pwncatrc +++ b/data/pwncatrc @@ -7,7 +7,7 @@ set -g privkey "data/pwncat" # Set the pwncat backdoor user and password set -g backdoor_user "pwncat" set -g backdoor_pass "pwncat" -set -g db "memory://" +set -g db "file://test.zodb" set -g on_load { # Run a command upon a stable connection diff --git a/pwncat/commands/__init__.py b/pwncat/commands/__init__.py index 49ae1a1..a8abe2a 100644 --- a/pwncat/commands/__init__.py +++ b/pwncat/commands/__init__.py @@ -108,7 +108,6 @@ def resolve_blocks(source: str): class DatabaseHistory(History): """ Yield history from the host entry in the database """ - # NOTE - This is nerfed because of ZODB changes... def __init__(self, manager): super().__init__() self.manager = manager @@ -116,23 +115,14 @@ class DatabaseHistory(History): def load_history_strings(self) -> Iterable[str]: """ Load the history from the database """ - if False: - with self.manager.new_db_session() as session: - for history in ( - session.query(pwncat.db.History) - .order_by(pwncat.db.History.id.desc()) - .all() - ): - yield history.command + with self.manager.db.transaction() as conn: + yield from reversed(conn.root.history) def store_string(self, string: str) -> None: """ Store a command in the database """ - if False: - history = pwncat.db.History(command=string) - - with self.manager.new_db_session() as session: - session.add(history) + with self.manager.db.transaction() as conn: + conn.root.history.append(string) class CommandParser: diff --git a/pwncat/db/__init__.py b/pwncat/db/__init__.py index 1995985..5bc3ba1 100644 --- a/pwncat/db/__init__.py +++ b/pwncat/db/__init__.py @@ -2,7 +2,6 @@ import pwncat from pwncat.db.binary import Binary -from pwncat.db.history import History from pwncat.db.persist import Persistence from pwncat.db.suid import SUID from pwncat.db.tamper import Tamper diff --git a/pwncat/db/history.py b/pwncat/db/history.py deleted file mode 100644 index be3601c..0000000 --- a/pwncat/db/history.py +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env python3 - -import persistent -from typing import Optional - - -class History(persistent.Persistent): - """Store history of ran commands on the target.""" - - def __init__(self, command): - - # The command ran on the target (e.g., "whoami") - self.command: Optional[str] = command diff --git a/pwncat/manager.py b/pwncat/manager.py index 4f8a7d3..6b1d690 100644 --- a/pwncat/manager.py +++ b/pwncat/manager.py @@ -120,6 +120,18 @@ class Session: self.log("registered new host w/ db") + def current_user(self) -> pwncat.db.User: + """ 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 """ + + 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 run(self, module: str, **kwargs): """ Run a module on this session """ @@ -341,8 +353,12 @@ class Manager: if not hasattr(conn.root, "targets"): conn.root.targets = persistent.list.PersistentList() - conn.transaction_manager.commit() - conn.close() + + if not hasattr(conn.root, "history"): + conn.root.history = persistent.list.PersistentList() + + conn.transaction_manager.commit() + conn.close() # Rebuild the command parser now that the database is available self.parser = CommandParser(self) diff --git a/pwncat/platform/__init__.py b/pwncat/platform/__init__.py index 0c3413b..8394ec6 100644 --- a/pwncat/platform/__init__.py +++ b/pwncat/platform/__init__.py @@ -504,6 +504,10 @@ class Platform(ABC): """ Retrieve a string describing the platform connection """ return str(self.channel) + @abstractmethod + def getuid(self): + """ Get the current user ID """ + @abstractmethod def getenv(self, name: str) -> str: """Get the value of an environment variable. @@ -513,128 +517,6 @@ class Platform(ABC): :rtype: str """ - @abstractmethod - def reload_users(self): - """Reload the user and group cache. This is automatically called - if the cache hasn't been built yet, but may be called manually - if you know the users have changed. This method is also called - if a lookup for a specific user or group ID fails.""" - - def iter_users(self) -> Generator["pwncat.db.User", None, None]: - """ Iterate over all users on the remote system """ - - with self.session.db as db: - users = db.query(pwncat.db.User).filter_by(host_id=self.session.host).all() - - if users is None: - self.reload_users() - - users = ( - db.query(pwncat.db.User).filter_by(host_id=self.session.host).all() - ) - - if users is not None: - for user in users: - _ = user.groups - yield user - - return - - def find_user( - self, - name: Optional[str] = None, - id: Optional[int] = None, - _recurse: bool = True, - ) -> "pwncat.db.User": - """Locate a user by name or UID. If the user/group cache has not - been built, then reload_users is automatically called. If the - lookup fails, reload_users is called automatically to ensure that - there has not been a user/group update remotely. If the user - still cannot be found, a KeyError is raised.""" - - with self.session.db as db: - user = db.query(pwncat.db.User).filter_by(host_id=self.session.host) - - if name is not None: - user = user.filter_by(name=name) - if id is not None: - user = user.filter_by(id=id) - - user = user.first() - if user is None and _recurse: - self.reload_users() - return self.find_user(name=name, id=id, _recurse=False) - elif user is None: - raise KeyError - - return user - - def update_user(self): - """ Force an update of the current user the next time it is requested. """ - - self._current_user = None - - def current_user(self): - """ Retrieve a user object for the current user """ - - if self._current_user is not None: - return self._current_user - - self._current_user = self.find_user(name=self.whoami()) - - return self._current_user - - def iter_groups(self) -> Generator["pwncat.db.Group", None, None]: - """ Iterate over all groups on the remote system """ - - with self.session.db as db: - groups = ( - db.query(pwncat.db.Group).filter_by(host_id=self.session.host).all() - ) - - if groups is None: - self.reload_users() - - groups = ( - db.query(pwncat.db.Group).filter_by(host_id=self.session.host).all() - ) - - if groups is not None: - for group in groups: - _ = group.members - yield group - - return - - def find_group( - self, - name: Optional[str] = None, - id: Optional[int] = None, - _recurse: bool = True, - ) -> "pwncat.db.Group": - """Locate a group by name or GID. If the user/group cache has not - been built, then reload_users is automatically called. If the - lookup fails, reload_users is called automatically to ensure that - there has not been a user/group update remotely. If the group - still cannot be found, a KeyError is raised.""" - - with self.session.db as db: - group = db.query(pwncat.db.Group).filter_by(host_id=self.session.host) - - if name is not None: - group = group.filter_by(name=name) - if id is not None: - group = group.filter_by(id=id) - - group = group.first() - if group is None and _recurse: - self.reload_users() - return self.find_group(name=name, id=id, _recurse=False) - elif group is None: - raise KeyError - - return group - @abstractmethod def stat(self, path: str) -> os.stat_result: """Run stat on a path on the remote system and return a stat result diff --git a/pwncat/platform/linux.py b/pwncat/platform/linux.py index 6e3ef81..448c1ef 100644 --- a/pwncat/platform/linux.py +++ b/pwncat/platform/linux.py @@ -708,6 +708,15 @@ class Linux(Platform): except CalledProcessError: return None + def getuid(self): + """ Retrieve the current user ID """ + + try: + proc = self.run(["id", "-ru"], capture_output=True, text=True, check=True) + return int(proc.stdout.rstrip("\n")) + except CalledProcessError as exc: + raise PlatformError(str(exc)) from exc + def getenv(self, name: str): try: diff --git a/test.py b/test.py index 521e3ff..dcf8ef9 100755 --- a/test.py +++ b/test.py @@ -12,7 +12,7 @@ manager = pwncat.manager.Manager("data/pwncatrc") # Tell the manager to create verbose sessions that # log all commands executed on the remote host -manager.config.set("verbose", True, glob=True) +# manager.config.set("verbose", True, glob=True) # Establish a session # session = manager.create_session("windows", host="192.168.56.10", port=4444) @@ -20,16 +20,4 @@ manager.config.set("verbose", True, glob=True) session = manager.create_session("linux", host="127.0.0.1", port=4444) # session = manager.create_session("windows", host="0.0.0.0", port=4444) -proc = session.platform.sudo( - ["env"], - env={"TEST": "hello world"}, - password="Super Secret Squirrel", - text=True, - stdout=subprocess.PIPE, -) -print(proc.stdout.readline()) - -proc.wait() - - -manager.interactive() +print(session.current_user()) From ad5ad1a9fbf48ebe0b3c07e0d6103909e43fde22 Mon Sep 17 00:00:00 2001 From: Caleb Stewart Date: Sun, 2 May 2021 14:41:25 -0400 Subject: [PATCH 2/2] Fixed pty spawn for interactive shell --- pwncat/platform/linux.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pwncat/platform/linux.py b/pwncat/platform/linux.py index 448c1ef..7ac85ad 100644 --- a/pwncat/platform/linux.py +++ b/pwncat/platform/linux.py @@ -547,11 +547,12 @@ class Linux(Platform): return pty_command = None + shell = self.getenv("SHELL") if pty_command is None: script_path = self.which("script") if script_path is not None: - pty_command = f""" exec {script_path} -qc /bin/sh /dev/null 2>&1\n""" + pty_command = f""" exec {script_path} -qc {shell} /dev/null 2>&1\n""" if pty_command is None: python_path = self.which( @@ -566,7 +567,7 @@ class Linux(Platform): ] ) if python_path is not None: - pty_command = f"""exec {python_path} -c "import pty; pty.spawn('/bin/sh')" 2>&1\n""" + pty_command = f"""exec {python_path} -c "import pty; pty.spawn('{shell} -i')" 2>&1\n""" if pty_command is not None: self.logger.info(pty_command.rstrip("\n"))