From 17b344f62f4c141667f0b49c3557e32fec1fe4dc Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Thu, 25 Oct 2018 14:17:31 +0300 Subject: [PATCH] 99% done with RCR, not yet been tested. --- monkey/common/utils/__init__.py | 0 monkey/common/utils/mongo_utils.py | 83 ++++++ monkey/common/utils/reg_utils.py | 25 ++ monkey/common/utils/wmi_utils.py | 27 ++ .../system_info/mimikatz_collector.py | 1 + .../system_info/windows_info_collector.py | 147 +--------- .../system_info/wmi_consts.py | 32 +++ .../cc/island_logger_default_config.json | 2 +- .../monkey_island/cc/resources/telemetry.py | 14 +- monkey/monkey_island/cc/services/node.py | 4 + .../monkey_island/cc/services/pth_report.py | 258 +++++++++--------- monkey/monkey_island/cc/services/report.py | 25 +- monkey/monkey_island/cc/services/user_info.py | 69 ++--- .../cc/services/wmi_info_handler.py | 21 +- .../cc/ui/src/components/Main.js | 1 - .../cc/ui/src/components/pages/ReportPage.js | 13 +- .../report-components/SharedAdmins.js | 42 --- .../report-components/SharedCreds.js | 41 --- 18 files changed, 375 insertions(+), 430 deletions(-) create mode 100644 monkey/common/utils/__init__.py create mode 100644 monkey/common/utils/mongo_utils.py create mode 100644 monkey/common/utils/reg_utils.py create mode 100644 monkey/common/utils/wmi_utils.py create mode 100644 monkey/infection_monkey/system_info/wmi_consts.py delete mode 100644 monkey/monkey_island/cc/ui/src/components/report-components/SharedAdmins.js delete mode 100644 monkey/monkey_island/cc/ui/src/components/report-components/SharedCreds.js diff --git a/monkey/common/utils/__init__.py b/monkey/common/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/common/utils/mongo_utils.py b/monkey/common/utils/mongo_utils.py new file mode 100644 index 000000000..7524a545e --- /dev/null +++ b/monkey/common/utils/mongo_utils.py @@ -0,0 +1,83 @@ +import wmi +import win32com + +__author__ = 'maor.rayzin' + + +class MongoUtils: + + def __init__(self): + # Static class + pass + + @staticmethod + def fix_obj_for_mongo(o): + if type(o) == dict: + return dict([(k, MongoUtils.fix_obj_for_mongo(v)) for k, v in o.iteritems()]) + + elif type(o) in (list, tuple): + return [MongoUtils.fix_obj_for_mongo(i) for i in o] + + elif type(o) in (int, float, bool): + return o + + elif type(o) in (str, unicode): + # mongo dosn't like unprintable chars, so we use repr :/ + return repr(o) + + elif hasattr(o, "__class__") and o.__class__ == wmi._wmi_object: + return MongoUtils.fix_wmi_obj_for_mongo(o) + + elif hasattr(o, "__class__") and o.__class__ == win32com.client.CDispatch: + try: + # objectSid property of ds_user is problematic and need thie special treatment. + # ISWbemObjectEx interface. Class Uint8Array ? + if str(o._oleobj_.GetTypeInfo().GetTypeAttr().iid) == "{269AD56A-8A67-4129-BC8C-0506DCFE9880}": + return o.Value + except: + pass + + try: + return o.GetObjectText_() + except: + pass + + return repr(o) + + else: + return repr(o) + + @staticmethod + def fix_wmi_obj_for_mongo(o): + row = {} + + for prop in o.properties: + try: + value = getattr(o, prop) + except wmi.x_wmi: + # This happens in Win32_GroupUser when the user is a domain user. + # For some reason, the wmi query for PartComponent fails. This table + # is actually contains references to Win32_UserAccount and Win32_Group. + # so instead of reading the content to the Win32_UserAccount, we store + # only the id of the row in that table, and get all the other information + # from that table while analyzing the data. + value = o.properties[prop].value + + row[prop] = MongoUtils.fix_obj_for_mongo(value) + + for method_name in o.methods: + if not method_name.startswith("GetOwner"): + continue + + method = getattr(o, method_name) + + try: + value = method() + value = MongoUtils.fix_obj_for_mongo(value) + row[method_name[3:]] = value + + except wmi.x_wmi: + continue + + return row + diff --git a/monkey/common/utils/reg_utils.py b/monkey/common/utils/reg_utils.py new file mode 100644 index 000000000..1e6c297b3 --- /dev/null +++ b/monkey/common/utils/reg_utils.py @@ -0,0 +1,25 @@ +import _winreg + +from common.utils.mongo_utils import MongoUtils + +__author__ = 'maor.rayzin' + + +class RegUtils: + + def __init__(self): + # Static class + pass + + @staticmethod + def get_reg_key(subkey_path, store=_winreg.HKEY_LOCAL_MACHINE): + key = _winreg.ConnectRegistry(None, store) + subkey = _winreg.OpenKey(key, subkey_path) + + d = dict([_winreg.EnumValue(subkey, i)[:2] for i in xrange(_winreg.QueryInfoKey(subkey)[0])]) + d = MongoUtils.fix_obj_for_mongo(d) + + subkey.Close() + key.Close() + + return d diff --git a/monkey/common/utils/wmi_utils.py b/monkey/common/utils/wmi_utils.py new file mode 100644 index 000000000..7b1dae455 --- /dev/null +++ b/monkey/common/utils/wmi_utils.py @@ -0,0 +1,27 @@ +import wmi + +from mongo_utils import MongoUtils + +__author__ = 'maor.rayzin' + + +class WMIUtils: + + def __init__(self): + # Static class + pass + + @staticmethod + def get_wmi_class(class_name, moniker="//./root/cimv2", properties=None): + _wmi = wmi.WMI(moniker=moniker) + + try: + if not properties: + wmi_class = getattr(_wmi, class_name)() + else: + wmi_class = getattr(_wmi, class_name)(properties) + + except wmi.x_wmi: + return + + return MongoUtils.fix_obj_for_mongo(wmi_class) diff --git a/monkey/infection_monkey/system_info/mimikatz_collector.py b/monkey/infection_monkey/system_info/mimikatz_collector.py index 1d8294ce5..4ef764251 100644 --- a/monkey/infection_monkey/system_info/mimikatz_collector.py +++ b/monkey/infection_monkey/system_info/mimikatz_collector.py @@ -57,6 +57,7 @@ class MimikatzCollector(object): Gets the logon info from mimikatz. Returns a dictionary of users with their known credentials. """ + LOG.info('Getting mimikatz logon information') if not self._isInit: return {} LOG.debug("Running mimikatz collector") diff --git a/monkey/infection_monkey/system_info/windows_info_collector.py b/monkey/infection_monkey/system_info/windows_info_collector.py index 8479fcf7f..abf0771fa 100644 --- a/monkey/infection_monkey/system_info/windows_info_collector.py +++ b/monkey/infection_monkey/system_info/windows_info_collector.py @@ -1,125 +1,20 @@ import os import logging - import sys -sys.coinit_flags = 0 # needed for proper destruction of the wmi python module -import wmi -import win32com -import _winreg -from mimikatz_collector import MimikatzCollector -from . import InfoCollector +sys.coinit_flags = 0 # needed for proper destruction of the wmi python module + import infection_monkey.config from infection_monkey.system_info.mimikatz_collector import MimikatzCollector from infection_monkey.system_info import InfoCollector +from infection_monkey.system_info.wmi_consts import WMI_CLASSES +from common.utils.wmi_utils import WMIUtils LOG = logging.getLogger(__name__) LOG.info('started windows info collector') __author__ = 'uri' -WMI_CLASSES = {"Win32_OperatingSystem", "Win32_ComputerSystem", "Win32_LoggedOnUser", "Win32_UserAccount", - "Win32_UserProfile", "Win32_Group", "Win32_GroupUser", "Win32_Product", "Win32_Service", - "Win32_OptionalFeature"} - -# These wmi queries are able to return data about all the users & machines in the domain. -# For these queries to work, the monkey shohuld be run on a domain machine and -# -# monkey should run as *** SYSTEM *** !!! -# -WMI_LDAP_CLASSES = {"ds_user": ("DS_sAMAccountName", "DS_userPrincipalName", - "DS_sAMAccountType", "ADSIPath", "DS_userAccountControl", - "DS_objectSid", "DS_objectClass", "DS_memberOf", - "DS_primaryGroupID", "DS_pwdLastSet", "DS_badPasswordTime", - "DS_badPwdCount", "DS_lastLogon", "DS_lastLogonTimestamp", - "DS_lastLogoff", "DS_logonCount", "DS_accountExpires"), - - "ds_group": ("DS_whenChanged", "DS_whenCreated", "DS_sAMAccountName", - "DS_sAMAccountType", "DS_objectSid", "DS_objectClass", - "DS_name", "DS_memberOf", "DS_member", "DS_instanceType", - "DS_cn", "DS_description", "DS_distinguishedName", "ADSIPath"), - - "ds_computer": ("DS_dNSHostName", "ADSIPath", "DS_accountExpires", - "DS_adminDisplayName", "DS_badPasswordTime", - "DS_badPwdCount", "DS_cn", "DS_distinguishedName", - "DS_instanceType", "DS_lastLogoff", "DS_lastLogon", - "DS_lastLogonTimestamp", "DS_logonCount", "DS_objectClass", - "DS_objectSid", "DS_operatingSystem", "DS_operatingSystemVersion", - "DS_primaryGroupID", "DS_pwdLastSet", "DS_sAMAccountName", - "DS_sAMAccountType", "DS_servicePrincipalName", "DS_userAccountControl", - "DS_whenChanged", "DS_whenCreated"), - } - - -def fix_obj_for_mongo(o): - if type(o) == dict: - return dict([(k, fix_obj_for_mongo(v)) for k, v in o.iteritems()]) - - elif type(o) in (list, tuple): - return [fix_obj_for_mongo(i) for i in o] - - elif type(o) in (int, float, bool): - return o - - elif type(o) in (str, unicode): - # mongo dosn't like unprintable chars, so we use repr :/ - return repr(o) - - elif hasattr(o, "__class__") and o.__class__ == wmi._wmi_object: - return fix_wmi_obj_for_mongo(o) - - elif hasattr(o, "__class__") and o.__class__ == win32com.client.CDispatch: - try: - # objectSid property of ds_user is problematic and need thie special treatment. - # ISWbemObjectEx interface. Class Uint8Array ? - if str(o._oleobj_.GetTypeInfo().GetTypeAttr().iid) == "{269AD56A-8A67-4129-BC8C-0506DCFE9880}": - return o.Value - except: - pass - - try: - return o.GetObjectText_() - except: - pass - - return repr(o) - - else: - return repr(o) - -def fix_wmi_obj_for_mongo(o): - row = {} - - for prop in o.properties: - try: - value = getattr(o, prop) - except wmi.x_wmi: - # This happens in Win32_GroupUser when the user is a domain user. - # For some reason, the wmi query for PartComponent fails. This table - # is actually contains references to Win32_UserAccount and Win32_Group. - # so instead of reading the content to the Win32_UserAccount, we store - # only the id of the row in that table, and get all the other information - # from that table while analyzing the data. - value = o.properties[prop].value - - row[prop] = fix_obj_for_mongo(value) - - for method_name in o.methods: - if not method_name.startswith("GetOwner"): - continue - - method = getattr(o, method_name) - - try: - value = method() - value = fix_obj_for_mongo(value) - row[method_name[3:]] = value - - except wmi.x_wmi: - continue - - return row - class WindowsInfoCollector(InfoCollector): """ @@ -147,8 +42,8 @@ class WindowsInfoCollector(InfoCollector): self.get_wmi_info() LOG.debug('finished get_wmi_info') - #self.get_reg_key(r"SYSTEM\CurrentControlSet\Control\Lsa") self.get_installed_packages() + LOG.debug('Got installed packages') mimikatz_collector = MimikatzCollector() mimikatz_info = mimikatz_collector.get_logon_info() @@ -156,39 +51,17 @@ class WindowsInfoCollector(InfoCollector): if "credentials" in self.info: self.info["credentials"].update(mimikatz_info) self.info["mimikatz"] = mimikatz_collector.get_mimikatz_text() + else: + LOG.info('No mimikatz info was gathered') return self.info def get_installed_packages(self): + LOG.info('getting installed packages') self.info["installed_packages"] = os.popen("dism /online /get-packages").read() self.info["installed_features"] = os.popen("dism /online /get-features").read() def get_wmi_info(self): + LOG.info('getting wmi info') for wmi_class_name in WMI_CLASSES: - self.info['wmi'][wmi_class_name] = self.get_wmi_class(wmi_class_name) - - def get_wmi_class(self, class_name, moniker="//./root/cimv2", properties=None): - _wmi = wmi.WMI(moniker=moniker) - - try: - if not properties: - wmi_class = getattr(_wmi, class_name)() - else: - wmi_class = getattr(_wmi, class_name)(properties) - - except wmi.x_wmi: - return - - return fix_obj_for_mongo(wmi_class) - - def get_reg_key(self, subkey_path, store=_winreg.HKEY_LOCAL_MACHINE): - key = _winreg.ConnectRegistry(None, store) - subkey = _winreg.OpenKey(key, subkey_path) - - d = dict([_winreg.EnumValue(subkey, i)[:2] for i in xrange(_winreg.QueryInfoKey(subkey)[0])]) - d = fix_obj_for_mongo(d) - - self.info['reg'][subkey_path] = d - - subkey.Close() - key.Close() + self.info['wmi'][wmi_class_name] = WMIUtils.get_wmi_class(wmi_class_name) diff --git a/monkey/infection_monkey/system_info/wmi_consts.py b/monkey/infection_monkey/system_info/wmi_consts.py new file mode 100644 index 000000000..a87e297d9 --- /dev/null +++ b/monkey/infection_monkey/system_info/wmi_consts.py @@ -0,0 +1,32 @@ +WMI_CLASSES = {"Win32_OperatingSystem", "Win32_ComputerSystem", "Win32_LoggedOnUser", "Win32_UserAccount", + "Win32_UserProfile", "Win32_Group", "Win32_GroupUser", "Win32_Product", "Win32_Service", + "Win32_OptionalFeature"} + +# These wmi queries are able to return data about all the users & machines in the domain. +# For these queries to work, the monkey should be run on a domain machine and +# +# monkey should run as *** SYSTEM *** !!! +# +WMI_LDAP_CLASSES = {"ds_user": ("DS_sAMAccountName", "DS_userPrincipalName", + "DS_sAMAccountType", "ADSIPath", "DS_userAccountControl", + "DS_objectSid", "DS_objectClass", "DS_memberOf", + "DS_primaryGroupID", "DS_pwdLastSet", "DS_badPasswordTime", + "DS_badPwdCount", "DS_lastLogon", "DS_lastLogonTimestamp", + "DS_lastLogoff", "DS_logonCount", "DS_accountExpires"), + + "ds_group": ("DS_whenChanged", "DS_whenCreated", "DS_sAMAccountName", + "DS_sAMAccountType", "DS_objectSid", "DS_objectClass", + "DS_name", "DS_memberOf", "DS_member", "DS_instanceType", + "DS_cn", "DS_description", "DS_distinguishedName", "ADSIPath"), + + "ds_computer": ("DS_dNSHostName", "ADSIPath", "DS_accountExpires", + "DS_adminDisplayName", "DS_badPasswordTime", + "DS_badPwdCount", "DS_cn", "DS_distinguishedName", + "DS_instanceType", "DS_lastLogoff", "DS_lastLogon", + "DS_lastLogonTimestamp", "DS_logonCount", "DS_objectClass", + "DS_objectSid", "DS_operatingSystem", "DS_operatingSystemVersion", + "DS_primaryGroupID", "DS_pwdLastSet", "DS_sAMAccountName", + "DS_sAMAccountType", "DS_servicePrincipalName", "DS_userAccountControl", + "DS_whenChanged", "DS_whenCreated"), + } + diff --git a/monkey/monkey_island/cc/island_logger_default_config.json b/monkey/monkey_island/cc/island_logger_default_config.json index 7c2886410..34a57b374 100644 --- a/monkey/monkey_island/cc/island_logger_default_config.json +++ b/monkey/monkey_island/cc/island_logger_default_config.json @@ -17,7 +17,7 @@ "info_file_handler": { "class": "logging.handlers.RotatingFileHandler", - "level": "DEBUG", + "level": "INFO", "formatter": "simple", "filename": "info.log", "maxBytes": 10485760, diff --git a/monkey/monkey_island/cc/resources/telemetry.py b/monkey/monkey_island/cc/resources/telemetry.py index efd5e2414..1680f7664 100644 --- a/monkey/monkey_island/cc/resources/telemetry.py +++ b/monkey/monkey_island/cc/resources/telemetry.py @@ -186,19 +186,11 @@ class Telemetry(flask_restful.Resource): Telemetry.add_system_info_creds_to_config(creds) Telemetry.replace_user_dot_with_comma(creds) if 'mimikatz' in telemetry_json['data']: - users_secrets = user_info.extract_secrets_from_mimikatz(telemetry_json['data'].get('mimikatz', '')) + users_secrets = user_info.MimikatzSecrets.\ + extract_secrets_from_mimikatz(telemetry_json['data'].get('mimikatz', '')) if 'wmi' in telemetry_json['data']: wmi_handler = WMIHandler(monkey_id, telemetry_json['data']['wmi'], users_secrets) - wmi_handler.add_groups_to_collection() - wmi_handler.add_users_to_collection() - wmi_handler.create_group_user_connection() - wmi_handler.insert_info_to_mongo() - - wmi_handler.add_admin(wmi_handler.info_for_mongo[wmi_handler.ADMINISTRATORS_GROUP_KNOWN_SID], monkey_id) - wmi_handler.update_admins_retrospective() - wmi_handler.update_critical_services(telemetry_json['data']['wmi']['Win32_Service'], - telemetry_json['data']['wmi']['Win32_Product'], - monkey_id) + wmi_handler.process_and_handle_wmi_info() @staticmethod def add_ip_to_ssh_keys(ip, ssh_info): diff --git a/monkey/monkey_island/cc/services/node.py b/monkey/monkey_island/cc/services/node.py index 371eefb5a..87b2a1aec 100644 --- a/monkey/monkey_island/cc/services/node.py +++ b/monkey/monkey_island/cc/services/node.py @@ -325,3 +325,7 @@ class NodeService: @staticmethod def get_node_hostname(node): return node['hostname'] if 'hostname' in node else node['os']['version'] + + @staticmethod + def get_hostname_by_id(node_id): + NodeService.get_node_hostname(mongo.db.monkey.find_one({'_id': node_id}, {'hostname': 1})) diff --git a/monkey/monkey_island/cc/services/pth_report.py b/monkey/monkey_island/cc/services/pth_report.py index 7a3dbc39b..11d2be821 100644 --- a/monkey/monkey_island/cc/services/pth_report.py +++ b/monkey/monkey_island/cc/services/pth_report.py @@ -1,16 +1,16 @@ -import uuid -from itertools import combinations, product +from itertools import product from cc.database import mongo from bson import ObjectId +from cc.services.node import NodeService + +__author__ = 'maor.rayzin' class PTHReportService(object): @staticmethod - def get_duplicated_passwords_nodes(): - users_cred_groups = [] - + def __dup_passwords_mongoquery(): pipeline = [ {"$match": { 'NTLM_secret': { @@ -26,74 +26,16 @@ class PTHReportService(object): }}, {'$match': {'count': {'$gt': 1}}} ] - docs = mongo.db.groupsandusers.aggregate(pipeline) - for doc in docs: - users_list = [] - for user in doc['Docs']: - hostname = None - if user['machine_id']: - machine = mongo.db.monkey.find_one({'_id': ObjectId(user['machine_id'])}, {'hostname': 1}) - if machine.get('hostname'): - hostname = machine['hostname'] - users_list.append({'username': user['name'], 'domain_name': user['domain_name'], - 'hostname': hostname}) - users_cred_groups.append({'cred_groups': users_list}) - - return users_cred_groups + return mongo.db.groupsandusers.aggregate(pipeline) @staticmethod - def get_duplicated_passwords_issues(): - user_groups = PTHReportService.get_duplicated_passwords_nodes() - issues = [] - users_gathered = [] - for group in user_groups: - for user_info in group['cred_groups']: - users_gathered.append(user_info['username']) - issues.append( - { - 'type': 'shared_passwords_domain' if user_info['domain_name'] else 'shared_passwords', - 'machine': user_info['hostname'] if user_info['hostname'] else user_info['domain_name'], - 'shared_with': [i['username'] for i in group['cred_groups']], - 'is_local': False if user_info['domain_name'] else True - } - ) - break - return issues + def __get_admin_on_machines_format(admin_on_machines): + + machines = mongo.db.monkey.find({'_id': {'$in': admin_on_machines}}, {'hostname': 1}) + return [i['hostname'] for i in list(machines)] @staticmethod - def get_shared_admins_nodes(): - admins = mongo.db.groupsandusers.find({'type': 1, 'admin_on_machines.1': {'$exists': True}}, - {'admin_on_machines': 1, 'name': 1, 'domain_name': 1}) - admins_info_list = [] - for admin in admins: - machines = mongo.db.monkey.find({'_id': {'$in': admin['admin_on_machines']}}, {'hostname': 1}) - - # appends the host names of the machines this user is admin on. - admins_info_list.append({'name': admin['name'],'domain_name': admin['domain_name'], - 'admin_on_machines': [i['hostname'] for i in list(machines)]}) - - return admins_info_list - - @staticmethod - def get_shared_admins_issues(): - admins_info = PTHReportService.get_shared_admins_nodes() - issues = [] - for admin in admins_info: - issues.append( - { - 'is_local': False, - 'type': 'shared_admins_domain', - 'machine': admin['domain_name'], - 'username': admin['name'], - 'shared_machines': admin['admin_on_machines'], - } - ) - - return issues - - @staticmethod - def get_strong_users_on_critical_machines_nodes(): - crit_machines = {} + def __strong_users_on_crit_query(): pipeline = [ { '$unwind': '$admin_on_machines' @@ -117,13 +59,82 @@ class PTHReportService(object): '$unwind': '$critical_machine' } ] - docs = mongo.db.groupsandusers.aggregate(pipeline) + return mongo.db.groupsandusers.aggregate(pipeline) + + @staticmethod + def get_duplicated_passwords_nodes(): + users_cred_groups = [] + docs = PTHReportService.__dup_passwords_mongoquery() + for doc in docs: + users_list = [ + { + 'username': user['name'], + 'domain_name': user['domain_name'], + 'hostname': NodeService.get_hostname_by_id(ObjectId(user['machine_id'])) + if user['machine_id'] else None + } for user in doc['Docs'] + ] + users_cred_groups.append({'cred_groups': users_list}) + + return users_cred_groups + + @staticmethod + def get_duplicated_passwords_issues(): + user_groups = PTHReportService.get_duplicated_passwords_nodes() + issues = [] + for group in user_groups: + user_info = group['cred_groups'][0] + issues.append( + { + 'type': 'shared_passwords_domain' if user_info['domain_name'] else 'shared_passwords', + 'machine': user_info['hostname'] if user_info['hostname'] else user_info['domain_name'], + 'shared_with': [i['username'] for i in group['cred_groups']], + 'is_local': False if user_info['domain_name'] else True + } + ) + return issues + + @staticmethod + def get_shared_admins_nodes(): + + # This mongo queries users the best solution to figure out if an array + # object has at least two objects in it, by making sure any value exists in the array index 1. + admins = mongo.db.groupsandusers.find({'type': 1, 'admin_on_machines.1': {'$exists': True}}, + {'admin_on_machines': 1, 'name': 1, 'domain_name': 1}) + return [ + { + 'name': admin['name'], + 'domain_name': admin['domain_name'], + 'admin_on_machines': PTHReportService.__get_admin_on_machines_format(admin['admin_on_machines']) + } for admin in admins + ] + + @staticmethod + def get_shared_admins_issues(): + admins_info = PTHReportService.get_shared_admins_nodes() + return [ + { + 'is_local': False, + 'type': 'shared_admins_domain', + 'machine': admin['domain_name'], + 'username': admin['name'], + 'shared_machines': admin['admin_on_machines'], + } + for admin in admins_info] + + @staticmethod + def get_strong_users_on_critical_machines_nodes(): + + crit_machines = {} + docs = PTHReportService.__strong_users_on_crit_query() + for doc in docs: hostname = str(doc['critical_machine']['hostname']) - if not hostname in crit_machines: - crit_machines[hostname] = {} - crit_machines[hostname]['threatening_users'] = [] - crit_machines[hostname]['critical_services'] = doc['critical_machine']['critical_services'] + if hostname not in crit_machines: + crit_machines[hostname] = { + 'threatening_users': [], + 'critical_services': doc['critical_machine']['critical_services'] + } crit_machines[hostname]['threatening_users'].append( {'name': str(doc['domain_name']) + '\\' + str(doc['name']), 'creds_location': doc['secret_location']}) @@ -131,107 +142,92 @@ class PTHReportService(object): @staticmethod def get_strong_users_on_crit_issues(): - issues = [] crit_machines = PTHReportService.get_strong_users_on_critical_machines_nodes() - for machine in crit_machines: - issues.append( - { - 'type': 'strong_users_on_crit', - 'machine': machine, - 'services': crit_machines[machine].get('critical_services'), - 'threatening_users': [i['name'] for i in crit_machines[machine]['threatening_users']] - } - ) - return issues + return [ + { + 'type': 'strong_users_on_crit', + 'machine': machine, + 'services': crit_machines[machine].get('critical_services'), + 'threatening_users': [i['name'] for i in crit_machines[machine]['threatening_users']] + } for machine in crit_machines + ] @staticmethod def get_strong_users_on_crit_details(): - table_entries = [] user_details = {} crit_machines = PTHReportService.get_strong_users_on_critical_machines_nodes() for machine in crit_machines: for user in crit_machines[machine]['threatening_users']: username = user['name'] if username not in user_details: - user_details[username] = {} - user_details[username]['machines'] = [] - user_details[username]['services'] = [] + user_details[username] = { + 'machines': [], + 'services': [] + } user_details[username]['machines'].append(machine) user_details[username]['services'] += crit_machines[machine]['critical_services'] - for user in user_details: - table_entries.append( - { - 'username': user, - 'machines': user_details[user]['machines'], - 'services_names': user_details[user]['services'] - } - ) - - return table_entries + return [ + { + 'username': user, + 'machines': user_details[user]['machines'], + 'services_names': user_details[user]['services'] + } for user in user_details + ] @staticmethod def generate_map_nodes(): - - nodes_list = [] monkeys = mongo.db.monkey.find({}, {'_id': 1, 'hostname': 1, 'critical_services': 1, 'ip_addresses': 1}) - for monkey in monkeys: - critical_services = monkey.get('critical_services', []) - nodes_list.append({ + + return [ + { 'id': monkey['_id'], 'label': '{0} : {1}'.format(monkey['hostname'], monkey['ip_addresses'][0]), - 'group': 'critical' if critical_services else 'normal', - 'services': critical_services, + 'group': 'critical' if monkey.get('critical_services', []) else 'normal', + 'services': monkey.get('critical_services', []), 'hostname': monkey['hostname'] - }) - - return nodes_list + } for monkey in monkeys + ] @staticmethod - def generate_edge_nodes(): + def generate_edges(): edges_list = [] - pipeline = [ + + comp_users = mongo.db.groupsandusers.find( { - '$match': {'admin_on_machines': {'$ne': []}, 'secret_location': {'$ne': []}, 'type': 1} + 'admin_on_machines': {'$ne': []}, + 'secret_location': {'$ne': []}, + 'type': 1 }, { - '$project': {'admin_on_machines': 1, 'secret_location': 1} + 'admin_on_machines': 1, 'secret_location': 1 } - ] - comp_users = mongo.db.groupsandusers.aggregate(pipeline) + ) for user in comp_users: - pairs = PTHReportService.generate_edges_tuples(user['admin_on_machines'], user['secret_location']) - for pair in pairs: + # A list comp, to get all unique pairs of attackers and victims. + for pair in [pair for pair in product(user['admin_on_machines'], user['secret_location']) + if pair[0] != pair[1]]: edges_list.append( { 'from': pair[1], 'to': pair[0], - 'id': str(uuid.uuid4()) + 'id': str(pair[1]) + str(pair[0]) } ) return edges_list - @staticmethod - def generate_edges_tuples(*lists): - - for t in combinations(lists, 2): - for pair in product(*t): - # Don't output pairs containing duplicated elements - if pair[0] != pair[1]: - yield pair - @staticmethod def get_pth_map(): return { 'nodes': PTHReportService.generate_map_nodes(), - 'edges': PTHReportService.generate_edge_nodes() + 'edges': PTHReportService.generate_edges() } @staticmethod def get_report(): - + pth_map = PTHReportService.get_pth_map() PTHReportService.get_strong_users_on_critical_machines_nodes() report = \ { @@ -242,8 +238,8 @@ class PTHReportService(object): 'pthmap': { - 'nodes': PTHReportService.generate_map_nodes(), - 'edges': PTHReportService.generate_edge_nodes() + 'nodes': pth_map.get('nodes'), + 'edges': pth_map.get('edges') } } diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/report.py index f82f1518d..26a5c87f1 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/report.py @@ -1,6 +1,5 @@ import itertools import functools -import pprint import ipaddress import logging @@ -160,7 +159,7 @@ class ReportService: @staticmethod def get_stolen_creds(): PASS_TYPE_DICT = {'password': 'Clear Password', 'lm_hash': 'LM hash', 'ntlm_hash': 'NTLM hash'} - creds = [] + creds = set() for telem in mongo.db.telemetry.find( {'telem_type': 'system_info_collection', 'data.credentials': {'$exists': True}}, {'data.credentials': 1, 'monkey_guid': 1} @@ -177,14 +176,9 @@ class ReportService: 'type': PASS_TYPE_DICT[pass_type], 'origin': origin } - if cred_row not in creds: - creds.append(cred_row) + creds.add(cred_row) logger.info('Stolen creds generated for reporting') - return creds - - @staticmethod - def get_pth_shared_passwords(): - pass + return list(creds) @staticmethod def get_ssh_keys(): @@ -544,7 +538,7 @@ class ReportService: domain_issues_dict = {} for issue in issues: if not issue.get('is_local', True): - machine = issue.get('machine', '').upper() + machine = issue.get('machine').upper() if machine not in domain_issues_dict: domain_issues_dict[machine] = [] domain_issues_dict[machine].append(issue) @@ -566,7 +560,7 @@ class ReportService: issues_dict = {} for issue in issues: if issue.get('is_local', True): - machine = issue.get('machine', '').upper() + machine = issue.get('machine').upper() if machine not in issues_dict: issues_dict[machine] = [] issues_dict[machine].append(issue) @@ -707,17 +701,14 @@ class ReportService: 'exploited': ReportService.get_exploited(), 'stolen_creds': ReportService.get_stolen_creds(), 'azure_passwords': ReportService.get_azure_creds(), - 'ssh_keys': ReportService.get_ssh_keys() + 'ssh_keys': ReportService.get_ssh_keys(), + 'strong_users': PTHReportService.get_strong_users_on_crit_details(), + 'pth_map': PTHReportService.get_pth_map() }, 'recommendations': { 'issues': issues, 'domain_issues': domain_issues - }, - 'pth': - { - 'strong_users': PTHReportService.get_strong_users_on_crit_details(), - 'map': PTHReportService.get_pth_map(), } } diff --git a/monkey/monkey_island/cc/services/user_info.py b/monkey/monkey_island/cc/services/user_info.py index c69a011a6..e233c1f31 100644 --- a/monkey/monkey_island/cc/services/user_info.py +++ b/monkey/monkey_island/cc/services/user_info.py @@ -1,45 +1,52 @@ +__author__ = 'maor.rayzin' + -def extract_sam_secrets(mim_string, users_dict): - users_secrets = mim_string.split("\n42.")[1].split("\nSAMKey :")[1].split("\n\n")[1:] +class MimikatzSecrets(object): - if mim_string.count("\n42.") != 2: - return {} + def __init__(self): + # Static class + pass - for sam_user_txt in users_secrets: - sam_user = dict([map(unicode.strip, line.split(":")) for line in - filter(lambda l: l.count(":") == 1, sam_user_txt.splitlines())]) - username = sam_user.get("User") - users_dict[username] = {} + @staticmethod + def extract_sam_secrets(mim_string, users_dict): + users_secrets = mim_string.split("\n42.")[1].split("\nSAMKey :")[1].split("\n\n")[1:] - ntlm = sam_user.get("NTLM") - if "[hashed secret]" not in ntlm: - continue + if mim_string.count("\n42.") != 2: + return {} - users_dict[username]['SAM'] = ntlm.replace("[hashed secret]", "").strip() + for sam_user_txt in users_secrets: + sam_user = dict([map(unicode.strip, line.split(":")) for line in + filter(lambda l: l.count(":") == 1, sam_user_txt.splitlines())]) + username = sam_user.get("User") + users_dict[username] = {} + ntlm = sam_user.get("NTLM") + if "[hashed secret]" not in ntlm: + continue -def extract_ntlm_secrets(mim_string, users_dict): + users_dict[username]['SAM'] = ntlm.replace("[hashed secret]", "").strip() - if mim_string.count("\n42.") != 2: - return {} + @staticmethod + def extract_ntlm_secrets(mim_string, users_dict): - ntds_users = mim_string.split("\n42.")[2].split("\nRID :")[1:] + if mim_string.count("\n42.") != 2: + return {} - for ntds_user_txt in ntds_users: - user = ntds_user_txt.split("User :")[1].splitlines()[0].replace("User :", "").strip() - ntlm = ntds_user_txt.split("* Primary\n NTLM :")[1].splitlines()[0].replace("NTLM :", "").strip() - ntlm = ntlm.replace("[hashed secret]", "").strip() - users_dict[user] = {} - if ntlm: - users_dict[user]['ntlm'] = ntlm + ntds_users = mim_string.split("\n42.")[2].split("\nRID :")[1:] + for ntds_user_txt in ntds_users: + user = ntds_user_txt.split("User :")[1].splitlines()[0].replace("User :", "").strip() + ntlm = ntds_user_txt.split("* Primary\n NTLM :")[1].splitlines()[0].replace("NTLM :", "").strip() + ntlm = ntlm.replace("[hashed secret]", "").strip() + users_dict[user] = {} + if ntlm: + users_dict[user]['ntlm'] = ntlm -def extract_secrets_from_mimikatz(mim_string): - users_dict = {} - extract_sam_secrets(mim_string, users_dict) - extract_ntlm_secrets(mim_string, users_dict) - - return users_dict - + @staticmethod + def extract_secrets_from_mimikatz(mim_string): + users_dict = {} + MimikatzSecrets.extract_sam_secrets(mim_string, users_dict) + MimikatzSecrets.extract_ntlm_secrets(mim_string, users_dict) + return users_dict diff --git a/monkey/monkey_island/cc/services/wmi_info_handler.py b/monkey/monkey_island/cc/services/wmi_info_handler.py index b4b12c7a3..61f85eb61 100644 --- a/monkey/monkey_island/cc/services/wmi_info_handler.py +++ b/monkey/monkey_island/cc/services/wmi_info_handler.py @@ -1,7 +1,9 @@ from cc.database import mongo +__author__ = 'maor.rayzin' -class WMIHandler: + +class WMIHandler(object): ADMINISTRATORS_GROUP_KNOWN_SID = '1-5-32-544' @@ -13,26 +15,29 @@ class WMIHandler: self.users_info = wmi_info['Win32_UserAccount'] self.groups_info = wmi_info['Win32_Group'] self.groups_and_users = wmi_info['Win32_GroupUser'] + self.products = wmi_info['Win32_Service'] + self.services = wmi_info['Win32_Product'] - def process_and_handle(self): + def process_and_handle_wmi_info(self): self.add_groups_to_collection() self.add_users_to_collection() self.create_group_user_connection() + self.insert_info_to_mongo() self.add_admin(self.info_for_mongo[self.ADMINISTRATORS_GROUP_KNOWN_SID], self.monkey_id) self.update_admins_retrospective() - self.insert_info_to_mongo() + self.update_critical_services() - def update_critical_services(self, wmi_services, wmi_products, machine_id): + def update_critical_services(self): critical_names = ("W3svc", "MSExchangeServiceHost", "MSSQLServer", "dns", 'MSSQL$SQLEXPRESS', 'SQL') - mongo.db.monkey.update({'_id': machine_id}, {'$set': {'critical_services': []}}) + mongo.db.monkey.update({'_id': self.monkey_id}, {'$set': {'critical_services': []}}) - services_names_list = [str(i['Name'])[2:-1] for i in wmi_services] - products_names_list = [str(i['Name'])[2:-2] for i in wmi_products] + services_names_list = [str(i['Name'])[2:-1] for i in self.services] + products_names_list = [str(i['Name'])[2:-2] for i in self.products] for name in critical_names: if name in services_names_list or name in products_names_list: - mongo.db.monkey.update({'_id': machine_id}, {'$addToSet': {'critical_services': name}}) + mongo.db.monkey.update({'_id': self.monkey_id}, {'$addToSet': {'critical_services': name}}) def build_entity_document(self, entity_info, monkey_id=None): general_properties_dict = { diff --git a/monkey/monkey_island/cc/ui/src/components/Main.js b/monkey/monkey_island/cc/ui/src/components/Main.js index 5a5a4e526..114775756 100644 --- a/monkey/monkey_island/cc/ui/src/components/Main.js +++ b/monkey/monkey_island/cc/ui/src/components/Main.js @@ -163,7 +163,6 @@ class AppComponent extends AuthComponent { {this.renderRoute('/run-monkey', )} {this.renderRoute('/infection/map', )} {this.renderRoute('/infection/telemetry', )} - {this.renderRoute('/pth', )} {this.renderRoute('/start-over', )} {this.renderRoute('/report', )} {this.renderRoute('/license', )} diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js index 98224541c..67dc9e0c4 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -9,9 +9,7 @@ import CollapsibleWellComponent from 'components/report-components/CollapsibleWe import {Line} from 'rc-progress'; import AuthComponent from '../AuthComponent'; import PassTheHashMapPageComponent from "./PassTheHashMapPage"; -import SharedCreds from "components/report-components/SharedCreds"; import StrongUsers from "components/report-components/StrongUsers"; -import SharedAdmins from "components/report-components/SharedAdmins"; let guardicoreLogoImage = require('../../images/guardicore-logo.png'); let monkeyLogoImage = require('../../images/monkey-icon.svg'); @@ -47,13 +45,10 @@ class ReportPageComponent extends AuthComponent { super(props); this.state = { report: {}, - pthreport: {}, - pthmap: {}, graph: {nodes: [], edges: []}, allMonkeysAreDead: false, runStarted: true }; - this.getPth } componentDidMount() { @@ -122,9 +117,7 @@ class ReportPageComponent extends AuthComponent { .then(res => res.json()) .then(res => { this.setState({ - report: res, - pthreport: res.pth.info, - pthmap: res.pth.map + report: res }); }); } @@ -475,7 +468,7 @@ class ReportPageComponent extends AuthComponent {
- +
); @@ -495,7 +488,7 @@ class ReportPageComponent extends AuthComponent { Access credentials |
- +

diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/SharedAdmins.js b/monkey/monkey_island/cc/ui/src/components/report-components/SharedAdmins.js deleted file mode 100644 index bf57065d5..000000000 --- a/monkey/monkey_island/cc/ui/src/components/report-components/SharedAdmins.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import ReactTable from 'react-table' - -let renderArray = function(val) { - return
{val.map(x =>
{x}
)}
; -}; - -const columns = [ - { - Header: 'Shared Admins Between Machines', - columns: [ - { Header: 'Username', accessor: 'username'}, - { Header: 'Domain', accessor: 'domain'}, - { Header: 'Machines', id: 'machines', accessor: x => renderArray(x.machines)}, - ] - } -]; - -const pageSize = 10; - -class SharedAdminsComponent extends React.Component { - constructor(props) { - super(props); - } - - render() { - let defaultPageSize = this.props.data.length > pageSize ? pageSize : this.props.data.length; - let showPagination = this.props.data.length > pageSize; - return ( -
- -
- ); - } -} - -export default SharedAdminsComponent; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/SharedCreds.js b/monkey/monkey_island/cc/ui/src/components/report-components/SharedCreds.js deleted file mode 100644 index f42494167..000000000 --- a/monkey/monkey_island/cc/ui/src/components/report-components/SharedCreds.js +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; -import ReactTable from 'react-table' - -let renderArray = function(val) { - console.log(val); - return
{val.map(x =>
{x}
)}
; -}; - -const columns = [ - { - Header: 'Shared Credentials', - columns: [ - {Header: 'Credential Group', id: 'cred_group', accessor: x => renderArray(x.cred_group) } - ] - } -]; - -const pageSize = 10; - -class SharedCredsComponent extends React.Component { - constructor(props) { - super(props); - } - - render() { - let defaultPageSize = this.props.data.length > pageSize ? pageSize : this.props.data.length; - let showPagination = this.props.data.length > pageSize; - return ( -
- -
- ); - } -} - -export default SharedCredsComponent;