Merge pull request #170 from guardicore/nadler/pth

Nadler/pth
This commit is contained in:
MaorCore 2018-11-06 12:16:18 +02:00 committed by GitHub
commit fa1e1ce33c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 2526 additions and 1084 deletions

View File

View File

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

View File

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

View File

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

View File

@ -13,4 +13,4 @@ PyInstaller
six
ecdsa
netifaces
ipaddress
ipaddress

View File

@ -44,8 +44,10 @@ class MimikatzCollector(object):
self._dll = ctypes.WinDLL(get_binary_file_path(self.MIMIKATZ_DLL_NAME))
collect_proto = ctypes.WINFUNCTYPE(ctypes.c_int)
get_proto = ctypes.WINFUNCTYPE(MimikatzCollector.LogonData)
get_text_output_proto = ctypes.WINFUNCTYPE(ctypes.c_wchar_p)
self._collect = collect_proto(("collect", self._dll))
self._get = get_proto(("get", self._dll))
self._get_text_output_proto = get_text_output_proto(("getTextOutput", self._dll))
self._isInit = True
except Exception:
LOG.exception("Error initializing mimikatz collector")
@ -55,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")
@ -64,6 +67,8 @@ class MimikatzCollector(object):
logon_data_dictionary = {}
hostname = socket.gethostname()
self.mimikatz_text = self._get_text_output_proto()
for i in range(entry_count):
entry = self._get()
@ -97,6 +102,9 @@ class MimikatzCollector(object):
except Exception:
LOG.exception("Error getting logon info")
return {}
def get_mimikatz_text(self):
return self.mimikatz_text
class LogonData(ctypes.Structure):
"""

View File

@ -1,10 +1,17 @@
import os
import logging
import sys
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'
@ -17,6 +24,8 @@ class WindowsInfoCollector(InfoCollector):
def __init__(self):
super(WindowsInfoCollector, self).__init__()
self._config = infection_monkey.config.WormConfiguration
self.info['reg'] = {}
self.info['wmi'] = {}
def get_info(self):
"""
@ -30,13 +39,29 @@ class WindowsInfoCollector(InfoCollector):
self.get_process_list()
self.get_network_info()
self.get_azure_info()
self._get_mimikatz_info()
self.get_wmi_info()
LOG.debug('finished get_wmi_info')
self.get_installed_packages()
LOG.debug('Got installed packages')
mimikatz_collector = MimikatzCollector()
mimikatz_info = mimikatz_collector.get_logon_info()
if mimikatz_info:
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_mimikatz_info(self):
if self._config.should_use_mimikatz:
LOG.info("Using mimikatz")
self.info["credentials"].update(MimikatzCollector().get_logon_info())
else:
LOG.info("Not using mimikatz")
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] = WMIUtils.get_wmi_class(wmi_class_name)

View File

@ -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"),
}

View File

@ -27,7 +27,7 @@
},
"root": {
"level": "INFO",
"level": "DEBUG",
"handlers": ["console", "info_file_handler"]
}
}

View File

@ -1,6 +1,5 @@
import json
import logging
import traceback
import copy
from datetime import datetime
@ -10,10 +9,12 @@ from flask import request
from cc.auth import jwt_required
from cc.database import mongo
from cc.services import mimikatz_utils
from cc.services.config import ConfigService
from cc.services.edge import EdgeService
from cc.services.node import NodeService
from cc.encryptor import encryptor
from cc.services.wmi_handler import WMIHandler
__author__ = 'Barak'
@ -170,6 +171,8 @@ class Telemetry(flask_restful.Resource):
@staticmethod
def process_system_info_telemetry(telemetry_json):
users_secrets = {}
monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']).get('_id')
if 'ssh_info' in telemetry_json['data']:
ssh_info = telemetry_json['data']['ssh_info']
Telemetry.encrypt_system_info_ssh_keys(ssh_info)
@ -182,6 +185,12 @@ class Telemetry(flask_restful.Resource):
Telemetry.encrypt_system_info_creds(creds)
Telemetry.add_system_info_creds_to_config(creds)
Telemetry.replace_user_dot_with_comma(creds)
if 'mimikatz' in telemetry_json['data']:
users_secrets = mimikatz_utils.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.process_and_handle_wmi_info()
@staticmethod
def add_ip_to_ssh_keys(ip, ssh_info):

View File

@ -0,0 +1,6 @@
"""This file will include consts values regarding the groupsandusers collection"""
__author__ = 'maor.rayzin'
USERTYPE = 1
GROUPTYPE = 2

View File

@ -0,0 +1,52 @@
__author__ = 'maor.rayzin'
class MimikatzSecrets(object):
def __init__(self):
# Static class
pass
@staticmethod
def extract_sam_secrets(mim_string, users_dict):
users_secrets = mim_string.split("\n42.")[1].split("\nSAMKey :")[1].split("\n\n")[1:]
if mim_string.count("\n42.") != 2:
return {}
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 not ntlm or "[hashed secret]" not in ntlm:
continue
users_dict[username]['SAM'] = ntlm.replace("[hashed secret]", "").strip()
@staticmethod
def extract_ntlm_secrets(mim_string, users_dict):
if mim_string.count("\n42.") != 2:
return {}
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
@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

View File

@ -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]
@ -320,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):
return NodeService.get_node_hostname(mongo.db.monkey.find_one({'_id': node_id}, {'hostname': 1}))

View File

