mirror of
https://github.com/calebstewart/pwncat.git
synced 2024-11-24 01:25:37 +01:00
Merge pull request #139 from calebstewart/issue-118-ssl-bind
- Added `ssl-bind` and `ssl-connect` channel protocols for encrypted shells - Added `ncat`-style arguments for the entrypoint and `connect` command (e.g. `--ssl` and `--ssl-cert`/`--ssl-key`) - Added query-string arguments to connection strings for both the entrypoint and the `connect` command.
This commit is contained in:
commit
d8a566a51d
@ -14,6 +14,11 @@ and simply didn't have the time to go back and retroactively create one.
|
||||
### Changed
|
||||
- Changed session tracking so session IDs aren't reused
|
||||
- Changed zsh prompt to match CWD of other shell prompts
|
||||
### Added
|
||||
- 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
|
||||
|
@ -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
|
||||
-----------------------
|
||||
|
@ -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
|
||||
---------------------------------
|
||||
|
||||
|
@ -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}")
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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]",
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
78
pwncat/channel/ssl_bind.py
Normal file
78
pwncat/channel/ssl_bind.py
Normal file
@ -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
|
22
pwncat/channel/ssl_connect.py
Normal file
22
pwncat/channel/ssl_connect.py
Normal file
@ -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)
|
@ -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<protocol>[-a-zA-Z0-9_]*://)?((?P<user>[^:@]*)?(?P<password>:(\\@|[^@])*)?@)?(?P<host>[^:]*)?(?P<port>:[0-9]*)?$"""
|
||||
r"""^(?P<protocol>[-a-zA-Z0-9_]*://)?((?P<user>[^:@]*)?(?P<password>:(\\@|[^@])*)?@)?(?P<host>[^:]*)?(?P<port>:[0-9]*)?(\?(?P<querystring>.*))?$"""
|
||||
)
|
||||
|
||||
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)
|
||||
|
9
test.py
9
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()
|
||||
|
Loading…
Reference in New Issue
Block a user