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