@ -0,0 +1,281 @@
from itertools import product
from cc.database import mongo
from bson import ObjectId
from cc.services.groups_and_users_consts import USERTYPE
from cc.services.node import NodeService
__author__ = 'maor.rayzin'
class PTHReportService(object):
"""
A static class supplying utils to produce a report based on the PTH related information
gathered via mimikatz and wmi.
"""
@staticmethod
def __dup_passwords_mongoquery():
"""
This function builds and queries the mongoDB for users that are using the same passwords. this is done
by comparing the NTLM hash found for each user by mimikatz.
:return:
A list of mongo documents (dicts in python) that look like this:
{
'_id': The NTLM hash,
'count': How many users share it.
'Docs': the name, domain name, _Id, and machine_id of the users
}
"""
pipeline = [
{"$match": {
'NTLM_secret': {
"$exists": "true", "$ne": None}
}},
{
"$group": {
"_id": {
"NTLM_secret": "$NTLM_secret"},
"count": {"$sum": 1},
"Docs": {"$push": {'_id': "$_id", 'name': '$name', 'domain_name': '$domain_name',
'machine_id': '$machine_id'}}
}},
{'$match': {'count': {'$gt': 1}}}
]
return mongo.db.groupsandusers.aggregate(pipeline)
@staticmethod
def __get_admin_on_machines_format(admin_on_machines, domain_name):
"""
This function finds for each admin user, which machines its an admin of, and compile them to a list.
:param admin_on_machines: A list of "monkey" documents "_id"s
:param domain_name: The admins' domain name
:return:
A list of formatted machines names *domain*\*hostname*, to use in shared admins issues.
"""
machines = mongo.db.monkey.find({'_id': {'$in': admin_on_machines}}, {'hostname': 1})
return [domain_name + '\\' + i['hostname'] for i in list(machines)]
@staticmethod
def __strong_users_on_crit_query():
"""
This function build and query the mongoDB for users that mimikatz was able to find cached NTLM hashes and
are administrators on machines with services predefined as important services thus making these machines
critical.
:return:
A list of said users
"""
pipeline = [
{
'$unwind': '$admin_on_machines'
},
{
'$match': {'type': USERTYPE, 'domain_name': {'$ne': None}}
},
{
'$lookup':
{
'from': 'monkey',
'localField': 'admin_on_machines',
'foreignField': '_id',
'as': 'critical_machine'
}
},
{
'$match': {'critical_machine.critical_services': {'$ne': []}}
},
{
'$unwind': '$critical_machine'
}
]
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['hostname'] + '\\' + 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.
# Excluding the name Administrator - its spamming the lists and not a surprise the domain Administrator account
# is shared.
admins = mongo.db.groupsandusers.find({'type': USERTYPE, 'name': {'$ne': 'Administrator'},
'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'], admin['domain_name'])
} 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['domain_name'] + '\\' + 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 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']})
return crit_machines
@staticmethod
def get_strong_users_on_crit_issues():
crit_machines = PTHReportService.get_strong_users_on_critical_machines_nodes()
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():
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] = {
'machines': [],
'services': []
}
user_details[username]['machines'].append(machine)
user_details[username]['services'] += crit_machines[machine]['critical_services']
return [
{
'username': user,
'machines': user_details[user]['machines'],
'services_names': user_details[user]['services']
} for user in user_details
]
@staticmethod
def generate_map_nodes():
monkeys = mongo.db.monkey.find({}, {'_id': 1, 'hostname': 1, 'critical_services': 1, 'ip_addresses': 1})
return [
{
'id': monkey['_id'],
'label': '{0} : {1}'.format(monkey['hostname'], monkey['ip_addresses'][0]),
'group': 'critical' if monkey.get('critical_services', []) else 'normal',
'services': monkey.get('critical_services', []),
'hostname': monkey['hostname']
} for monkey in monkeys
]
@staticmethod
def generate_edges():
edges_list = []
comp_users = mongo.db.groupsandusers.find(
{
'admin_on_machines': {'$ne': []},
'secret_location': {'$ne': []},
'type': USERTYPE
},
{
'admin_on_machines': 1, 'secret_location': 1
}
)
for user in comp_users:
# 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(pair[1]) + str(pair[0])
}
)
return edges_list
@staticmethod
def get_pth_map():
return {
'nodes': PTHReportService.generate_map_nodes(),
'edges': PTHReportService.generate_edges()
}
@staticmethod
def get_report():
pth_map = PTHReportService.get_pth_map()
PTHReportService.get_strong_users_on_critical_machines_nodes()
report = \
{
'report_info':
{
'strong_users_table': PTHReportService.get_strong_users_on_crit_details()
},
'pthmap':
{
'nodes': pth_map.get('nodes'),
'edges': pth_map.get('edges')
}
}
return report

View File

