Merge pull request #227 from calebstewart/issue-91-paramiko-ng

Add support for SSHAgent and Key Types outside of RSA
This commit is contained in:
Caleb Stewart 2021-12-26 03:21:14 -05:00 committed by GitHub
commit 6a913f0fb3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 112 additions and 112 deletions

View File

@ -11,6 +11,8 @@ and simply didn't have the time to go back and retroactively create one.
### Changed ### Changed
- Fixed parsing of `--ssl` argument in main entrypoint ([#225](https://github.com/calebstewart/pwncat/issues/225)) - Fixed parsing of `--ssl` argument in main entrypoint ([#225](https://github.com/calebstewart/pwncat/issues/225))
- Replaced `paramiko` with `paramiko-ng`
- Utilized Paramiko SSHClient which will also utilize the SSHAgent if available by default and supports key types aside from RSA ([#91](https://github.com/calebstewart/pwncat/issues/91))
## [0.5.1] - 2021-12-07 ## [0.5.1] - 2021-12-07
Minor bug fixes. Mainly typos from changing the package name. Minor bug fixes. Mainly typos from changing the package name.

65
poetry.lock generated
View File

@ -120,7 +120,7 @@ test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"]
[[package]] [[package]]
name = "cryptography" name = "cryptography"
version = "3.4.7" version = "36.0.1"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
category = "main" category = "main"
optional = false optional = false
@ -131,11 +131,11 @@ cffi = ">=1.12"
[package.extras] [package.extras]
docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"]
docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"]
pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"]
sdist = ["setuptools-rust (>=0.11.4)"] sdist = ["setuptools_rust (>=0.11.4)"]
ssh = ["bcrypt (>=3.1.5)"] ssh = ["bcrypt (>=3.1.5)"]
test = ["pytest (>=6.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] test = ["pytest (>=6.2.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"]
[[package]] [[package]]
name = "docutils" name = "docutils"
@ -261,23 +261,20 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
pyparsing = ">=2.0.2" pyparsing = ">=2.0.2"
[[package]] [[package]]
name = "paramiko" name = "paramiko-ng"
version = "2.7.2" version = "2.8.8"
description = "SSH2 protocol library" description = "SSH2 protocol library"
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.dependencies] [package.dependencies]
bcrypt = ">=3.1.3" bcrypt = ">=3"
cryptography = ">=2.5" cryptography = ">=1.6"
pynacl = ">=1.0.1"
[package.extras] [package.extras]
all = ["pyasn1 (>=0.1.7)", "pynacl (>=1.0.1)", "bcrypt (>=3.1.3)", "invoke (>=1.3)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"] ed25519 = ["pynacl"]
ed25519 = ["pynacl (>=1.0.1)", "bcrypt (>=3.1.3)"] gssapi = ["gssapi", "pyasn1"]
gssapi = ["pyasn1 (>=0.1.7)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"]
invoke = ["invoke (>=1.3)"]
[[package]] [[package]]
name = "persistent" name = "persistent"
@ -787,7 +784,7 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.9" python-versions = "^3.9"
content-hash = "9357cb6dd124d9b201849436e710ae4baec36f65104fa505e5e155c60d602e79" content-hash = "23945db2620e31067d3b8810462ebaa6627fde4220ed01d49ba009e89962a725"
[metadata.files] [metadata.files]
alabaster = [ alabaster = [
@ -931,18 +928,26 @@ commonmark = [
{file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"},
] ]
cryptography = [ cryptography = [
{file = "cryptography-3.4.7-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1"}, {file = "cryptography-36.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:73bc2d3f2444bcfeac67dd130ff2ea598ea5f20b40e36d19821b4df8c9c5037b"},
{file = "cryptography-3.4.7-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250"}, {file = "cryptography-36.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:2d87cdcb378d3cfed944dac30596da1968f88fb96d7fc34fdae30a99054b2e31"},
{file = "cryptography-3.4.7-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2"}, {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74d6c7e80609c0f4c2434b97b80c7f8fdfaa072ca4baab7e239a15d6d70ed73a"},
{file = "cryptography-3.4.7-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6"}, {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:6c0c021f35b421ebf5976abf2daacc47e235f8b6082d3396a2fe3ccd537ab173"},
{file = "cryptography-3.4.7-cp36-abi3-manylinux2014_x86_64.whl", hash = "sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959"}, {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d59a9d55027a8b88fd9fd2826c4392bd487d74bf628bb9d39beecc62a644c12"},
{file = "cryptography-3.4.7-cp36-abi3-win32.whl", hash = "sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d"}, {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3"},
{file = "cryptography-3.4.7-cp36-abi3-win_amd64.whl", hash = "sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca"}, {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:94ae132f0e40fe48f310bba63f477f14a43116f05ddb69d6fa31e93f05848ae2"},
{file = "cryptography-3.4.7-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873"}, {file = "cryptography-36.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7be0eec337359c155df191d6ae00a5e8bbb63933883f4f5dffc439dac5348c3f"},
{file = "cryptography-3.4.7-pp36-pypy36_pp73-manylinux2014_x86_64.whl", hash = "sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d"}, {file = "cryptography-36.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e0344c14c9cb89e76eb6a060e67980c9e35b3f36691e15e1b7a9e58a0a6c6dc3"},
{file = "cryptography-3.4.7-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177"}, {file = "cryptography-36.0.1-cp36-abi3-win32.whl", hash = "sha256:4caa4b893d8fad33cf1964d3e51842cd78ba87401ab1d2e44556826df849a8ca"},
{file = "cryptography-3.4.7-pp37-pypy37_pp73-manylinux2014_x86_64.whl", hash = "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9"}, {file = "cryptography-36.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:391432971a66cfaf94b21c24ab465a4cc3e8bf4a939c1ca5c3e3a6e0abebdbcf"},
{file = "cryptography-3.4.7.tar.gz", hash = "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713"}, {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb5829d027ff82aa872d76158919045a7c1e91fbf241aec32cb07956e9ebd3c9"},
{file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebc15b1c22e55c4d5566e3ca4db8689470a0ca2babef8e3a9ee057a8b82ce4b1"},
{file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:596f3cd67e1b950bc372c33f1a28a0692080625592ea6392987dba7f09f17a94"},
{file = "cryptography-36.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:30ee1eb3ebe1644d1c3f183d115a8c04e4e603ed6ce8e394ed39eea4a98469ac"},
{file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec63da4e7e4a5f924b90af42eddf20b698a70e58d86a72d943857c4c6045b3ee"},
{file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca238ceb7ba0bdf6ce88c1b74a87bffcee5afbfa1e41e173b1ceb095b39add46"},
{file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:ca28641954f767f9822c24e927ad894d45d5a1e501767599647259cbf030b903"},
{file = "cryptography-36.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:39bdf8e70eee6b1c7b289ec6e5d84d49a6bfa11f8b8646b5b3dfe41219153316"},
{file = "cryptography-36.0.1.tar.gz", hash = "sha256:53e5c1dc3d7a953de055d77bef2ff607ceef7a2aac0353b5d630ab67f7423638"},
] ]
docutils = [ docutils = [
{file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"},
@ -1052,9 +1057,9 @@ packaging = [
{file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"},
{file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"},
] ]
paramiko = [ paramiko-ng = [
{file = "paramiko-2.7.2-py2.py3-none-any.whl", hash = "sha256:4f3e316fef2ac628b05097a637af35685183111d4bc1b5979bd397c2ab7b5898"}, {file = "paramiko-ng-2.8.8.tar.gz", hash = "sha256:d11191bda27dea3d4cd080c41912bd286ba3879c49851b6c7fc286c2fd0d4497"},
{file = "paramiko-2.7.2.tar.gz", hash = "sha256:7f36f4ba2c0d81d219f4595e35f70d56cc94f9ac40a6acdf51d6ca210ce65035"}, {file = "paramiko_ng-2.8.8-py2.py3-none-any.whl", hash = "sha256:21c8ad4d86006862ef32f6a1f68e563d3dd07fef4336b89a61d2f4c465806e8e"},
] ]
persistent = [ persistent = [
{file = "persistent-4.7.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:ed939f58937f43a67a0f60abb0057e7f31e90d35b6a215159b04248368696930"}, {file = "persistent-4.7.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:ed939f58937f43a67a0f60abb0057e7f31e90d35b6a215159b04248368696930"},

View File

@ -8,7 +8,7 @@ An optional port argument is also accepted.
""" """
import os import os
import socket import socket
from typing import Optional from typing import Union, TextIO, Optional
import paramiko import paramiko
from prompt_toolkit import prompt from prompt_toolkit import prompt
@ -46,65 +46,31 @@ class Ssh(Channel):
password = prompt("Password: ", is_password=True) password = prompt("Password: ", is_password=True)
try: try:
# Connect to the remote host's ssh server client = paramiko.client.SSHClient()
sock = socket.create_connection((host, port)) client.set_missing_host_key_policy(paramiko.client.AutoAddPolicy)
except Exception as exc:
raise ChannelError(self, str(exc))
# Create a paramiko SSH transport layer around the socket client.connect(
t = paramiko.Transport(sock) hostname=host,
try: port=port,
t.start_client() username=user,
except paramiko.SSHException: password=password,
sock.close() pkey=load_private_key(identity),
raise ChannelError(self, "ssh negotiation failed") allow_agent=True,
look_for_keys=False,
)
if identity is not None: columns, rows = os.get_terminal_size(0)
try: shell = client.invoke_shell(width=columns, height=rows)
# Load the private key for the user shell.setblocking(0)
if isinstance(identity, str):
key = paramiko.RSAKey.from_private_key_file(
os.path.expanduser(identity)
)
else:
key = paramiko.RSAKey.from_private_key(identity)
except paramiko.ssh_exception.SSHException:
password = prompt("RSA Private Key Passphrase: ", is_password=True)
try:
if isinstance(identity, str):
key = paramiko.RSAKey.from_private_key_file(identity, password)
else:
# Seek back to the beginning of the file (the above load read the whole file)
identity.seek(0)
key = paramiko.RSAKey.from_private_key(identity, password)
except paramiko.ssh_exception.SSHException:
raise ChannelError(self, "invalid private key or passphrase")
# Attempt authentication self.client = shell
try: self.address = (host, port)
t.auth_publickey(user, key) self._connected = True
except paramiko.ssh_exception.AuthenticationException as exc:
raise ChannelError(self, str(exc))
else:
try:
t.auth_password(user, password)
except paramiko.ssh_exception.AuthenticationException as exc:
raise ChannelError(self, str(exc))
if not t.is_authenticated(): except paramiko.ssh_exception.AuthenticationException as exc:
t.close() raise ChannelError(self, f"ssh authentication failed: {str(exc)}") from exc
sock.close() except (paramiko.ssh_exception.SSHException, socket.error) as exc:
raise ChannelError(self, "authentication failed") raise ChannelError(self, f"ssh connection failed: {str(exc)}") from exc
# Open an interactive session
chan = t.open_session()
chan.get_pty()
chan.invoke_shell()
chan.setblocking(0)
self.client = chan
self.address = (host, port)
self._connected = True
@property @property
def connected(self): def connected(self):
@ -153,3 +119,36 @@ class Ssh(Channel):
pass pass
return data return data
def load_private_key(identity: Union[str, TextIO], passphrase: str = None):
"""Load a private key and return the appropriate PKey object"""
if identity is None:
return None
try:
if isinstance(identity, str):
return paramiko.pkey.load_private_key_file(
os.path.expanduser(identity), password=passphrase
)
identity.seek(0)
return paramiko.pkey.load_private_key(identity.read(), password=passphrase)
except paramiko.PasswordRequiredException:
# Bad passphrase
if passphrase is not None:
raise
try:
# No passphrase, prompt for one
passphrase = prompt("Private Key Passphrase: ", is_password=True)
except KeyboardInterrupt:
passphrase = None
# No passphrase given, re-raise
if passphrase is None:
raise
# Try again with the given passphrase
return load_private_key(identity, passphrase=passphrase)

View File

@ -5,9 +5,8 @@ should not generally need to use these types except as reference
when interacting with data returned by an enumeration module. when interacting with data returned by an enumeration module.
""" """
import time import time
import pathlib
import tempfile
import subprocess import subprocess
from io import StringIO
from typing import Callable, Optional from typing import Callable, Optional
import rich.markup import rich.markup
@ -356,30 +355,24 @@ class PrivateKey(Implant):
else: else:
raise ModuleFailed(f"unknown username for uid={self.uid}") raise ModuleFailed(f"unknown username for uid={self.uid}")
with tempfile.NamedTemporaryFile("w") as filp: try:
filp.write(self.content) # Connect via SSH
filp.flush() session = manager.create_session(
"linux",
host=target.public_address[0],
user=user.name,
identity=StringIO(self.content + "\n"),
)
except (ChannelError, PlatformError) as exc:
manager.log(
f"[yellow]warning[/yellow]: {self.source} implant failed; removing implant types."
)
self.authorized = False
self.types.remove("implant.remote")
self.types.remove("implant.replace")
raise ModuleFailed(str(exc)) from exc
pathlib.Path(filp.name).chmod(0o600) return session
try:
# Connect via SSH
session = manager.create_session(
"linux",
host=target.public_address[0],
user=user.name,
identity=filp.name,
)
except (ChannelError, PlatformError) as exc:
manager.log(
f"[yellow]warning[/yellow]: {self.source} implant failed; removing implant types."
)
self.authorized = False
self.types.remove("implant.remote")
self.types.remove("implant.replace")
raise ModuleFailed(str(exc)) from exc
return session
class EscalationReplace(Fact): class EscalationReplace(Fact):

View File

@ -40,8 +40,9 @@ rich = "^10.4.0"
python-rapidjson = "^1.0" python-rapidjson = "^1.0"
ZODB3 = "^3.11.0" ZODB3 = "^3.11.0"
zodburi = "^2.5.0" zodburi = "^2.5.0"
paramiko = "^2.7.2"
Jinja2 = "^3.0.1" Jinja2 = "^3.0.1"
paramiko-ng = "^2.8.8"
PyNaCl = "^1.4.0"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
isort = "^5.8.0" isort = "^5.8.0"