Skip to content

Commit 5bbb0e2

Browse files
authored
Merge pull request #1114 from stratosphereips/alya/small_bug_fixes
Add multiple reconnection attempts to telnet detection
2 parents f02645e + be788bd commit 5bbb0e2

File tree

7 files changed

+211
-39
lines changed

7 files changed

+211
-39
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
- Add multiple telnet reconnection attempts detection
2+
13
1.1.4.1 (Dec 3rd, 2024)
24
- Fix abstract class starting with the rest of the modules.
35
- Fix the updating of the MAC vendors database used in slips.

modules/flowalerts/conn.py

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
from slips_files.common.flow_classifier import FlowClassifier
1616

1717

18+
NOT_ESTAB = "Not Established"
19+
ESTAB = "Established"
20+
21+
1822
class Conn(IFlowalertsAnalyzer):
1923
def init(self):
2024
# get the default gateway
@@ -38,6 +42,7 @@ def init(self):
3842
self.classifier = FlowClassifier()
3943
self.our_ips = utils.get_own_ips()
4044
self.input_type: str = self.db.get_input_type()
45+
self.multiple_reconnection_attempts_threshold = 5
4146

4247
def read_configuration(self):
4348
conf = ConfigParser()
@@ -171,7 +176,7 @@ def check_unknown_port(self, profileid, twid, flow):
171176
"""
172177
if not flow.dport:
173178
return
174-
if flow.interpreted_state != "Established":
179+
if flow.interpreted_state != ESTAB:
175180
# detect unknown ports on established conns only
176181
return False
177182

@@ -195,6 +200,54 @@ def check_unknown_port(self, profileid, twid, flow):
195200
self.set_evidence.unknown_port(twid, flow)
196201
return True
197202

203+
def is_telnet(self, flow) -> bool:
204+
try:
205+
dport = int(flow.dport)
206+
except ValueError:
207+
# binetflow icmp ports are hex strings
208+
return False
209+
210+
telnet_ports = (23, 2323)
211+
return dport in telnet_ports and flow.proto.lower() == "tcp"
212+
213+
def check_multiple_telnet_reconnection_attempts(
214+
self, profileid, twid, flow
215+
):
216+
if flow.interpreted_state != NOT_ESTAB:
217+
return
218+
219+
if not self.is_telnet(flow):
220+
return
221+
222+
key = f"{flow.saddr}-{flow.daddr}-telnet"
223+
# add this conn to the stored number of reconnections
224+
current_reconnections = self.db.get_reconnections_for_tw(
225+
profileid, twid
226+
)
227+
try:
228+
reconnections, uids = current_reconnections[key]
229+
reconnections += 1
230+
uids.append(flow.uid)
231+
current_reconnections[key] = (reconnections, uids)
232+
except KeyError:
233+
current_reconnections[key] = (1, [flow.uid])
234+
reconnections = 1
235+
236+
if reconnections < 4:
237+
# update the reconnections ctr in the db
238+
self.db.set_reconnections(profileid, twid, current_reconnections)
239+
return
240+
241+
self.set_evidence.multiple_telnet_reconnection_attempts(
242+
twid, flow, reconnections, current_reconnections[key][1]
243+
)
244+
245+
# reset the reconnection attempts of this src->dst since an evidence
246+
# is set
247+
current_reconnections[key] = (0, [])
248+
249+
self.db.set_reconnections(profileid, twid, current_reconnections)
250+
198251
def check_multiple_reconnection_attempts(self, profileid, twid, flow):
199252
"""
200253
Alerts when 5+ reconnection attempts from the same source IP to
@@ -219,7 +272,8 @@ def check_multiple_reconnection_attempts(self, profileid, twid, flow):
219272
current_reconnections[key] = (1, [flow.uid])
220273
reconnections = 1
221274

