diff --git a/monkey/monkey_island/cc/resources/telemetry.py b/monkey/monkey_island/cc/resources/telemetry.py index 0ef453e98..0c390a192 100644 --- a/monkey/monkey_island/cc/resources/telemetry.py +++ b/monkey/monkey_island/cc/resources/telemetry.py @@ -197,11 +197,19 @@ class Telemetry(flask_restful.Resource): Telemetry.create_group_user_connection(info_for_mongo, group_user_dict) for entity in info_for_mongo.values(): if entity['machine_id']: + # Handling for local entities. mongo.db.groupsandusers.update({'SID': entity['SID'], 'machine_id': entity['machine_id']}, entity, upsert=True) else: + # Handlings for domain entities. if not mongo.db.groupsandusers.find_one({'SID': entity['SID']}): mongo.db.groupsandusers.insert_one(entity) + else: + # if entity is domain entity, add the monkey id of current machine to secrets_location. + # (found on this machine) + if entity.get('NTLM_secret'): + mongo.db.groupsandusers.update_one({'SID': entity['SID'], 'type': 1}, + {'$addToSet': {'secret_location': monkey_id}}) Telemetry.add_admin(info_for_mongo[group_info.ADMINISTRATORS_GROUP_KNOWN_SID], monkey_id) Telemetry.update_admins_retrospective(info_for_mongo) @@ -209,6 +217,8 @@ class Telemetry(flask_restful.Resource): telemetry_json['data']['wmi']['Win32_Product'], monkey_id) + + @staticmethod def update_critical_services(wmi_services, wmi_products, machine_id): critical_names = ("W3svc", "MSExchangeServiceHost", "MSSQLServer", "dns", 'MSSQL$SQLEXPRESS', 'SQL') diff --git a/monkey/monkey_island/cc/services/node.py b/monkey/monkey_island/cc/services/node.py index 47cd9cd21..371eefb5a 100644 --- a/monkey/monkey_island/cc/services/node.py +++ b/monkey/monkey_island/cc/services/node.py @@ -97,6 +97,11 @@ class NodeService: def get_monkey_label_by_id(monkey_id): return NodeService.get_monkey_label(NodeService.get_monkey_by_id(monkey_id)) + @staticmethod + def get_monkey_critical_services(monkey_id): + critical_services = mongo.db.monkey.find_one({'_id': monkey_id}, {'critical_services': 1}).get('critical_services', []) + return critical_services + @staticmethod def get_monkey_label(monkey): label = monkey["hostname"] + " : " + monkey["ip_addresses"][0] diff --git a/monkey/monkey_island/cc/services/pth_report.py b/monkey/monkey_island/cc/services/pth_report.py index 726fba143..bc995242d 100644 --- a/monkey/monkey_island/cc/services/pth_report.py +++ b/monkey/monkey_island/cc/services/pth_report.py @@ -1,3 +1,7 @@ +import uuid +from itertools import combinations, product + +from cc.services.edge import EdgeService from cc.services.pth_report_utils import PassTheHashReport, Machine from cc.database import mongo from bson import ObjectId @@ -49,7 +53,6 @@ class PTHReportService(object): @staticmethod def get_duplicated_passwords_issues(): - # TODO: Fix bug if both local and non local account share the same password user_groups = PTHReportService.get_duplicated_passwords_nodes() issues = [] users_gathered = [] @@ -98,195 +101,130 @@ class PTHReportService(object): return issues - @staticmethod - def old_get_shared_local_admins_nodes(pth): - dups = dict(map(lambda x: (x, len(pth.GetSharedAdmins(x))), pth.machines)) - shared_admin_machines = [] - for m, count in sorted(dups.iteritems(), key=lambda (k, v): (v, k), reverse=True): - if count <= 0: - continue - shared_admin_account_list = [] - - for sid in pth.GetSharedAdmins(m): - shared_admin_account_list.append(pth.GetUsernameBySid(sid)) - - machine = { - 'ip': m.GetIp(), - 'hostname': m.GetHostName(), - 'domain': m.GetDomainName(), - 'services_names': m.GetCriticalServicesInstalled(), - 'user_count': count, - 'admins_accounts': shared_admin_account_list - } - - shared_admin_machines.append(machine) - - return shared_admin_machines - - @staticmethod - def get_strong_users_on_crit_services_by_machine(pth): - threatening = dict(map(lambda x: (x, len(pth.GetThreateningUsersByVictim(x))), pth.GetCritialServers())) - strong_users_crit_list = [] - - for m, count in sorted(threatening.iteritems(), key=lambda (k, v): (v, k), reverse=True): - if count <= 0: - continue - - threatening_users_attackers_dict = {} - for sid in pth.GetThreateningUsersByVictim(m): - username = pth.GetUsernameBySid(sid) - threatening_users_attackers_dict[username] = [] - for mm in pth.GetAttackersBySid(sid): - if m == mm: - continue - threatening_users_attackers_dict[username] = mm.GetIp() - - machine = { - 'ip': m.GetIp(), - 'hostname': m.GetHostName(), - 'domain': m.GetDomainName(), - 'services': m.GetCriticalServicesInstalled(), - 'threatening_users': threatening_users_attackers_dict - } - strong_users_crit_list.append(machine) - return strong_users_crit_list - - @staticmethod - def get_strong_users_on_crit_services_by_user(pth): - critical_servers = pth.GetCritialServers() - strong_users_dict = {} - - for server in critical_servers: - users = pth.GetThreateningUsersByVictim(server) - for sid in users: - username = pth.GetUsernameBySid(sid) - if username not in strong_users_dict: - strong_users_dict[username] = { - 'services_names': [], - 'machines': [] + def get_strong_users_on_critical_machines_nodes(): + crit_machines = {} + pipeline = [ + { + '$unwind': '$admin_on_machines' + }, + { + '$match': {'type': 1, 'domain_name': {'$ne': None}} + }, + { + '$lookup': + { + 'from': 'monkey', + 'localField': 'admin_on_machines', + 'foreignField': '_id', + 'as': 'critical_machine' } - strong_users_dict[username]['username'] = username - strong_users_dict[username]['domain'] = server.GetDomainName() - strong_users_dict[username]['services_names'] += server.GetCriticalServicesInstalled() - strong_users_dict[username]['machines'].append(server.GetHostName()) - - return strong_users_dict.values() - - @staticmethod - def get_strong_users_on_non_crit_services(pth): - threatening = dict(map(lambda x: (x, len(pth.GetThreateningUsersByVictim(x))), pth.GetNonCritialServers())) - - strong_users_non_crit_list = [] - - for m, count in sorted(threatening.iteritems(), key=lambda (k, v): (v, k), reverse=True): - if count <= 0: - continue - - threatening_users_attackers_dict = {} - for sid in pth.GetThreateningUsersByVictim(m): - username = pth.GetUsernameBySid(sid) - threatening_users_attackers_dict[username] = [] - for mm in pth.GetAttackersBySid(sid): - if m == mm: - continue - threatening_users_attackers_dict[username] = mm.GetIp() - - machine = { - 'ip': m.GetIp(), - 'hostname': m.GetHostName(), - 'domain': m.GetDomainName(), - 'services_names': [], - 'user_count': count, - 'threatening_users': threatening_users_attackers_dict + }, + { + '$match': {'critical_machine.critical_services': {'$ne': []}} + }, + { + '$unwind': '$critical_machine' } - strong_users_non_crit_list.append(machine) - return strong_users_non_crit_list - - - + ] + docs = mongo.db.groupsandusers.aggregate(pipeline) + 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'] + crit_machines[hostname]['threatening_users'].append( + {'name': str(doc['domain_name']) + '\\' + str(doc['name']), + 'creds_location': doc['secret_location']}) + return crit_machines @staticmethod - def strong_users_on_crit_issues(strong_users): + def get_strong_users_on_crit_issues(): issues = [] - for machine in strong_users: + crit_machines = PTHReportService.get_strong_users_on_critical_machines_nodes() + for machine in crit_machines: issues.append( { 'type': 'strong_users_on_crit', - 'machine': machine.get('hostname'), - 'services': machine.get('services'), - 'ip': machine.get('ip'), - 'threatening_users': machine.get('threatening_users').keys() + 'machine': machine, + 'services': crit_machines[machine].get('critical_services'), + 'threatening_users': [i['name'] for i in crit_machines[machine]['threatening_users']] } ) return issues @staticmethod - def get_machine_details(node_id): - machine = Machine(node_id) - node = {} - if machine.latest_system_info: - node = { - "id": str(node_id), - "label": '{0} : {1}'.format(machine.GetHostName(), machine.GetIp()), - 'group': 'critical' if machine.IsCriticalServer() else 'normal', - 'users': list(machine.GetCachedUsernames()), - 'ips': [machine.GetIp()], - 'services': machine.GetCriticalServicesInstalled(), - 'hostname': machine.GetHostName() - } - return node + def generate_map_nodes(): - @staticmethod - def generate_map_nodes(pth): nodes_list = [] - for node_id in pth.vertices: - node = PTHReportService.get_machine_details(node_id) - nodes_list.append(node) + 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({ + 'id': monkey['_id'], + 'label': '{0} : {1}'.format(monkey['hostname'], monkey['ip_addresses'][0]), + 'group': 'critical' if critical_services else 'normal', + 'services': critical_services, + 'hostname': monkey['hostname'] + }) return nodes_list @staticmethod - def get_issues_list(issues): - issues_dict = {} + def generate_edge_nodes(): + edges_list = [] + pipeline = [ + { + '$match': {'admin_on_machines': {'$ne': []}, 'secret_location': {'$ne': []}, 'type': 1} + }, + { + '$project': {'admin_on_machines': 1, 'secret_location': 1} + } + ] + comp_users = mongo.db.groupsandusers.aggregate(pipeline) - for issue in issues: - machine = issue['machine'] - if machine not in issues_dict: - issues_dict[machine] = [] - issues_dict[machine].append(issue) + for user in comp_users: + pairs = PTHReportService.generate_edges_tuples(user['admin_on_machines'], user['secret_location']) + for pair in pairs: + edges_list.append( + { + 'from': pair[0], + 'to': pair[1], + 'id': str(uuid.uuid4()) + } + ) + 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 - return issues_dict @staticmethod def get_report(): - + PTHReportService.get_strong_users_on_critical_machines_nodes() issues = [] - pth = PassTheHashReport() - - strong_users_on_crit_services = PTHReportService.get_strong_users_on_crit_services_by_user(pth) - strong_users_on_non_crit_services = PTHReportService.get_strong_users_on_non_crit_services(pth) - - issues += PTHReportService.get_duplicated_passwords_issues() - # issues += PTHReportService.get_shared_local_admins_issues(local_admin_shared) - # issues += PTHReportService.strong_users_on_crit_issues( - # PTHReportService.get_strong_users_on_crit_services_by_machine(pth)) - report = \ { 'report_info': { - 'strong_users_on_crit_services': strong_users_on_crit_services, - 'strong_users_on_non_crit_services': strong_users_on_non_crit_services, 'pth_issues': issues }, 'pthmap': { - 'nodes': PTHReportService.generate_map_nodes(pth), - 'edges': pth.edges + 'nodes': PTHReportService.generate_map_nodes(), + 'edges': PTHReportService.generate_edge_nodes() } } - return report \ No newline at end of file + + return report diff --git a/monkey/monkey_island/cc/services/pth_report_utils.py b/monkey/monkey_island/cc/services/pth_report_utils.py index 61fe78765..13fe3654f 100644 --- a/monkey/monkey_island/cc/services/pth_report_utils.py +++ b/monkey/monkey_island/cc/services/pth_report_utils.py @@ -604,6 +604,10 @@ class PassTheHashReport(object): RIGHT_ARROW = u"\u2192" return "%s %s %s" % (attacker_label, RIGHT_ARROW, victim_label) + + + + def get_edges_by_sid(self): edges_list = [] diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/report.py index 3af346f34..552250b35 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/report.py @@ -118,10 +118,7 @@ class ReportService: [NodeService.get_displayed_node_by_id(node['_id'], True) for node in mongo.db.node.find({}, {'_id': 1})] \ + [NodeService.get_displayed_node_by_id(monkey['_id'], True) for monkey in mongo.db.monkey.find({}, {'_id': 1})] - for node in nodes: - pth_services = PTHReportService.get_machine_details(NodeService.get_monkey_by_id(node['id']) - .get('guid', None)).get('services', None) formatted_nodes.append( { 'label': node['label'], @@ -130,7 +127,7 @@ class ReportService: (x['hostname'] for x in (NodeService.get_displayed_node_by_id(edge['from'], True) for edge in EdgeService.get_displayed_edges_by_to(node['id'], True))), - 'services': node['services'] + pth_services if pth_services else [] + 'services': node['services'] }) logger.info('Scanned nodes generated for reporting') @@ -561,7 +558,8 @@ class ReportService: ReportService.get_tunnels, ReportService.get_island_cross_segment_issues, ReportService.get_azure_issues, - PTHReportService.get_duplicated_passwords_issues + PTHReportService.get_duplicated_passwords_issues, + PTHReportService.get_strong_users_on_crit_issues ] issues = functools.reduce(lambda acc, issue_gen: acc + issue_gen(), ISSUE_GENERATORS, []) @@ -720,7 +718,6 @@ class ReportService: 'pth': { 'map': pth_report.get('pthmap'), - 'info': pth_report.get('report_info') } } 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 f45a5dab3..f4982a9e8 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -410,18 +410,19 @@ class ReportPageComponent extends AuthComponent { generateReportRecommendationsSection() { return (
-

- Recommendations -

-
- {this.generateIssues(this.state.report.recommendations.issues)} -

Domain related recommendations

{this.generateIssues(this.state.report.recommendations.domain_issues)}
+

+ Machine related Recommendations +

+
+ {this.generateIssues(this.state.report.recommendations.issues)} +
+
); } @@ -474,9 +475,6 @@ class ReportPageComponent extends AuthComponent {
-
- -
); } @@ -487,6 +485,9 @@ class ReportPageComponent extends AuthComponent {

Credential Map

+

+ This map visualizes possible attack paths through the network using credential compromise. Paths represent lateral movement opportunities by attackers. +