Added initial gtfobins documentation
This commit is contained in:
parent
94ea7ea969
commit
0a060bdd4c
|
@ -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
|
|
@ -14,3 +14,4 @@ prompt commands or more complicated privilege escalation or persistence methods.
|
|||
privesc.rst
|
||||
persist.rst
|
||||
victim.rst
|
||||
gtfobins.rst
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue