From 701d589c774f7c41451c1bcbffef9d4d4e5f9434 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 14 Oct 2021 11:34:39 -0400 Subject: [PATCH 1/2] Agent: Include domain with usernames in PowerShell exploiter Fixes #1486 --- .../exploit/powershell_utils/credentials.py | 40 ++++- .../infection_monkey/exploit/conftest.py | 21 +++ .../powershell_utils/test_credentials.py | 151 ++++++++++-------- .../exploit/test_powershell.py | 3 + 4 files changed, 144 insertions(+), 71 deletions(-) create mode 100644 monkey/tests/unit_tests/infection_monkey/exploit/conftest.py diff --git a/monkey/infection_monkey/exploit/powershell_utils/credentials.py b/monkey/infection_monkey/exploit/powershell_utils/credentials.py index 0e7d0ebab..ee5aa2656 100644 --- a/monkey/infection_monkey/exploit/powershell_utils/credentials.py +++ b/monkey/infection_monkey/exploit/powershell_utils/credentials.py @@ -1,7 +1,10 @@ +import logging from dataclasses import dataclass from enum import Enum from itertools import product -from typing import List, Union +from typing import List, Tuple, Union + +logger = logging.getLogger(__name__) class SecretType(Enum): @@ -25,16 +28,43 @@ def get_credentials( nt_hashes: List[str], is_windows: bool, ) -> List[Credentials]: + username_domain_combinations = _get_username_domain_combinations(usernames, is_windows) + 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)) + credentials.extend(_get_username_only_credentials(username_domain_combinations, is_windows)) + credentials.extend(_get_username_password_credentials(username_domain_combinations, passwords)) + credentials.extend(_get_username_lm_hash_credentials(username_domain_combinations, lm_hashes)) + credentials.extend(_get_username_nt_hash_credentials(username_domain_combinations, nt_hashes)) return credentials +def _get_username_domain_combinations(usernames: List[str], is_windows) -> List[str]: + username_domain_combinations = set(usernames) + for u in usernames: + username_domain_combinations.add(f".\\{u}") + + if is_windows: + try: + domain, current_username = _get_current_user_and_domain() + username_domain_combinations.add(current_username) + username_domain_combinations.add(f"{domain}\\{current_username}") + username_domain_combinations.add(f".\\{current_username}") + for u in usernames: + username_domain_combinations.add(f"{domain}\\{u}") + except Exception as ex: + logger.error(f"Failed to get the current user's username and domain name: {ex}") + + return list(username_domain_combinations) + + +def _get_current_user_and_domain() -> Tuple[str, str]: + import win32api + + return win32api.GetUserNameEx(win32api.NameSamCompatible).split("\\") + + # On Windows systems, when username == None and password == None, the current user's credentials # will be used to attempt to log into the victim only on the first hop, from island # to a machine. Propagating after the first hop is not possible at the moment. diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/conftest.py b/monkey/tests/unit_tests/infection_monkey/exploit/conftest.py new file mode 100644 index 000000000..c0d84708b --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/exploit/conftest.py @@ -0,0 +1,21 @@ +import sys +from collections import namedtuple +from unittest.mock import MagicMock + +import pytest + +DomainUser = namedtuple("DomainUser", ["domain", "username"]) + + +@pytest.fixture(scope="module") +def local_user(): + return DomainUser("TEST-DOMAIN", "localuser") + + +@pytest.fixture(scope="module") +def patch_win32api_get_user_name(local_user): + win32api = MagicMock() + win32api.GetUserNameEx = MagicMock(return_value=f"{local_user.domain}\\{local_user.username}") + win32api.NameSamCompatible = None + + sys.modules["win32api"] = win32api 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 0954d9dc8..74d35c03f 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,97 +1,116 @@ +import sys +from unittest.mock import MagicMock + +import pytest + from infection_monkey.exploit.powershell_utils.credentials import ( Credentials, SecretType, get_credentials, ) +# Use the path_win32api_get_user_name fixture for all tests in this module +pytestmark = pytest.mark.usefixtures("patch_win32api_get_user_name") + 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) +@pytest.fixture(scope="module") +def windows_false_usernames(): + usernames = TEST_USERNAMES.copy() + usernames.extend([f".\\{u}" for u in TEST_USERNAMES]) - assert len(credentials) == 1 - assert credentials[0] == Credentials(username=None, secret=None, secret_type=SecretType.CACHED) + return usernames + + +@pytest.fixture(scope="module") +def windows_true_usernames(local_user): + usernames = TEST_USERNAMES.copy() + usernames.append(local_user.username) + usernames = ( + usernames + + [f".\\{u}" for u in usernames] + + [f"{local_user.domain}\\{u}" for u in usernames] + ) + + return usernames + + +def test_get_credentials__empty_windows_true(): + results = get_credentials([], [], [], [], True) + + assert Credentials(username=None, secret=None, secret_type=SecretType.CACHED) in results def test_get_credentials__empty_windows_false(): - credentials = get_credentials([], [], [], [], False) + results = get_credentials([], [], [], [], False) - assert len(credentials) == 0 + assert len(results) == 0 -def test_get_credentials__username_only_windows_true(): - credentials = get_credentials(TEST_USERNAMES, [], [], [], True) - - assert len(credentials) == 5 - 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 assert_secrets_in_results(usernames, secrets, secret_type, results): + for u in usernames: + for s in secrets: + assert Credentials(username=u, secret=s, secret_type=secret_type) in results -def test_get_credentials__username_only_windows_false(): - credentials = get_credentials(TEST_USERNAMES, [], [], [], False) +def test_get_credentials__username_only_windows_true(windows_true_usernames): + results = get_credentials(TEST_USERNAMES, [], [], [], True) - assert len(credentials) == 2 - 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 len(results) == 19 + assert_secrets_in_results(windows_true_usernames, [""], SecretType.PASSWORD, results) + assert_secrets_in_results(windows_true_usernames, [None], SecretType.CACHED, results) -def test_get_credentials__username_password_windows_true(): - credentials = get_credentials(TEST_USERNAMES, TEST_PASSWORDS, [], [], True) +def test_get_credentials__username_only_windows_false(windows_false_usernames): + results = get_credentials(TEST_USERNAMES, [], [], [], False) - assert len(credentials) == 9 - for user in TEST_USERNAMES: - for password in TEST_PASSWORDS: - assert ( - Credentials(username=user, secret=password, secret_type=SecretType.PASSWORD) - in credentials - ) + assert len(results) == 4 + assert_secrets_in_results(windows_false_usernames, [""], SecretType.PASSWORD, results) -def test_get_credentials__username_lm_hash_windows_false(): - credentials = get_credentials(TEST_USERNAMES, TEST_PASSWORDS, TEST_LM_HASHES, [], False) +def test_get_credentials__username_password_windows_true(windows_true_usernames): + results = get_credentials(TEST_USERNAMES, TEST_PASSWORDS, [], [], True) - 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 - ) + assert_secrets_in_results(windows_true_usernames, TEST_PASSWORDS, SecretType.PASSWORD, results) -def test_get_credentials__username_nt_hash_windows_false(): - credentials = get_credentials( - TEST_USERNAMES, TEST_PASSWORDS, TEST_LM_HASHES, TEST_NT_HASHES, False - ) +def test_get_credentials__username_lm_hash_windows_false(windows_false_usernames): + results = get_credentials(TEST_USERNAMES, TEST_PASSWORDS, TEST_LM_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 - ) + assert len(results) == 20 + assert_secrets_in_results(windows_false_usernames, TEST_LM_HASHES, SecretType.LM_HASH, results) + + +def test_get_credentials__username_lm_hash_windows_true(windows_true_usernames): + results = get_credentials(TEST_USERNAMES, TEST_PASSWORDS, TEST_LM_HASHES, [], True) + + assert_secrets_in_results(windows_true_usernames, TEST_LM_HASHES, SecretType.LM_HASH, results) + + +def test_get_credentials__username_nt_hash_windows_false(windows_false_usernames): + results = get_credentials(TEST_USERNAMES, TEST_PASSWORDS, TEST_LM_HASHES, TEST_NT_HASHES, False) + + assert len(results) == 28 + assert_secrets_in_results(windows_false_usernames, TEST_NT_HASHES, SecretType.NT_HASH, results) + + +def test_get_credentials__username_nt_hash_windows_true(windows_true_usernames): + results = get_credentials(TEST_USERNAMES, TEST_PASSWORDS, TEST_LM_HASHES, TEST_NT_HASHES, True) + + assert_secrets_in_results(windows_true_usernames, TEST_NT_HASHES, SecretType.NT_HASH, results) + + +def test_get_credentials__get_username_failure(windows_false_usernames): + win32api = MagicMock() + win32api.GetUserNameEx = MagicMock(side_effect=Exception("win32api test failure")) + win32api.NameSamCompatible = None + sys.modules["win32api"] = win32api + + results = get_credentials(TEST_USERNAMES, [], [], [], True) + + assert len(results) == 9 + assert_secrets_in_results(windows_false_usernames, [""], SecretType.PASSWORD, results) 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 7c2a04710..ef3da4538 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py @@ -9,6 +9,9 @@ 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 +pytestmark = pytest.mark.usefixtures("patch_win32api_get_user_name") + USER_LIST = ["user1", "user2"] PASSWORD_LIST = ["pass1", "pass2"] LM_HASH_LIST = ["bogo_lm_1"] From 8d7a5a410c045509ce8ecc3018121109c780e76f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 14 Oct 2021 13:56:05 -0400 Subject: [PATCH 2/2] BB: Remove ".\\m0nk3y" user from Powershell user list This user was added to work around issue #1486. Since d4a1c2bda resolves that issue, this user can be removed from the config for this test. --- 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 96ba0b908..a282b2a0a 100644 --- a/envs/monkey_zoo/blackbox/config_templates/powershell.py +++ b/envs/monkey_zoo/blackbox/config_templates/powershell.py @@ -20,7 +20,7 @@ class PowerShell(ConfigTemplate): ], "basic.credentials.exploit_password_list": ["Passw0rd!"], "basic_network.scope.depth": 2, - "basic.credentials.exploit_user_list": ["m0nk3y", "m0nk3y-user", ".\\m0nk3y"], + "basic.credentials.exploit_user_list": ["m0nk3y", "m0nk3y-user"], "internal.classes.finger_classes": ["PingScanner"], "internal.network.tcp_scanner.HTTP_PORTS": [], "internal.network.tcp_scanner.tcp_target_ports": [],