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.
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:
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:
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 thetest
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 theservices
property. A ValueError is raised if the init system is not understood bypwncat
. 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 supportedinit
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 thevictim.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
andenv
. Ifdelim
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 thevictim.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 runningpwncat
session or from thehost
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 timepwncat
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 forpwncat
.
-
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 callingvictim.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]
- mode (str) – the mode string as with
-
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