diff --git a/docs/detection_modules.md b/docs/detection_modules.md index 8d8fbe7340..d8a1afa03f 100644 --- a/docs/detection_modules.md +++ b/docs/detection_modules.md @@ -394,6 +394,9 @@ For example, if the domain in the traffic is _here.testing.com_, Slips first checks if the exact domain _here.testing.com_ is in any blacklist, and if there is no match, it checks if the domain _testing.com_ is in any blacklists too. +Slips also sets evidence about DNS answers of blacklisted domains. If test.com is blacklisted, and test.com resolves to 1.2.3.4, slips sets +an evidence when it encouters the DNS answer with 1.2.3.4. + ### Matching of JA3 Hashes Every time Slips encounters an TLS flow, diff --git a/docs/features.md b/docs/features.md index 90b29ebbc8..6b4a15c588 100644 --- a/docs/features.md +++ b/docs/features.md @@ -640,7 +640,8 @@ File hashes and URLs aren't supported. Slips gets every IP it can find in the network and tries to see if it is in any blacklist. -If a match is found, it generates an evidence, if no exact match is found, it searches the Blacklisted ranges taken from different TI feeds +If a match is found, it generates an evidence, if no exact match is found, +it searches the Blacklisted ranges taken from different TI feeds #### Matching of Domains diff --git a/modules/threat_intelligence/threat_intelligence.py b/modules/threat_intelligence/threat_intelligence.py index 20a06d35be..2a1a6fe940 100644 --- a/modules/threat_intelligence/threat_intelligence.py +++ b/modules/threat_intelligence/threat_intelligence.py @@ -1669,26 +1669,20 @@ def is_malicious_domain( timestamp, profileid, twid, - ): + set_evidence=True, + ) -> Tuple[dict, bool] | bool: """Evaluates a domain to determine if it is recognized as malicious based on threat intelligence data stored offline. If the domain is - identified as - malicious, it records an evidence entry and marks the + identified as malicious, it records an evidence entry and marks the domain in the database. + setting an evidence is optional here determined by the set_evidence + flag Returns: bool: False if the domain is ignored or not found in the - offline threat intelligence data, indicating no further action - is required. Otherwise, it does not explicitly return - a value but performs operations to record the - malicious domain evidence. - - Side Effects: - - Generates and stores an evidence entry for the malicious - domain in the database. - - Marks the domain as malicious in the database, enhancing - the system's future recognition of this threat. + offline threat intelligence data, and the malicious domain TI + info if its malicious. """ if self.is_ignored_domain(domain): return False @@ -1697,19 +1691,21 @@ def is_malicious_domain( if not domain_info: return False - self.set_evidence_malicious_domain( - domain, - uid, - timestamp, - domain_info, - is_subdomain, - profileid, - twid, - ) + if set_evidence: + self.set_evidence_malicious_domain( + domain, + uid, + timestamp, + domain_info, + is_subdomain, + profileid, + twid, + ) # mark this domain as malicious in our database domain_info = {"threatintelligence": domain_info} self.db.set_info_for_domains(domain, domain_info) + return domain_info, is_subdomain def update_local_file(self, filename): """Checks for updates to a specified local threat intelligence @@ -1797,6 +1793,93 @@ def should_lookup(self, ip: str, protocol: str, ip_state: str) -> bool: return False return True + def is_dns_answer_of_a_malicious_query( + self, + answer, + uid, + timestamp, + profileid, + twid, + dns_query, + ): + """ + Detects the DNS answers of malicious DNS queries. + even if the IPs returned in the answer is not in one of slips' TI feeds. + :param answer: the ip coming in the dns answer + """ + if utils.is_ignored_ip(answer): + return False + + malicious_query_feed_info = self.is_malicious_domain( + dns_query, uid, timestamp, profileid, twid, set_evidence=False + ) + + if not malicious_query_feed_info: + return False + + # was the subdomain of the query malicious, or the domain? + domain_info, is_subdomain = malicious_query_feed_info + + try: + domain_info = domain_info["threatintelligence"] + except KeyError: + return False + + confidence: float = 0.7 if is_subdomain else 1 + + srcip = profileid.split("_")[-1] + threat_level: float = utils.threat_levels[ + domain_info.get("threat_level", "high") + ] + threat_level: ThreatLevel = ThreatLevel(threat_level) + description: str = ( + f"DNS answer of a blacklisted query. Query: {dns_query}. " + f"Answer: {answer}, " + f"Description: {domain_info.get('description', '')}, " + f"Query found in feed: {domain_info['source']}, " + f"Confidence: {confidence}" + ) + + tags = domain_info.get("tags", None) + if tags: + description += f", Tags: {tags}. " + + evidence = Evidence( + evidence_type=EvidenceType.THREAT_INTELLIGENCE_ANSWER_OF_BLACKLISTED_QUERY, + attacker=Attacker( + direction=Direction.DST, ioc_type=IoCType.IP, value=answer + ), + victim=Victim( + ioc_type=IoCType.IP, + direction=Direction.SRC, + value=srcip, + ), + threat_level=threat_level, + confidence=confidence, + description=description, + profile=ProfileID(ip=answer), + timewindow=TimeWindow(number=int(twid.replace("timewindow", ""))), + uid=[uid], + timestamp=utils.convert_ts_format(timestamp, utils.alerts_format), + ) + + self.db.set_evidence(evidence) + + evidence = Evidence( + evidence_type=EvidenceType.THREAT_INTELLIGENCE_ANSWER_OF_BLACKLISTED_QUERY, + attacker=Attacker( + direction=Direction.SRC, ioc_type=IoCType.IP, value=srcip + ), + threat_level=threat_level, + confidence=confidence, + description=description, + profile=ProfileID(ip=srcip), + timewindow=TimeWindow(number=int(twid.replace("timewindow", ""))), + uid=[uid], + timestamp=utils.convert_ts_format(timestamp, utils.alerts_format), + ) + self.db.set_evidence(evidence) + def pre_main(self): utils.drop_root_privs_permanently() # Load the local Threat Intelligence files that are @@ -1832,13 +1915,22 @@ def main(self): to_lookup = data.get("to_lookup", "") # detect the type given because sometimes, # http.log host field has ips OR domains - type_ = utils.detect_ioc_type(to_lookup) + ioc_type = utils.detect_ioc_type(to_lookup) # ip_state can be "srcip" or "dstip" ip_state = data.get("ip_state") - if type_ == "ip": + if ioc_type == "ip": ip = to_lookup if self.should_lookup(ip, protocol, ip_state): + if is_dns_response: + self.is_dns_answer_of_a_malicious_query( + ip, + uid, + timestamp, + profileid, + twid, + dns_query, + ) self.is_malicious_ip( ip, uid, @@ -1861,7 +1953,7 @@ def main(self): twid, is_dns_response=is_dns_response, ) - elif type_ == "domain": + elif ioc_type == "domain": if is_dns_response: self.is_malicious_cname( dns_query, to_lookup, uid, timestamp, profileid, twid @@ -1870,7 +1962,7 @@ def main(self): self.is_malicious_domain( to_lookup, uid, timestamp, profileid, twid ) - elif type_ == "url": + elif ioc_type == "url": self.is_malicious_url( to_lookup, uid, timestamp, daddr, profileid, twid ) diff --git a/slips_files/core/database/redis_db/ioc_handler.py b/slips_files/core/database/redis_db/ioc_handler.py index 2428a05920..f17483f056 100644 --- a/slips_files/core/database/redis_db/ioc_handler.py +++ b/slips_files/core/database/redis_db/ioc_handler.py @@ -290,7 +290,13 @@ def is_blacklisted_ssl(self, sha1): def _match_exact_domain(self, domain: str) -> Optional[Dict[str, str]]: """checks if the given domain is blacklisted. - checks only the exact given domain, no subdomains""" + checks only the exact given domain, no subdomains + returns something like + {"description": "x.com", + "source": "own_malicious_iocs.csv", + "threat_level": "medium", + "tags": "local TI file"} + """ domain_description = self.rcache.hget( self.constants.IOC_DOMAINS, domain ) diff --git a/slips_files/core/database/redis_db/profile_handler.py b/slips_files/core/database/redis_db/profile_handler.py index b2411233e5..9396e3ce93 100644 --- a/slips_files/core/database/redis_db/profile_handler.py +++ b/slips_files/core/database/redis_db/profile_handler.py @@ -248,7 +248,7 @@ def add_out_dns(self, profileid, twid, flow): ) # send each dns answer to TI module for answer in flow.answers: - if "TXT" in answer: + if "TXT" in answer or answer == "": continue extra_info = { diff --git a/slips_files/core/structures/evidence.py b/slips_files/core/structures/evidence.py index 0bf6d8524e..aa0192ba81 100644 --- a/slips_files/core/structures/evidence.py +++ b/slips_files/core/structures/evidence.py @@ -106,6 +106,7 @@ class EvidenceType(Enum): THREAT_INTELLIGENCE_TO_BLACKLISTED_IP = auto() THREAT_INTELLIGENCE_BLACKLISTED_DNS_ANSWER = auto() THREAT_INTELLIGENCE_BLACKLISTED_DOMAIN = auto() + THREAT_INTELLIGENCE_ANSWER_OF_BLACKLISTED_QUERY = auto() MALICIOUS_DOWNLOADED_FILE = auto() THREAT_INTELLIGENCE_MALICIOUS_URL = auto() diff --git a/zeek-scripts/__load__.zeek b/zeek-scripts/__load__.zeek index c142733a25..2d7b149299 100644 --- a/zeek-scripts/__load__.zeek +++ b/zeek-scripts/__load__.zeek @@ -88,7 +88,7 @@ redef digest_salt = "Please change this value."; @load protocols/ssh/interesting-hostnames # Detect SQL injection attacks. -@load protocols/http/detect-sqli +@load protocols/http/detect-sql-injection.zeek #### Network File Handling ####