def check_target_is_user_by_name(self, user: str, domain: str) -> bool: """ 检查目标账号是否为 OU=Users """ key = user + REDIS_KEY_USERNAME_IS_USERS_SUFFIX record = self.redis.get_str_value(key) # 存在redis缓存记录 if record: if record == "true": return True else: return False # 不存在 则通过ldap查询,再更新redis缓存 else: ldap = LDAPSearch(domain) user_entry = ldap.search_by_name(user=user, attributes=["cn"]) if user_entry: dn = str(user_entry.entry_dn) if "OU=Users".lower() in dn.lower() or "CN=Users".lower( ) in dn.lower(): self.redis.set_str_value( key, "true", expire=ACCOUNT_INFO_REDIS_EXPIRE_TIME) return True self.redis.set_str_value(key, "false", expire=ACCOUNT_INFO_REDIS_EXPIRE_TIME) return False
def check_target_is_admin_by_sid(self, sid: str, domain: str) -> bool: """ 检查一个账户是否拥有管理员权限 """ key = sid + REDIS_KEY_SID_IS_ADMIN_SUFFIX record = self.redis.get_str_value(key) # 存在redis缓存记录 if record: if record == "true": return True else: return False # 不存在 则通过ldap查询,再更新redis缓存 else: ldap = LDAPSearch(domain) user_entry = ldap.search_by_sid(sid=sid, attributes=["adminCount"]) if user_entry: entry_attributes = user_entry.entry_attributes_as_dict if len(entry_attributes["adminCount"] ) > 0 and entry_attributes["adminCount"][0] == 1: self.redis.set_str_value( key, "true", expire=ACCOUNT_INFO_REDIS_EXPIRE_TIME) return True self.redis.set_str_value(key, "false", expire=ACCOUNT_INFO_REDIS_EXPIRE_TIME) return False
def check_target_is_user_by_sid(self, sid: str, domain: str) -> bool: """ 检查目标账号是否为 OU=Users """ key = sid + REDIS_KEY_SID_IS_USERS_SUFFIX record = self.redis.get_str_value(key) # 存在redis缓存记录 if record: if record == "true": return True else: return False # 不存在 则通过ldap查询,再更新redis缓存 else: ldap = LDAPSearch(domain) user_entry = ldap.search_by_sid(sid=sid, attributes=["cn"]) if user_entry: dn = user_entry.entry_dn if "OU=Users" in dn: self.redis.set_str_value( key, "true", expire=ACCOUNT_INFO_REDIS_EXPIRE_TIME) return True self.redis.set_str_value(key, "false", expire=ACCOUNT_INFO_REDIS_EXPIRE_TIME) return False
def check_target_is_aes_support(self, name: str, domain: str) -> bool: key = name + REDIS_KEY_USERNAME_AES_SUPPORT_SUFFIX # 先查redis is_support = self.redis.get_str_value(key) # if is_support is not None: return is_support == "true" else: ldap = LDAPSearch(domain) user_entry = ldap.search_by_name( name, attributes=["msDS-SupportedEncryptionTypes"]) if not user_entry: return False support_types = user_entry.entry_attributes_as_dict[ "msDS-SupportedEncryptionTypes"] if len(support_types) == 0: return False support_types = support_types[0] # 等于8 支持AES128加密 if support_types >= 8: self.redis.set_str_value(key, "true") return True else: self.redis.set_str_value(key, "false") return False
def init_sensitive_groups(domain): logger.info("init sensitive groups.") domain = get_netbios_domain(domain) ldap_search = LDAPSearch(domain) redis = RedisHelper() mongo = MongoHelper(uri=MongoConfig.uri, db=MongoConfig.db, collection=MongoConfig.settings_collection) sensitive_groups = [] for item in default_sensitive_groups(domain): if len(item["sid"]) > 0: sensitive_groups.append(item) else: entry = ldap_search.search_by_name(item["name"], attributes=["objectSid"]) if not entry or len( entry.entry_attributes_as_dict["objectSid"]) == 0: continue sid = entry.entry_attributes_as_dict["objectSid"][0] item["sid"] = sid sensitive_groups.append(item) logger.info(",".join(list(map(lambda x: x["name"], sensitive_groups)))) sensitive_entry = mongo.find_one({"name": "sensitive_entry"})["value"] sensitive_entry["group"] = sensitive_groups mongo.update_one({"name": "sensitive_entry"}, {"$set": { "value": sensitive_entry }}, upsert=True) redis.set_str_value("sensitive_entry" + REDIS_KEY_SUFFIX, simplejson.dumps(sensitive_entry))
def user_is_sensitive_by_sid(self, sid: str, domain: str) -> bool: """ 检查某个用户是否为敏感用户 1. adminCount 1 2. 属于敏感组 3. 蜜罐账户 4. 自定义敏感用户 LDAP查询是性能瓶颈,需要使用redis进行缓存加速 """ # 蜜罐账户 for user in main_config.honeypot_account: if user["sid"] == sid: return True # 自定义敏感用户 for user in main_config.sensitive_users: if user["sid"] == sid: return True # 先查缓存 _cache_is_sensitive = self.get_target_sensitive_cache(sid) if _cache_is_sensitive is not None: return _cache_is_sensitive == "true" # LDAP 查询较慢,性能瓶颈 ldap = LDAPSearch(domain) user_entry = ldap.search_by_sid(sid, attributes=["adminCount", "memberOf"]) if not user_entry: self.set_target_sensitive_cache(sid, "false") return False # adminCount if len(user_entry.entry_attributes_as_dict["adminCount"]) > 0 and \ user_entry.entry_attributes_as_dict["adminCount"][0] == 1: self.set_target_sensitive_cache(sid, "true") return True # 敏感组 groups = user_entry.entry_attributes_as_dict["memberOf"] sensitive_groups = list( map(lambda x: x["name"], main_config.sensitive_groups)) for g in groups: g_name = get_cn_from_dn(g) if g_name in sensitive_groups: self.set_target_sensitive_cache(sid, "true") return True self.set_target_sensitive_cache(sid, "false") return False
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 = ldap.search_by_sid(target_sid, attributes=["cn"]) if not target: return target_user_name = str(target["cn"]) return self._generate_alert_doc(target_user_name=target_user_name)
def get_user_info_by_sid(self, sid: str, domain: str) -> User: key = sid + REDIS_KEY_SID_USERNAME_SUFFIX # 先查redis user_name = self.redis.get_str_value(key) if not user_name: ldap = LDAPSearch(domain) user_entry = ldap.search_by_sid(sid, attributes=["sAMAccountName"]) if not user_entry: return None user_name = user_entry.entry_attributes_as_dict["sAMAccountName"][ 0] self.redis.set_str_value(key, user_name, expire=ACCOUNT_INFO_REDIS_EXPIRE_TIME) user = User({ "user_name": user_name, "user_sid": sid, "logon_id": "", "domain_name": domain }) return user
def get_all_unconstrained_delegation_users(domain: str): """ 获取所有 无约束委派 权限的用户 """ d = Delegation() ldap_search = LDAPSearch(domain) entries = ldap_search.search_unconstrained_accounts() if entries: for each in entries: assert isinstance(each, Entry) user = User({ "user_name": each.entry_attributes_as_dict["sAMAccountName"][0], "user_sid": each.entry_attributes_as_dict["objectSid"][0], "domain_name": domain }) d.new_delegation_record(user=user, delegation_type=UNCONSTRAINED_DELEGATION)
def user_is_sensitive_by_sid(self, sid: str, domain: str) -> bool: """ 检查某个用户是否为敏感用户 1. adminCount 1 2. 属于敏感组 3. 蜜罐账户 4. 自定义敏感用户 """ # 蜜罐账户 for user in main_config.honeypot_account: if user["sid"] == sid: return True # 自定义敏感用户 for user in main_config.sensitive_users: if user["sid"] == sid: return True ldap = LDAPSearch(domain) user_entry = ldap.search_by_sid(sid, attributes=["adminCount", "memberOf"]) if not user_entry: return False # adminCount if len(user_entry.entry_attributes_as_dict["adminCount"]) > 0 and \ user_entry.entry_attributes_as_dict["adminCount"][0] == 1: return True # 敏感组 groups = user_entry.entry_attributes_as_dict["memberOf"] sensitive_groups = list( map(lambda x: x["name"], main_config.sensitive_groups)) for g in groups: g_name = get_cn_from_dn(g) if g_name in sensitive_groups: return True return False
def get_user_info_by_name(self, user_name: str, domain: str) -> User: key = user_name + REDIS_KEY_USERNAME_SID_SUFFIX # 先查redis user_sid = self.redis.get_str_value(key) # redis 缓存未命中 再查mongo if not user_sid: ldap = LDAPSearch(domain) user_entry = ldap.search_by_name(user_name, attributes=["objectSid"]) if not user_entry: return user_sid = user_entry.entry_attributes_as_dict["objectSid"][0] self.redis.set_str_value(key, user_sid, expire=ACCOUNT_INFO_REDIS_EXPIRE_TIME) user = User({ "user_name": user_name, "user_sid": user_sid, "logon_id": "", "domain_name": domain }) return user
def get_all_dc_names(domain: str): """ 将DC列表入库 """ domain = get_netbios_domain(domain) logger.info("Search all domain controllers using LDAP.") dc_name_list = [] ldap_search = LDAPSearch(domain) dc_list = ldap_search.search_domain_controller() for each in dc_list: dc_name = str(each["cn"]) dc_name_list.append(dc_name) mongo = MongoHelper(MongoConfig.uri, MongoConfig.db, MongoConfig.settings_collection) doc = {domain: dc_name_list} logger.info(",".join(dc_name_list)) logger.info( "domain controller count: {count}".format(count=len(dc_name_list))) logger.info("Save all domain controllers to settings.") mongo.update_one({"name": "dc_name_list"}, {"$set": {"value": doc}}, True) redis = RedisHelper() redis.set_str_value("dc_name_list" + REDIS_KEY_SUFFIX, simplejson.dumps(doc))
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 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 _get_support_aes_account(self, domain): ldap = LDAPSearch(domain) entry = ldap.get_support_aes_account() if entry: return str(entry["sAMAccountName"])