def __init__(self): super().__init__(code=ALERT_CODE, title=TITLE, desc=DESC_TEMPLATE) self.parser = SDDLParser() self.account_info = AccountInfo() self.watch_object_class = [ "container", "domainDNS", "groupPolicyContainer" ]
class ModifySensitiveGroup(DetectBase): def __init__(self): super().__init__(code=ALERT_CODE, title=TITLE, desc=DESC_TEMPLATE) self.account_info = AccountInfo() def run(self, log: Log): self.init(log=log) group_name = log.target_info.user_name sensitive_groups = list( map(lambda x: x["name"], main_config.sensitive_groups)) if group_name in sensitive_groups: # 添加到了敏感组,更新redis缓存 self.account_info.set_target_sensitive_cache( self.log.event_data["MemberSid"], "true") return self._generate_alert_doc() def _generate_alert_doc(self, **kwargs) -> dict: source_ip = self._get_source_ip_by_logon_id( self.log.subject_info.logon_id, self.log.subject_info.full_user_name) form_data = { "source_ip": source_ip, "source_workstation": self._get_workstation_by_source_ip(source_ip), "group_name": self.log.target_info.user_name, "group_sid": self.log.event_data["TargetSid"], "target_user_name": get_cn_from_dn(self.log.event_data["MemberName"]), "target_user_dn": self.log.event_data["MemberName"], "target_user_sid": self.log.event_data["MemberSid"], "target_domain": self.log.target_info.domain_name, "source_user_name": self.log.subject_info.user_name, "source_user_sid": self.log.subject_info.user_sid, "source_logon_id": self.log.subject_info.logon_id, "source_domain": self.log.subject_info.domain_name, "privilege_list": self.log.event_data["PrivilegeList"] } doc = self._get_base_doc(level=self._get_level(), unique_id=self._get_unique_id( self.code, self.log.subject_info.user_name), form_data=form_data) return doc def _get_level(self) -> str: return HIGH_LEVEL
class SPNChange(object): def __init__(self): self.account_info = AccountInfo() self.account_activity = AccountActivity(activity_type=SPN_CHANGE) def run(self, log: Log): if log.event_data["AttributeLDAPDisplayName"] != "servicePrincipalName": return if log.event_data["ObjectClass"] != "user": return if log.event_data["OperatorType"] not in ["%%14675", "%%14674"]: return domain = log.event_data["DSName"] target_user_name = get_cn_from_dn(log.object_info.dn) target_user_info = self.account_info.get_user_info_by_name( target_user_name, domain) form_data = { "operator": { "user_name": log.subject_info.user_name, "sid": log.subject_info.user_sid, "logon_id": log.subject_info.logon_id }, "operator_type": "add" if log.event_data["OperatorType"] == "%%14674" else "remove", "value": log.event_data["AttributeValue"], "object_dn": log.object_info.dn } self.account_activity.save_activity(domain=log.event_data["DSName"], user_name=target_user_name, sid=target_user_info.user_sid, dc_name=log.dc_host_name, timestamp=log.utc_log_time, data=form_data)
def __init__(self): super().__init__(code=ALERT_CODE, title=TITLE, desc=DESC_TEMPLATE) self.account_info = AccountInfo() self.account_history = AccountHistory()
class SearchUserDetail(DetectBase): def __init__(self): super().__init__(code=ALERT_CODE, title=TITLE, desc=DESC_TEMPLATE) self.account_info = AccountInfo() self.account_history = AccountHistory() def run(self, log: Log): self.init(log=log) if log.object_info.type != "SAM_USER": return user_name = log.subject_info.user_name if user_name.endswith("$"): return target_sid = log.object_info.name if not target_sid.startswith("S-1-5-21-"): return # 查询自身也忽略 if log.subject_info.user_sid == target_sid: return # 判断域名是否存在于需要检查的域名中 domain_name = log.subject_info.domain_name if filter_domain(domain_name): return # 如果账号是管理员 直接忽略 if self.account_info.check_target_is_admin_by_sid( sid=log.subject_info.user_sid, domain=domain_name): return # 判断操作者是否为Users if not self.account_info.check_target_is_user_by_name( user_name, domain_name): return # 判断是否为敏感用户 if not self.account_info.user_is_sensitive_by_sid(sid=target_sid, domain=domain_name): return ldap = LDAPSearch(domain_name) target_user_name = str( ldap.search_by_sid(target_sid, attributes=["cn"])["cn"]) return self._generate_alert_doc(target_user_name=target_user_name) def _generate_alert_doc(self, **kwargs) -> dict: source_ip = self._get_source_ip_by_logon_id( self.log.subject_info.logon_id, self.log.subject_info.full_user_name) form_data = { "source_workstation": self._get_workstation_by_source_ip(source_ip), "source_ip": source_ip, "source_user_name": self.log.subject_info.user_name, "source_user_sid": self.log.subject_info.user_sid, "source_logon_id": self.log.subject_info.logon_id, "source_domain": self.log.subject_info.domain_name, **kwargs } doc = self._get_base_doc(level=self._get_level(), unique_id=self._get_unique_id( self.code, self.log.subject_info.user_name), form_data=form_data) return doc def _get_level(self) -> str: return LOW_LEVEL
class NTLMRelay(DetectBase): def __init__(self): super().__init__(code=ALERT_CODE, title=TITLE, desc=DESC_TEMPLATE) self.account_history = AccountHistory() self.account_info = AccountInfo() self.es = ElasticHelper() def run(self, log: Log): self.init(log=log) # 处于数据统计时间内,不检测 if datetime_now_obj() < main_config.learning_end_time: return if not log.source_info.ip_address: return if log.event_data["AuthenticationPackageName"] != "NTLM": return work_station = log.source_info.work_station_name netbios_name = get_netbios_domain(log.target_info.domain_name) if filter_domain(netbios_name): return # 为较小误报 目前只考虑来源主机为敏感主机的行为 if not self.account_info.computer_is_sensitive_by_name( work_station, domain=netbios_name): return ip_address = log.source_info.ip_address if ip_filter(ip_address): return # 根据主机名去查最近的认证IP last_ip = self.account_history.search_last_ip_by_workstation( work_station) if not last_ip or last_ip == "unknown" or last_ip == ip_address: return # 二次确认,如果上次认证IP与当前IP不相同,则对主机名进行解析,判断IP是否相等 resolver_ips = self._get_host_ip(log) if ip_address in resolver_ips: return if "V1" in log.event_data["LmPackageName"]: version = "v1" else: version = "v2" relay_workstation = self.account_history.get_last_workstation_by_ip( ip_address) return self._generate_alert_doc(relay_workstation=relay_workstation, ntlm_version=version, resolver_ips=resolver_ips, last_ip=last_ip) def _generate_alert_doc(self, **kwargs) -> dict: form_data = { "source_ip": self.log.source_info.ip_address, "relay_ip": self.log.source_info.ip_address, "source_workstation": self.log.source_info.work_station_name, "target_user_name": self.log.target_info.user_name, "target_domain": self.log.target_info.domain_name, "target_user_sid": self.log.target_info.user_sid, "target_logon_id": self.log.target_info.logon_id, "dc_hostname": self.log.dc_host_name, **kwargs } doc = self._get_base_doc(level=self._get_level(), unique_id=self._get_unique_id( self.code, self.log.source_info.ip_address), form_data=form_data) return doc def _get_level(self) -> str: return HIGH_LEVEL def _get_host_ip(self, log: Log) -> list: """ 对主机名进行DNS解析 """ dns_name = "{workstation}.{domain}".format( workstation=log.source_info.work_station_name, domain=self.get_FQDN_domain(log.target_info.domain_name)) try: ip_list = get_ip_from_domain(dns_name) except Exception as e: return [] return ip_list def get_FQDN_domain(self, domain) -> str: for each in main_config.domain_list: if each.startswith(domain.lower()): return each return ""
def __init__(self): super().__init__(code=ALERT_CODE, title=TITLE, desc=DESC_TEMPLATE) self.es = ElasticHelper() self.account_info = AccountInfo()
class PsLoggedOn(DetectBase): def __init__(self): super().__init__(code=ALERT_CODE, title=TITLE, desc=DESC_TEMPLATE) self.es = ElasticHelper() self.account_info = AccountInfo() def run(self, log: Log): self.init(log=log) if log.event_data["ShareName"] != r"\\*\IPC$": return if log.event_data["RelativeTargetName"] != "srvsvc": return # 忽略域管理员的访问 if self.account_info.check_target_is_admin_by_sid( sid=log.subject_info.user_sid, domain=log.subject_info.domain_name): return # 存在向前查找,则延迟确认 if "_delay_info" not in log.record: self.delay_confirm_log() return self.es.wait_log_in_database(log.dc_computer_name, log.record_number) is_match = self._search_forward(log) if is_match: return self._generate_alert_doc() def _generate_alert_doc(self, **kwargs) -> dict: form_data = { "source_workstation": self._get_workstation_by_source_ip( self.log.source_info.ip_address), "source_ip": self.log.source_info.ip_address, "source_user_name": self.log.subject_info.user_name, "source_user_sid": self.log.subject_info.user_sid, "source_logon_id": self.log.subject_info.logon_id, "source_domain": self.log.subject_info.domain_name, "dc_hostname": self.log.dc_host_name, **kwargs } doc = self._get_base_doc(level=self._get_level(), unique_id=self._get_unique_id( self.code, self.log.source_info.ip_address), form_data=form_data) return doc def _get_level(self) -> str: return LOW_LEVEL def _search_forward(self, log: Log): """ """ base = { "index": ElasticConfig.event_log_index, "doc_type": ElasticConfig.event_log_doc_type } # 开始向前查找 msearch_body = [base] body_winreg = self._get_query(log, "winreg") msearch_body.append(body_winreg) msearch_body.append(base) body_lsarpc = self._get_query(log, "lsarpc") msearch_body.append(body_lsarpc) results = self.es.multi_search( body=msearch_body, index=ElasticConfig.event_log_index, doc_type=ElasticConfig.event_log_doc_type) for each in results["responses"]: if each.get("error"): logger.error("PsLoggedOn module - multi search error: " + each.get("error").get("reason")) raise MsearchException() elif each["hits"]["total"] == 0: return False return True def _get_query(self, log: Log, relative_target_name): computer_term = get_term_statement("computer_name", log.dc_computer_name), ago_time = move_n_min(utc_to_datetime(log.utc_log_time), 1) time_str = datetime_to_utc(ago_time) logon_id_term = get_term_statement("event_data.SubjectLogonId.keyword", log.subject_info.logon_id) share_name_term = get_term_statement("event_data.ShareName.keyword", r"\\*\IPC$") time_term = get_time_range("gt", time_str), relative_term = get_term_statement( "event_data.RelativeTargetName.keyword", relative_target_name) query = { "query": get_must_statement(logon_id_term, share_name_term, relative_term, time_term, computer_term), "_source": False, "size": 1 } return query
def __init__(self): self.account_info = AccountInfo() self.account_activity = AccountActivity(activity_type=SPN_CHANGE)
class EnumerateGroup(DetectBase): def __init__(self): super().__init__(code=ALERT_CODE, title=TITLE, desc=DESC_TEMPLATE) self.account_info = AccountInfo() def run(self, log: Log): self.init(log=log) if log.object_info.server != "Security Account Manager": return if log.object_info.type != "SAM_GROUP": return sid = log.object_info.name if not sid.startswith("S-1-5-21-"): return user_name = log.subject_info.user_name if user_name.endswith("$"): return # 如果账号是管理员 直接忽略 if self.account_info.check_target_is_admin_by_sid( sid=sid, domain=log.subject_info.domain_name): return # 判断账号是否为 Users,如果不是,直接退出 if not self.account_info.check_target_is_user_by_name( user_name, log.subject_info.domain_name): return group_name = self._get_group_name(sid, log.subject_info.domain_name) if not group_name: return sensitive_groups = list( map(lambda x: x["name"], main_config.sensitive_groups)) if group_name in sensitive_groups: return self._generate_alert_doc(group_name=group_name) def _get_group_name(self, sid, domain): ldap = LDAPSearch(domain) entry = ldap.search_by_sid(sid, attributes=["cn"]) if entry: return str(entry["cn"]) def _generate_alert_doc(self, **kwargs) -> dict: source_ip = self._get_source_ip_by_logon_id( self.log.subject_info.logon_id, self.log.subject_info.full_user_name) form_data = { "source_workstation": self._get_workstation_by_source_ip(source_ip), "source_ip": source_ip, "source_user_name": self.log.subject_info.user_name, "source_user_sid": self.log.subject_info.user_sid, "source_logon_id": self.log.subject_info.logon_id, "source_domain": self.log.subject_info.domain_name, **kwargs } doc = self._get_base_doc(level=self._get_level(), unique_id=self._get_unique_id( self.code, self.log.subject_info.user_name), form_data=form_data) return doc def _get_level(self) -> str: return LOW_LEVEL
def __init__(self): super().__init__(code=ALERT_CODE, title=TITLE, desc=DESC_TEMPLATE) self.delegation = Delegation() self.account_info = AccountInfo()
class ResBasedConsDelegation(DetectBase): def __init__(self): super().__init__(code=ALERT_CODE, title=TITLE, desc=DESC_TEMPLATE) self.delegation = Delegation() self.account_info = AccountInfo() def run(self, log: Log): self.init(log=log) if log.event_data[ "AttributeLDAPDisplayName"] != "msDS-AllowedToActOnBehalfOfOtherIdentity": return # 只检测敏感计算机 account = get_cn_from_dn(log.object_info.dn) domain = get_domain_from_dn(log.object_info.dn) if not self.account_info.computer_is_sensitive_by_name( account, domain=get_netbios_domain(domain)): return ldap = LDAPSearch(domain=domain) entry = ldap.search_by_cn( cn=account, attributes=["sid", "msDS-AllowedToActOnBehalfOfOtherIdentity"]) if entry is None: return entry_sid = str(entry["sid"]) sd = SR_SECURITY_DESCRIPTOR(entry.entry_attributes_as_dict[ "msDS-AllowedToActOnBehalfOfOtherIdentity"][0]) # 拥有特殊DACL权限的SID列表 ace_list = [] for ace in sd["Dacl"].aces: ace_list.append({ "type_name": ace["TypeName"], "sid": ace['Ace']['Sid'].formatCanonical() }) sid_list = list(map(lambda ace: ace["sid"], ace_list)) sid_list = sorted(list(set(sid_list))) target_account_info = User({ "user_name": account, "user_sid": entry_sid }) # 查询历史委派记录 record = self.delegation.find_res_constrained_delegation_by_name( name=account) # 不存在记录 则新建 并直接告警 if not record: self.delegation.new_delegation_record( user=target_account_info, delegation_type=RES_BASED_CONSTRAINED_DELEGATION, allowed_to=sid_list) return self._generate_alert_doc( target_computer=account, target_user_name=target_account_info.user_name, target_user_sid=target_account_info.user_sid, add_allowed_sid=sid_list, old_allowed_sid=[]) # 存在记录且不变,退出 if sid_list == record["allowed_to"]: return # 存在记录 对比历史的sid 无新增 更新记录 退出 new_sids = self._get_new_sid(new_list=sid_list, old_list=record["allowed_to"]) if len(new_sids) == 0: self.delegation.update_delegation( sid=entry_sid, delegation_type=RES_BASED_CONSTRAINED_DELEGATION, allowed_to=sid_list) return # 存在记录 有新增 更新记录 告警 if len(new_sids) > 0: self.delegation.update_delegation( sid=entry_sid, delegation_type=RES_BASED_CONSTRAINED_DELEGATION, allowed_to=sid_list) return self._generate_alert_doc( target_computer=account, target_user_name=target_account_info.user_name, target_user_sid=target_account_info.user_sid, add_allowed_sid=new_sids, old_allowed_sid=record["allowed_to"]) def _generate_alert_doc(self, **kwargs) -> dict: source_ip = self._get_source_ip_by_logon_id( self.log.subject_info.logon_id, self.log.subject_info.full_user_name) form_data = { "source_workstation": self._get_workstation_by_source_ip(source_ip), "source_ip": source_ip, "source_user_name": self.log.subject_info.user_name, "source_domain": self.log.subject_info.domain_name, **kwargs } doc = self._get_base_doc(level=self._get_level(), unique_id=self._get_unique_id( self.code, self.log.subject_info.user_name), form_data=form_data) return doc def _get_level(self) -> str: return HIGH_LEVEL def _get_new_sid(self, new_list, old_list) -> list: result = [] for each in new_list: if each not in old_list: result.append(each) return result
class UnknownAdminLogin(DetectBase): def __init__(self): super().__init__(code=ALERT_CODE, title=TITLE, desc=DESC_TEMPLATE) self.account_info = AccountInfo() def run(self, log: Log): self.init(log=log) sid = log.subject_info.user_sid user_name = log.subject_info.user_name domain_name = log.subject_info.domain_name if domain_name.lower() == "window manager": return if not self._is_in_domain_list(domain_name): return if len(log.subject_info.user_sid.split("-")) == 4: return # 排除域控计算机账户的本地特权登录 if user_name.endswith("$"): domain = get_netbios_domain(domain_name) if user_name[:-1] in main_config.dc_name_list[domain]: return if self.account_info.check_target_is_admin_by_sid(sid=sid, domain=domain_name): return return self._generate_alert_doc() def _generate_alert_doc(self, **kwargs) -> dict: source_ip = self._get_source_ip_by_logon_id( self.log.subject_info.logon_id, self.log.subject_info.full_user_name) form_data = { "source_workstation": self._get_workstation_by_source_ip(source_ip), "source_ip": source_ip, "target_user_name": self.log.subject_info.user_name, "target_user_sid": self.log.subject_info.user_sid, "target_logon_id": self.log.subject_info.logon_id, "target_domain": self.log.subject_info.domain_name, "dc_hostname": self.log.dc_host_name, **kwargs } doc = self._get_base_doc(level=self._get_level(), unique_id=self._get_unique_id( self.code, self.log.subject_info.user_name), form_data=form_data) return doc def _get_level(self) -> str: return HIGH_LEVEL def _is_in_domain_list(self, domain: str) -> bool: """ 域名是否在已知需要监控的域列表中 """ domain = get_netbios_domain(domain) for each in main_config.domain_list: if domain == get_netbios_domain(each): return True return False
class ACLModify(DetectBase): def __init__(self): super().__init__(code=ALERT_CODE, title=TITLE, desc=DESC_TEMPLATE) self.parser = SDDLParser() self.account_info = AccountInfo() self.watch_object_class = [ "container", "domainDNS", "groupPolicyContainer" ] def run(self, log: Log): self.init(log=log) if log.object_info.class_ not in self.watch_object_class: return if log.event_data["AttributeLDAPDisplayName"] != "nTSecurityDescriptor": return abnormal_ace_list = [] abnormal_users = [] content = self.parser.parse(log.event_data["AttributeValue"]) domain = log.subject_info.domain_name for ace in content["dacl_ace_list"]: trustee = ace["trustee"] # 判断是否为SID,因为这种ACL几乎都是默认的用户,很少特殊指定用户 if trustee.startswith("S-1-5-21-"): # 首先检查 该SID是否为某个用户?(Users),如果不是,则忽略掉 if not self.account_info.check_target_is_user_by_sid( trustee, domain): continue # 获取用户信息 user = self.account_info.get_user_info_by_sid(sid=trustee, domain=domain) # 目标是否为管理员权限 if self.account_info.check_target_is_admin_by_sid( trustee, domain): continue if user.user_name in abnormal_users: continue abnormal_users.append(user.user_name) abnormal_ace = self._get_abnormal_ace(ace, domain) abnormal_ace_list.append(abnormal_ace) if len(abnormal_ace_list) > 0: return self._generate_alert_doc( object_class=log.object_info.class_, abnormal_ace_list=abnormal_ace_list, parsed_sddl=content, abnormal_users=abnormal_users) def _generate_alert_doc(self, **kwargs) -> dict: source_ip = self._get_source_ip_by_logon_id( self.log.subject_info.logon_id, self.log.subject_info.full_user_name) form_data = { "source_workstation": self._get_workstation_by_source_ip(source_ip), "source_ip": source_ip, "source_user_name": self.log.subject_info.user_name, "source_user_sid": self.log.subject_info.user_sid, "source_logon_id": self.log.subject_info.logon_id, "source_domain": self.log.subject_info.domain_name, "object_info": self.log.object_info.get_doc(), **kwargs } doc = self._get_base_doc(level=self._get_level(), unique_id=self._get_unique_id( self.code, self.log.subject_info.user_name), form_data=form_data) return doc def _get_level(self) -> str: return HIGH_LEVEL def _get_abnormal_ace(self, ace, domain) -> dict: trustee = ace["trustee"] ace["user_name"] = "unknown" user_info = self.account_info.get_user_info_by_sid(sid=trustee, domain=domain) if user_info: ace["user_name"] = user_info.user_name return copy.deepcopy(ace)
class GPODelegation(DetectBase): def __init__(self): super().__init__(code=ALERT_CODE, title=TITLE, desc=DESC_TEMPLATE) self.account_info = AccountInfo() def run(self, log: Log): self.init(log=log) if not log.object_info.dn: return assert isinstance(log.object_info.dn, str) # 组策略对象 if not log.object_info.dn.lower().startswith("cn=policies,cn=system,"): return if not log.object_info.class_ == "container": return value = log.event_data["AttributeValue"] parser = SDDLParser() value_obj = parser.parse(value) domain = log.subject_info.domain_name abnormal_ace_list = [] abnormal_users = [] for each in value_obj["dacl_ace_list"]: trustee = each.get("trustee") if trustee.startswith("S-1-5-21-"): # 首先检查 该SID是否为某个用户?(Users),如果不是,则忽略掉 if not self.account_info.check_target_is_user_by_sid( trustee, domain): continue user = self.account_info.get_user_info_by_sid(trustee, domain) abnormal_users.append(user.user_name) abnormal_ace_list.append(each) if len(abnormal_ace_list) > 0: return self._generate_alert_doc( abnormal_ace_list=abnormal_ace_list, parsed_sddl=value_obj, abnormal_users=abnormal_users) def _generate_alert_doc(self, **kwargs) -> dict: source_ip = self._get_source_ip_by_logon_id( self.log.subject_info.logon_id, self.log.subject_info.full_user_name) form_data = { "source_ip": source_ip, "source_workstation": self._get_workstation_by_source_ip(source_ip), "source_user_name": self.log.subject_info.user_name, "source_user_sid": self.log.subject_info.user_sid, "source_logon_id": self.log.subject_info.logon_id, "source_domain": self.log.subject_info.domain_name, "object_info": self.log.object_info.get_doc(), **kwargs } doc = self._get_base_doc(level=self._get_level(), unique_id=self._get_unique_id( self.code, self.log.subject_info.user_name), form_data=form_data) return doc def _get_level(self) -> str: return HIGH_LEVEL