From 0a060bdd4c1051597227e98b8e8d06bf4f0d315a Mon Sep 17 00:00:00 2001 From: Caleb Stewart Date: Thu, 21 May 2020 01:12:38 -0400 Subject: [PATCH] Added initial gtfobins documentation --- docs/source/api/gtfobins.rst | 230 +++++++++++++++++++++++++++++++++++ docs/source/api/index.rst | 1 + pwncat/gtfobins.py | 48 ++++++-- 3 files changed, 269 insertions(+), 10 deletions(-) create mode 100644 docs/source/api/gtfobins.rst diff --git a/docs/source/api/gtfobins.rst b/docs/source/api/gtfobins.rst new file mode 100644 index 0000000..cd39ab2 --- /dev/null +++ b/docs/source/api/gtfobins.rst @@ -0,0 +1,230 @@ +GTFOBins Abstraction Layer +========================== + +``pwncat`` implements an abstraction of the fantastic GTFOBins_ project. This project catalogs +known methods of file read, file write and shell access with commonly accessible binaries. + +The ``pwncat.gtfobins`` module along with the ``data/gtfobins.json`` database provides a +programmatic way of enumerating and searching for known GTFObins techniques for performing +various capabilities. It is able to generate payloads for gaining a shell, file read, and +file write in standard, SUID or sudo modes. + +For the standard mode, ``gtfobins`` provides ``pwncat`` a way to generically refer to file +read and write operations without depending on specific remote binaries being available. +The likelihood of no methods of file read being available on a remote system is very low, +however the probability of something like ``dd`` to be missing (however odd that would be) +is much higher. In this way, things like ``pwncat.victim.open`` can operate in a generic +way without resulting in dependencies on specific remote binaries. + +Further, the ``gtfobins`` modules has abstracted away the idea of SUID and sudo to provide a +uniform interface for generating payloads which gain file read/write or shell with known +SUID or sudo privileges. The ``gtfobins`` module knows how and where to insert special options +to enable taking advantage of SUID binaries and also knows how to parse sudo command +specifications to enumerate available binaries and produce payloads compatible with the given +sudo specification. + +Module Organization +------------------- + +The GTFObins module is at it's core a database lookup. Currently, this database is a JSON +file which generically describes a large subset of the greater GTFObins project and +describes how to build payloads for each binaries different capabilities. + +The top-level module (the ``GTFOBins`` class) provides access to this database. It is +initialized with a path to the database file (``data/gtfobins.json``) and callable +which represents the ``which`` application for the target system. It should resolve +binary names into their fullpaths on the remote system. It also takes a second boolean +parameter which indicates where the returned string should be quoted as with ``shlex.quote``. + +Payloads are generated from individual methods, which are all an implementation of the +``pwncat.gtfobins.Method`` class. A method is an implementation of a specific capability +for a specific binary. They contain the payload, command arguments, input and exit command +needed to execute a specific capability with a specific binary. These methods are defined +in the database, which will be described further down. + +A ``pwncat.gtfobins.Binary`` object is instantiated for every binary described in the +database. Each binary is described simply by it's name and a list of methods taken +from the database. At a generic level, the binary doesn't know the path on the remote +system, which it will need to build a payload with any given method. + +When enumerating methods, the ``Binary`` and ``GTFOBins`` objects will both return +instances of the ``MethodWrapper`` class. This class provides the actual payload +building mechanism. It is the glue that puts a specific binary path, SUID state and +sudo specification together with a specific ``Method`` object. You will not interact +with ``Method`` objects directly when using this module. + +Retrieving a Method Wrapper +--------------------------- + +Method wrappers are created in three two ways. They can be built automatically by the +``GTFOBins`` object by iterating through all known binaries and using the provided +``which`` callable to locate valid remote binaries. This is done through the +``iter_methods`` function: + +.. code-block:: python + + for method in pwncat.victim.gtfo.iter_methods(Capability.READ, Stream.ANY): + print("We could read a file with {method.binary_path}!") + +This works well when you don't need any special permissions, but just need to generate +a payload for a specific capability. You have no requirements beyond your capability. + +However, sometimes you know a specific binary that you can use, but you're not sure +what you can do with it. This can happen when performing privilege escalation. Perhaps +you can run a specific binary as another user, but you'd like to leverage this for +more access. In this case, you can provide the binary path to the ``iter_binary`` +method to iterate methods for that specific binary. In this case, the ``GTFOBins`` +module will not utilize the ``which`` callable. It trusts you that the given binary +path you provided exists, and yields method wrappers for the capabilities you requested, +if any. + +.. code-block:: python + + for method in pwncat.victim.gtfo.iter_binary("/bin/bash", Capability.ALL, Stream.ANY): + print(f"We could perform {method.cap} with /bin/bash!") + +The last way of generating a method wrapper is used when you know that a user can +run commands via sudo with a specific specification. You'd like to know if GTFObins can +provide any useful capabilities with this command. For this, you can use the +``iter_sudo`` method which will iterator over all methods which are capable of being +executed under the given sudo specification. + +.. code-block:: python + + for method in pwncat.victim.gtfo.iter_sudo("/usr/bin/git log*", caps=Capability.ALL): + print(f"You could perform {method.cap} with /usr/bin/git!") + +``GTFOBins`` is able to parse the sudo command specification and identify if the allowed +parameters to the command overlap with the needed parameters for different methods. If +the specification is ``ALL`` or ends with an asterisk, this is often possible. If it doesn't, +then it will try to make the parameters fit the specification and decide if the +capabilitiy is feasible. + +Generating a Payload by Capability +---------------------------------- + +Once you have identified a specific method (and have a method wrapper), generating a payload is +easy. The ``MethodWrapper`` class provides the ``build`` function which will be all components +of the payload. Each payload consists of three items: + +* The base payload +* The input sent to the application +* The command used to exit the application + +The base payload is the command sent to the target host which will trigger the action specified +by the method capability. The input is the a bytes object which is sent to the standard input +of the application to trigger the action. The command used to exit is a bytes object which when +sent to the applications standard input should cleanly exit the application and return the user +to a normal shell. The last two are optional, but may be required and should always be sent +if returned from ``build``. If a method doesn't need them, they will be empty bytes objects +and you can safely send them to the application anyway. + +The ``build`` function takes variable arguments because the specific parameters required +for each capability are different: + +* A SHELL capability requires the following arguments: + - shell: the shell to execute +* A READ capability requires the following arguments: + - lfile: the path to the local file to read +* A WRITE capability with a RAW stream requires the following arguments: + - lfile: the path to the local file to write to + - length: the number of bytes of data which will be written +* A WRITE capability with any other stream type requires: + - lfile: the path to the local file to write to + +In the case of a read payload, the content of the file is assumed to be sent to standard output +of the command executed via the base payload. For write payloads, the new content for the file +is sent to the standard input of the base payload command **after** any input data returned from +the ``build`` function and **before** sending the exit bytes. + +Putting It All Together +----------------------- + +There's a lot of information up above, so here's an example of using the GTFOBins module. For +file read and file write. First up, we will read the ``/etc/passwd`` file and print the name +of all users on the remote system: + +.. code-block:: python + + from pwncat import victim + + try: + # Find a reader from GTFObins + method = next(victim.gtfo.iter_methods(caps=Capability.READ, stream=Stream.ANY)) + except StopIteration: + raise RuntimeError("no available gtfobins readers!") + + # Build the payload + payload, input_data, exit_cmd = method.build(lfile="/etc/passwd") + + # Run the payload on the remote host. + pipe = self.subprocess( + payload, + "r", + data=input_data.encode("utf-8"), + exit_cmd=exit_cmd.encode("utf-8"), + name=path, + ) + + # Wrap the pipe in the decoder for this method (possible base64) + with method.wrap_stream(pipe) as pipe: + for line in pipe: + line = line.decode("utf-8").strip() + print("Found user:", line.split(":")[0]) + +This might seem long and laberous, but it is infinitely better than depending on a specific +file read method or attempting to account for multiple read methods each time you want to read +a file (although, luckily ``pwncat.victim.open`` already wraps this for you ;). Next, we'll +take a look at writing a file. + +.. code-block:: python + + from pwncat import victim + + # The data we will write + data = b"Hello from a new file!" + + try: + # Find a writer from GTFObins + method = next(victim.gtfo.iter_methods(caps=Capability.WRITE, stream=Stream.RAW)) + except StopIteration: + raise RuntimeError("no available gtfobins readers!") + + # Build the payload + payload, input_data, exit_cmd = method.build(lfile="/tmp/new-file", length=len(data)) + + # Run the payload on the remote host. + pipe = self.subprocess( + payload, + "w", + data=input_data.encode("utf-8"), + exit_cmd=exit_cmd.encode("utf-8"), + name=path, + ) + + with method.wrap_stream(pipe) as pipe: + pipe.write(data) + + +GTFOBins Utility Classes +------------------------ + +.. autoclass:: pwncat.gtfobins.Capability + :members: + +.. autoclass:: pwncat.gtfobins.Stream + :members: + +The GTFOBins Object +------------------- + +.. autoclass:: pwncat.gtfobins.GTFOBins + :members: + +The MethodWrapper Object +------------------------ + +.. autoclass:: pwncat.gtfobins.MethodWrapper + :members: + +.. _GTFOBins: https://gtfobins.github.io \ No newline at end of file diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst index bc3c83e..197132b 100644 --- a/docs/source/api/index.rst +++ b/docs/source/api/index.rst @@ -14,3 +14,4 @@ prompt commands or more complicated privilege escalation or persistence methods. privesc.rst persist.rst victim.rst + gtfobins.rst diff --git a/pwncat/gtfobins.py b/pwncat/gtfobins.py index 7e84b36..51e540c 100644 --- a/pwncat/gtfobins.py +++ b/pwncat/gtfobins.py @@ -35,34 +35,40 @@ class Capability(Flag): capabilities which a given binary supports. """ READ = auto() + """ File read """ WRITE = auto() + """ File write """ SHELL = auto() + """ Shell access """ ALL = READ | SHELL | WRITE + """ All capabilities, used for iter_* methods """ NONE = 0 + """ No capabilities. Should never happen. """ class Stream(Flag): """ What time of streaming data is required for a specific method. - - * RAW - The terminal is placed in raw mode and raw binary data transfer - is supported. - * PRINT - The terminal is left in normal mode and raw transfer is - supported but will only be successful for printable data. - * HEX - The terminal is left in normal mode, but raw data is supported - by transferring data in HEX encoding. - * BASE64 - Same as HEX, but data is transferred in base64. """ RAW = auto() + """ A raw, unencoded stream of data. If writing, this mode requires + a ``length`` parameter to indicate how many bytes of data to transfer. """ PRINT = auto() + """ Supports reading/writing printable data only """ HEX = auto() + """ Supports reading/writing hex-encoded data """ BASE64 = auto() + """ Supports reading/writing base64 data """ ANY = RAW | PRINT | HEX | BASE64 + """ Used with the iter_* methods. Shortcut for searching for any stream """ NONE = 0 + """ No stream method. Should never happen. """ class Method: + """ Abstract method class built from the JSON database """ + def __init__(self, binary: "Binary", cap: Capability, data: Dict[str, Any]): """ Create a new method associated with the given binary. """ @@ -192,6 +198,12 @@ class Method: class MethodWrapper: + """ + Wraps a method and full binary path pair which together are capable of + generating a payload to perform the specified capability. + + """ + def __init__(self, method: Method, binary_path: str): """ Create a Method Wrapper which references a specific binary path. and method arguments. """ @@ -202,8 +214,8 @@ class MethodWrapper: """ Wrap the given BinaryIO pipe with the appropriate stream wrapper for this method. For "RAW" or "PRINT" streams, this is a null wrapper. For BASE64 and HEX streams, this will automatically decode the data as - it is streamed. This method will also wrap in TextIOWrapper if "b" is - not specified in `mode`. """ + it is streamed. Closing the wrapper will automatically close the underlying + pipe. """ if self.stream is Stream.RAW or self.stream is Stream.PRINT: return pipe @@ -243,6 +255,10 @@ class MethodWrapper: return wrapped def build(self, **kwargs) -> Tuple[str, str, str]: + """ Build the payload for this method and binary path. Depending on + capability and stream type, different named parameters are required. + + """ return self.payload(**kwargs), self.input(**kwargs), self.exit(**kwargs) def payload(self, **kwargs) -> str: @@ -335,6 +351,18 @@ class Binary: class GTFOBins: + """ + Wrapper around the GTFOBins database. Provides access to searching for methods + of performing various capabilities generically. All iterations yield MethodWrapper + objects. + + :param gtfobins: path to the gtfobins database + :type gtfobins: str + :param which: a callable which resolves binary basenames to full paths. A second + parameter indicates whether the returned path should be quoted as with shlex.quote. + :type which: Callable[[str, Optional[bool]], str] + """ + def __init__(self, gtfobins: str, which: Callable[[str], str]): """ Create a new GTFOBins object. This will load the JSON gtfobins data file specified in the `gtfobins` parameter. The `which` method is