diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c30cfa..3e29a21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,11 @@ and simply didn't have the time to go back and retroactively create one. - Changed LinuxWriter close routine again to account for needed EOF signals ([#140](https://github.com/calebstewart/pwncat/issues/140)) ### Added - Added better file io test cases +- Added `ssl-bind` and `ssl-connect` channel protocols for encrypted shells +- Added `ncat`-style ssl arguments to entrypoint and `connect` command +- Added query-string arguments to connection strings for both the entrypoint + and the `connect` command. + ## [0.4.2] - 2021-06-15 Quick patch release due to corrected bug in `ChannelFile` which caused command diff --git a/docs/source/installation.rst b/docs/source/installation.rst index b9bc894..f683151 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -23,34 +23,34 @@ After installation, you can use pwncat via the installed script: .. code-block:: bash $ pwncat --help - usage: pwncat [-h] [--config CONFIG] [--identity IDENTITY] [--listen] - [--platform PLATFORM] [--port PORT] [--list] + usage: pwncat [-h] [--version] [--download-plugins] [--config CONFIG] [--ssl] [--ssl-cert SSL_CERT] + [--ssl-key SSL_KEY] [--identity IDENTITY] [--listen] [--platform PLATFORM] [--port PORT] [--list] [[protocol://][user[:password]@][host][:port]] [port] - Start interactive pwncat session and optionally connect to existing victim - via a known platform and channel type. This entrypoint can also be used to - list known implants on previous targets. + Start interactive pwncat session and optionally connect to existing victim via a known platform and channel type. This + entrypoint can also be used to list known implants on previous targets. positional arguments: [protocol://][user[:password]@][host][:port] Connection string describing victim - port Alternative port number to support netcat-style - syntax + port Alternative port number to support netcat-style syntax optional arguments: -h, --help show this help message and exit + --version, -v Show version number and exit + --download-plugins Pre-download all Windows builtin plugins and exit immediately --config CONFIG, -c CONFIG Custom configuration file (default: ./pwncatrc) + --ssl Connect or listen with SSL + --ssl-cert SSL_CERT Certificate for SSL-encrypted listeners (PEM) + --ssl-key SSL_KEY Key for SSL-encrypted listeners (PEM) --identity IDENTITY, -i IDENTITY Private key for SSH authentication - --listen, -l Enable the `bind` protocol (supports netcat-style - syntax) + --listen, -l Enable the `bind` protocol (supports netcat-style syntax) --platform PLATFORM, -m PLATFORM Name of the platform to use (default: linux) - --port PORT, -p PORT Alternative way to specify port to support netcat- - style syntax - --list List installed implants with remote connection - capability + --port PORT, -p PORT Alternative way to specify port to support netcat-style syntax + --list List installed implants with remote connection capability Windows Plugin Binaries ----------------------- diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 4228770..c2118b5 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -34,6 +34,11 @@ with three different C2 protocols: ``bind``, ``connect``, and ``ssh``. The first modes simply open a raw socket and assume there is a shell on the other end. In SSH mode, we legitimately authenticate to the victim host with provided credentials and utilize the SSH shell channel as our C2 channel. +pwncat also implements SSL-wrapped versions of ``bind`` and ``connect`` protocols aptly named ``ssl-bind`` +and ``ssl-connect``. These protocols function largely the same as bind/connect, except that they operate +over an encrypted SSL tunnel. You must use an encrypted bind or reverse shell on the victim side such +as ``ncat --ssl`` or `socat OPENSSL-LISTEN:`. + pwncat exposes these different C2 channel protocols via the ``protocol`` field of the connection string discussed below. @@ -42,22 +47,30 @@ Connecting to a Victim Connecting to a victim is accomplished through a connection string. Connection strings are versatile ways to describe the parameters to a specific C2 Channel/Protocol. This looks something like: -``[protocol://][user[:password]]@[host:][port]`` +``[protocol://][user[:password]]@[host:][port][?arg1=value&arg2=value]`` Each field in the connection string translates to a parameter passed to the C2 channel. Some channels don't require all the parameters. For example, a ``bind`` or ``connect`` channel doesn't required a username or -a password. +a password. If there is not an explicit argument or parsed value within the above format, you can use the +query string arguments to specify arbitrary channel arguments. You cannot specify the same argument twice +(e.g. ``connect://hostname:1111?port=4444``). If the ``protocol`` field is not specified, pwncat will attempt to figure out the correct protocol contextually. The following rules apply: - If a user and host are provided, assume ``ssh`` protocol -- If no user is provided but a host and port are provided, assume protocol is ``connect`` +- If no user is provided but a host, port and the ``--ssl`` argument, assume protocol is ``ssl-connect`` +- If no user is provided but a host and port are provided and no ``--ssl``, assume protocol is ``connect`` +- If no user or host is provided (or host is ``0.0.0.0``) and the ``certfile``, ``keyfile``, or + ``--ssl`` arguments are provided, protocol is assumed to be ``ssl-bind`` - If no user or host is provided (or host is ``0.0.0.0``), protocol is assumed to be ``bind`` -- If a second positional integer parameter is specified, the protocol is assumed to be ``connect`` - - This is the ``netcat`` syntax seen in the below examples for the ``connect`` protocol. -- If the ``-l`` parameter is used, the protocol is assumed to be ``bind``. - - This is the ``netcat`` syntax seen in the below examples for the ``bind`` protocol. +- If a second positional integer parameter is specified and ``--ssl`` is not, the protocol is assumed + to be ``connect`` +- If a second positional integer parameter is specified and ``--ssl`` is provided, the protocol is + assumed to be ``ssl-connect`` +- If the ``-l`` parameter is used and the ``certfile``, ``keyfile``, or ``--ssl`` arguments are + provided, the protocol is assumed to be ``ssl-bind``. +- If the ``-l`` parameter is used alone, then the protocol is assumed to be ``bind`` Connecting to a victim bind shell --------------------------------- @@ -75,6 +88,21 @@ address which is routable (e.g. not NAT'd). The ``connect`` protocol provides th # Connection string with assumed protocol pwncat 192.168.1.1:4444 +Connecting to a victim encrypted bind shell +------------------------------------------- + +In this case, the victim is running a ssl-wrapped bind shell on an open port. The victim must be available at an +address which is routable (e.g. not NAT'd). The ``ssl-connect`` protocol provides this capability. + +.. code-block:: bash + :caption: Connecting to a bind shell at 1.1.1.1:4444 + + # Full connection string + pwncat connect://192.168.1.1:4444 + # ncat style syntax + pwncat --ssl 192.168.1.1 4444 + pwncat --ssl 192.168.1.1:4444 + Catching a victim reverse shell ------------------------------- @@ -94,6 +122,29 @@ victim machine. This mode is accessed via the ``bind`` protocol. # Assumed protocol, assumed bind address pwncat :4444 +Catching a victim encrypted reverse shell +----------------------------------------- + +In this case, the victim was exploited in such a way that they open an ssl 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 ``ssl-bind`` protocol. + +If the explicit ``ssl-bind`` protocol or the ``--ssl`` argument is provided without an explicit certfile +or keyfile, a self-signed certificate is generated with dummy attributes. The certfile and keyfile can +both point to the same bundled PEM file if both the key and certificate are present. + +.. code-block:: bash + :caption: Catching a reverse shell + + # ncat style syntax + pwncat --ssl --ssl-cert cert.pem --ssl-key cert.pem -lp 4444 + # Full connection string + pwncat ssl-bind://0.0.0.0:4444?certfile=/path/to/cert.pem&keyfile=/path/to/key.pem + # Auto-generated self-signed certificate + pwncat --ssl -lp 4444 + # Auto-generated self-signed certificate with explicit protocol + pwncat ssl-bind://0.0.0.0:4444 + Connecting to a Remote SSH Server --------------------------------- diff --git a/pwncat/__main__.py b/pwncat/__main__.py index 53590ec..e64ac7c 100644 --- a/pwncat/__main__.py +++ b/pwncat/__main__.py @@ -39,6 +39,17 @@ def main(): default=None, help="Custom configuration file (default: ./pwncatrc)", ) + parser.add_argument("--ssl", action="store_true", help="Connect or listen with SSL") + parser.add_argument( + "--ssl-cert", + default=None, + help="Certificate for SSL-encrypted listeners (PEM)", + ) + parser.add_argument( + "--ssl-key", + default=None, + help="Key for SSL-encrypted listeners (PEM)", + ) parser.add_argument( "--identity", "-i", @@ -149,36 +160,74 @@ def main(): or args.listen or args.identity is not None ): - protocol = None - user = None - password = None - host = None - port = None + query_args = {} + query_args["protocol"] = None + query_args["user"] = None + query_args["password"] = None + query_args["host"] = None + query_args["port"] = None + query_args["platform"] = args.platform + query_args["identity"] = args.identity + query_args["certfile"] = args.ssl_cert + query_args["keyfile"] = args.ssl_key + query_args["ssl"] = args.ssl + querystring = None if args.connection_string: m = connect.Command.CONNECTION_PATTERN.match(args.connection_string) - protocol = m.group("protocol") - user = m.group("user") - password = m.group("password") - host = m.group("host") - port = m.group("port") + query_args["protocol"] = m.group("protocol") + query_args["user"] = m.group("user") + query_args["password"] = m.group("password") + query_args["host"] = m.group("host") + query_args["port"] = m.group("port") + querystring = m.group("querystring") - if protocol is not None: - protocol = protocol.removesuffix("://") + if query_args["protocol"] is not None: + query_args["protocol"] = query_args["protocol"].removesuffix("://") - if host is not None and host == "": - host = None + if querystring is not None: + for arg in querystring.split("&"): + if arg.find("=") == -1: + continue - if protocol is not None and args.listen: + key, *value = arg.split("=") + + if key in query_args and query_args[key] is not None: + console.log(f"[red]error[/red]: multiple values for {key}") + return + + query_args[key] = "=".join(value) + + if query_args["host"] is not None and query_args["host"] == "": + query_args["host"] = None + + if query_args["protocol"] is not None and args.listen: console.log( "[red]error[/red]: --listen is not compatible with an explicit connection string" ) return + if ( + query_args["certfile"] is None and query_args["keyfile"] is not None + ) or (query_args["certfile"] is not None and query_args["keyfile"] is None): + console.log( + "[red]error[/red]: both a ssl certificate and key file are required" + ) + return + + if query_args["certfile"] is not None or query_args["keyfile"] is not None: + query_args["ssl"] = True + + if query_args["protocol"] is not None and args.ssl: + console.log( + "[red]error[/red]: --ssl is incompatible with an explicit protocol" + ) + return + if ( sum( [ - port is not None, + query_args["port"] is not None, args.port is not None, args.pos_port is not None, ] @@ -189,22 +238,24 @@ def main(): return if args.port is not None: - port = args.port + query_args["port"] = args.port if args.pos_port is not None: - port = args.pos_port + query_args["port"] = args.pos_port - if port is not None: + if query_args["port"] is not None: try: - port = int(port.lstrip(":")) + query_args["port"] = int(query_args["port"].lstrip(":")) except ValueError: - console.log(f"[red]error[/red]: {port}: invalid port number") + console.log( + f"[red]error[/red]: {query_args['port'].lstrip(':')}: invalid port number" + ) return # Attempt to reconnect via installed implants if ( - protocol is None - and password is None - and port is None + query_args["protocol"] is None + and query_args["password"] is None + and query_args["port"] is None and args.identity is None ): db = manager.db.open() @@ -213,7 +264,10 @@ def main(): # Locate all installed implants for target in db.root.targets: - if target.guid != host and target.public_address[0] != host: + if ( + target.guid != query_args["host"] + and target.public_address[0] != query_args["host"] + ): continue # Collect users @@ -236,13 +290,16 @@ def main(): ) as progress: task = progress.add_task("", status="...") for target, implant_user, implant in implants: - # Check correct user - if user is not None and implant_user.name != user: + # Check correct query_args["user"] + if ( + query_args["user"] is not None + and implant_user.name != query_args["user"] + ): continue # Check correct platform if ( - args.platform is not None - and target.platform != args.platform + query_args["platform"] is not None + and target.platform != query_args["platform"] ): continue @@ -267,13 +324,7 @@ def main(): else: try: manager.create_session( - platform=args.platform, - protocol=protocol, - user=user, - password=password, - host=host, - port=port, - identity=args.identity, + **query_args, ) except (ChannelError, PlatformError) as exc: manager.log(f"connection failed: {exc}") diff --git a/pwncat/channel/__init__.py b/pwncat/channel/__init__.py index 5fcaab1..f5a02d1 100644 --- a/pwncat/channel/__init__.py +++ b/pwncat/channel/__init__.py @@ -581,9 +581,19 @@ def create(protocol: Optional[str] = None, **kwargs) -> Channel: or kwargs["host"] == "0.0.0.0" or kwargs["host"] is None ): - protocols.append("bind") + if ( + kwargs.get("certfile") is not None + or kwargs.get("keyfile") is not None + or kwargs.get("ssl") + ): + protocols.append("ssl-bind") + else: + protocols.append("bind") else: - protocols.append("connect") + if kwargs.get("ssl"): + protocols.append("ssl-connect") + else: + protocols.append("connect") else: protocols = [protocol] @@ -600,8 +610,12 @@ from pwncat.channel.ssh import Ssh # noqa: E402 from pwncat.channel.bind import Bind # noqa: E402 from pwncat.channel.socket import Socket # noqa: E402 from pwncat.channel.connect import Connect # noqa: E402 +from pwncat.channel.ssl_bind import SSLBind # noqa: E402 +from pwncat.channel.ssl_connect import SSLConnect # noqa: E402 register("socket", Socket) register("bind", Bind) register("connect", Connect) register("ssh", Ssh) +register("ssl-bind", SSLBind) +register("ssl-connect", SSLConnect) diff --git a/pwncat/channel/bind.py b/pwncat/channel/bind.py index eaf7b18..33921fe 100644 --- a/pwncat/channel/bind.py +++ b/pwncat/channel/bind.py @@ -28,6 +28,12 @@ class Bind(Socket): if not host or host == "": host = "0.0.0.0" + if isinstance(port, str): + try: + port = int(port) + except ValueError: + raise ChannelError(self, "invalid port number") + if port is None: raise ChannelError(self, "no port specified") @@ -51,6 +57,8 @@ class Bind(Socket): self._socket_connected(client) except KeyboardInterrupt: raise ChannelError(self, "listener aborted") + except socket.error as exc: + raise ChannelError(self, str(exc)) finally: self.server.close() diff --git a/pwncat/channel/connect.py b/pwncat/channel/connect.py index a40214d..e34b534 100644 --- a/pwncat/channel/connect.py +++ b/pwncat/channel/connect.py @@ -24,10 +24,16 @@ class Connect(Socket): def __init__(self, host: str, port: int, **kwargs): if not host: - raise ChannelError("no host address provided") + raise ChannelError(self, "no host address provided") if port is None: - raise ChannelError("no port provided") + raise ChannelError(self, "no port provided") + + if isinstance(port, str): + try: + port = int(port) + except ValueError: + raise ChannelError(self, "invalid port") with Progress( f"connecting to [blue]{host}[/blue]:[cyan]{port}[/cyan]", diff --git a/pwncat/channel/socket.py b/pwncat/channel/socket.py index befdc67..3a51475 100644 --- a/pwncat/channel/socket.py +++ b/pwncat/channel/socket.py @@ -15,6 +15,7 @@ utilize this class to instantiate a session via an established socket. manager.interactive() """ import os +import ssl import errno import fcntl import socket @@ -48,6 +49,9 @@ class Socket(Channel): def __init__(self, client: socket.socket = None, **kwargs): + if isinstance(client, str): + raise ChannelError(self, f"expected socket object not {repr(type(client))}") + if client is not None: # Report host and port number to base channel host, port = client.getpeername() @@ -91,11 +95,14 @@ class Socket(Channel): while written < len(data): try: written += self.client.send(data[written:]) - except BlockingIOError: + except (BlockingIOError, ssl.SSLWantWriteError, ssl.SSLWantReadError): pass except BrokenPipeError as exc: self._connected = False raise ChannelClosed(self) from exc + except (ssl.SSLEOFError, ssl.SSLSyscallError, ssl.SSLZeroReturnError) as exc: + self._connected = False + raise ChannelClosed(self) from exc return len(data) @@ -124,6 +131,11 @@ class Socket(Channel): try: data = data + self.client.recv(count) return data + except ssl.SSLWantReadError: + return data + except (ssl.SSLEOFError, ssl.SSLSyscallError, ssl.SSLZeroReturnError) as exc: + self._connected = False + raise ChannelClosed(self) from exc except socket.error as exc: if exc.args[0] == errno.EAGAIN or exc.args[0] == errno.EWOULDBLOCK: return data diff --git a/pwncat/channel/ssh.py b/pwncat/channel/ssh.py index 0937571..4ffb363 100644 --- a/pwncat/channel/ssh.py +++ b/pwncat/channel/ssh.py @@ -33,8 +33,14 @@ class Ssh(Channel): if port is None: port = 22 + if isinstance(port, str): + try: + port = int(port) + except ValueError: + raise ChannelError(self, "invalid port") + if not user or user is None: - raise ChannelError("you must specify a user") + raise ChannelError(self, "you must specify a user") if password is None and identity is None: password = prompt("Password: ", is_password=True) @@ -51,7 +57,7 @@ class Ssh(Channel): t.start_client() except paramiko.SSHException: sock.close() - raise ChannelError("ssh negotiation failed") + raise ChannelError(self, "ssh negotiation failed") if identity is not None: try: @@ -67,23 +73,23 @@ class Ssh(Channel): try: key = paramiko.RSAKey.from_private_key_file(identity, password) except paramiko.ssh_exception.SSHException: - raise ChannelError("invalid private key or passphrase") + raise ChannelError(self, "invalid private key or passphrase") # Attempt authentication try: t.auth_publickey(user, key) except paramiko.ssh_exception.AuthenticationException as exc: - raise ChannelError(str(exc)) + raise ChannelError(self, str(exc)) else: try: t.auth_password(user, password) except paramiko.ssh_exception.AuthenticationException as exc: - raise ChannelError(str(exc)) + raise ChannelError(self, str(exc)) if not t.is_authenticated(): t.close() sock.close() - raise ChannelError("authentication failed") + raise ChannelError(self, "authentication failed") # Open an interactive session chan = t.open_session() diff --git a/pwncat/channel/ssl_bind.py b/pwncat/channel/ssl_bind.py new file mode 100644 index 0000000..9ffbdb8 --- /dev/null +++ b/pwncat/channel/ssl_bind.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +import ssl +import datetime +import tempfile + +from cryptography import x509 +from cryptography.x509.oid import NameOID +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import rsa + +from pwncat.channel import ChannelError +from pwncat.channel.bind import Bind + + +class SSLBind(Bind): + def __init__(self, certfile: str = None, keyfile: str = None, **kwargs): + super().__init__(**kwargs) + + self.context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + + if certfile is None and keyfile is None: + certfile = keyfile = self._generate_self_signed_cert() + + self.context.load_cert_chain(certfile, keyfile) + + self.server = self.context.wrap_socket(self.server) + + def connect(self): + + try: + super().connect() + except ssl.SSLError as exc: + raise ChannelError(self, str(exc)) + + def _generate_self_signed_cert(self): + """Generate a self-signed certificate""" + + with tempfile.NamedTemporaryFile("wb", delete=False) as filp: + key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + filp.write( + key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + ) + + # Literally taken from: https://cryptography.io/en/latest/x509/tutorial/ + subject = issuer = x509.Name( + [ + x509.NameAttribute(NameOID.COUNTRY_NAME, u"US"), + x509.NameAttribute(NameOID.COUNTRY_NAME, u"US"), + x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u"California"), + x509.NameAttribute(NameOID.LOCALITY_NAME, u"San Francisco"), + x509.NameAttribute(NameOID.ORGANIZATION_NAME, u"My Company"), + x509.NameAttribute(NameOID.COMMON_NAME, u"mysite.com"), + ] + ) + cert = ( + x509.CertificateBuilder() + .subject_name(subject) + .issuer_name(issuer) + .public_key(key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(datetime.datetime.utcnow()) + .not_valid_after( + datetime.datetime.utcnow() + datetime.timedelta(days=365) + ) + .add_extension( + x509.SubjectAlternativeName([x509.DNSName(u"localhost")]), + critical=False, + ) + .sign(key, hashes.SHA256()) + ) + + filp.write(cert.public_bytes(serialization.Encoding.PEM)) + + return filp.name diff --git a/pwncat/channel/ssl_connect.py b/pwncat/channel/ssl_connect.py new file mode 100644 index 0000000..28d8c71 --- /dev/null +++ b/pwncat/channel/ssl_connect.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +import ssl + +from pwncat.channel import ChannelError +from pwncat.channel.connect import Connect + + +class SSLConnect(Connect): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def _socket_connected(self, client): + try: + self.context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + self.context.check_hostname = False + self.context.verify_mode = ssl.VerifyMode.CERT_NONE + + client = self.context.wrap_socket(client) + except ssl.SSLError as exc: + raise ChannelError(str(exc)) + + super()._socket_connected(client) diff --git a/pwncat/commands/connect.py b/pwncat/commands/connect.py index cd77bf7..179447a 100644 --- a/pwncat/commands/connect.py +++ b/pwncat/commands/connect.py @@ -58,6 +58,16 @@ class Command(CommandDefinition): action="store_true", help="List installed implants with remote connection capability", ), + "--ssl-cert": Parameter( + Complete.LOCAL_FILE, + help="Certificate for SSL-encrypted listeners (PEM)", + ), + "--ssl-key": Parameter( + Complete.LOCAL_FILE, help="Key for SSL-encrypted listeners (PEM)" + ), + "--ssl": Parameter( + Complete.NONE, action="store_true", help="Connect or listen with SSL" + ), "connection_string": Parameter( Complete.NONE, metavar="[protocol://][user[:password]@][host][:port]", @@ -73,16 +83,23 @@ class Command(CommandDefinition): } LOCAL = True CONNECTION_PATTERN = re.compile( - r"""^(?P[-a-zA-Z0-9_]*://)?((?P[^:@]*)?(?P:(\\@|[^@])*)?@)?(?P[^:]*)?(?P:[0-9]*)?$""" + r"""^(?P[-a-zA-Z0-9_]*://)?((?P[^:@]*)?(?P:(\\@|[^@])*)?@)?(?P[^:]*)?(?P:[0-9]*)?(\?(?P.*))?$""" ) def run(self, manager: "pwncat.manager.Manager", args): - protocol = None - user = None - password = None - host = None - port = None + query_args = {} + query_args["protocol"] = None + query_args["user"] = None + query_args["password"] = None + query_args["host"] = None + query_args["port"] = None + query_args["platform"] = args.platform + query_args["identity"] = args.identity + query_args["certfile"] = args.ssl_cert + query_args["keyfile"] = args.ssl_key + query_args["ssl"] = args.ssl + querystring = None used_implant = None if args.list: @@ -128,28 +145,53 @@ class Command(CommandDefinition): if args.connection_string: m = self.CONNECTION_PATTERN.match(args.connection_string) - protocol = m.group("protocol") - user = m.group("user") - password = m.group("password") - host = m.group("host") - port = m.group("port") + query_args["protocol"] = m.group("protocol") + query_args["user"] = m.group("user") + query_args["password"] = m.group("password") + query_args["host"] = m.group("host") + query_args["port"] = m.group("port") + querystring = m.group("querystring") - if protocol is not None: - protocol = protocol.removesuffix("://") + if query_args["protocol"] is not None: + query_args["protocol"] = query_args["protocol"].removesuffix("://") - if host is not None and host == "": - host = None + if querystring is not None: + for arg in querystring.split("&"): + if arg.find("=") == -1: + continue - if protocol is not None and args.listen: + key, *value = arg.split("=") + + if key in query_args and query_args[key] is not None: + console.log(f"[red]error[/red]: multiple values for {key}") + return + + query_args[key] = "=".join(value) + + if query_args["host"] is not None and query_args["host"] == "": + query_args["host"] = None + + if query_args["protocol"] is not None and args.listen: console.log( "[red]error[/red]: --listen is not compatible with an explicit connection string" ) return + if (query_args["certfile"] is None and query_args["keyfile"] is not None) or ( + query_args["certfile"] is not None and query_args["keyfile"] is None + ): + console.log( + "[red]error[/red]: both a ssl certificate and key file are required" + ) + return + + if query_args["certfile"] is not None or query_args["keyfile"] is not None: + query_args["ssl"] = True + if ( sum( [ - port is not None, + query_args["port"] is not None, args.port is not None, args.pos_port is not None, ] @@ -159,23 +201,27 @@ class Command(CommandDefinition): console.log("[red]error[/red]: multiple ports specified") return - if args.port is not None: - port = args.port - if args.pos_port is not None: - port = args.pos_port + console.log(args.pos_port) - if port is not None: + if args.port is not None: + query_args["port"] = args.port + if args.pos_port is not None: + query_args["port"] = args.pos_port + + if query_args["port"] is not None: try: - port = int(port.lstrip(":")) + query_args["port"] = int(query_args["port"].lstrip(":")) except ValueError: - console.log(f"[red]error[/red]: {port}: invalid port number") + console.log( + f"[red]error[/red]: {query_args['port'].lstrip(':')}: invalid port number" + ) return # Attempt to reconnect via installed implants if ( - protocol is None - and password is None - and port is None + query_args["protocol"] is None + and query_args["password"] is None + and query_args["port"] is None and args.identity is None ): db = manager.db.open() @@ -184,7 +230,10 @@ class Command(CommandDefinition): # Locate all installed implants for target in db.root.targets: - if target.guid != host and target.public_address[0] != host: + if ( + target.guid != query_args["host"] + and target.public_address[0] != query_args["host"] + ): continue # Collect users @@ -207,11 +256,17 @@ class Command(CommandDefinition): ) as progress: task = progress.add_task("", status="...") for target, implant_user, implant in implants: - # Check correct user - if user is not None and implant_user.name != user: + # Check correct query_args["user"] + if ( + query_args["user"] is not None + and implant_user.name != query_args["user"] + ): continue # Check correct platform - if args.platform is not None and target.platform != args.platform: + if ( + query_args["platform"] is not None + and target.platform != query_args["platform"] + ): continue progress.update( @@ -225,17 +280,10 @@ class Command(CommandDefinition): used_implant = implant break except ModuleFailed: + db.transaction_manager.commit() continue if used_implant is not None: manager.target.log(f"connected via {used_implant.title(manager.target)}") else: - manager.create_session( - platform=args.platform, - protocol=protocol, - user=user, - password=password, - host=host, - port=port, - identity=args.identity, - ) + manager.create_session(**query_args) diff --git a/test.py b/test.py index 17b8b7a..ffb073c 100755 --- a/test.py +++ b/test.py @@ -19,12 +19,11 @@ with pwncat.manager.Manager("data/pwncatrc") as manager: # session = manager.create_session("windows", host="192.168.56.10", port=4444) # session = manager.create_session("windows", host="192.168.122.11", port=4444) # session = manager.create_session("linux", host="pwncat-ubuntu", port=4444) - session = manager.create_session("linux", host="127.0.0.1", port=4444) + # session = manager.create_session("linux", host="127.0.0.1", port=4444) + session = manager.create_session( + "linux", certfile="/tmp/cert.pem", keyfile="/tmp/cert.pem", port=4444 + ) # session.platform.powershell("amsiutils") - with open("/tmp/random", "rb") as source: - with session.platform.open("/tmp/random", "wb") as destination: - shutil.copyfileobj(source, destination) - manager.interactive()