mirror of
https://github.com/calebstewart/pwncat.git
synced 2024-11-27 19:04:15 +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
|
||||||
- Changed session tracking so session IDs aren't reused
|
- Changed session tracking so session IDs aren't reused
|
||||||
- Changed zsh prompt to match CWD of other shell prompts
|
- 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
|
## [0.4.2] - 2021-06-15
|
||||||
Quick patch release due to corrected bug in `ChannelFile` which caused command
|
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
|
.. code-block:: bash
|
||||||
|
|
||||||
$ pwncat --help
|
$ pwncat --help
|
||||||
usage: pwncat [-h] [--config CONFIG] [--identity IDENTITY] [--listen]
|
usage: pwncat [-h] [--version] [--download-plugins] [--config CONFIG] [--ssl] [--ssl-cert SSL_CERT]
|
||||||
[--platform PLATFORM] [--port PORT] [--list]
|
[--ssl-key SSL_KEY] [--identity IDENTITY] [--listen] [--platform PLATFORM] [--port PORT] [--list]
|
||||||
[[protocol://][user[:password]@][host][:port]] [port]
|
[[protocol://][user[:password]@][host][:port]] [port]
|
||||||
|
|
||||||
Start interactive pwncat session and optionally connect to existing victim
|
Start interactive pwncat session and optionally connect to existing victim via a known platform and channel type. This
|
||||||
via a known platform and channel type. This entrypoint can also be used to
|
entrypoint can also be used to list known implants on previous targets.
|
||||||
list known implants on previous targets.
|
|
||||||
|
|
||||||
positional arguments:
|
positional arguments:
|
||||||
[protocol://][user[:password]@][host][:port]
|
[protocol://][user[:password]@][host][:port]
|
||||||
Connection string describing victim
|
Connection string describing victim
|
||||||
port Alternative port number to support netcat-style
|
port Alternative port number to support netcat-style syntax
|
||||||
syntax
|
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-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
|
--config CONFIG, -c CONFIG
|
||||||
Custom configuration file (default: ./pwncatrc)
|
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
|
--identity IDENTITY, -i IDENTITY
|
||||||
Private key for SSH authentication
|
Private key for SSH authentication
|
||||||
--listen, -l Enable the `bind` protocol (supports netcat-style
|
--listen, -l Enable the `bind` protocol (supports netcat-style syntax)
|
||||||
syntax)
|
|
||||||
--platform PLATFORM, -m PLATFORM
|
--platform PLATFORM, -m PLATFORM
|
||||||
Name of the platform to use (default: linux)
|
Name of the platform to use (default: linux)
|
||||||
--port PORT, -p PORT Alternative way to specify port to support netcat-
|
--port PORT, -p PORT Alternative way to specify port to support netcat-style syntax
|
||||||
style syntax
|
--list List installed implants with remote connection capability
|
||||||
--list List installed implants with remote connection
|
|
||||||
capability
|
|
||||||
|
|
||||||
Windows Plugin Binaries
|
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
|
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.
|
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
|
pwncat exposes these different C2 channel protocols via the ``protocol`` field of the connection string
|
||||||
discussed below.
|
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
|
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:
|
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
|
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
|
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
|
If the ``protocol`` field is not specified, pwncat will attempt to figure out the correct protocol
|
||||||
contextually. The following rules apply:
|
contextually. The following rules apply:
|
||||||
|
|
||||||
- If a user and host are provided, assume ``ssh`` protocol
|
- 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 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``
|
- If a second positional integer parameter is specified and ``--ssl`` is not, the protocol is assumed
|
||||||
- This is the ``netcat`` syntax seen in the below examples for the ``connect`` protocol.
|
to be ``connect``
|
||||||
- If the ``-l`` parameter is used, the protocol is assumed to be ``bind``.
|
- If a second positional integer parameter is specified and ``--ssl`` is provided, the protocol is
|
||||||
- This is the ``netcat`` syntax seen in the below examples for the ``bind`` protocol.
|
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
|
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
|
# Connection string with assumed protocol
|
||||||
pwncat 192.168.1.1:4444
|
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
|
Catching a victim reverse shell
|
||||||
-------------------------------
|
-------------------------------
|
||||||
|
|
||||||
@ -94,6 +122,29 @@ victim machine. This mode is accessed via the ``bind`` protocol.
|
|||||||
# Assumed protocol, assumed bind address
|
# Assumed protocol, assumed bind address
|
||||||
pwncat :4444
|
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
|
Connecting to a Remote SSH Server
|
||||||
---------------------------------
|
---------------------------------
|
||||||
|
|
||||||
|
@ -39,6 +39,17 @@ def main():
|
|||||||
default=None,
|
default=None,
|
||||||
help="Custom configuration file (default: ./pwncatrc)",
|
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(
|
parser.add_argument(
|
||||||
"--identity",
|
"--identity",
|
||||||
"-i",
|
"-i",
|
||||||
@ -149,36 +160,74 @@ def main():
|
|||||||
or args.listen
|
or args.listen
|
||||||
or args.identity is not None
|
or args.identity is not None
|
||||||
):
|
):
|
||||||
protocol = None
|
query_args = {}
|
||||||
user = None
|
query_args["protocol"] = None
|
||||||
password = None
|
query_args["user"] = None
|
||||||
host = None
|
query_args["password"] = None
|
||||||
port = 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:
|
if args.connection_string:
|
||||||
m = connect.Command.CONNECTION_PATTERN.match(args.connection_string)
|
m = connect.Command.CONNECTION_PATTERN.match(args.connection_string)
|
||||||
protocol = m.group("protocol")
|
query_args["protocol"] = m.group("protocol")
|
||||||
user = m.group("user")
|
query_args["user"] = m.group("user")
|
||||||
password = m.group("password")
|
query_args["password"] = m.group("password")
|
||||||
host = m.group("host")
|
query_args["host"] = m.group("host")
|
||||||
port = m.group("port")
|
query_args["port"] = m.group("port")
|
||||||
|
querystring = m.group("querystring")
|
||||||
|
|
||||||
if protocol is not None:
|
if query_args["protocol"] is not None:
|
||||||
protocol = protocol.removesuffix("://")
|
query_args["protocol"] = query_args["protocol"].removesuffix("://")
|
||||||
|
|
||||||
if host is not None and host == "":
|
if querystring is not None:
|
||||||
host = 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(
|
console.log(
|
||||||
"[red]error[/red]: --listen is not compatible with an explicit connection string"
|
"[red]error[/red]: --listen is not compatible with an explicit connection string"
|
||||||
)
|
)
|
||||||
return
|
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 (
|
if (
|
||||||
sum(
|
sum(
|
||||||
[
|
[
|
||||||
port is not None,
|
query_args["port"] is not None,
|
||||||
args.port is not None,
|
args.port is not None,
|
||||||
args.pos_port is not None,
|
args.pos_port is not None,
|
||||||
]
|
]
|
||||||
@ -189,22 +238,24 @@ def main():
|
|||||||
return
|
return
|
||||||
|
|
||||||
if args.port is not None:
|
if args.port is not None:
|
||||||
port = args.port
|
query_args["port"] = args.port
|
||||||
if args.pos_port is not None:
|
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:
|
try:
|
||||||
port = int(port.lstrip(":"))
|
query_args["port"] = int(query_args["port"].lstrip(":"))
|
||||||
except ValueError:
|
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
|
return
|
||||||
|
|
||||||
# Attempt to reconnect via installed implants
|
# Attempt to reconnect via installed implants
|
||||||
if (
|
if (
|
||||||
protocol is None
|
query_args["protocol"] is None
|
||||||
and password is None
|
and query_args["password"] is None
|
||||||
and port is None
|
and query_args["port"] is None
|
||||||
and args.identity is None
|
and args.identity is None
|
||||||
):
|
):
|
||||||
db = manager.db.open()
|
db = manager.db.open()
|
||||||
@ -213,7 +264,10 @@ def main():
|
|||||||
# Locate all installed implants
|
# Locate all installed implants
|
||||||
for target in db.root.targets:
|
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
|
continue
|
||||||
|
|
||||||
# Collect users
|
# Collect users
|
||||||
@ -236,13 +290,16 @@ def main():
|
|||||||
) as progress:
|
) as progress:
|
||||||
task = progress.add_task("", status="...")
|
task = progress.add_task("", status="...")
|
||||||
for target, implant_user, implant in implants:
|
for target, implant_user, implant in implants:
|
||||||
# Check correct user
|
# Check correct query_args["user"]
|
||||||
if user is not None and implant_user.name != user:
|
if (
|
||||||
|
query_args["user"] is not None
|
||||||
|
and implant_user.name != query_args["user"]
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
# Check correct platform
|
# Check correct platform
|
||||||
if (
|
if (
|
||||||
args.platform is not None
|
query_args["platform"] is not None
|
||||||
and target.platform != args.platform
|
and target.platform != query_args["platform"]
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -267,13 +324,7 @@ def main():
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
manager.create_session(
|
manager.create_session(
|
||||||
platform=args.platform,
|
**query_args,
|
||||||
protocol=protocol,
|
|
||||||
user=user,
|
|
||||||
password=password,
|
|
||||||
host=host,
|
|
||||||
port=port,
|
|
||||||
identity=args.identity,
|
|
||||||
)
|
)
|
||||||
except (ChannelError, PlatformError) as exc:
|
except (ChannelError, PlatformError) as exc:
|
||||||
manager.log(f"connection failed: {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"] == "0.0.0.0"
|
||||||
or kwargs["host"] is None
|
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:
|
else:
|
||||||
protocols.append("connect")
|
if kwargs.get("ssl"):
|
||||||
|
protocols.append("ssl-connect")
|
||||||
|
else:
|
||||||
|
protocols.append("connect")
|
||||||
else:
|
else:
|
||||||
protocols = [protocol]
|
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.bind import Bind # noqa: E402
|
||||||
from pwncat.channel.socket import Socket # noqa: E402
|
from pwncat.channel.socket import Socket # noqa: E402
|
||||||
from pwncat.channel.connect import Connect # 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("socket", Socket)
|
||||||
register("bind", Bind)
|
register("bind", Bind)
|
||||||
register("connect", Connect)
|
register("connect", Connect)
|
||||||
register("ssh", Ssh)
|
register("ssh", Ssh)
|
||||||
|
register("ssl-bind", SSLBind)
|
||||||
|
register("ssl-connect", SSLConnect)
|
||||||
|
@ -28,6 +28,12 @@ class Bind(Socket):
|
|||||||
if not host or host == "":
|
if not host or host == "":
|
||||||
host = "0.0.0.0"
|
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:
|
if port is None:
|
||||||
raise ChannelError(self, "no port specified")
|
raise ChannelError(self, "no port specified")
|
||||||
|
|
||||||
@ -51,6 +57,8 @@ class Bind(Socket):
|
|||||||
self._socket_connected(client)
|
self._socket_connected(client)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
raise ChannelError(self, "listener aborted")
|
raise ChannelError(self, "listener aborted")
|
||||||
|
except socket.error as exc:
|
||||||
|
raise ChannelError(self, str(exc))
|
||||||
finally:
|
finally:
|
||||||
self.server.close()
|
self.server.close()
|
||||||
|
|
||||||
|
@ -24,10 +24,16 @@ class Connect(Socket):
|
|||||||
|
|
||||||
def __init__(self, host: str, port: int, **kwargs):
|
def __init__(self, host: str, port: int, **kwargs):
|
||||||
if not host:
|
if not host:
|
||||||
raise ChannelError("no host address provided")
|
raise ChannelError(self, "no host address provided")
|
||||||
|
|
||||||
if port is None:
|
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(
|
with Progress(
|
||||||
f"connecting to [blue]{host}[/blue]:[cyan]{port}[/cyan]",
|
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()
|
manager.interactive()
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
import ssl
|
||||||
import errno
|
import errno
|
||||||
import fcntl
|
import fcntl
|
||||||
import socket
|
import socket
|
||||||
@ -48,6 +49,9 @@ class Socket(Channel):
|
|||||||
|
|
||||||
def __init__(self, client: socket.socket = None, **kwargs):
|
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:
|
if client is not None:
|
||||||
# Report host and port number to base channel
|
# Report host and port number to base channel
|
||||||
host, port = client.getpeername()
|
host, port = client.getpeername()
|
||||||
@ -91,11 +95,14 @@ class Socket(Channel):
|
|||||||
while written < len(data):
|
while written < len(data):
|
||||||
try:
|
try:
|
||||||
written += self.client.send(data[written:])
|
written += self.client.send(data[written:])
|
||||||
except BlockingIOError:
|
except (BlockingIOError, ssl.SSLWantWriteError, ssl.SSLWantReadError):
|
||||||
pass
|
pass
|
||||||
except BrokenPipeError as exc:
|
except BrokenPipeError as exc:
|
||||||
self._connected = False
|
self._connected = False
|
||||||
raise ChannelClosed(self) from exc
|
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)
|
return len(data)
|
||||||
|
|
||||||
@ -124,6 +131,11 @@ class Socket(Channel):
|
|||||||
try:
|
try:
|
||||||
data = data + self.client.recv(count)
|
data = data + self.client.recv(count)
|
||||||
return data
|
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:
|
except socket.error as exc:
|
||||||
if exc.args[0] == errno.EAGAIN or exc.args[0] == errno.EWOULDBLOCK:
|
if exc.args[0] == errno.EAGAIN or exc.args[0] == errno.EWOULDBLOCK:
|
||||||
return data
|
return data
|
||||||
|
@ -33,8 +33,14 @@ class Ssh(Channel):
|
|||||||
if port is None:
|
if port is None:
|
||||||
port = 22
|
port = 22
|
||||||
|
|
||||||
|
if isinstance(port, str):
|
||||||
|
try:
|
||||||
|
port = int(port)
|
||||||
|
except ValueError:
|
||||||
|
raise ChannelError(self, "invalid port")
|
||||||
|
|
||||||
if not user or user is None:
|
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:
|
if password is None and identity is None:
|
||||||
password = prompt("Password: ", is_password=True)
|
password = prompt("Password: ", is_password=True)
|
||||||
@ -51,7 +57,7 @@ class Ssh(Channel):
|
|||||||
t.start_client()
|
t.start_client()
|
||||||
except paramiko.SSHException:
|
except paramiko.SSHException:
|
||||||
sock.close()
|
sock.close()
|
||||||
raise ChannelError("ssh negotiation failed")
|
raise ChannelError(self, "ssh negotiation failed")
|
||||||
|
|
||||||
if identity is not None:
|
if identity is not None:
|
||||||
try:
|
try:
|
||||||
@ -67,23 +73,23 @@ class Ssh(Channel):
|
|||||||
try:
|
try:
|
||||||
key = paramiko.RSAKey.from_private_key_file(identity, password)
|
key = paramiko.RSAKey.from_private_key_file(identity, password)
|
||||||
except paramiko.ssh_exception.SSHException:
|
except paramiko.ssh_exception.SSHException:
|
||||||
raise ChannelError("invalid private key or passphrase")
|
raise ChannelError(self, "invalid private key or passphrase")
|
||||||
|
|
||||||
# Attempt authentication
|
# Attempt authentication
|
||||||
try:
|
try:
|
||||||
t.auth_publickey(user, key)
|
t.auth_publickey(user, key)
|
||||||
except paramiko.ssh_exception.AuthenticationException as exc:
|
except paramiko.ssh_exception.AuthenticationException as exc:
|
||||||
raise ChannelError(str(exc))
|
raise ChannelError(self, str(exc))
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
t.auth_password(user, password)
|
t.auth_password(user, password)
|
||||||
except paramiko.ssh_exception.AuthenticationException as exc:
|
except paramiko.ssh_exception.AuthenticationException as exc:
|
||||||
raise ChannelError(str(exc))
|
raise ChannelError(self, str(exc))
|
||||||
|
|
||||||
if not t.is_authenticated():
|
if not t.is_authenticated():
|
||||||
t.close()
|
t.close()
|
||||||
sock.close()
|
sock.close()
|
||||||
raise ChannelError("authentication failed")
|
raise ChannelError(self, "authentication failed")
|
||||||
|
|
||||||
# Open an interactive session
|
# Open an interactive session
|
||||||
chan = t.open_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",
|
action="store_true",
|
||||||
help="List installed implants with remote connection capability",
|
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(
|
"connection_string": Parameter(
|
||||||
Complete.NONE,
|
Complete.NONE,
|
||||||
metavar="[protocol://][user[:password]@][host][:port]",
|
metavar="[protocol://][user[:password]@][host][:port]",
|
||||||
@ -73,16 +83,23 @@ class Command(CommandDefinition):
|
|||||||
}
|
}
|
||||||
LOCAL = True
|
LOCAL = True
|
||||||
CONNECTION_PATTERN = re.compile(
|
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):
|
def run(self, manager: "pwncat.manager.Manager", args):
|
||||||
|
|
||||||
protocol = None
|
query_args = {}
|
||||||
user = None
|
query_args["protocol"] = None
|
||||||
password = None
|
query_args["user"] = None
|
||||||
host = None
|
query_args["password"] = None
|
||||||
port = 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
|
used_implant = None
|
||||||
|
|
||||||
if args.list:
|
if args.list:
|
||||||
@ -128,28 +145,53 @@ class Command(CommandDefinition):
|
|||||||
|
|
||||||
if args.connection_string:
|
if args.connection_string:
|
||||||
m = self.CONNECTION_PATTERN.match(args.connection_string)
|
m = self.CONNECTION_PATTERN.match(args.connection_string)
|
||||||
protocol = m.group("protocol")
|
query_args["protocol"] = m.group("protocol")
|
||||||
user = m.group("user")
|
query_args["user"] = m.group("user")
|
||||||
password = m.group("password")
|
query_args["password"] = m.group("password")
|
||||||
host = m.group("host")
|
query_args["host"] = m.group("host")
|
||||||
port = m.group("port")
|
query_args["port"] = m.group("port")
|
||||||
|
querystring = m.group("querystring")
|
||||||
|
|
||||||
if protocol is not None:
|
if query_args["protocol"] is not None:
|
||||||
protocol = protocol.removesuffix("://")
|
query_args["protocol"] = query_args["protocol"].removesuffix("://")
|
||||||
|
|
||||||
if host is not None and host == "":
|
if querystring is not None:
|
||||||
host = 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(
|
console.log(
|
||||||
"[red]error[/red]: --listen is not compatible with an explicit connection string"
|
"[red]error[/red]: --listen is not compatible with an explicit connection string"
|
||||||
)
|
)
|
||||||
return
|
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 (
|
if (
|
||||||
sum(
|
sum(
|
||||||
[
|
[
|
||||||
port is not None,
|
query_args["port"] is not None,
|
||||||
args.port is not None,
|
args.port is not None,
|
||||||
args.pos_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")
|
console.log("[red]error[/red]: multiple ports specified")
|
||||||
return
|
return
|
||||||
|
|
||||||
if args.port is not None:
|
console.log(args.pos_port)
|
||||||
port = args.port
|
|
||||||
if args.pos_port is not None:
|
|
||||||
port = 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:
|
try:
|
||||||
port = int(port.lstrip(":"))
|
query_args["port"] = int(query_args["port"].lstrip(":"))
|
||||||
except ValueError:
|
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
|
return
|
||||||
|
|
||||||
# Attempt to reconnect via installed implants
|
# Attempt to reconnect via installed implants
|
||||||
if (
|
if (
|
||||||
protocol is None
|
query_args["protocol"] is None
|
||||||
and password is None
|
and query_args["password"] is None
|
||||||
and port is None
|
and query_args["port"] is None
|
||||||
and args.identity is None
|
and args.identity is None
|
||||||
):
|
):
|
||||||
db = manager.db.open()
|
db = manager.db.open()
|
||||||
@ -184,7 +230,10 @@ class Command(CommandDefinition):
|
|||||||
# Locate all installed implants
|
# Locate all installed implants
|
||||||
for target in db.root.targets:
|
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
|
continue
|
||||||
|
|
||||||
# Collect users
|
# Collect users
|
||||||
@ -207,11 +256,17 @@ class Command(CommandDefinition):
|
|||||||
) as progress:
|
) as progress:
|
||||||
task = progress.add_task("", status="...")
|
task = progress.add_task("", status="...")
|
||||||
for target, implant_user, implant in implants:
|
for target, implant_user, implant in implants:
|
||||||
# Check correct user
|
# Check correct query_args["user"]
|
||||||
if user is not None and implant_user.name != user:
|
if (
|
||||||
|
query_args["user"] is not None
|
||||||
|
and implant_user.name != query_args["user"]
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
# Check correct platform
|
# 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
|
continue
|
||||||
|
|
||||||
progress.update(
|
progress.update(
|
||||||
@ -225,17 +280,10 @@ class Command(CommandDefinition):
|
|||||||
used_implant = implant
|
used_implant = implant
|
||||||
break
|
break
|
||||||
except ModuleFailed:
|
except ModuleFailed:
|
||||||
|
db.transaction_manager.commit()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if used_implant is not None:
|
if used_implant is not None:
|
||||||
manager.target.log(f"connected via {used_implant.title(manager.target)}")
|
manager.target.log(f"connected via {used_implant.title(manager.target)}")
|
||||||
else:
|
else:
|
||||||
manager.create_session(
|
manager.create_session(**query_args)
|
||||||
platform=args.platform,
|
|
||||||
protocol=protocol,
|
|
||||||
user=user,
|
|
||||||
password=password,
|
|
||||||
host=host,
|
|
||||||
port=port,
|
|
||||||
identity=args.identity,
|
|
||||||
)
|
|
||||||
|
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.56.10", port=4444)
|
||||||
# session = manager.create_session("windows", host="192.168.122.11", 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="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")
|
# 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()
|
manager.interactive()
|
||||||
|
Loading…
Reference in New Issue
Block a user