222-
if reconnections < 5:
275+
if reconnections < self.multiple_reconnection_attempts_threshold:
276+
self.db.set_reconnections(profileid, twid, current_reconnections)
223277
return
224278

225279
self.set_evidence.multiple_reconnection_attempts(
@@ -511,7 +565,7 @@ def check_conn_to_port_0(self, profileid, twid, flow):
511565
)
512566

513567
def detect_connection_to_multiple_ports(self, profileid, twid, flow):
514-
if flow.proto != "tcp" or flow.interpreted_state != "Established":
568+
if flow.proto != "tcp" or flow.interpreted_state != ESTAB:
515569
return
516570

517571
dport_name = flow.appproto
@@ -525,7 +579,7 @@ def detect_connection_to_multiple_ports(self, profileid, twid, flow):
525579
# Connection to multiple ports to the destination IP
526580
if profileid.split("_")[1] == flow.saddr:
527581
direction = "Dst"
528-
state = "Established"
582+
state = ESTAB
529583
protocol = "TCP"
530584
role = "Client"
531585
type_data = "IPs"
@@ -565,7 +619,7 @@ def detect_connection_to_multiple_ports(self, profileid, twid, flow):
565619
# Happens in the mode 'all'
566620
elif profileid.split("_")[-1] == flow.daddr:
567621
direction = "Src"
568-
state = "Established"
622+
state = ESTAB
569623
protocol = "TCP"
570624
role = "Server"
571625
type_data = "IPs"
@@ -608,7 +662,7 @@ def check_non_http_port_80_conns(self, twid, flow):
608662
str(flow.dport) == "80"
609663
and flow.proto.lower() == "tcp"
610664
and str(flow.appproto).lower() != "http"
611-
and flow.interpreted_state == "Established"
665+
and flow.interpreted_state == ESTAB
612666
and (flow.sbytes + flow.dbytes) != 0
613667
):
614668
self.set_evidence.non_http_port_80_conn(twid, flow)
@@ -734,6 +788,9 @@ async def analyze(self, msg):
734788
self.check_long_connection(twid, flow)
735789
self.check_unknown_port(profileid, twid, flow)
736790
self.check_multiple_reconnection_attempts(profileid, twid, flow)
791+
self.check_multiple_telnet_reconnection_attempts(
792+
profileid, twid, flow
793+
)
737794
self.check_conn_to_port_0(profileid, twid, flow)
738795
self.check_different_localnet_usage(
739796
twid, flow, what_to_check="dstip"

modules/flowalerts/dns.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -309,17 +309,17 @@ def check_high_entropy_dns_answers(self, twid, flow):
309309
)
310310

311311
def check_invalid_dns_answers(self, twid, flow):
312-
# this function is used to check for certain IP
313-
# answers to DNS queries being blocked
314-
# (perhaps by ad blockers) and set to the following IP values
315-
# currently hardcoding blocked ips
316-
invalid_answers = {"127.0.0.1", "0.0.0.0"}
312+
"""
313+
this function is used to check for private IPs in the answers of
314+
a dns queries.
315+
probably means the queries is being blocked
316+
(perhaps by ad blockers) and set to a private IP value
317+
"""
317318
if not flow.answers:
318319
return
319320

320321
for answer in flow.answers:
321-
if answer in invalid_answers and flow.query != "localhost":
322-
# blocked answer found
322+
if utils.is_private_ip(answer) and flow.query != "localhost":
323323
self.set_evidence.invalid_dns_answer(twid, flow, answer)
324324
# delete answer from redis cache to prevent
325325
# associating this dns answer with this domain/query and

modules/flowalerts/set_evidence.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,45 @@ def self_signed_certificates(self, twid, flow) -> None:
881881
)
882882
self.db.set_evidence(evidence)
883883

884+
def multiple_telnet_reconnection_attempts(
885+
self, twid, flow, reconnections, uids: List[str]
886+
):
887+
"""
888+
Set evidence for 4+ telnet unsuccessful attempts.
889+
"""
890+
confidence: float = 0.5
891+
threat_level: ThreatLevel = ThreatLevel.MEDIUM
892+
893+
twid: int = int(twid.replace("timewindow", ""))
894+
895+
description = (
896+
f"Multiple Telnet reconnection attempts from IP: {flow.saddr} "
897+
f"to Destination IP: {flow.daddr} "
898+
f"reconnections: {reconnections}"
899+
)
900+
evidence: Evidence = Evidence(
901+
evidence_type=EvidenceType.MULTIPLE_RECONNECTION_ATTEMPTS,
902+
attacker=Attacker(
903+
direction=Direction.SRC,
904+
attacker_type=IoCType.IP,
905+
value=flow.saddr,
906+
),
907+
victim=Victim(
908+
direction=Direction.DST,
909+
victim_type=IoCType.IP,
910+
value=flow.daddr,
911+
),
912+
threat_level=threat_level,
913+
confidence=confidence,
914+
description=description,
915+
profile=ProfileID(ip=flow.saddr),
916+
timewindow=TimeWindow(number=twid),
917+
uid=uids,
918+
timestamp=flow.starttime,
919+
)
920+
921+
self.db.set_evidence(evidence)
922+
884923
def multiple_reconnection_attempts(
885924
self, twid, flow, reconnections, uids: List[str]
886925
) -> None:

slips_files/common/slips_utils.py

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616
import sys
1717
import ipaddress
1818
import aid_hash
19-
from typing import Any, Optional
19+
from typing import (
20+
Any,
21+
Optional,
22+
Union,
23+
)
2024
from dataclasses import is_dataclass, asdict
2125
from enum import Enum
2226

@@ -265,7 +269,7 @@ def convert_format(self, ts, required_format: str):
265269

266270
# convert to the req format
267271
if required_format == "iso":
268-
return datetime_obj.astimezone().isoformat()
272+
return datetime_obj.astimezone(tz=self.local_tz).isoformat()
269273
elif required_format == "unixtimestamp":
270274
return datetime_obj.timestamp()
271275
else:
@@ -390,20 +394,19 @@ def is_port_in_use(self, port: int) -> bool:
390394
sock.close()
391395
return True
392396

393-
def is_private_ip(self, ip_obj: ipaddress) -> bool:
394-
"""
395-
This function replaces the ipaddress library 'is_private'
396-
because it does not work correctly and it does not ignore
397-
the ips 0.0.0.0 or 255.255.255.255
398-
"""
399-
# Is it a well-formed ipv4 or ipv6?
400-
r_value = False
401-
if ip_obj and ip_obj.is_private:
402-
if ip_obj != ipaddress.ip_address(
403-
"0.0.0.0"
404-
) and ip_obj != ipaddress.ip_address("255.255.255.255"):
405-
r_value = True
406-
return r_value
397+
def is_private_ip(
398+
self, ip: Union[ipaddress.IPv4Address, ipaddress.IPv6Address, str]
399+
) -> bool:
400+
ip_classes = {ipaddress.IPv4Address, ipaddress.IPv6Address}
401+
for class_ in ip_classes:
402+
if isinstance(ip, class_):
403+
return ip and ip.is_private
404+
405+
if self.detect_ioc_type(ip) != "ip":
406+
return False
407+
# convert the given str ip to obj
408+
ip_obj = ipaddress.ip_address(ip)
409+
return ip_obj.is_private
407410

