Merge branch 'master' of https://github.com/calebstewart/pwncat
This commit is contained in:
commit
3801b50f14
|
@ -27,3 +27,4 @@ alias down download
|
|||
# "!ls" run "local ls" given the below directives
|
||||
shortcut ! local
|
||||
shortcut @ run
|
||||
|
||||
|
|
|
@ -23,41 +23,19 @@ def main():
|
|||
# Default log-level is "INFO"
|
||||
logging.getLogger().setLevel(logging.INFO)
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="pwncat",
|
||||
description="""
|
||||
A "living of the land"-based C2 platform.
|
||||
|
||||
Aside from the "--config" argument, all other arguments are treated as a pwncat
|
||||
command which is parsed after parsing the configuration script.
|
||||
""",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--config", "-c", help="A configuration script to execute after loading"
|
||||
)
|
||||
args, rest = parser.parse_known_args()
|
||||
|
||||
# Build the victim object
|
||||
pwncat.victim = Victim(args.config)
|
||||
pwncat.victim = Victim()
|
||||
|
||||
# Run the configuration script
|
||||
if args.config:
|
||||
with open(args.config, "r") as filp:
|
||||
config_script = filp.read()
|
||||
pwncat.victim.command_parser.eval(config_script, args.config)
|
||||
# Arguments to `pwncat` are considered arguments to `connect`
|
||||
# We use the `prog_name` argument to make the help for "connect"
|
||||
# display "pwncat" in the usage. This is just a visual fix, and
|
||||
# isn't used anywhere else.
|
||||
pwncat.victim.command_parser.dispatch_line(
|
||||
shlex.join(["connect"] + sys.argv[1:]), prog_name="pwncat"
|
||||
)
|
||||
|
||||
# Run any remaining command line arguments as one command
|
||||
if rest:
|
||||
pwncat.victim.command_parser.dispatch_line(shlex.join(rest))
|
||||
|
||||
# if no connection was established in the configuration,
|
||||
# drop to the pwncat prompt. Don't allow raw access until
|
||||
# a connection is made.
|
||||
# Only continue if we successfully connected
|
||||
if not pwncat.victim.connected:
|
||||
util.warn("no connection established, entering command mode")
|
||||
pwncat.victim.state = util.State.COMMAND
|
||||
if not pwncat.victim.connected:
|
||||
util.error("no connection established. exiting.")
|
||||
exit(0)
|
||||
|
||||
# Setup the selector to wait for data asynchronously from both streams
|
||||
|
@ -69,6 +47,10 @@ def main():
|
|||
done = False
|
||||
|
||||
try:
|
||||
# This loop is only used to funnel data between the local
|
||||
# and remote hosts when in raw mode. During the `pwncat`
|
||||
# prompt, the main loop is handled by the CommandParser
|
||||
# class `run` method.
|
||||
while not done:
|
||||
for k, _ in selector.select():
|
||||
if k.fileobj is sys.stdin:
|
||||
|
|
|
@ -242,7 +242,7 @@ class CommandParser:
|
|||
traceback.print_exc()
|
||||
continue
|
||||
|
||||
def dispatch_line(self, line: str):
|
||||
def dispatch_line(self, line: str, prog_name: str = None):
|
||||
""" Parse the given line of command input and dispatch a command """
|
||||
|
||||
# Account for blank or whitespace only lines
|
||||
|
@ -284,11 +284,20 @@ class CommandParser:
|
|||
args = [a.encode("utf-8").decode("unicode_escape") for a in args]
|
||||
|
||||
try:
|
||||
if prog_name:
|
||||
temp_name = command.parser.prog
|
||||
command.parser.prog = prog_name
|
||||
prog_name = temp_name
|
||||
|
||||
# Parse the arguments
|
||||
args = command.parser.parse_args(args)
|
||||
|
||||
# Run the command
|
||||
command.run(args)
|
||||
|
||||
if prog_name:
|
||||
command.parser.prog = prog_name
|
||||
|
||||
except SystemExit:
|
||||
# The arguments were icncorrect
|
||||
return
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
#!/usr/bin/env python3
|
||||
from colorama import Fore
|
||||
import ipaddress
|
||||
import socket
|
||||
|
||||
import paramiko
|
||||
from prompt_toolkit import prompt
|
||||
|
||||
import pwncat
|
||||
from pwncat import util
|
||||
from pwncat.commands.base import (
|
||||
|
@ -20,6 +24,13 @@ 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",
|
||||
),
|
||||
"--listen,-l": parameter(
|
||||
Complete.NONE,
|
||||
action=StoreConstOnce,
|
||||
|
@ -78,10 +89,20 @@ class Command(CommandDefinition):
|
|||
"--user,-u": parameter(
|
||||
Complete.NONE,
|
||||
help="The user to reconnect as; if this is a system method, this parameter is ignored.",
|
||||
action=StoreForAction(["reconnect"]),
|
||||
action=StoreForAction(["reconnect", "ssh"]),
|
||||
),
|
||||
"--password,-P": parameter(
|
||||
Complete.NONE,
|
||||
help="The password for the specified user for SSH connections",
|
||||
action=StoreForAction(["ssh"]),
|
||||
),
|
||||
"--identity,-i": parameter(
|
||||
Complete.NONE,
|
||||
help="The private key for authentication for SSH connections",
|
||||
action=StoreForAction(["ssh"]),
|
||||
),
|
||||
}
|
||||
DEFAULTS = {"action": "list"}
|
||||
DEFAULTS = {"action": "none"}
|
||||
LOCAL = True
|
||||
|
||||
def run(self, args):
|
||||
|
@ -90,52 +111,157 @@ class Command(CommandDefinition):
|
|||
util.error("connect can only be called prior to an active connection!")
|
||||
return
|
||||
|
||||
if args.action == "listen":
|
||||
util.progress(f"binding to {args.host}:{args.port}")
|
||||
|
||||
# Create the socket server
|
||||
server = socket.create_server((args.host, args.port), reuse_port=True)
|
||||
|
||||
if args.config:
|
||||
try:
|
||||
# Wait for a connection
|
||||
(client, address) = server.accept()
|
||||
except KeyboardInterrupt:
|
||||
util.warn(f"aborting listener...")
|
||||
return
|
||||
# Load the configuration
|
||||
with open(args.config, "r") as filp:
|
||||
pwncat.victim.command_parser.eval(filp.read(), args.config)
|
||||
except OSError as exc:
|
||||
self.parser.error(str(exc))
|
||||
|
||||
util.success(f"received connection from {address[0]}:{address[1]}")
|
||||
pwncat.victim.connect(client)
|
||||
elif args.action == "connect":
|
||||
util.progress(f"connecting to {args.host}:{args.port}")
|
||||
if args.action == "none":
|
||||
if pwncat.victim.client is None:
|
||||
self.parser.print_help()
|
||||
return
|
||||
|
||||
# Connect to the remote host
|
||||
client = socket.create_connection((args.host, args.port))
|
||||
try:
|
||||
if args.action == "listen":
|
||||
if not args.host:
|
||||
args.host = "0.0.0.0"
|
||||
|
||||
util.success(f"connection to {args.host}:{args.port} established")
|
||||
pwncat.victim.connect(client)
|
||||
elif args.action == "ssh":
|
||||
raise NotImplementedError
|
||||
elif args.action == "reconnect":
|
||||
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")
|
||||
util.progress(f"binding to {args.host}:{args.port}")
|
||||
|
||||
# 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
|
||||
host_hash = host.hash
|
||||
except ValueError:
|
||||
host_hash = args.host
|
||||
|
||||
# Reconnect to the given host
|
||||
try:
|
||||
pwncat.victim.reconnect(host_hash)
|
||||
except PersistenceError as exc:
|
||||
util.error(f"{host_hash}: connection failed: {exc}")
|
||||
return
|
||||
else:
|
||||
util.error(f"{args.action}: invalid action")
|
||||
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:
|
||||
# Load the private key for the user
|
||||
key = paramiko.RSAKey.from_private_key_file(
|
||||
pwncat.victim.config["privkey"]
|
||||
)
|
||||
except:
|
||||
password = prompt(
|
||||
"RSA Private Key Passphrase: ", is_password=True
|
||||
)
|
||||
key = paramiko.RSAKey.from_private_key_file(
|
||||
pwncat.victim.config["privkey"], password
|
||||
)
|
||||
|
||||
# Attempt authentication
|
||||
try:
|
||||
t.auth_publickey(args.user, key)
|
||||
except paramiko.ssh_exception.AuthenticationException:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
t.auth_password(args.user, args.password)
|
||||
except paramiko.ssh_exception.AuthenticationException:
|
||||
pass
|
||||
|
||||
if not t.is_authenticated():
|
||||
t.close()
|
||||
sock.close()
|
||||
util.error("authentication failed")
|
||||
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")
|
||||
finally:
|
||||
if pwncat.victim.client is None and args.exit:
|
||||
raise SystemExit
|
||||
|
|
|
@ -10,6 +10,7 @@ class Command(CommandDefinition):
|
|||
|
||||
PROG = "help"
|
||||
ARGS = {"topic": parameter(Complete.NONE, nargs="?")}
|
||||
LOCAL = True
|
||||
|
||||
def run(self, args):
|
||||
if args.topic:
|
||||
|
|
|
@ -88,7 +88,7 @@ class Victim:
|
|||
"script",
|
||||
]
|
||||
|
||||
def __init__(self, config_path: str):
|
||||
def __init__(self):
|
||||
""" Initialize a new Pty Handler. This will handle creating the PTY and
|
||||
setting the local terminal to raw. It also maintains the state to open a
|
||||
local terminal if requested and exit raw mode. """
|
||||
|
@ -143,7 +143,9 @@ class Victim:
|
|||
# The host object as seen by the database
|
||||
self.host: pwncat.db.Host = None
|
||||
|
||||
def reconnect(self, hostid: str):
|
||||
def reconnect(
|
||||
self, hostid: str, requested_method: str = None, requested_user: str = None
|
||||
):
|
||||
"""
|
||||
Reconnect to the host identified by the provided host hash. The host hash can be
|
||||
retrieved from the ``sysinfo`` command of a running ``pwncat`` session or from
|
||||
|
@ -152,6 +154,11 @@ class Victim:
|
|||
information probed from the last time ``pwncat`` connected to it.
|
||||
|
||||
:param hostid: the unique host hash generated from the last pwncat session
|
||||
:param requested_method: the persistence method to utilize for reconnection, if not specified,
|
||||
all methods will be tried in order until one works.
|
||||
:param requested_user: the user to connect as. if any specified, all users will be tried in
|
||||
order until one works. if no method is specified, only methods for this user
|
||||
will be tried.
|
||||
"""
|
||||
|
||||
# Create the database engine, and then create the schema
|
||||
|
@ -169,6 +176,13 @@ class Victim:
|
|||
raise persist.PersistenceError("{hostid}: invalid host hash")
|
||||
|
||||
for username, method in self.persist.installed:
|
||||
if requested_method and requested_method != method.name:
|
||||
continue
|
||||
if requested_user and (
|
||||
(requested_user != "root" and method.system)
|
||||
or (requested_user != username)
|
||||
):
|
||||
continue
|
||||
try:
|
||||
util.progress(
|
||||
f"attempting host reconnection via {method.format(username)}"
|
||||
|
@ -485,7 +499,7 @@ class Victim:
|
|||
"""
|
||||
|
||||
util.progress("identifying init system")
|
||||
with open("/proc/1/comm", "r") as filp:
|
||||
with self.open("/proc/1/comm", "r") as filp:
|
||||
init = filp.read()
|
||||
|
||||
if "systemd" in init:
|
||||
|
|
Loading…
Reference in New Issue