From 6d9e18fdc943a39799dbaa7d24fa7c32dad6a99f Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 23 Mar 2022 14:26:15 +0530 Subject: [PATCH 1/9] Island: Add 5985 and 5986 to TCP ports --- monkey/monkey_island/cc/services/config_schema/internal.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monkey/monkey_island/cc/services/config_schema/internal.py b/monkey/monkey_island/cc/services/config_schema/internal.py index da628fcce..e825a9098 100644 --- a/monkey/monkey_island/cc/services/config_schema/internal.py +++ b/monkey/monkey_island/cc/services/config_schema/internal.py @@ -91,6 +91,8 @@ INTERNAL = { 3306, 7001, 8088, + 5985, + 5986, ], "description": "List of TCP ports the monkey will check whether " "they're open", From 4614e2207ddbb99fe4fea9a6562152907b6a0220 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 23 Mar 2022 15:59:53 +0530 Subject: [PATCH 2/9] Agent: Decide if SSL is to be used in auth_options.py --- monkey/infection_monkey/exploit/powershell.py | 17 +++++++++-------- .../exploit/powershell_utils/auth_options.py | 18 ++++++++++++++---- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 8bdf7e571..1a76f4044 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -49,13 +49,11 @@ class PowerShellExploiter(HostExploiter): self._client = None def _exploit_host(self): - try: - use_ssl = self._is_client_using_https() - except PowerShellRemotingDisabledError as e: - logger.info(e) - self.exploit_result.error_message = ( - "PowerShell Remoting appears to be disabled on the remote host" - ) + if not self._is_any_default_port_open(): + message = "No default PowerShell remoting ports are open." + self.exploit_result.error_message = message + logger.debug(message) + return self.exploit_result credentials = get_credentials( @@ -66,7 +64,7 @@ class PowerShellExploiter(HostExploiter): is_windows_os(), ) - auth_options = [get_auth_options(creds, use_ssl) for creds in credentials] + auth_options = [get_auth_options(creds, self.host) for creds in credentials] self._client = self._authenticate_via_brute_force(credentials, auth_options) @@ -89,6 +87,9 @@ class PowerShellExploiter(HostExploiter): return self.exploit_result + def _is_any_default_port_open(self) -> bool: + return "tcp-5985" in self.host.services or "tcp-5986" in self.host.services + def _is_client_using_https(self) -> bool: try: logger.debug("Checking if powershell remoting is enabled over HTTP.") diff --git a/monkey/infection_monkey/exploit/powershell_utils/auth_options.py b/monkey/infection_monkey/exploit/powershell_utils/auth_options.py index 1f53c1df5..cde316c90 100644 --- a/monkey/infection_monkey/exploit/powershell_utils/auth_options.py +++ b/monkey/infection_monkey/exploit/powershell_utils/auth_options.py @@ -1,6 +1,7 @@ from dataclasses import dataclass from infection_monkey.exploit.powershell_utils.credentials import Credentials, SecretType +from infection_monkey.model.host import VictimHost AUTH_BASIC = "basic" AUTH_NEGOTIATE = "negotiate" @@ -16,17 +17,26 @@ class AuthOptions: ssl: bool -def get_auth_options(credentials: Credentials, use_ssl: bool) -> AuthOptions: - ssl = _get_ssl(credentials, use_ssl) +def get_auth_options(credentials: Credentials, host: VictimHost) -> 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, use_ssl): +def _get_ssl(credentials: Credentials, host: VictimHost) -> bool: + # Check if default PSRemoting ports are open. Prefer with SSL, if both are. + if "tcp-5986" in host.services: # Default for HTTPS + use_ssl = True + elif "tcp-5985" in host.services: # Default for HTTP + use_ssl = False + # Passwordless login only works with SSL false, AUTH_BASIC and ENCRYPTION_NEVER - return False if credentials.secret == "" else use_ssl + if credentials.secret == "": + use_ssl = False + + return use_ssl def _get_auth_type(credentials: Credentials): From e947f335fff41b28bf9734c5de6c0aaad2247531 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 23 Mar 2022 16:03:10 +0530 Subject: [PATCH 3/9] Agent: Remove unused functions in PowerShell exploiter --- monkey/infection_monkey/exploit/powershell.py | 53 +------------------ 1 file changed, 1 insertion(+), 52 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 1a76f4044..588a7566d 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -3,19 +3,13 @@ from pathlib import Path from typing import List, Optional from infection_monkey.exploit.HostExploiter import HostExploiter -from infection_monkey.exploit.powershell_utils.auth_options import ( - AUTH_NEGOTIATE, - ENCRYPTION_AUTO, - AuthOptions, - get_auth_options, -) +from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions, get_auth_options from infection_monkey.exploit.powershell_utils.credentials import ( Credentials, SecretType, get_credentials, ) from infection_monkey.exploit.powershell_utils.powershell_client import ( - AuthenticationError, IPowerShellClient, PowerShellClient, ) @@ -90,51 +84,6 @@ class PowerShellExploiter(HostExploiter): def _is_any_default_port_open(self) -> bool: return "tcp-5985" in self.host.services or "tcp-5986" in self.host.services - def _is_client_using_https(self) -> bool: - try: - logger.debug("Checking if powershell remoting is enabled over HTTP.") - self._try_http() - return False - except AuthenticationError: - return False - except Exception as e: - logger.debug(f"Powershell remoting over HTTP seems disabled: {e}") - - try: - logger.debug("Checking if powershell remoting is enabled over HTTPS.") - self._try_https() - return True - except AuthenticationError: - return True - except Exception as e: - logger.debug(f"Powershell remoting over HTTPS seems disabled: {e}") - raise PowerShellRemotingDisabledError("Powershell remoting seems to be disabled.") - - def _try_http(self): - self._try_ssl_login(use_ssl=False) - - def _try_https(self): - self._try_ssl_login(use_ssl=True) - - def _try_ssl_login(self, use_ssl: bool): - # '.\' is machine qualifier if the user is in the local domain - # which happens if we try to exploit a machine on second hop - credentials = Credentials( - username=".\\dummy_username", - secret="dummy_password", - secret_type=SecretType.PASSWORD, - ) - - auth_options = AuthOptions( - auth_type=AUTH_NEGOTIATE, - encryption=ENCRYPTION_AUTO, - ssl=use_ssl, - ) - - # TODO: Report login attempt or find a better way of detecting if SSL is enabled - client = PowerShellClient(self.host.ip_addr, credentials, auth_options) - client.connect() - def _authenticate_via_brute_force( self, credentials: List[Credentials], auth_options: List[AuthOptions] ) -> Optional[IPowerShellClient]: From 4b84ba3fc06ca052dc5955f791d95a94a1025d8d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 23 Mar 2022 12:40:10 -0400 Subject: [PATCH 4/9] Tests: Fix unit tests for powershell_utils.auth_options --- .../powershell_utils/test_auth_options.py | 107 ++++++++++++------ 1 file changed, 75 insertions(+), 32 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options.py b/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options.py index ce5449051..4efa129b4 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options.py @@ -1,3 +1,7 @@ +from unittest.mock import MagicMock + +import pytest + # from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions from infection_monkey.exploit.powershell_utils.auth_options import ( AUTH_BASIC, @@ -16,85 +20,124 @@ CREDENTIALS_LM_HASH = Credentials("user4", "LM_HASH:NONE", SecretType.LM_HASH) CREDENTIALS_NT_HASH = Credentials("user5", "NONE:NT_HASH", SecretType.NT_HASH) -def test_get_auth_options__ssl_true_with_password(): - auth_options = get_auth_options(CREDENTIALS_WITH_PASSWORD, use_ssl=True) +def _create_host(http_enabled, https_enabled): + host = MagicMock() + host.services = {} + + if http_enabled: + host.services["tcp-5985"] = {} + + if https_enabled: + host.services["tcp-5986"] = {} + + return host + + +@pytest.fixture +def https_only_host(): + return _create_host(False, True) + + +@pytest.fixture +def http_only_host(): + return _create_host(True, False) + + +@pytest.fixture +def http_and_https_both_enabled_host(): + return _create_host(True, True) + + +@pytest.fixture +def powershell_disabled_host(): + return _create_host(False, 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_true_empty_password(): - auth_options = get_auth_options(CREDENTIALS_EMPTY_PASSWORD, use_ssl=True) - - assert not auth_options.ssl - - -def test_get_auth_options__ssl_true_none_password(): - auth_options = get_auth_options(CREDENTIALS_NONE_PASSWORD, use_ssl=True) +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_false_with_password(): - auth_options = get_auth_options(CREDENTIALS_WITH_PASSWORD, use_ssl=False) +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_false_empty_password(): - auth_options = get_auth_options(CREDENTIALS_EMPTY_PASSWORD, use_ssl=False) +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_none_password(): - auth_options = get_auth_options(CREDENTIALS_NONE_PASSWORD, use_ssl=False) +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__auth_type_with_password(): - auth_options = get_auth_options(CREDENTIALS_WITH_PASSWORD, use_ssl=False) +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(): - auth_options = get_auth_options(CREDENTIALS_EMPTY_PASSWORD, use_ssl=False) +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(): - auth_options = get_auth_options(CREDENTIALS_NONE_PASSWORD, use_ssl=False) +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(): - auth_options = get_auth_options(CREDENTIALS_LM_HASH, use_ssl=False) +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(): - auth_options = get_auth_options(CREDENTIALS_NT_HASH, use_ssl=False) +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(): - auth_options = get_auth_options(CREDENTIALS_WITH_PASSWORD, use_ssl=False) +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(): - auth_options = get_auth_options(CREDENTIALS_EMPTY_PASSWORD, use_ssl=False) +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(): - auth_options = get_auth_options(CREDENTIALS_NONE_PASSWORD, use_ssl=False) +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 3d7586f7139967cf649ca70533003e051c23a9b5 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 23 Mar 2022 12:45:05 -0400 Subject: [PATCH 5/9] Agent: Fix edge case handling in auth_options._get_ssl() If the host has neither the HTTP or HTTPS port enabled, return False. --- .../exploit/powershell_utils/auth_options.py | 17 +++++++++-------- .../powershell_utils/test_auth_options.py | 5 +++++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell_utils/auth_options.py b/monkey/infection_monkey/exploit/powershell_utils/auth_options.py index cde316c90..0ae8cb266 100644 --- a/monkey/infection_monkey/exploit/powershell_utils/auth_options.py +++ b/monkey/infection_monkey/exploit/powershell_utils/auth_options.py @@ -26,17 +26,18 @@ def get_auth_options(credentials: Credentials, host: VictimHost) -> AuthOptions: def _get_ssl(credentials: Credentials, host: VictimHost) -> bool: - # Check if default PSRemoting ports are open. Prefer with SSL, if both are. - if "tcp-5986" in host.services: # Default for HTTPS - use_ssl = True - elif "tcp-5985" in host.services: # Default for HTTP - use_ssl = False - # Passwordless login only works with SSL false, AUTH_BASIC and ENCRYPTION_NEVER if credentials.secret == "": - use_ssl = False + return False - return use_ssl + # Check if default PSRemoting ports are open. Prefer with SSL, if both are. + if "tcp-5986" in host.services: # Default for HTTPS + return True + + if "tcp-5985" in host.services: # Default for HTTP + return False + + return False def _get_auth_type(credentials: Credentials): diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options.py b/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options.py index 4efa129b4..7d550c59e 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options.py @@ -53,6 +53,11 @@ def powershell_disabled_host(): return _create_host(False, False) +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) From 385449101d2c4bd41829868882c9479ad2ac014d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 23 Mar 2022 12:50:24 -0400 Subject: [PATCH 6/9] Tests: Move host fixtures to conftest.py --- .../infection_monkey/exploit/conftest.py | 33 +++++++++++++++++ .../powershell_utils/test_auth_options.py | 37 ------------------- 2 files changed, 33 insertions(+), 37 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/conftest.py b/monkey/tests/unit_tests/infection_monkey/exploit/conftest.py index c0d84708b..142a3065a 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/conftest.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/conftest.py @@ -19,3 +19,36 @@ def patch_win32api_get_user_name(local_user): win32api.NameSamCompatible = None sys.modules["win32api"] = win32api + + +def _create_host(http_enabled, https_enabled): + host = MagicMock() + host.services = {} + + if http_enabled: + host.services["tcp-5985"] = {} + + if https_enabled: + host.services["tcp-5986"] = {} + + return host + + +@pytest.fixture +def https_only_host(): + return _create_host(False, True) + + +@pytest.fixture +def http_only_host(): + return _create_host(True, False) + + +@pytest.fixture +def http_and_https_both_enabled_host(): + return _create_host(True, True) + + +@pytest.fixture +def powershell_disabled_host(): + return _create_host(False, False) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options.py b/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options.py index 7d550c59e..fe18ccf9e 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options.py @@ -1,7 +1,3 @@ -from unittest.mock import MagicMock - -import pytest - # from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions from infection_monkey.exploit.powershell_utils.auth_options import ( AUTH_BASIC, @@ -20,39 +16,6 @@ CREDENTIALS_LM_HASH = Credentials("user4", "LM_HASH:NONE", SecretType.LM_HASH) CREDENTIALS_NT_HASH = Credentials("user5", "NONE:NT_HASH", SecretType.NT_HASH) -def _create_host(http_enabled, https_enabled): - host = MagicMock() - host.services = {} - - if http_enabled: - host.services["tcp-5985"] = {} - - if https_enabled: - host.services["tcp-5986"] = {} - - return host - - -@pytest.fixture -def https_only_host(): - return _create_host(False, True) - - -@pytest.fixture -def http_only_host(): - return _create_host(True, False) - - -@pytest.fixture -def http_and_https_both_enabled_host(): - return _create_host(True, True) - - -@pytest.fixture -def powershell_disabled_host(): - return _create_host(False, False) - - 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 From c28e200a25e51fd352eb10b99b40dfbc27650e31 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 23 Mar 2022 12:51:55 -0400 Subject: [PATCH 7/9] Agent: Remove disused PowerShellRemotingDisabledError --- monkey/infection_monkey/exploit/powershell.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 588a7566d..052b1f88f 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -22,10 +22,6 @@ from infection_monkey.utils.threading import interruptible_iter logger = logging.getLogger(__name__) -class PowerShellRemotingDisabledError(Exception): - pass - - class RemoteAgentCopyError(Exception): pass From 06899be264e89d0877eddafb956c0736653ce371 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 23 Mar 2022 13:25:22 -0400 Subject: [PATCH 8/9] Tests: Fix tests for PowerShellExploiter --- monkey/infection_monkey/exploit/powershell.py | 2 +- .../infection_monkey/exploit/conftest.py | 11 +-- .../exploit/test_powershell.py | 72 +++++-------------- 3 files changed, 26 insertions(+), 59 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 052b1f88f..3d5a41131 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -40,7 +40,7 @@ class PowerShellExploiter(HostExploiter): def _exploit_host(self): if not self._is_any_default_port_open(): - message = "No default PowerShell remoting ports are open." + message = "PowerShell Remoting appears to be disabled on the remote host" self.exploit_result.error_message = message logger.debug(message) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/conftest.py b/monkey/tests/unit_tests/infection_monkey/exploit/conftest.py index 142a3065a..7d4265395 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/conftest.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/conftest.py @@ -21,8 +21,9 @@ def patch_win32api_get_user_name(local_user): sys.modules["win32api"] = win32api -def _create_host(http_enabled, https_enabled): +def _create_windows_host(http_enabled, https_enabled): host = MagicMock() + host.os = {"type": "windows"} host.services = {} if http_enabled: @@ -36,19 +37,19 @@ def _create_host(http_enabled, https_enabled): @pytest.fixture def https_only_host(): - return _create_host(False, True) + return _create_windows_host(False, True) @pytest.fixture def http_only_host(): - return _create_host(True, False) + return _create_windows_host(True, False) @pytest.fixture def http_and_https_both_enabled_host(): - return _create_host(True, True) + return _create_windows_host(True, True) @pytest.fixture def powershell_disabled_host(): - return _create_host(False, False) + return _create_windows_host(False, False) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py index c88ce99d7..698c7ac2d 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py @@ -5,8 +5,6 @@ from unittest.mock import MagicMock import pytest from infection_monkey.exploit import powershell -from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions -from infection_monkey.exploit.powershell_utils.credentials import Credentials from infection_monkey.model.host import VictimHost # Use the path_win32api_get_user_name fixture for all tests in this module @@ -19,19 +17,12 @@ NT_HASH_LIST = ["bogo_nt_1", "bogo_nt_2"] DROPPER_TARGET_PATH_64 = "C:\\agent64" -class AuthenticationErrorForTests(Exception): - pass - - mock_agent_repository = MagicMock() mock_agent_repository.get_agent_binary.return_value = BytesIO(b"BINARY_EXECUTABLE") -victim_host = VictimHost("127.0.0.1") -victim_host.os["type"] = "windows" - @pytest.fixture -def powershell_arguments(): +def powershell_arguments(http_and_https_both_enabled_host): options = { "dropper_target_path_win_64": DROPPER_TARGET_PATH_64, "credentials": { @@ -42,7 +33,7 @@ def powershell_arguments(): }, } arguments = { - "host": victim_host, + "host": http_and_https_both_enabled_host, "options": options, "current_depth": 2, "telemetry_messenger": MagicMock(), @@ -56,18 +47,13 @@ def powershell_arguments(): def powershell_exploiter(monkeypatch): pe = powershell.PowerShellExploiter() - monkeypatch.setattr(powershell, "AuthenticationError", AuthenticationErrorForTests) monkeypatch.setattr(powershell, "is_windows_os", lambda: True) return pe -def test_powershell_disabled(monkeypatch, powershell_exploiter, powershell_arguments): - mock_powershell_client = MagicMock() - mock_powershell_client.connect = MagicMock(side_effect=Exception) - monkeypatch.setattr( - powershell, "PowerShellClient", MagicMock(return_value=mock_powershell_client) - ) +def test_powershell_disabled(powershell_exploiter, powershell_arguments, powershell_disabled_host): + powershell_arguments["host"] = powershell_disabled_host exploit_result = powershell_exploiter.exploit_host(**powershell_arguments) assert not exploit_result.exploitation_success @@ -75,15 +61,10 @@ def test_powershell_disabled(monkeypatch, powershell_exploiter, powershell_argum assert "disabled" in exploit_result.error_message -def test_powershell_http(monkeypatch, powershell_exploiter, powershell_arguments): - def allow_http(_, credentials: Credentials, auth_options: AuthOptions): - if not auth_options.ssl: - raise AuthenticationErrorForTests - else: - raise Exception +def test_powershell_http(monkeypatch, powershell_exploiter, powershell_arguments, http_only_host): + powershell_arguments["host"] = http_only_host mock_powershell_client = MagicMock() - mock_powershell_client.connect = MagicMock(side_effect=allow_http) monkeypatch.setattr( powershell, "PowerShellClient", MagicMock(return_value=mock_powershell_client) ) @@ -94,29 +75,26 @@ def test_powershell_http(monkeypatch, powershell_exploiter, powershell_arguments assert not call_args[0][2].ssl -def test_powershell_https(monkeypatch, powershell_exploiter, powershell_arguments): - def allow_https(_, credentials: Credentials, auth_options: AuthOptions): - if auth_options.ssl: - raise AuthenticationErrorForTests - else: - raise Exception +def test_powershell_https(monkeypatch, powershell_exploiter, powershell_arguments, https_only_host): + powershell_arguments["host"] = https_only_host mock_powershell_client = MagicMock() - mock_powershell_client.connect = MagicMock(side_effect=allow_https) - monkeypatch.setattr( - powershell, "PowerShellClient", MagicMock(return_value=mock_powershell_client) - ) + mock_powershell_client.connect = MagicMock(side_effect=Exception("Failed login")) + mock_powershell_client_constructor = MagicMock(return_value=mock_powershell_client) + monkeypatch.setattr(powershell, "PowerShellClient", mock_powershell_client_constructor) powershell_exploiter.exploit_host(**powershell_arguments) - for call_args in mock_powershell_client.call_args_list: - if call_args[0][1].secret != "" and call_args[0][1].secret != "dummy_password": + for call_args in mock_powershell_client_constructor.call_args_list: + if call_args[0][1].secret != "": assert call_args[0][2].ssl + else: + assert not call_args[0][2].ssl def test_no_valid_credentials(monkeypatch, powershell_exploiter, powershell_arguments): mock_powershell_client = MagicMock() - mock_powershell_client.connect = MagicMock(side_effect=AuthenticationErrorForTests) + mock_powershell_client.connect = MagicMock(side_effect=Exception("Failed login")) monkeypatch.setattr( powershell, "PowerShellClient", MagicMock(return_value=mock_powershell_client) ) @@ -127,16 +105,6 @@ def test_no_valid_credentials(monkeypatch, powershell_exploiter, powershell_argu assert "Unable to authenticate" in exploit_result.error_message -def authenticate(mock_client): - def inner(_, credentials: Credentials, auth_options: AuthOptions): - if credentials.username == "user1" and credentials.secret == "pass2": - return mock_client - else: - raise AuthenticationErrorForTests("Invalid credentials") - - return inner - - def test_successful_copy(monkeypatch, powershell_exploiter, powershell_arguments): mock_client = MagicMock() @@ -188,11 +156,9 @@ def test_successful_propagation(monkeypatch, powershell_exploiter, powershell_ar def test_login_attempts_correctly_reported(monkeypatch, powershell_exploiter, powershell_arguments): - # 1st call is for determining HTTP/HTTPs. 6 remaining calls are actual login attempts. the 6th - # login attempt doesn't throw an exception, signifying that login with credentials was - # successful. - connection_attempts = [True, Exception, Exception, Exception, Exception, Exception, True] - mock_powershell_client = MagicMock(side_effect=connection_attempts) + # First 5 login attempts fail. The 6th is successful. + connection_attempts = [Exception, Exception, Exception, Exception, Exception, True] + mock_powershell_client = MagicMock() mock_powershell_client.connect = MagicMock(side_effect=connection_attempts) monkeypatch.setattr( powershell, "PowerShellClient", MagicMock(return_value=mock_powershell_client) From 45658b5559b3c0b3dafbe4c6ff88a5f973fc83f7 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 23 Mar 2022 13:49:51 -0400 Subject: [PATCH 9/9] Agent: Skip empty password attempts in PowerShell if HTTP disabled --- monkey/infection_monkey/exploit/powershell.py | 27 ++++++++++++++++--- .../exploit/test_powershell.py | 25 +++++++++++++++-- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 3d5a41131..457eccd11 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -39,7 +39,7 @@ class PowerShellExploiter(HostExploiter): self._client = None def _exploit_host(self): - if not self._is_any_default_port_open(): + if not self._any_powershell_port_is_open(): message = "PowerShell Remoting appears to be disabled on the remote host" self.exploit_result.error_message = message logger.debug(message) @@ -77,13 +77,21 @@ class PowerShellExploiter(HostExploiter): return self.exploit_result - def _is_any_default_port_open(self) -> bool: - return "tcp-5985" in self.host.services or "tcp-5986" in self.host.services + def _any_powershell_port_is_open(self) -> bool: + return self._http_powershell_port_is_open() or self._https_powershell_port_is_open() + + def _http_powershell_port_is_open(self) -> bool: + return "tcp-5985" in self.host.services + + def _https_powershell_port_is_open(self) -> bool: + return "tcp-5986" in self.host.services def _authenticate_via_brute_force( self, credentials: List[Credentials], auth_options: List[AuthOptions] ) -> Optional[IPowerShellClient]: - for (creds, opts) in interruptible_iter(zip(credentials, auth_options), self.interrupt): + creds_opts_pairs = filter(self.check_ssl_setting_is_valid, zip(credentials, auth_options)) + for (creds, opts) in interruptible_iter(creds_opts_pairs, self.interrupt): + try: client = PowerShellClient(self.host.ip_addr, creds, opts) client.connect() @@ -105,6 +113,17 @@ class PowerShellExploiter(HostExploiter): return None + def check_ssl_setting_is_valid(self, creds_opts_pair): + opts = creds_opts_pair[1] + + if opts.ssl and not self._https_powershell_port_is_open(): + return False + + if not opts.ssl and not self._http_powershell_port_is_open(): + return False + + return True + def _report_login_attempt(self, result: bool, credentials: Credentials): if credentials.secret_type in [SecretType.PASSWORD, SecretType.CACHED]: self.report_login_attempt(result, credentials.username, password=credentials.secret) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py index 698c7ac2d..78ba133af 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py @@ -76,8 +76,6 @@ def test_powershell_http(monkeypatch, powershell_exploiter, powershell_arguments def test_powershell_https(monkeypatch, powershell_exploiter, powershell_arguments, https_only_host): - powershell_arguments["host"] = https_only_host - mock_powershell_client = MagicMock() mock_powershell_client.connect = MagicMock(side_effect=Exception("Failed login")) mock_powershell_client_constructor = MagicMock(return_value=mock_powershell_client) @@ -85,11 +83,15 @@ def test_powershell_https(monkeypatch, powershell_exploiter, powershell_argument powershell_exploiter.exploit_host(**powershell_arguments) + non_ssl_calls = 0 for call_args in mock_powershell_client_constructor.call_args_list: if call_args[0][1].secret != "": assert call_args[0][2].ssl else: assert not call_args[0][2].ssl + non_ssl_calls += 1 + + assert non_ssl_calls > 0 def test_no_valid_credentials(monkeypatch, powershell_exploiter, powershell_arguments): @@ -185,3 +187,22 @@ def test_build_monkey_execution_command(): assert f"-d {depth}" in cmd assert executable_path in cmd + + +def test_skip_http_only_logins( + monkeypatch, powershell_exploiter, powershell_arguments, https_only_host +): + # Only HTTPS is enabled on the destination, so we should never try to connect with "" empty + # password, since connection with empty password requires SSL == False. + powershell_arguments["host"] = https_only_host + + mock_powershell_client = MagicMock() + mock_powershell_client.connect = MagicMock(side_effect=Exception("Failed login")) + mock_powershell_client_constructor = MagicMock(return_value=mock_powershell_client) + monkeypatch.setattr(powershell, "PowerShellClient", mock_powershell_client_constructor) + + powershell_exploiter.exploit_host(**powershell_arguments) + + for call_args in mock_powershell_client_constructor.call_args_list: + assert call_args[0][1].secret != "" + assert call_args[0][2].ssl