Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add red highlight for profiles generating alerts #1118

Open
wants to merge 41 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
6e6bc6f
Merge pull request #1055 from stratosphereips/develop
AlyaGomaa Oct 31, 2024
fb6478e
Merge pull request #1090 from stratosphereips/develop
AlyaGomaa Nov 29, 2024
8faa292
Merge pull request #1099 from stratosphereips/develop
AlyaGomaa Dec 3, 2024
b7c63fa
add support to ip ranges in the client_ip in slips.yaml
AlyaGomaa Dec 13, 2024
7179e76
test_threat_intelligence.py: update test_is_inbound_traffic test
AlyaGomaa Dec 13, 2024
14f6924
add support to ip ranges in the client_ip in slips.yaml
AlyaGomaa Dec 13, 2024
34f16f6
test_threat_intelligence.py: update test_is_inbound_traffic test
AlyaGomaa Dec 13, 2024
89cdba0
Merge remote-tracking branch 'origin/alya/fix_profiler_crashing_when_…
AlyaGomaa Dec 13, 2024
fbfed02
profiler: fix syntax err
AlyaGomaa Dec 13, 2024
ac54141
Add red highlight for profiles generating alerts
AbhiramMasna Dec 14, 2024
98e195c
profiler: fix printing empty MAC address
AlyaGomaa Dec 16, 2024
cc4995a
fix profiler unit tests
AlyaGomaa Dec 16, 2024
60719a9
Merge pull request #1117 from stratosphereips/alya/fix_profiler_crash…
AlyaGomaa Dec 17, 2024
5af6b02
ti: make connection to and from blacklisted IPs 2 different evidence
AlyaGomaa Dec 17, 2024
c0d4a4c
update ti unit tests
AlyaGomaa Dec 17, 2024
0ae0e61
Merge pull request #1125 from stratosphereips/alya/improve_blackliste…
AlyaGomaa Dec 17, 2024
9f8a6ca
conn.py: fix problem setting multiple reconnection attempts evidence
AlyaGomaa Dec 18, 2024
a13c1b6
add an integration test for checking multiple reconnection attempts
AlyaGomaa Dec 18, 2024
ac13666
Merge pull request #1127 from stratosphereips/alya/fix_multiple_recon…
AlyaGomaa Dec 18, 2024
8da18d4
ip_info: get domain age and registrant using whois
AlyaGomaa Dec 19, 2024
a439e10
ssl.py: detect CN and hostname mismatch
AlyaGomaa Dec 19, 2024
965e203
set_evidence:set 2 evidence on CN and hostname mismatch
AlyaGomaa Dec 19, 2024
c0f4c7b
ip_info: get the registrant of the sld in the registrant of the main …
AlyaGomaa Dec 19, 2024
adc2c0c
set_evidence: better description of CN mismatch evidence
AlyaGomaa Dec 19, 2024
5a82467
ssl: better matching of the 2 domains orgs when checking for CN mismatch
AlyaGomaa Dec 19, 2024
7b9b953
slips_utils: remove dead code
AlyaGomaa Dec 19, 2024
1321b5b
ssl: in domains_belong_to_same_org() return the common org if found
AlyaGomaa Dec 19, 2024
828b5bd
update ip info unit tests
AlyaGomaa Dec 19, 2024
1e0063e
add unit tests for CN mismatch evidence
AlyaGomaa Dec 19, 2024
6412dab
update the docs about CN mismatch evidence
AlyaGomaa Dec 19, 2024
31a2c1e
update ip info unit tests
AlyaGomaa Dec 19, 2024
11d02b9
delete redis_tests.py
AlyaGomaa Dec 19, 2024
b436cc9
Merge pull request #1129 from stratosphereips/alya/detect_SSL_Certifi…
AlyaGomaa Dec 19, 2024
9cc18ad
Merge remote-tracking branch 'origin/develop' into develop
AlyaGomaa Dec 19, 2024
5c54770
evidence: remove extra \n at the end of some evidence
AlyaGomaa Dec 19, 2024
99e77e5
build(deps): bump psutil from 6.0.0 to 6.1.1 in /install
dependabot[bot] Dec 20, 2024
1d69cc1
build(deps): bump ruff from 0.6.8 to 0.8.4 in /install
dependabot[bot] Dec 20, 2024
0122ad1
Merge pull request #1130 from stratosphereips/dependabot/pip/install/…
AlyaGomaa Jan 2, 2025
1904e62
Merge pull request #1131 from stratosphereips/dependabot/pip/install/…
AlyaGomaa Jan 2, 2025
d714290
Add red highlight for profiles generating alerts
AbhiramMasna Dec 14, 2024
a44c7a2
Merge remote-tracking branch 'AbhiramMasna/highlight-alerts' into for…
AlyaGomaa Jan 2, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .gitignore
Binary file not shown.
6 changes: 4 additions & 2 deletions config/slips.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,15 @@ parameters:
# export_format can be tsv or json. this parameter is ignored if
# export_labeled_flows is set to no
export_format: json
# These are the IPs that we see the majority of traffic going out of from.
# These are the IPs that we see the majority of traffic going out of.
# for example, this can be your own IP or some computer you’re monitoring
# when using slips on an interface, this client IP is automatically set as
# your own IP and is used to improve detections
# it would be useful to specify it when analyzing pcaps or zeek logs
# client_ips : [10.0.0.1, 172.16.0.9, 172.217.171.238]
# all private client ips should belong to the same local network
# client_ips : [10.0.0.1, 11.0.0.0/24]
client_ips: []

