This commit is contained in:
John Hammond 2020-05-23 03:09:32 -04:00
commit 3801b50f14
6 changed files with 213 additions and 80 deletions

View File

@ -27,3 +27,4 @@ alias down download
# "!ls" run "local ls" given the below directives
shortcut ! local
shortcut @ run

View File

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

View File

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

View File

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

View File

@ -10,6 +10,7 @@ class Command(CommandDefinition):
PROG = "help"
ARGS = {"topic": parameter(Complete.NONE, nargs="?")}
LOCAL = True
def run(self, args):
if args.topic:

View File

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