diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst index c05a836..b490aaf 100644 --- a/docs/source/api/index.rst +++ b/docs/source/api/index.rst @@ -12,4 +12,5 @@ prompt commands or more complicated privilege escalation or persistence methods. commandparser.rst privesc.rst + persist.rst victim.rst diff --git a/docs/source/api/persist.rst b/docs/source/api/persist.rst new file mode 100644 index 0000000..fa88ee2 --- /dev/null +++ b/docs/source/api/persist.rst @@ -0,0 +1,78 @@ +Persistence Methods +=================== + +Persistence methods are implemented through an abstract ``PersistenceMethod`` base class which defines methods +for installing and removing various persistence methods. The persistence module and associated base classes +are defined in the ``pwncat/persist/__init__.py`` script. + +Persistence methods are loaded using the ``pkgutil`` python module automatically from the ``pwncat/persist`` +subdirectory. Any module implementing a ``Method`` class which inherits from the ``PersistenceMethod`` +base class will be imported as an available persistence method. + +Implementing Persistence Methods +-------------------------------- + +A persistence method is implemented by creating a new script under the ``pwncat/persist`` directory +which implements a ``Method`` class. This class must inherit from the ``PersistenceMethod`` base +class and implement the ``install`` and ``remove`` functions. + +A privilege escalation method also defines a few properties as class variables which govern how +the method is utilized by ``pwncat``. The ``system`` variable is a boolean defining whether this +persistence method allows access only as ``root`` or is installed on a per-user basis. If this +item is true, all ``user`` options in further methods are ignored. + +The ``name`` variable is used to create user-readable formatted strings representing this persistence +method. Lastly, the ``local`` variable is a boolean defining whether the persistence method allows +local escalation to the specified user. + +Three methods can be overridden within the ``PersistenceMethod`` base class. The first is the ``install`` +method. This method takes a ``user`` parameter (if not a system method), and should install persistence +as the specified user. If there is a problem or error during installation, ``PersistenceError`` should +be raised with a description of the error. + +Next, the ``remove`` method must be implemented to undo the actions of the ``install`` method. It takes +the same ``user`` argument, and upon error should also raise ``PersistenceError``. + +Lastly, the ``escalate`` method is only required if ``local`` is true. It should leverage this +persistence method to gain access shell access as the specified user (again, user should be ignored +for system methods). This is used as a shortcut in the implementation of the ``privesc`` command +to utilize local persistence methods to escalate to different users. + +Locating and Installing Persistence Methods +------------------------------------------- + +If you would like to programmatically install, remove or locate privilege escalation methods, +you can use the ``pwncat.victim.persist`` module. This module provides a generic interface +to enumerate available methods, list installed methods, and locate methods by name. + +The ``install`` method takes a method name and an optional user. This will locate the identified +method and call it's ``install`` routine. If installation is successful, it will register the +method in the database as installed and also register a corresponding tamper object to track +the installation. If the method does not exist or failed to install, a ``PersistenceError`` +exception is raised. + +The ``register`` method takes the same parameters as the ``install`` method. It will register the +specified method as being installed but not perform the installation routine. This is useful +when a module installs a persistence method in a non-standard way and needs to register this +installation with the framework. For example, the ``privesc`` module may install SSH authorized +keys via a privesc file writer. If this happens, it will register this persistence with the ``persist`` +module for tracking. + +The ``remove`` is the inverse of the ``install`` method, and will completely remove the given +persistence method. + +To find a persistence method by name, you can use the ``find`` method which returns an iterator +of known persistence methods. The ``persist`` module is also an iterator which will yield all +known persistence methods. + +The Persistence Module Class +---------------------------- + +.. autoclass:: pwncat.persist.Persistence + :members: + +The Base Persistence Method Class +--------------------------------- + +.. autoclass:: pwncat.persist.PersistenceMethod + :members: diff --git a/pwncat/persist/__init__.py b/pwncat/persist/__init__.py index 20bf6a1..3d142de 100644 --- a/pwncat/persist/__init__.py +++ b/pwncat/persist/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import functools import pkgutil -from typing import Optional, Dict, Iterator +from typing import Optional, Dict, Iterator, Tuple from colorama import Fore import pwncat @@ -20,6 +20,13 @@ def persistence_tamper_removal(name: str, user: Optional[str] = None): class Persistence: + """ + This class abstracts the management of persistence methods and is accessible at runtime + via ``pwncat.victim.persist``. It provides methods of enumerating available persistence + methods, enumerating installed persistence methods, installing methods, and removing + methods. + """ + def __init__(self): self.methods: Dict[str, "PersistenceMethod"] = {} @@ -29,8 +36,17 @@ class Persistence: self.methods[method.name] = method def install(self, name: str, user: Optional[str] = None): - """ Add persistence as the specified user. If the specified persistence - method is system method, the "user" argument is ignored. """ + """ + Install the specified method by name. If the specified method is not a system + method, ``user`` specifies the user to install this method as. Otherwise, the + ``user`` parameter is ignored. + + This method raises a PersistenceError if installation failed or the given method + does not exist. + + :param name: the name of the persistence method to install + :param user: the user to install persistence as + """ try: method = next(self.find(name)) except StopIteration: @@ -45,9 +61,17 @@ class Persistence: self.register(name, user) def register(self, name: str, user: Optional[str] = None): - """ Register a persistence method as pre-installed. This is useful for some privilege escalation + """ + Register a persistence method as pre-installed. This is useful for some privilege escalation which automatically adds things equivalent to persistent, but without the - persistence module itself (e.g. backdooring /etc/passwd or SSH keys). """ + persistence module itself (e.g. backdooring /etc/passwd or SSH keys). + + This method raises a PersistenceError if the given persistence method + does not exist. + + :param name: the method to register as pre-installed + :param user: the user the method was installed as + """ method = next(self.find(name)) @@ -61,47 +85,54 @@ class Persistence: ) @property - def installed(self) -> Iterator["PersistenceMethod"]: - """ Retrieve a list of installed persistence methods """ + def installed(self) -> Iterator[Tuple[str, "PersistenceMethod"]]: + """ + Enumerate all installed persistence methods. + + :return: An iterator of tuples of (username,PeristenceMethod) + """ for persist in pwncat.victim.host.persistence: yield persist.user, self.methods[persist.method] @property - def available(self) -> Iterator[str]: - """ Yield all the known methods """ + def available(self) -> Iterator["PersistenceMethod"]: + """ + Enumerate all available persistence methods + + :return: Iterator of available persistence methods + """ yield from self.methods.values() - def find( - self, - name: Optional[str] = None, - user: Optional[str] = None, - installed: bool = False, - local: Optional[bool] = None, - system: Optional[bool] = None, - ) -> Iterator["PersistenceMethod"]: - + def find(self, name: Optional[str] = None,) -> Iterator["PersistenceMethod"]: + """ + Locate persistence methods matching the given name. + + :param name: the name of the persistence module to locate + :return: Iterator of persistence methods matching the name + """ for method in self.methods.values(): if name is not None and method.name != name: # not the requested method continue - if installed: - if user is not None or system is None or method.system == system: - if not method.installed(user): - continue - else: - # the user was not specified and this module is not a - # system module. We can't check install state, so we - # err on the side of caution here. - continue - if local is not None and method.local != local: - continue # All checks passed. Yield the method. yield method def remove(self, name: str, user: Optional[str] = None, from_tamper: bool = False): - """ Remove the specified persistence method from the remote victim + """ + Remove the specified persistence method from the remote victim if the given persistence method is a system method, the "user" - argument is ignored. """ + argument is ignored. + + Raises a ``PersistenceError`` if the given method doesn't exist or removal + failed. + + The ``from_tamper`` parameter should not be used and is only used + for internal removal from within the tamper subsystem. + + :param name: the name of the method to remove + :param user: the user which was used to install this method + :param from_tamper: whether we are removing from the tamper removal system + """ try: method = next(self.find(name)) except StopIteration: @@ -138,7 +169,9 @@ class Persistence: class PersistenceMethod: - """ Base persistence method class """ + """ Base persistence method class. The docstring for your method class will + become the long-form help for this method (viewable with ``persist -l -m {method-name}``) + """ def __init__(self): pass @@ -162,9 +195,21 @@ class PersistenceMethod: raise NotImplementedError def install(self, user: Optional[str] = None): + """ + Install this method of persistence as the given user. Raise a + ``PersistenceError`` if installation fails. + + :param user: the user to install persistence as + """ raise NotImplementedError def remove(self, user: Optional[str] = None): + """ + Remove this method of persistence as the given user. Raise a + ``PersistenceError`` if removal fails. + + :param user: the user to remove persistence as + """ raise NotImplementedError def installed(self, user: Optional[str] = None) -> bool: