This commit isn't final. I want to reorganise the code structure a bit,

to make it prettier and readable, also to add docs.

Still need to update the report's text.
This commit is contained in:
maor.rayzin 2018-10-14 17:57:15 +03:00
parent 9a05d0e87d
commit 822e54f373
6 changed files with 124 additions and 169 deletions

View File

@ -197,11 +197,19 @@ class Telemetry(flask_restful.Resource):
Telemetry.create_group_user_connection(info_for_mongo, group_user_dict) Telemetry.create_group_user_connection(info_for_mongo, group_user_dict)
for entity in info_for_mongo.values(): for entity in info_for_mongo.values():
if entity['machine_id']: if entity['machine_id']:
# Handling for local entities.
mongo.db.groupsandusers.update({'SID': entity['SID'], mongo.db.groupsandusers.update({'SID': entity['SID'],
'machine_id': entity['machine_id']}, entity, upsert=True) 'machine_id': entity['machine_id']}, entity, upsert=True)
else: else:
# Handlings for domain entities.
if not mongo.db.groupsandusers.find_one({'SID': entity['SID']}): if not mongo.db.groupsandusers.find_one({'SID': entity['SID']}):
mongo.db.groupsandusers.insert_one(entity) 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.add_admin(info_for_mongo[group_info.ADMINISTRATORS_GROUP_KNOWN_SID], monkey_id)
Telemetry.update_admins_retrospective(info_for_mongo) Telemetry.update_admins_retrospective(info_for_mongo)
@ -209,6 +217,8 @@ class Telemetry(flask_restful.Resource):
telemetry_json['data']['wmi']['Win32_Product'], telemetry_json['data']['wmi']['Win32_Product'],
monkey_id) monkey_id)
@staticmethod @staticmethod
def update_critical_services(wmi_services, wmi_products, machine_id): def update_critical_services(wmi_services, wmi_products, machine_id):
critical_names = ("W3svc", "MSExchangeServiceHost", "MSSQLServer", "dns", 'MSSQL$SQLEXPRESS', 'SQL') critical_names = ("W3svc", "MSExchangeServiceHost", "MSSQLServer", "dns", 'MSSQL$SQLEXPRESS', 'SQL')

View File

@ -97,6 +97,11 @@ class NodeService:
def get_monkey_label_by_id(monkey_id): def get_monkey_label_by_id(monkey_id):
return NodeService.get_monkey_label(NodeService.get_monkey_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 @staticmethod
def get_monkey_label(monkey): def get_monkey_label(monkey):
label = monkey["hostname"] + " : " + monkey["ip_addresses"][0] label = monkey["hostname"] + " : " + monkey["ip_addresses"][0]

View File

@ -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.services.pth_report_utils import PassTheHashReport, Machine
from cc.database import mongo from cc.database import mongo
from bson import ObjectId from bson import ObjectId
@ -49,7 +53,6 @@ class PTHReportService(object):
@staticmethod @staticmethod
def get_duplicated_passwords_issues(): 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() user_groups = PTHReportService.get_duplicated_passwords_nodes()
issues = [] issues = []
users_gathered = [] users_gathered = []
@ -98,195 +101,130 @@ class PTHReportService(object):
return issues return issues
@staticmethod @staticmethod
def old_get_shared_local_admins_nodes(pth): def get_strong_users_on_critical_machines_nodes():
dups = dict(map(lambda x: (x, len(pth.GetSharedAdmins(x))), pth.machines)) crit_machines = {}
shared_admin_machines = [] pipeline = [
for m, count in sorted(dups.iteritems(), key=lambda (k, v): (v, k), reverse=True): {
if count <= 0: '$unwind': '$admin_on_machines'
continue },
shared_admin_account_list = [] {
'$match': {'type': 1, 'domain_name': {'$ne': None}}
for sid in pth.GetSharedAdmins(m): },
shared_admin_account_list.append(pth.GetUsernameBySid(sid)) {
'$lookup':
machine = { {
'ip': m.GetIp(), 'from': 'monkey',
'hostname': m.GetHostName(), 'localField': 'admin_on_machines',
'domain': m.GetDomainName(), 'foreignField': '_id',
'services_names': m.GetCriticalServicesInstalled(), 'as': 'critical_machine'
'user_count': count,
'admins_accounts': shared_admin_account_list
} }
},
shared_admin_machines.append(machine) {
'$match': {'critical_machine.critical_services': {'$ne': []}}
return shared_admin_machines },
{
@staticmethod '$unwind': '$critical_machine'
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 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 @staticmethod
def get_strong_users_on_crit_services_by_user(pth): def get_strong_users_on_crit_issues():
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': []
}
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
}
strong_users_non_crit_list.append(machine)
return strong_users_non_crit_list
@staticmethod
def strong_users_on_crit_issues(strong_users):
issues = [] issues = []
for machine in strong_users: crit_machines = PTHReportService.get_strong_users_on_critical_machines_nodes()
for machine in crit_machines:
issues.append( issues.append(
{ {
'type': 'strong_users_on_crit', 'type': 'strong_users_on_crit',
'machine': machine.get('hostname'), 'machine': machine,
'services': machine.get('services'), 'services': crit_machines[machine].get('critical_services'),
'ip': machine.get('ip'), 'threatening_users': [i['name'] for i in crit_machines[machine]['threatening_users']]
'threatening_users': machine.get('threatening_users').keys()
} }
) )
return issues return issues
@staticmethod @staticmethod
def get_machine_details(node_id): def generate_map_nodes():
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
@staticmethod
def generate_map_nodes(pth):
nodes_list = [] nodes_list = []
for node_id in pth.vertices: monkeys = mongo.db.monkey.find({}, {'_id': 1, 'hostname': 1, 'critical_services': 1, 'ip_addresses': 1})
node = PTHReportService.get_machine_details(node_id) for monkey in monkeys:
nodes_list.append(node) 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 return nodes_list
@staticmethod @staticmethod
def get_issues_list(issues): def generate_edge_nodes():
issues_dict = {} 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: for user in comp_users:
machine = issue['machine'] pairs = PTHReportService.generate_edges_tuples(user['admin_on_machines'], user['secret_location'])
if machine not in issues_dict: for pair in pairs:
issues_dict[machine] = [] edges_list.append(
issues_dict[machine].append(issue) {
'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 @staticmethod
def get_report(): def get_report():
PTHReportService.get_strong_users_on_critical_machines_nodes()
issues = [] 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 = \
{ {
'report_info': '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 'pth_issues': issues
}, },
'pthmap': 'pthmap':
{ {
'nodes': PTHReportService.generate_map_nodes(pth), 'nodes': PTHReportService.generate_map_nodes(),
'edges': pth.edges 'edges': PTHReportService.generate_edge_nodes()
} }
} }
return report return report

View File

@ -604,6 +604,10 @@ class PassTheHashReport(object):
RIGHT_ARROW = u"\u2192" RIGHT_ARROW = u"\u2192"
return "%s %s %s" % (attacker_label, RIGHT_ARROW, victim_label) return "%s %s %s" % (attacker_label, RIGHT_ARROW, victim_label)
def get_edges_by_sid(self): def get_edges_by_sid(self):
edges_list = [] edges_list = []

View File

@ -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(node['_id'], True) for node in mongo.db.node.find({}, {'_id': 1})] \
+ [NodeService.get_displayed_node_by_id(monkey['_id'], True) for monkey in + [NodeService.get_displayed_node_by_id(monkey['_id'], True) for monkey in
mongo.db.monkey.find({}, {'_id': 1})] mongo.db.monkey.find({}, {'_id': 1})]
for node in nodes: 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( formatted_nodes.append(
{ {
'label': node['label'], 'label': node['label'],
@ -130,7 +127,7 @@ class ReportService:
(x['hostname'] for x in (x['hostname'] for x in
(NodeService.get_displayed_node_by_id(edge['from'], True) (NodeService.get_displayed_node_by_id(edge['from'], True)
for edge in EdgeService.get_displayed_edges_by_to(node['id'], 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') logger.info('Scanned nodes generated for reporting')
@ -561,7 +558,8 @@ class ReportService:
ReportService.get_tunnels, ReportService.get_tunnels,
ReportService.get_island_cross_segment_issues, ReportService.get_island_cross_segment_issues,
ReportService.get_azure_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, []) issues = functools.reduce(lambda acc, issue_gen: acc + issue_gen(), ISSUE_GENERATORS, [])
@ -720,7 +718,6 @@ class ReportService:
'pth': 'pth':
{ {
'map': pth_report.get('pthmap'), 'map': pth_report.get('pthmap'),
'info': pth_report.get('report_info')
} }
} }

View File

@ -410,18 +410,19 @@ class ReportPageComponent extends AuthComponent {
generateReportRecommendationsSection() { generateReportRecommendationsSection() {
return ( return (
<div id="recommendations"> <div id="recommendations">
<h3>
Recommendations
</h3>
<div>
{this.generateIssues(this.state.report.recommendations.issues)}
</div>
<h3> <h3>
Domain related recommendations Domain related recommendations
</h3> </h3>
<div> <div>
{this.generateIssues(this.state.report.recommendations.domain_issues)} {this.generateIssues(this.state.report.recommendations.domain_issues)}
</div> </div>
<h3>
Machine related Recommendations
</h3>
<div>
{this.generateIssues(this.state.report.recommendations.issues)}
</div>
</div> </div>
); );
} }
@ -474,9 +475,6 @@ class ReportPageComponent extends AuthComponent {
<div style={{marginBottom: '20px'}}> <div style={{marginBottom: '20px'}}>
<StolenPasswords data={this.state.report.glance.stolen_creds.concat(this.state.report.glance.ssh_keys)}/> <StolenPasswords data={this.state.report.glance.stolen_creds.concat(this.state.report.glance.ssh_keys)}/>
</div> </div>
<div>
<StrongUsers data = {this.state.pthreport.strong_users_on_crit_services} />
</div>
</div> </div>
); );
} }
@ -487,6 +485,9 @@ class ReportPageComponent extends AuthComponent {
<h3> <h3>
Credential Map Credential Map
</h3> </h3>
<p>
This map visualizes possible attack paths through the network using credential compromise. Paths represent lateral movement opportunities by attackers.
</p>
<div> <div>
<PassTheHashMapPageComponent graph={this.state.pthmap} /> <PassTheHashMapPageComponent graph={this.state.pthmap} />
</div> </div>