#############################
detection:
# This threshold is the minimum accumulated threat level per
Expand Down
5 changes: 5 additions & 0 deletions dataset/test14-malicious-zeek-dir/conn.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{"ts":79.681977,"uid":"Cw6JBU12nYsbZElIpa","id.orig_h":"10.0.2.15","id.orig_p":546,"id.resp_h":"123.22.123.22","id.resp_p":547,"proto":"tcp","duration":63.02690299999995,"orig_bytes":588,"resp_bytes":0,"conn_state":"REJ","local_orig":false,"local_resp":false,"missed_bytes":0,"history":"D","orig_pkts":7,"orig_ip_bytes":924,"resp_pkts":0,"resp_ip_bytes":0,"orig_l2_addr":"08:00:27:ef:ee:34","resp_l2_addr":"33:33:00:01:00:02"}
{"ts":79.681977,"uid":"Cw6JBU12nYsbZElIpa","id.orig_h":"10.0.2.15","id.orig_p":546,"id.resp_h":"123.22.123.22","id.resp_p":547,"proto":"tcp","duration":63.02690299999995,"orig_bytes":588,"resp_bytes":0,"conn_state":"REJ","local_orig":false,"local_resp":false,"missed_bytes":0,"history":"D","orig_pkts":7,"orig_ip_bytes":924,"resp_pkts":0,"resp_ip_bytes":0,"orig_l2_addr":"08:00:27:ef:ee:34","resp_l2_addr":"33:33:00:01:00:02"}
{"ts":79.681977,"uid":"Cw6JBU12nYsbZElIpa","id.orig_h":"10.0.2.15","id.orig_p":546,"id.resp_h":"123.22.123.22","id.resp_p":547,"proto":"tcp","duration":63.02690299999995,"orig_bytes":588,"resp_bytes":0,"conn_state":"REJ","local_orig":false,"local_resp":false,"missed_bytes":0,"history":"D","orig_pkts":7,"orig_ip_bytes":924,"resp_pkts":0,"resp_ip_bytes":0,"orig_l2_addr":"08:00:27:ef:ee:34","resp_l2_addr":"33:33:00:01:00:02"}
{"ts":79.681977,"uid":"Cw6JBU12nYsbZElIpa","id.orig_h":"10.0.2.15","id.orig_p":546,"id.resp_h":"123.22.123.22","id.resp_p":547,"proto":"tcp","duration":63.02690299999995,"orig_bytes":588,"resp_bytes":0,"conn_state":"REJ","local_orig":false,"local_resp":false,"missed_bytes":0,"history":"D","orig_pkts":7,"orig_ip_bytes":924,"resp_pkts":0,"resp_ip_bytes":0,"orig_l2_addr":"08:00:27:ef:ee:34","resp_l2_addr":"33:33:00:01:00:02"}
{"ts":79.681977,"uid":"Cw6JBU12nYsbZElIpa","id.orig_h":"10.0.2.15","id.orig_p":546,"id.resp_h":"123.22.123.22","id.resp_p":547,"proto":"tcp","duration":63.02690299999995,"orig_bytes":588,"resp_bytes":0,"conn_state":"REJ","local_orig":false,"local_resp":false,"missed_bytes":0,"history":"D","orig_pkts":7,"orig_ip_bytes":924,"resp_pkts":0,"resp_ip_bytes":0,"orig_l2_addr":"08:00:27:ef:ee:34","resp_l2_addr":"33:33:00:01:00:02"}
9 changes: 9 additions & 0 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ Slips checks if the destination address or the destination server name belongs t