408411
def is_ignored_ip(self, ip: str) -> bool:
409412
"""
@@ -414,16 +417,15 @@ def is_ignored_ip(self, ip: str) -> bool:
414417
ip_obj = ipaddress.ip_address(ip)
415418
except (ipaddress.AddressValueError, ValueError):
416419
return True
420+
417421
# Is the IP multicast, private? (including localhost)
418422
# The broadcast address 255.255.255.255 is reserved.
419-
return bool(
420-
(
421-
ip_obj.is_multicast
422-
or self.is_private_ip(ip_obj)
423-
or ip_obj.is_link_local
424-
or ip_obj.is_reserved
425-
or ".255" in ip_obj.exploded
426-
)
423+
return (
424+
ip_obj.is_multicast
425+
or self.is_private_ip(ip_obj)
426+
or ip_obj.is_link_local
427+
or ip_obj.is_loopback
428+
or ip_obj.is_reserved
427429
)
428430

429431
def get_sha256_hash(self, filename: str):

tests/test_conn.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,78 @@ def test_check_unknown_port_true_case(mocker):
185185
mock_set_evidence.assert_called_once_with(twid, flow)
186186

187187

188+
@pytest.mark.parametrize(
189+
"origstate, saddr, daddr, dport, uids, interpreted_state, expected_calls",
190+
[
191+
( # Testcase1:5 rejections, evidence should be set
192+
"REJ",
193+
"192.168.1.1",
194+
"192.168.1.2",
195+
23,
196+
[f"uid_{i}" for i in range(4)],
197+
"Not Established",
198+
1,
199+
),
200+
( # Testcase2: Less than 5 rejections, no evidence
201+
"RST",
202+
"192.168.1.1",
203+
"192.168.1.2",
204+
2323,
205+
[f"uid_{i}" for i in range(4)],
206+
"Not Established",
207+
1,
208+
),
209+
( # Testcase3: Non-REJ state, no evidence
210+
"Established",
211+
"192.168.1.1",
212+
"192.168.1.2",
213+
23,
214+
["uid_1"],
215+
"Established",
216+
0,
217+
),
218+
],
219+
)
220+
def test_check_multiple_telnet_reconnection_attempts(
221+
origstate, saddr, daddr, dport, uids, interpreted_state, expected_calls
222+
):
223+
"""
224+
Tests the check_multiple_telnet_reconnection_attempts function
225+
with various scenarios.
226+
"""
227+
conn = ModuleFactory().create_conn_analyzer_obj()
228+
conn.set_evidence.multiple_telnet_reconnection_attempts = Mock()
229+
conn.db.get_reconnections_for_tw.return_value = {}
230+
231+
for uid in uids:
232+
flow = Conn(
233+
starttime="1726249372.312124",
234+
uid=uid,
235+
saddr=saddr,
236+
daddr=daddr,
237+
dur=1,
238+
proto="tcp",
239+
appproto="",
240+
sport="0",
241+
dport=dport,
242+
spkts=0,
243+
dpkts=0,
244+
sbytes=0,
245+
dbytes=0,
246+
smac="",
247+
dmac="",
248+
state=origstate,
249+
history="",
250+
)
251+
flow.interpreted_state = interpreted_state
252+
conn.check_multiple_telnet_reconnection_attempts(profileid, twid, flow)
253+
254+
assert (
255+
conn.set_evidence.multiple_telnet_reconnection_attempts.call_count
256+
== expected_calls
257+
)
258+
259+
188260
@pytest.mark.parametrize(
189261
"origstate, saddr, daddr, dport, uids, expected_calls",
190262
[

tests/test_slips_utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,9 @@ def test_assert_microseconds(input_value, expected_output):
186186
# testcase4: Public IPv4 address
187187
(ipaddress.ip_address("8.8.8.8"), False),
188188
# testcase5: Special IP address 0.0.0.0
189-
(ipaddress.ip_address("0.0.0.0"), False),
189+
(ipaddress.ip_address("0.0.0.0"), True),
190190
# testcase6: Broadcast IP address 255.255.255.255
191-
(ipaddress.ip_address("255.255.255.255"), False),
191+
(ipaddress.ip_address("255.255.255.255"), True),
192192
],
193193
)
194194
def test_is_private_ip(ip_address, expected_result):

0 commit comments

Comments
 (0)