forked from p34709852/monkey
commit
fa1e1ce33c
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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)
|
|
@ -13,4 +13,4 @@ PyInstaller
|
|||
six
|
||||
ecdsa
|
||||
netifaces
|
||||
ipaddress
|
||||
ipaddress
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"),
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
},
|
||||
|
||||
"root": {
|
||||
"level": "INFO",
|
||||
"level": "DEBUG",
|
||||
"handlers": ["console", "info_file_handler"]
|
||||
}
|
||||
}
|
|
@ -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):
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
"""This file will include consts values regarding the groupsandusers collection"""
|
||||
|
||||
__author__ = 'maor.rayzin'
|
||||
|
||||
USERTYPE = 1
|
||||
GROUPTYPE = 2
|
|
@ -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
|
|
@ -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}))
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
@ -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",
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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
|
||||
{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
|
||||
{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
|
||||
{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;
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
|
@ -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 isn’t 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;
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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 |
Loading…
Reference in New Issue