If not, slips generates an alert.


### CN URL Mismatch

Zeek logs each Certificate CN in ssl.log
For each CN Slips encounters, it checks if the server name is the same as the CN
Or if it belongs to the same org as the CN. if not, slips triggers an evidence



### High entropy DNS TXT answers

Slips check every DNS answer with TXT record for high entropy
Expand Down
12 changes: 10 additions & 2 deletions docs/flowalerts.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ The detection techniques are:
- DNS ARPA Scans
- SSH version changing
- Incompatible CN
- CN URL Mismatch
- Weird HTTP methods
- Non-SSL connections on port 443
- Non-HTTP connections on port 80
Expand Down Expand Up @@ -278,10 +279,17 @@ If they are different, slips generates an alert

Zeek logs each Certificate CN in ssl.log

When slips enccounters a cn that claims to belong to any of Slips supported orgs (Google, Microsoft, Apple or Twitter)
When slips encounters a cn that claims to belong to any of Slips supported orgs (Google, Microsoft, Apple or Twitter)
Slips checks if the destination address or the destination server name belongs to these org.

If not, slips generates an alert.
If not, slips generates an evidence.


## CN URL Mismatch

Zeek logs each Certificate CN in ssl.log
For each CN Slips encounters, it checks if the server name is the same as the CN
Or if it belongs to the same org as the CN. if not, slips triggers an evidence

## Weird HTTP methods

Expand Down
4 changes: 2 additions & 2 deletions install/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ matplotlib==3.9.2
recommonmark==0.7.1
scikit_learn
slackclient==2.9.4
psutil==6.0.0
psutil==6.1.1
six==1.16.0
pytest==8.3.2
pytest-mock==3.14.0
Expand All @@ -35,7 +35,7 @@ yappi==1.6.0
pytest-sugar==1.0.0
aid_hash
black==24.8.0
ruff==0.6.8
ruff==0.8.4
pre-commit==4.0.1
coverage==7.6.1
pyyaml
Expand Down
10 changes: 5 additions & 5 deletions modules/flowalerts/conn.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,19 +267,19 @@ def check_multiple_reconnection_attempts(self, profileid, twid, flow):
reconnections, uids = current_reconnections[key]
reconnections += 1
uids.append(flow.uid)
current_reconnections[key] = (reconnections, uids)
except KeyError:
current_reconnections[key] = (1, [flow.uid])
uids = [flow.uid]
reconnections = 1

current_reconnections[key] = (reconnections, uids)
if reconnections < self.multiple_reconnection_attempts_threshold:
self.db.set_reconnections(profileid, twid, current_reconnections)
return

