From fce965c0c861b6946f8116f93fc79da3374d2270 Mon Sep 17 00:00:00 2001 From: Caleb Stewart Date: Wed, 20 May 2020 23:00:17 -0400 Subject: [PATCH] Added initial privilege escalation api documentation --- docs/source/api/index.rst | 1 + docs/source/api/privesc.rst | 100 ++++++++++++++++++++++++++++++++++++ docs/source/api/victim.rst | 77 ++++++++++++++++++++++++++- pwncat/privesc/base.py | 85 +++++++++++++++++++++++++++--- pwncat/remote/service.py | 26 ++++++++-- 5 files changed, 277 insertions(+), 12 deletions(-) create mode 100644 docs/source/api/privesc.rst diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst index b1bd352..c05a836 100644 --- a/docs/source/api/index.rst +++ b/docs/source/api/index.rst @@ -11,4 +11,5 @@ prompt commands or more complicated privilege escalation or persistence methods. :caption: Contents commandparser.rst + privesc.rst victim.rst diff --git a/docs/source/api/privesc.rst b/docs/source/api/privesc.rst new file mode 100644 index 0000000..f61ad3a --- /dev/null +++ b/docs/source/api/privesc.rst @@ -0,0 +1,100 @@ +Privilege Escalation Modules +============================ + +Privilege escalation in ``pwncat`` is implemented using a pluggable privilege escalation framework +which allows new methods to be easily implemented and integrated into ``pwncat``. All privilege +escalation methods inherit from the ``pwncat.privesc.base.Method`` class and are implemented under +the ``pwncat/privesc`` directory. + +Methods vs Techniques +--------------------- + +Privelege escalation methods may implement multiple techniques. Techniques represent a single action +which a specific privilege escalation method can perform. Each technique is identified by it's method, +the user which the action can be performed as, a Capability and some method specific data. + +Capabilities are one of ``READ``, ``WRITE`` or ``SHELL`` and are specified with the +``pwncat.gtfobins.Capability`` flags. Each technique must specify one and only one capability. + +Privilege escalation is implemented by iterating over all known methods and enumerating all techniques. +After techniques are gathered, ``pwncat`` attempts to put the different file read, write or shell +techniques together to perform some action. For example, it might use a shell technique to.. well... +get a shell. However, ``pwncat`` may also attempt to read a file with a shell technique or gain a +shell with a file read technique. The individual privilege escalation methods do not need to worry +about this, though. They only need to enumerate all available techniques and implement the +associated execution methods for those techniques. + +Implementing a Privilege Escalation Method +------------------------------------------ + +Privilege escalation methods normally take the form of common vulnerabilities or misconfigurations +in the target host. For example, there are built-in privesc methods for SUID binaries, sudo privileges +and a few common vulnerabilities. Each method implements up to five different class methods. + +The first method is the ``check`` method. This is a ``classmethod`` which simply tests to make sure +that the dependencies of this privesc method are available. It should check that the required +binaries, packages or libraries associated with this escalation are available. By default, the base +class will check that all binaries specified in the class variable ``BINARIES`` are present on the +remote system. If anything is missing from the remote system rendering this method unusable, the check +method should raise a ``PrivescError`` exception with a description of what is missing. + +The next method is the ``enumerate`` method. This function returns a list of ``pwncat.privesc.base.Technique`` +objects, each describing a technique which this method is capable of performing on the remote host. +For example, the SUID method iterates over all known SUID binaries and checks for file write, file +read or shell capabilities with GTFObins. It returns techniques which overlap with the capabilities +requested: + +.. code-block:: python + + def enumerate(self, caps: Capability = Capability.ALL) -> List[Technique]: + """ Find all techniques known at this time """ + + # Update the cache for the current user + self.find_suid() + + known_techniques = [] + for suid in pwncat.victim.host.suid: + try: + binary = pwncat.victim.gtfo.find_binary(suid.path, caps) + except BinaryNotFound: + continue + + for method in binary.iter_methods(suid.path, caps, Stream.ANY): + known_techniques.append( + Technique(suid.owner.name, self, method, method.cap) + ) + + return known_techniques + +The last three methods all take a parameter of a ``Technique`` object. This ``Technique`` will +be one of the techniques returned from ``enumerate`` by this method. They implement the three +capabilities which are possible. The first is the ``execute`` method. This method is used to +escalate privileges and gain a shell as the user specified in the technique. This type of +technique is returned, for example, from a SUID ``/bin/bash``, because we are able to directly +gain a shell as the owning user. It should perform the escalation and return with the remote +host currently at a prompt for the new user. If there are any issues or errors, it will raise a +``PrivescError`` with the description of the problem. The return value of this function is a +bytes object which can exit the terminal and return to the previous user. In a simple case, this +could be just "exit". In a more complicated case, like getting a shell from within ``vim``, this +may include control sequences to exit the shell and the containing application. + +Next, methods can implement the ``read_file`` function. This function returns a file-like object +used to read data from a remote file as the user specified in the technique. This is possible, +for example in situations where a binary such as ``cat`` is SUID. Again, if there is an issue, +a ``PrivescError`` is raised. + +The last method which may be implemented is the ``write_file`` method. This method will write +the given data to a file as the user specified in the technique. The method does not return +any data and should simply write the requested data using the technique specified. + +Privilege Escalation Method Class +--------------------------------- + +.. autoclass:: pwncat.privesc.base.Method + :members: + +Technique Class +--------------- + +.. autoclass:: pwncat.privesc.base.Technique + :members: diff --git a/docs/source/api/victim.rst b/docs/source/api/victim.rst index cb1fcd8..50a4a24 100644 --- a/docs/source/api/victim.rst +++ b/docs/source/api/victim.rst @@ -94,9 +94,84 @@ which required a length argument. This is important because transfer of raw bina the output length to be known. If the length is not passed, the data will be automatically encoded (for example, with base64) before uploading, and decoded automatically on the receiving end. +Working with remote services +---------------------------- + +``pwncat`` will attempt to figure out what type of init system is being used on the target host and provide +an abstracted interface to system services. The abstractions are available under the ``pwncat/remote/service.py`` file. +Currently, ``pwncat`` only supports SystemD, but the interface is abstracted to support other init systems +such as SysVInit or Upstart if the interface is implemented. + +The ``pwncat.remote.service.service_map`` maps names of init systems to their abstract ``RemoteService`` +class implementation. This is how ``pwncat`` selects the appropriate remote service backend. + +Regardless of the underlying init system, ``pwncat`` provides methods for querying known services, enabling +auto-start, starting, stopping and creation of remote services. + +To query a list of remote services, you can use the ``pwncat.victim.services`` property. This is an iterator +yielding each abstracted service object. Each object contains a name, description, and state as well as +methods for starting, stopping, enabling or disabling the service. This functionality obviously depends +on you having the correct permission to manage the services, however retrieve the state and list of +services should work regardless of your permission level. + +.. code-block:: python + + from pwncat import victim + + for service in victim.services: + print(f"{service.name} is {'running' if service.running else 'stopped'}") + +To find a specific service by name, there is a ``find_service`` method which returns an individual +remote service object. If the service is not found, a ValueError is raised. + +.. code-block:: python + + from pwncat import victim + + sshd = victim.find_service("sshd") + + +The interface for creating services is provided through the ``create_service`` method, which allows +you to specify a target binary name which serves as the entrypoint for your service as well as a name +description, and enabled state. A ``PermissionError`` is raised if you do not have permission to create +the specified service. This method also returns a wrapped ``RemoteService`` object for the newly +created service. + +.. code-block:: python + + from pwncat import victim + + pwncat = victim.create_service(name="pwncat", + description="a malicious service", + target="/usr/bin/pwncat_service", + runas="root", + enable=True, + user=False) + pwncat.start() + +Starting, stopping or enabling a service is as easy as calling a method or setting a property: + +.. code-block:: python + + from pwncat import victim + + try: + sshd = victim.find_service("sshd") + sshd.enabled = False + sshd.stop() + except PermissionError: + print("you don't have permission to modify sshd :(") + except ValueError: + print("sshd doesn't exist!") The Victim Object ----------------- .. autoclass:: pwncat.remote.victim.Victim - :members: \ No newline at end of file + :members: + +Remote Service Object +--------------------- + +.. autoclass:: pwncat.remote.service.RemoteService + :members: diff --git a/pwncat/privesc/base.py b/pwncat/privesc/base.py index b5bfc32..aa7eee3 100644 --- a/pwncat/privesc/base.py +++ b/pwncat/privesc/base.py @@ -19,15 +19,30 @@ class PrivescError(Exception): @dataclass class Technique: + """ + An individual technique which was found to be possible by a privilege escalation + method. + + :param user: the user this technique provides access as + :param method: the method this technique is associated with + :param ident: method-specific identifier + :param capabilities: a GTFObins capability this technique provides + """ + # The user that this technique will move to user: str + """ The user this technique provides access as """ # The method that will be used method: "Method" + """ The method which this technique is associated with """ # The unique identifier for this method (can be anything, specific to the # method) ident: Any + """ Method specific identifier. This can be anything the method needs + to identify this specific technique. It can also be unused. """ # The GTFObins capabilities required for this technique to work capabilities: Capability + """ The GTFOBins capabilities this technique provides. """ def __str__(self): cap_names = { @@ -42,10 +57,25 @@ class Technique: class Method: + """ + Generic privilege escalation method. You must implement at a minimum the enumerate + method. Also, for any capabilities which you are capable of generating techniques for, + you must implement the corresponding methods: + + * ``Capability.SHELL`` - ``execute`` + * ``Capability.READ`` - ``read_file`` + * ``Capability.WRITE`` - ``write_file`` + + Further, you can also implement the ``check`` class method to verify applicability of + this method to the remote victim and the ``get_name`` method to generate a printable + representation of a given technique for this method (as seen in ``privesc`` output). + """ # Binaries which are needed on the remote host for this privesc name = "unknown" + """ Name of this method """ BINARIES = [] + """ List of binaries to verify presence in the default ``check`` method """ @classmethod def check(cls, pty: "pwncat.pty.PtyHandler") -> bool: @@ -58,24 +88,63 @@ class Method: self.pty = pty def enumerate(self, capability: int = Capability.ALL) -> List[Technique]: - """ Enumerate all possible escalations to the given users """ + """ + Enumerate all possible techniques known and possible on the remote host for + this method. This should only enumerate techniques with overlapping capabilities + as specified by the ``capability`` parameter. + + :param capability: the requested capabilities to enumerate + :return: A list of potentially working techniques + """ raise NotImplementedError("no enumerate method implemented") - def execute(self, technique: Technique): - """ Execute the given technique to move laterally to the given user. - Raise a PrivescError if there was a problem. """ + def execute(self, technique: Technique) -> bytes: + """ + Execute the given technique to gain a shell. This is only called for techniques + providing the Capability.SHELL capability. If there is a problem with escalation, + the shell should be returned to normal and a ``PrivescError`` should be raised. + + :param technique: the technique to execute + :return: a bytes object which will exit the new shell + """ raise NotImplementedError("no execute method implemented") def read_file(self, filename: str, technique: Technique) -> RemoteBinaryPipe: - """ Execute a read_file action with the given technique and return a - remote file pipe which will yield the file contents. """ + """ + Open the given file for reading and return a file-like object, as the user + specified in the technique. This is only called for techniques providing the + Capability.READ capability. If an error occurs, a ``PrivescError`` should be + raised with a description of the problem. + + :param filename: path to the remote file + :param technique: the technique to utilize + :return: Binary file-like object representing the remote file + """ raise NotImplementedError("no read_file implementation") def write_file(self, filename: str, data: bytes, technique: Technique): - """ Execute a write_file action with the given technique. """ + """ + Write the data to the given filename on the remote host as the user + specified in the technique. This is only called for techniques providing the + Capability.WRITE capability. If an error occurs, ``PrivescError`` should + be raised with a description of the problem. + + This will overwrite the remote file if it exists! + + :param filename: the remote file name to write + :param data: the data to write + :param technique: the technique to user + """ raise NotImplementedError("no write_file implementation") - def get_name(self, tech: Technique): + def get_name(self, tech: Technique) -> str: + """ + Generate a human-readable and formatted name for this method/technique + combination. + + :param tech: a technique applicable to this object + :return: a formatted string + """ return str(self) def __str__(self): diff --git a/pwncat/remote/service.py b/pwncat/remote/service.py index 03c2316..2cbce56 100644 --- a/pwncat/remote/service.py +++ b/pwncat/remote/service.py @@ -16,6 +16,18 @@ class ServiceState(Enum): class RemoteService: + """ + Abstract service interface. Interfaces for specific init systems are implemented as + a subclass of the RemoteService class. The class methods defined here should be + redefined to access and enumerate the underlying init system. + + :param name: the service name + :param user: whether this service is a user specific service + :param running: whether this service is currently running + :param description: a long description for this service + + """ + def __init__(self, name: str, running: bool, description: str, user: bool = False): self.name: str = name self.user: bool = user @@ -24,6 +36,13 @@ class RemoteService: @classmethod def enumerate(cls, user: bool = False) -> Iterator["RemoteService"]: + """ + Enumerate installed services on the remote host. This is overloaded for a + specific init system. + + :param user: whether to enumerate user specific services + :return: An iterator for remote service objects + """ raise NotImplementedError def start(self): @@ -39,13 +58,14 @@ class RemoteService: raise NotImplementedError @property - def stopped(self): + def stopped(self) -> bool: """ Check if the service is stopped """ return not self.running @property - def enabled(self): - """ Check if the service is enabled at boot """ + def enabled(self) -> bool: + """ Check if the service is enabled at boot. The setter will attempt to + enable or disable this service for auto-start. """ raise NotImplementedError