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:

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.

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.

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:

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.

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

class pwncat.gtfobins.Capability

The capabilities of a given GTFOBin Binary. A binary may have multiple implementations of each capability, but these flags indicate a list of all capabilities which a given binary supports.

ALL = 7

All capabilities, used for iter_* methods

NONE = 0

No capabilities. Should never happen.

READ = 1

File read

SHELL = 4

Shell access

WRITE = 2

File write

class pwncat.gtfobins.Stream

What time of streaming data is required for a specific method.

ANY = 15

Used with the iter_* methods. Shortcut for searching for any stream

BASE64 = 8

Supports reading/writing base64 data

HEX = 4

Supports reading/writing hex-encoded data

NONE = 0

No stream method. Should never happen.

PRINT = 2

Supports reading/writing printable data only

RAW = 1

A raw, unencoded stream of data. If writing, this mode requires a length parameter to indicate how many bytes of data to transfer.

The GTFOBins Object

class pwncat.gtfobins.GTFOBins(gtfobins: str, which: Callable[[str], str])

Wrapper around the GTFOBins database. Provides access to searching for methods of performing various capabilities generically. All iterations yield MethodWrapper objects.

Parameters:
  • gtfobins (str) – path to the gtfobins database
  • which (Callable[[str, Optional[bool]], str]) – a callable which resolves binary basenames to full paths. A second parameter indicates whether the returned path should be quoted as with shlex.quote.
find_binary(binary_path: str, caps: pwncat.gtfobins.Capability = <Capability.ALL: 7>)

Locate a binary by name. Only return a binary if the capabilities overlap. Raise an BinaryNotFound exception if the capabilities don’t match or the given binary doesn’t exist on the remote system.

iter_binary(binary_path: str, caps: pwncat.gtfobins.Capability = <Capability.ALL: 7>, stream: pwncat.gtfobins.Stream = None, spec: str = None) → Generator[pwncat.gtfobins.MethodWrapper, None, None]

Iterate over methods for the given remote binary path. A binary will be located by taking the basename of the given path, and the cross- referencing with the given capabilities and stream types.

iter_methods(caps: pwncat.gtfobins.Capability = <Capability.ALL: 7>, stream: pwncat.gtfobins.Stream = None, spec: str = None) → Generator[pwncat.gtfobins.MethodWrapper, None, None]

Iterate over methods which provide the given capabilities

iter_sudo(spec: str, caps: pwncat.gtfobins.Capability = <Capability.ALL: 7>, stream: pwncat.gtfobins.Stream = None, **kwargs)

Iterate over methods which are sudo-capable w/ the given sudo spec. This will restrict the search to those binaries which match the given sudo command spec.

parse_binary_data(binary_data: Dict[str, List[Dict[str, Any]]])

Parse the given GTFObins binary information into the associated in-memory binary objects

resolve_binaries(target: str, **args)

resolve any missing binaries with the self.which method

The MethodWrapper Object

class pwncat.gtfobins.MethodWrapper(method: pwncat.gtfobins.Method, binary_path: str)

Wraps a method and full binary path pair which together are capable of generating a payload to perform the specified capability.

build(**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.

cap

Access this methods capabilities

stream

Access this methods stream type

wrap_stream(pipe: BinaryIO) → IO

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. Closing the wrapper will automatically close the underlying pipe.