self.set_evidence.multiple_reconnection_attempts(
twid, flow, reconnections
twid, flow, reconnections, uids
)
# reset the reconnection attempts of this src->dst
# reset the reconnection counter of this src->dst
current_reconnections[key] = (0, [])

self.db.set_reconnections(profileid, twid, current_reconnections)
Expand Down Expand Up @@ -440,7 +440,7 @@ def should_ignore_conn_without_dns(self, flow) -> bool:
# from the internet to our ip, the dns res was probably
# made on their side before connecting to us,
# so we shouldn't be doing this detection on this ip
or flow.daddr in self.client_ips
or utils.is_ip_in_client_ips(flow.daddr, self.client_ips)
# because there's no dns.log to know if the dns was made
or self.input_type == "zeek_log_file"
or self.db.is_doh_server(flow.daddr)
Expand Down
65 changes: 65 additions & 0 deletions modules/flowalerts/set_evidence.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,71 @@ class SetEvidnceHelper:
def __init__(self, db):
self.db = db

def cn_url_mismatch(self, twid, cn, flow):
twid_number: int = int(twid.replace("timewindow", ""))
confidence: float = 0.8
description: str = (
f"a CN mismatch. The common name (CN) '{cn}' in the SSL "
f"certificate for the domain '{flow.server_name}' does not match "
f"the server's domain."
)

# to add a correlation between the 2 evidence in alerts.json
evidence_id_of_dstip_as_the_attacker = str(uuid4())
evidence_id_of_srcip_as_the_attacker = str(uuid4())
evidence: Evidence = Evidence(
id=evidence_id_of_srcip_as_the_attacker,
rel_id=[evidence_id_of_dstip_as_the_attacker],
evidence_type=EvidenceType.CN_URL_MISMATCH,
attacker=Attacker(
direction=Direction.SRC,
attacker_type=IoCType.IP,
value=flow.saddr,
),
victim=Victim(
direction=Direction.DST,
victim_type=IoCType.DOMAIN,
value=flow.server_name,
),
threat_level=ThreatLevel.LOW,
description=description,
profile=ProfileID(ip=flow.saddr),
timewindow=TimeWindow(number=twid_number),
uid=[flow.uid],
timestamp=flow.starttime,
confidence=confidence,
src_port=flow.sport,
dst_port=flow.dport,
)
self.db.set_evidence(evidence)

evidence: Evidence = Evidence(
id=evidence_id_of_dstip_as_the_attacker,
rel_id=[evidence_id_of_srcip_as_the_attacker],
evidence_type=EvidenceType.CN_URL_MISMATCH,
attacker=Attacker(
direction=Direction.DST,
attacker_type=IoCType.DOMAIN,
value=flow.server_name,
),
victim=Victim(
direction=Direction.SRC,
victim_type=IoCType.IP,
value=flow.saddr,
),
threat_level=ThreatLevel.MEDIUM,
description=description,
profile=ProfileID(ip=flow.daddr),
timewindow=TimeWindow(number=twid_number),
uid=[flow.uid],
timestamp=flow.starttime,
confidence=confidence,
src_port=flow.sport,
dst_port=flow.dport,
)

self.db.set_evidence(evidence)

def doh(self, twid, flow):
twid_number: int = int(twid.replace("timewindow", ""))
description: str = f"using DNS over HTTPs. DNS server: {flow.daddr} "
Expand Down
103 changes: 96 additions & 7 deletions modules/flowalerts/ssl.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import asyncio
import json
from typing import (
Union,
)

from typing import Union, Optional, List
import re
import tldextract
from slips_files.common.abstracts.flowalerts_analyzer import (
IFlowalertsAnalyzer,
)
Expand Down Expand Up @@ -84,7 +83,7 @@ def detect_malicious_ja3(self, twid, flow):
if flow.ja3s in malicious_ja3_dict:
self.set_evidence.malicious_ja3s(twid, flow, malicious_ja3_dict)