@ -12,6 +12,7 @@ from cc.services.config import ConfigService
from cc.services.edge import EdgeService
from cc.services.node import NodeService
from cc.utils import local_ip_addresses, get_subnets
from pth_report import PTHReportService
from common.network.network_range import NetworkRange
__author__ = "itay.mizeretz"
@ -50,11 +51,14 @@ class ReportService:
STOLEN_SSH_KEYS = 7
STRUTS2 = 8
WEBLOGIC = 9,
HADOOP = 10
HADOOP = 10,
PTH_CRIT_SERVICES_ACCESS = 11
class WARNINGS_DICT(Enum):
CROSS_SEGMENT = 0
TUNNEL = 1
SHARED_LOCAL_ADMIN = 2
SHARED_PASSWORDS = 3
@staticmethod
def get_first_monkey_time():
@ -106,25 +110,28 @@ class ReportService:
@staticmethod
def get_scanned():
formatted_nodes = []
nodes = \
[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})]
nodes = [
{
'label': node['label'],
'ip_addresses': node['ip_addresses'],
'accessible_from_nodes':
(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']
}
for node in nodes]
for node in nodes:
formatted_nodes.append(
{
'label': node['label'],
'ip_addresses': node['ip_addresses'],
'accessible_from_nodes':
(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']
})
logger.info('Scanned nodes generated for reporting')
return nodes
return formatted_nodes
@staticmethod
def get_exploited():
@ -163,13 +170,14 @@ class ReportService:
origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname']
for user in monkey_creds:
for pass_type in monkey_creds[user]:
creds.append(
cred_row = \
{
'username': user.replace(',', '.'),
'type': PASS_TYPE_DICT[pass_type],
'origin': origin
}
)
if cred_row not in creds:
creds.append(cred_row)
logger.info('Stolen creds generated for reporting')
return creds
@ -520,23 +528,43 @@ class ReportService:
return cross_segment_issues
@staticmethod
def get_domain_issues():
ISSUE_GENERATORS = [
PTHReportService.get_duplicated_passwords_issues,
PTHReportService.get_shared_admins_issues,
]
issues = functools.reduce(lambda acc, issue_gen: acc + issue_gen(), ISSUE_GENERATORS, [])
domain_issues_dict = {}
for issue in issues:
if not issue.get('is_local', True):
machine = issue.get('machine').upper()
if machine not in domain_issues_dict:
domain_issues_dict[machine] = []
domain_issues_dict[machine].append(issue)
logger.info('Domain issues generated for reporting')
return domain_issues_dict
@staticmethod
def get_issues():
ISSUE_GENERATORS = [
ReportService.get_exploits,
ReportService.get_tunnels,
ReportService.get_island_cross_segment_issues,
ReportService.get_azure_issues
ReportService.get_azure_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_dict = {}
for issue in issues:
machine = issue['machine']
if machine not in issues_dict:
issues_dict[machine] = []
issues_dict[machine].append(issue)
if issue.get('is_local', True):
machine = issue.get('machine').upper()
if machine not in issues_dict:
issues_dict[machine] = []
issues_dict[machine].append(issue)
logger.info('Issues generated for reporting')
return issues_dict
@ -602,6 +630,8 @@ class ReportService:
elif issue['type'].endswith('_password') and issue['password'] in config_passwords and \
issue['username'] in config_users or issue['type'] == 'ssh':
issues_byte_array[ReportService.ISSUES_DICT.WEAK_PASSWORD.value] = True
elif issue['type'] == 'strong_users_on_crit':
issues_byte_array[ReportService.ISSUES_DICT.PTH_CRIT_SERVICES_ACCESS.value] = True
elif issue['type'].endswith('_pth') or issue['type'].endswith('_password'):
issues_byte_array[ReportService.ISSUES_DICT.STOLEN_CREDS.value] = True
@ -609,7 +639,7 @@ class ReportService:
@staticmethod
def get_warnings_overview(issues, cross_segment_issues):
warnings_byte_array = [False] * 2
warnings_byte_array = [False] * len(ReportService.WARNINGS_DICT)
for machine in issues:
for issue in issues[machine]:
@ -617,6 +647,10 @@ class ReportService:
warnings_byte_array[ReportService.WARNINGS_DICT.CROSS_SEGMENT.value] = True
elif issue['type'] == 'tunnel':
warnings_byte_array[ReportService.WARNINGS_DICT.TUNNEL.value] = True
elif issue['type'] == 'shared_admins':
warnings_byte_array[ReportService.WARNINGS_DICT.SHARED_LOCAL_ADMIN.value] = True
elif issue['type'] == 'shared_passwords':
warnings_byte_array[ReportService.WARNINGS_DICT.SHARED_PASSWORDS.value] = True
if len(cross_segment_issues) != 0:
warnings_byte_array[ReportService.WARNINGS_DICT.CROSS_SEGMENT.value] = True
@ -640,6 +674,7 @@ class ReportService:
@staticmethod
def get_report():
domain_issues = ReportService.get_domain_issues()
issues = ReportService.get_issues()
config_users = ReportService.get_config_users()
config_passwords = ReportService.get_config_passwords()
@ -667,11 +702,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
'issues': issues,
'domain_issues': domain_issues
}
}

View File

@ -0,0 +1,155 @@
from cc.database import mongo
from cc.services.groups_and_users_consts import USERTYPE, GROUPTYPE
__author__ = 'maor.rayzin'
class WMIHandler(object):
ADMINISTRATORS_GROUP_KNOWN_SID = '1-5-32-544'
def __init__(self, monkey_id, wmi_info, user_secrets):
self.monkey_id = monkey_id
self.info_for_mongo = {}
self.users_secrets = user_secrets
self.users_info = wmi_info['Win32_UserAccount']
self.groups_info = wmi_info['Win32_Group']
self.groups_and_users = wmi_info['Win32_GroupUser']
self.services = wmi_info['Win32_Service']
self.products = wmi_info['Win32_Product']
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.update_critical_services()
def update_critical_services(self):
critical_names = ("W3svc", "MSExchangeServiceHost", "dns", 'MSSQL$SQLEXPRES')
mongo.db.monkey.update({'_id': self.monkey_id}, {'$set': {'critical_services': []}})
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': self.monkey_id}, {'$addToSet': {'critical_services': name}})
def build_entity_document(self, entity_info, monkey_id=None):
general_properties_dict = {
'SID': str(entity_info['SID'])[4:-1],
'name': str(entity_info['Name'])[2:-1],
'machine_id': monkey_id,
'member_of': [],
'admin_on_machines': []
}
if monkey_id:
general_properties_dict['domain_name'] = None
else:
general_properties_dict['domain_name'] = str(entity_info['Domain'])[2:-1]
return general_properties_dict
def add_users_to_collection(self):
for user in self.users_info:
if not user.get('LocalAccount'):
base_entity = self.build_entity_document(user)
else:
base_entity = self.build_entity_document(user, self.monkey_id)
base_entity['NTLM_secret'] = self.users_secrets.get(base_entity['name'], {}).get('ntlm')
base_entity['SAM_secret'] = self.users_secrets.get(base_entity['name'], {}).get('sam')
base_entity['secret_location'] = []
base_entity['type'] = USERTYPE
self.info_for_mongo[base_entity.get('SID')] = base_entity
def add_groups_to_collection(self):
for group in self.groups_info:
if not group.get('LocalAccount'):
base_entity = self.build_entity_document(group)
else:
base_entity = self.build_entity_document(group, self.monkey_id)
base_entity['entities_list'] = []
base_entity['type'] = GROUPTYPE
self.info_for_mongo[base_entity.get('SID')] = base_entity
def create_group_user_connection(self):
for group_user_couple in self.groups_and_users:
group_part = group_user_couple['GroupComponent']
child_part = group_user_couple['PartComponent']
group_sid = str(group_part['SID'])[4:-1]
groups_entities_list = self.info_for_mongo[group_sid]['entities_list']
child_sid = ''
if type(child_part) in (unicode, str):
child_part = str(child_part)
name = None
domain_name = None
if "cimv2:Win32_UserAccount" in child_part:
# domain user
domain_name = child_part.split('cimv2:Win32_UserAccount.Domain="')[1].split('",Name="')[0]
name = child_part.split('cimv2:Win32_UserAccount.Domain="')[1].split('",Name="')[1][:-2]
if "cimv2:Win32_Group" in child_part:
# domain group
domain_name = child_part.split('cimv2:Win32_Group.Domain="')[1].split('",Name="')[0]
name = child_part.split('cimv2:Win32_Group.Domain="')[1].split('",Name="')[1][:-2]
for entity in self.info_for_mongo:
if self.info_for_mongo[entity]['name'] == name and \
self.info_for_mongo[entity]['domain'] == domain_name:
child_sid = self.info_for_mongo[entity]['SID']
else:
child_sid = str(child_part['SID'])[4:-1]
if child_sid and child_sid not in groups_entities_list:
groups_entities_list.append(child_sid)
if child_sid:
if child_sid in self.info_for_mongo:
self.info_for_mongo[child_sid]['member_of'].append(group_sid)
def insert_info_to_mongo(self):
for entity in self.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': USERTYPE},
{'$addToSet': {'secret_location': self.monkey_id}})
def update_admins_retrospective(self):
for profile in self.info_for_mongo:
groups_from_mongo = mongo.db.groupsandusers.find({
'SID': {'$in': self.info_for_mongo[profile]['member_of']}},
{'admin_on_machines': 1})
for group in groups_from_mongo:
if group['admin_on_machines']:
mongo.db.groupsandusers.update_one({'SID': self.info_for_mongo[profile]['SID']},
{'$addToSet': {'admin_on_machines': {
'$each': group['admin_on_machines']}}})
def add_admin(self, group, machine_id):
for sid in group['entities_list']:
mongo.db.groupsandusers.update_one({'SID': sid},
{'$addToSet': {'admin_on_machines': machine_id}})
entity_details = mongo.db.groupsandusers.find_one({'SID': sid},
{'type': USERTYPE, 'entities_list': 1})
if entity_details.get('type') == GROUPTYPE:
self.add_admin(entity_details, machine_id)