def detect_incompatible_cn(self, profileid, twid, flow):
def detect_incompatible_cn(self, twid, flow):
"""
Detects if a certificate claims that it's CN (common name) belongs
to an org that the domain doesn't belong to
Expand Down Expand Up @@ -144,10 +143,99 @@ def detect_doh(self, twid, flow):
self.set_evidence.doh(twid, flow)
self.db.set_ip_info(flow.daddr, {"is_doh_server": True})

@staticmethod
def get_root_domain(domain):
extracted = tldextract.extract(domain)
return f"{extracted.domain}.{extracted.suffix}"

def domains_belong_to_same_org(self, domain1, domain2) -> Optional[str]:
"""
Checks if the 2 domains belong to the same org
- by comparing the slds of both of them
- by checking the whois registrant info from the
db for both of them
Raises ValueError when info for one of the domains isnt found in
the db
returns the common org if they belong to the same org
"""
root1 = self.get_root_domain(domain1)
root2 = self.get_root_domain(domain2)
# same root domain e.g., example.com and www.example.com
if root1 == root2:
return root1

domain1_info: dict = self.db.get_domain_data(domain1)
if not (domain1_info and "Org" in domain1_info):
raise ValueError

domain2_info: dict = self.db.get_domain_data(domain2)
if not (domain2_info and "Org" in domain2_info):
raise ValueError

domain1_org = domain1_info["Org"]
domain2_org = domain2_info["Org"]

if domain1_org.lower() == domain2_org.lower():
return domain1_org

domain2_org_list: List[str] = domain2_org.split(" ")
# this way of matching ensures that we dont alert on
# Yahoo Assets LLC and Yahoo Ad Tech LLC
for word in domain1_org.split(" "):
if word in ("LLC", "Corp", "Inc", "Ltd", "Org"):
continue
if word in domain2_org_list:
return word

return

@staticmethod
def extract_cn(certificate_string: str) -> Optional[str]:
"""
extracts the CN (common name) from a given certificate string.

:param certificate_string: the certificate string
:return: the CN value or None if not found
"""
match = re.search(r"CN=([^,]+)", certificate_string)
if match:
return match.group(1)
return None

def detect_cn_url_mismatch(self, twid, flow):
"""
detected a hostname mismatch in the SSL certificate.
This happens when the common name to which an SSL Certificate is
issued (e.g., www.example.com) doesn't exactly match
the name displayed in the URL bar.
"""
if not flow.subject:
return False

# get the common name from the subject field
cn = self.extract_cn(flow.subject)
if not cn:
return

# use the cn as regex
cn_regex = cn.replace(".", "\.").replace("*", ".*")

# check if the server name matches the cn
if re.match(cn_regex, flow.server_name):
return

# regex of the cn doesnt match, check orgs of the domains
try:
if self.domains_belong_to_same_org(cn, flow.server_name):
return
except ValueError:
# we dont have info about one of the domains
return
self.set_evidence.cn_url_mismatch(twid, cn, flow)

async def analyze(self, msg: dict):
if utils.is_msg_intended_for(msg, "new_ssl"):
msg = json.loads(msg["data"])
profileid = msg["profileid"]
twid = msg["twid"]
flow = self.classifier.convert_to_flow_obj(msg["flow"])

Expand All @@ -159,8 +247,9 @@ async def analyze(self, msg: dict):

self.check_self_signed_certs(twid, flow)
self.detect_malicious_ja3(twid, flow)
self.detect_incompatible_cn(profileid, twid, flow)
self.detect_incompatible_cn(twid, flow)
self.detect_doh(twid, flow)
self.detect_cn_url_mismatch(twid, flow)

elif utils.is_msg_intended_for(msg, "new_flow"):
msg = json.loads(msg["data"])
Expand Down
Loading