File diff suppressed because it is too large Load Diff

View File

@ -31,14 +31,14 @@
"babel-preset-react": "^6.24.1",
"babel-preset-stage-0": "^6.5.0",
"bower-webpack-plugin": "^0.1.9",
"chai": "^4.1.2",
"copyfiles": "^2.0.0",
"chai": "^4.2.0",
"copyfiles": "^2.1.0",
"css-loader": "^1.0.0",
"eslint": "^5.3.0",
"eslint-loader": "^2.1.0",
"eslint": "^5.6.1",
"eslint-loader": "^2.1.1",
"eslint-plugin-react": "^7.11.1",
"file-loader": "^1.1.11",
"glob": "^7.0.0",
"glob": "^7.1.3",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"karma": "^3.0.0",
@ -48,44 +48,44 @@
"karma-mocha-reporter": "^2.2.5",
"karma-phantomjs-launcher": "^1.0.0",
"karma-sourcemap-loader": "^0.3.5",
"karma-webpack": "^3.0.0",
"karma-webpack": "^3.0.5",
"minimist": "^1.2.0",
"mocha": "^5.2.0",
"null-loader": "^0.1.1",
"open": "0.0.5",
"phantomjs-prebuilt": "^2.1.16",
"react-addons-test-utils": "^15.6.2",
"react-hot-loader": "^4.3.4",
"react-hot-loader": "^4.3.11",
"rimraf": "^2.6.2",
"style-loader": "^0.22.1",
"url-loader": "^1.1.0",
"webpack": "^4.16.5",
"webpack-cli": "^3.1.0",
"webpack-dev-server": "^3.1.5"
"url-loader": "^1.1.2",
"webpack": "^4.20.2",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.9"
},
"dependencies": {
"bootstrap": "3.3.7",
"core-js": "^2.5.7",
"downloadjs": "^1.4.7",
"fetch": "^1.1.0",
"js-file-download": "^0.4.1",
"js-file-download": "^0.4.4",
"json-loader": "^0.5.7",
"jwt-decode": "^2.2.0",
"moment": "^2.22.2",
"normalize.css": "^8.0.0",
"npm": "^6.3.0",
"npm": "^6.4.1",
"prop-types": "^15.6.2",
"rc-progress": "^2.2.5",
"react": "^16.4.2",
"react-bootstrap": "^0.32.1",
"rc-progress": "^2.2.6",
"react": "^16.5.2",
"react-bootstrap": "^0.32.4",
"react-copy-to-clipboard": "^5.0.1",
"react-data-components": "^1.2.0",
"react-dimensions": "^1.3.0",
"react-dom": "^16.4.2",
"react-dom": "^16.5.2",
"react-fa": "^5.0.0",
"react-graph-vis": "^1.0.2",
"react-json-tree": "^0.11.0",
"react-jsonschema-form": "^1.0.4",
"react-jsonschema-form": "^1.0.5",
"react-redux": "^5.0.7",
"react-router-dom": "^4.3.1",
"react-table": "^6.8.6",

View File

@ -7,6 +7,7 @@ import RunServerPage from 'components/pages/RunServerPage';
import ConfigurePage from 'components/pages/ConfigurePage';
import RunMonkeyPage from 'components/pages/RunMonkeyPage';
import MapPage from 'components/pages/MapPage';
import PassTheHashMapPage from 'components/pages/PassTheHashMapPage';
import TelemetryPage from 'components/pages/TelemetryPage';
import StartOverPage from 'components/pages/StartOverPage';
import ReportPage from 'components/pages/ReportPage';

View File

@ -1,4 +1,4 @@
let groupNames = ['clean_unknown', 'clean_linux', 'clean_windows', 'exploited_linux', 'exploited_windows', 'island',
const groupNames = ['clean_unknown', 'clean_linux', 'clean_windows', 'exploited_linux', 'exploited_windows', 'island',
'island_monkey_linux', 'island_monkey_linux_running', 'island_monkey_windows', 'island_monkey_windows_running',
'manual_linux', 'manual_linux_running', 'manual_windows', 'manual_windows_running', 'monkey_linux',
'monkey_linux_running', 'monkey_windows', 'monkey_windows_running'];
@ -17,7 +17,22 @@ let getGroupsOptions = () => {
return groupOptions;
};
export const options = {
const groupNamesPth = ['normal', 'critical'];
let getGroupsOptionsPth = () => {
let groupOptions = {};
for (let groupName of groupNamesPth) {
groupOptions[groupName] =
{
shape: 'image',
size: 50,
image: require('../../images/nodes/pth/' + groupName + '.png')
};
}
return groupOptions;
};
export const basic_options = {
autoResize: true,
layout: {
improvedLayout: false
@ -34,10 +49,22 @@ export const options = {
avoidOverlap: 0.5
},
minVelocity: 0.75
},
groups: getGroupsOptions()
}
};
export const options = (() => {
let opts = JSON.parse(JSON.stringify(basic_options)); /* Deep copy */
opts.groups = getGroupsOptions();
return opts;
})();
export const optionsPth = (() => {
let opts = JSON.parse(JSON.stringify(basic_options)); /* Deep copy */
opts.groups = getGroupsOptionsPth();
opts.physics.barnesHut.gravitationalConstant = -20000;
return opts;
})();
export function edgeGroupToColor(group) {
switch (group) {
case 'exploited':

View File

@ -0,0 +1,247 @@
import React from 'react';
import {Icon} from 'react-fa';
import Toggle from 'react-toggle';
import {OverlayTrigger, Tooltip} from 'react-bootstrap';
import download from 'downloadjs'
import PreviewPaneComponent from 'components/map/preview-pane/PreviewPane';
class InfMapPreviewPaneComponent extends PreviewPaneComponent {
osRow(asset) {
return (
<tr>
<th>Operating System</th>
<td>{asset.os.charAt(0).toUpperCase() + asset.os.slice(1)}</td>
</tr>
);
}
ipsRow(asset) {
return (
<tr>
<th>IP Addresses</th>
<td>{asset.ip_addresses.map(val => <div key={val}>{val}</div>)}</td>
</tr>
);
}
servicesRow(asset) {
return (
<tr>
<th>Services</th>
<td>{asset.services.map(val => <div key={val}>{val}</div>)}</td>
</tr>
);
}
accessibleRow(asset) {
return (
<tr>
<th>
Accessible From&nbsp;
{this.generateToolTip('List of machine which can access this one using a network protocol')}
</th>
<td>{asset.accessible_from_nodes.map(val => <div key={val}>{val}</div>)}</td>
</tr>
);
}
statusRow(asset) {
return (
<tr>
<th>Status</th>
<td>{(asset.dead) ? 'Dead' : 'Alive'}</td>
</tr>
);
}
forceKill(event, asset) {
let newConfig = asset.config;
newConfig['alive'] = !event.target.checked;
this.authFetch('/api/monkey/' + asset.guid,
{
method: 'PATCH',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({config: newConfig})
});
}
forceKillRow(asset) {
return (
<tr>
<th>
Force Kill&nbsp;
{this.generateToolTip('If this is on, monkey will die next time it communicates')}
</th>
<td>
<Toggle id={asset.id} checked={!asset.config.alive} icons={false} disabled={asset.dead}
onChange={(e) => this.forceKill(e, asset)}/>
</td>
</tr>
);
}
unescapeLog(st) {
return st.substr(1, st.length - 2) // remove quotation marks on beginning and end of string.
.replace(/\\n/g, "\n")
.replace(/\\r/g, "\r")
.replace(/\\t/g, "\t")
.replace(/\\b/g, "\b")
.replace(/\\f/g, "\f")
.replace(/\\"/g, '\"')
.replace(/\\'/g, "\'")
.replace(/\\&/g, "\&");
}
downloadLog(asset) {
this.authFetch('/api/log?id=' + asset.id)
.then(res => res.json())
.then(res => {
let timestamp = res['timestamp'];
timestamp = timestamp.substr(0, timestamp.indexOf('.'));
let filename = res['monkey_label'].split(':').join('-') + ' - ' + timestamp + '.log';
let logContent = this.unescapeLog(res['log']);
download(logContent, filename, 'text/plain');
});
}
downloadLogRow(asset) {
return (
<tr>
<th>
Download Log
</th>
<td>
<a type="button" className="btn btn-primary"
disabled={!asset.has_log}
onClick={() => this.downloadLog(asset)}>Download</a>
</td>
</tr>
);
}
exploitsTimeline(asset) {
if (asset.exploits.length === 0) {
return (<div/>);
}
return (
<div>
<h4 style={{'marginTop': '2em'}}>
Exploit Timeline&nbsp;
{this.generateToolTip('Timeline of exploit attempts. Red is successful. Gray is unsuccessful')}
</h4>
<ul className="timeline">
{asset.exploits.map(exploit =>
<li key={exploit.timestamp}>
<div className={'bullet ' + (exploit.result ? 'bad' : '')}/>
<div>{new Date(exploit.timestamp).toLocaleString()}</div>
<div>{exploit.origin}</div>
<div>{exploit.exploiter}</div>
</li>
)}
</ul>
</div>
)
}
assetInfo(asset) {
return (
<div>
<table className="table table-condensed">
<tbody>
{this.osRow(asset)}
{this.ipsRow(asset)}
{this.servicesRow(asset)}
{this.accessibleRow(asset)}
</tbody>
</table>
{this.exploitsTimeline(asset)}
</div>
);
}
infectedAssetInfo(asset) {
return (
<div>
<table className="table table-condensed">
<tbody>
{this.osRow(asset)}
{this.statusRow(asset)}
{this.ipsRow(asset)}
{this.servicesRow(asset)}
{this.accessibleRow(asset)}
{this.forceKillRow(asset)}
{this.downloadLogRow(asset)}
</tbody>
</table>
{this.exploitsTimeline(asset)}
</div>
);
}
scanInfo(edge) {
return (
<div>
<table className="table table-condensed">
<tbody>
<tr>
<th>Operating System</th>
<td>{edge.os.type}</td>
</tr>
<tr>
<th>IP Address</th>
<td>{edge.ip_address}</td>
</tr>
<tr>
<th>Services</th>
<td>{edge.services.map(val => <div key={val}>{val}</div>)}</td>
</tr>
</tbody>
</table>
{
(edge.exploits.length === 0) ?
'' :
<div>
<h4 style={{'marginTop': '2em'}}>Timeline</h4>
<ul className="timeline">
{edge.exploits.map(exploit =>
<li key={exploit.timestamp}>
<div className={'bullet ' + (exploit.result ? 'bad' : '')}/>
<div>{new Date(exploit.timestamp).toLocaleString()}</div>
<div>{exploit.origin}</div>
<div>{exploit.exploiter}</div>
</li>
)}
</ul>
</div>
}
</div>
);
}
islandEdgeInfo() {
return (
<div>
</div>
);
}
getInfoByProps() {
switch (this.props.type) {
case 'edge':
return this.scanInfo(this.props.item);
case 'node':
return this.props.item.group.includes('monkey', 'manual') ?
this.infectedAssetInfo(this.props.item) : this.assetInfo(this.props.item);
case 'island_edge':
return this.islandEdgeInfo();
}
return null;
}
}
export default InfMapPreviewPaneComponent;

View File

@ -0,0 +1,63 @@
import React from 'react';
import {Icon} from 'react-fa';
import Toggle from 'react-toggle';
import {OverlayTrigger, Tooltip} from 'react-bootstrap';
import download from 'downloadjs'
import PreviewPaneComponent from 'components/map/preview-pane/PreviewPane';
class PthPreviewPaneComponent extends PreviewPaneComponent {
nodeInfo(asset) {
return (
<div>
<table className="table table-condensed">
<tbody>
<tr>
<th>Hostname</th>
<td>{asset.hostname}</td>
</tr>
<tr>
<th>IP Addresses</th>
<td>{asset.ips.map(val => <div key={val}>{val}</div>)}</td>
</tr>
<tr>
<th>Services</th>
<td>{asset.services.map(val => <div key={val}>{val}</div>)}</td>
</tr>
<tr>
<th>Compromised Users</th>
<td>{asset.users.map(val => <div key={val}>{val}</div>)}</td>
</tr>
</tbody>
</table>
</div>
);
}
edgeInfo(edge) {
return (
<div>
<table className="table table-condensed">
<tbody>
<tr>
<th>Compromised Users</th>
<td>{edge.users.map(val => <div key={val}>{val}</div>)}</td>
</tr>
</tbody>
</table>
</div>
);
}
getInfoByProps() {
switch (this.props.type) {
case 'edge':
return this.edgeInfo(this.props.item);
case 'node':
return this.nodeInfo(this.props.item);
}
return null;
}
}
export default PthPreviewPaneComponent;

View File

@ -2,7 +2,7 @@ import React from 'react';
import {Col, Modal} from 'react-bootstrap';
import {Link} from 'react-router-dom';
import {Icon} from 'react-fa';
import PreviewPane from 'components/map/preview-pane/PreviewPane';
import InfMapPreviewPaneComponent from 'components/map/preview-pane/InfMapPreviewPane';
import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph';
import {options, edgeGroupToColor} from 'components/map/MapOptions';
import AuthComponent from '../AuthComponent';
@ -186,7 +186,7 @@ class MapPageComponent extends AuthComponent {
</div>
: ''}
<PreviewPane item={this.state.selected} type={this.state.selectedType}/>
<InfMapPreviewPaneComponent item={this.state.selected} type={this.state.selectedType}/>
</Col>
</div>
);

View File

@ -0,0 +1,58 @@
import React from 'react';
import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph';
import AuthComponent from '../AuthComponent';
import {optionsPth, edgeGroupToColorPth, options} from '../map/MapOptions';
import PreviewPane from "../map/preview-pane/PreviewPane";
import {Col} from "react-bootstrap";
import {Link} from 'react-router-dom';
import {Icon} from 'react-fa';
import PthPreviewPaneComponent from "../map/preview-pane/PthPreviewPane";
class PassTheHashMapPageComponent extends AuthComponent {
constructor(props) {
super(props);
this.state = {
graph: props.graph,
selected: null,
selectedType: null
};
}
events = {
select: event => this.selectionChanged(event)
};
selectionChanged(event) {
if (event.nodes.length === 1) {
let displayedNode = this.state.graph.nodes.find(
function (node) {
return node['id'] === event.nodes[0];
});
this.setState({selected: displayedNode, selectedType: 'node'})
}
else if (event.edges.length === 1) {
let displayedEdge = this.state.graph.edges.find(
function (edge) {
return edge['id'] === event.edges[0];
});
this.setState({selected: displayedEdge, selectedType: 'edge'});
}
else {
this.setState({selected: null, selectedType: null});
}
}
render() {
return (
<div>
<Col xs={12}>
<div style={{height: '70vh'}}>
<ReactiveGraph graph={this.state.graph} options={optionsPth} events={this.events}/>
</div>
</Col>
</div>
);
}
}
export default PassTheHashMapPageComponent;

View File

@ -8,6 +8,8 @@ import StolenPasswords from 'components/report-components/StolenPasswords';
import CollapsibleWellComponent from 'components/report-components/CollapsibleWell';
import {Line} from 'rc-progress';
import AuthComponent from '../AuthComponent';
import PassTheHashMapPageComponent from "./PassTheHashMapPage";
import StrongUsers from "components/report-components/StrongUsers";
let guardicoreLogoImage = require('../../images/guardicore-logo.png');
let monkeyLogoImage = require('../../images/monkey-icon.svg');
@ -26,13 +28,17 @@ class ReportPageComponent extends AuthComponent {
STOLEN_SSH_KEYS: 7,
STRUTS2: 8,
WEBLOGIC: 9,
HADOOP: 10
HADOOP: 10,
PTH_CRIT_SERVICES_ACCESS: 11
};
Warning =
{
CROSS_SEGMENT: 0,
TUNNEL: 1
TUNNEL: 1,
SHARED_LOCAL_ADMIN: 2,
SHARED_PASSWORDS: 3,
SHARED_PASSWORDS_DOMAIN: 4
};
constructor(props) {
@ -48,7 +54,6 @@ class ReportPageComponent extends AuthComponent {
componentDidMount() {
this.updateMonkeysRunning().then(res => this.getReportFromServer(res));
this.updateMapFromServer();
this.interval = setInterval(this.updateMapFromServer, 5000);
}
componentWillUnmount() {
@ -334,6 +339,8 @@ class ReportPageComponent extends AuthComponent {
CVE-2017-10271</a>)</li> : null }
{this.state.report.overview.issues[this.Issue.HADOOP] ?
<li>Hadoop/Yarn servers are vulnerable to remote code execution.</li> : null }
{this.state.report.overview.issues[this.Issue.PTH_CRIT_SERVICES_ACCESS] ?
<li>Mimikatz found login credentials of a user who has admin access to a server defined as critical.</li>: null }
</ul>
</div>
:
@ -359,6 +366,10 @@ class ReportPageComponent extends AuthComponent {
communicate.</li> : null}
{this.state.report.overview.warnings[this.Warning.TUNNEL] ?
<li>Weak segmentation - Machines were able to communicate over unused ports.</li> : null}
{this.state.report.overview.warnings[this.Warning.SHARED_LOCAL_ADMIN] ?
<li>Shared local administrator account - Different machines have the same account as a local administrator.</li> : null}
{this.state.report.overview.warnings[this.Warning.SHARED_PASSWORDS] ?
<li>Multiple users have the same password</li> : null}
</ul>
</div>
:
@ -390,11 +401,18 @@ class ReportPageComponent extends AuthComponent {
return (
<div id="recommendations">
<h3>
Recommendations
Domain related recommendations
</h3>
<div>
{this.generateIssues(this.state.report.recommendations.domain_issues)}
</div>
<h3>
Machine related Recommendations
</h3>
<div>
{this.generateIssues(this.state.report.recommendations.issues)}
</div>
</div>
);
}
@ -443,9 +461,36 @@ class ReportPageComponent extends AuthComponent {
<div style={{marginBottom: '20px'}}>
<ScannedServers data={this.state.report.glance.scanned}/>
</div>
<div>
<div style={{position: 'relative', height: '80vh'}}>
{this.generateReportPthMap()}
</div>
<div style={{marginBottom: '20px'}}>
<StolenPasswords data={this.state.report.glance.stolen_creds.concat(this.state.report.glance.ssh_keys)}/>
</div>
<div>
<StrongUsers data = {this.state.report.glance.strong_users} />
</div>
</div>
);
}
generateReportPthMap() {
return (
<div id="pth">
<h3>
Credential Map
</h3>
<p>
This map visualizes possible attack paths through the network using credential compromise. Paths represent lateral movement opportunities by attackers.
</p>
<div className="map-legend">
<b>Legend: </b>
<span>Access credentials <i className="fa fa-lg fa-minus" style={{color: '#0158aa'}}/></span> <b style={{color: '#aeaeae'}}> | </b>
</div>
<div>
<PassTheHashMapPageComponent graph={this.state.report.glance.pth_map} />
</div>
<br />
</div>
);
}
@ -706,6 +751,57 @@ class ReportPageComponent extends AuthComponent {
);
}
generateSharedCredsDomainIssue(issue) {
return (
<li>
Some domain users are sharing passwords, this should be fixed by changing passwords.
<CollapsibleWellComponent>
These users are sharing access password:
{this.generateInfoBadges(issue.shared_with)}.
</CollapsibleWellComponent>
</li>
);
}
generateSharedCredsIssue(issue) {
return (
<li>
Some users are sharing passwords, this should be fixed by changing passwords.
<CollapsibleWellComponent>
These users are sharing access password:
{this.generateInfoBadges(issue.shared_with)}.
</CollapsibleWellComponent>
</li>
);
}
generateSharedLocalAdminsIssue(issue) {
return (
<li>
Make sure the right administrator accounts are managing the right machines, and that there isnt an unintentional local admin sharing.
<CollapsibleWellComponent>
Here is a list of machines which the account <span
className="label label-primary">{issue.username}</span> is defined as an administrator:
{this.generateInfoBadges(issue.shared_machines)}
</CollapsibleWellComponent>
</li>
);
}
generateStrongUsersOnCritIssue(issue) {
return (
<li>
This critical machine is open to attacks via strong users with access to it.
<CollapsibleWellComponent>
The services: {this.generateInfoBadges(issue.services)} have been found on the machine
thus classifying it as a critical machine.
These users has access to it:
{this.generateInfoBadges(issue.threatening_users)}.
</CollapsibleWellComponent>
</li>
);
}
generateTunnelIssue(issue) {
return (
<li>
@ -812,6 +908,18 @@ class ReportPageComponent extends AuthComponent {
case 'island_cross_segment':
data = this.generateIslandCrossSegmentIssue(issue);
break;
case 'shared_passwords':
data = this.generateSharedCredsIssue(issue);
break;
case 'shared_passwords_domain':
data = this.generateSharedCredsDomainIssue(issue);
break;
case 'shared_admins_domain':
data = this.generateSharedLocalAdminsIssue(issue);
break;
case 'strong_users_on_crit':
data = this.generateStrongUsersOnCritIssue(issue);
break;
case 'tunnel':
data = this.generateTunnelIssue(issue);
break;

View File

@ -2,10 +2,7 @@ import React from 'react';
import ReactTable from 'react-table'
let renderArray = function(val) {
if (val.length === 0) {
return '';
}
return val.reduce((total, new_str) => total + ', ' + new_str);
return <div>{val.map(x => <div>{x}</div>)}</div>;
};
const columns = [

View File

@ -2,10 +2,7 @@ import React from 'react';
import ReactTable from 'react-table'
let renderArray = function(val) {
if (val.length === 0) {
return '';
}
return val.reduce((total, new_str) => total + ', ' + new_str);
return <div>{val.map(x => <div>{x}</div>)}</div>;
};
const columns = [

View File

@ -0,0 +1,43 @@
import React from 'react';
import ReactTable from 'react-table'
let renderArray = function(val) {
console.log(val);
return <div>{val.map(x => <div>{x}</div>)}</div>;
};
const columns = [
{
Header: 'Powerful Users',
columns: [
{ Header: 'Username', accessor: 'username'},
{ Header: 'Machines', id: 'machines', accessor: x => renderArray(x.machines)},
{ Header: 'Services', id: 'services', accessor: x => renderArray(x.services_names)}
]
}
];
const pageSize = 10;
class StrongUsersComponent 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 (
<div className="data-table-container">
<ReactTable
columns={columns}
data={this.props.data}
showPagination={showPagination}
defaultPageSize={defaultPageSize}
/>
</div>
);
}
}
export default StrongUsersComponent;

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB