From 936ba78f2bfc94862f60685503b1193a43cfe73d Mon Sep 17 00:00:00 2001 From: "C.J. May" Date: Wed, 17 Jul 2024 16:02:01 +0000 Subject: [PATCH 1/6] save previous alert state --- respotter.py | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/respotter.py b/respotter.py index 529d81b..9c0d69b 100644 --- a/respotter.py +++ b/respotter.py @@ -4,7 +4,7 @@ from datetime import datetime, timedelta from ipaddress import ip_network import json -from multiprocessing import Process +from multiprocessing import Process, Lock from scapy.all import * from scapy.layers.dns import DNS, DNSQR from scapy.layers.inet import IP, UDP @@ -39,6 +39,7 @@ def __init__(self, teams_webhook="", syslog_address="", ): + # initialize logger self.log = logging.getLogger('respotter') formatter = logging.Formatter('') handler = logging.StreamHandler() @@ -50,15 +51,29 @@ def __init__(self, formatter = logging.Formatter('Respotter {processName}[{process}]: {message}', style='{') handler.setFormatter(formatter) self.log.addHandler(handler) - conf.checkIPaddr = False # multicast/broadcast responses won't come from dst IP + # import configuration self.delay = delay self.excluded_protocols = excluded_protocols self.hostname = hostname self.is_daemon = False self.timeout = timeout self.verbosity = verbosity - self.responder_alerts = {} - self.vulnerable_alerts = {} + # state persistence + self.state_lock = Lock() + try: + with open("state/state.json", "r+") as state_file: + try: + previous_state = json.load(state_file) + self.responder_alerts = previous_state["responder_alerts"] + self.vulnerable_alerts = previous_state["vulnerable_alerts"] + except json.JSONDecodeError: + raise FileNotFoundError + except FileNotFoundError: + self.responder_alerts = {} + self.vulnerable_alerts = {} + with open("state/state.json", "w") as state_file: + json.dump({"responder_alerts": {}, "vulnerable_alerts": {}}, state_file) + # get broadcast IP for Netbios if subnet: try: network = ip_network(subnet) @@ -68,6 +83,7 @@ def __init__(self, elif "nbns" not in self.excluded_protocols: self.log.error(f"[!] ERROR: subnet CIDR not configured. Netbios protocol will be disabled.") self.excluded_protocols.append("nbns") + # setup webhooks self.webhooks = {} for service in ["teams", "slack", "discord"]: webhook = eval(f"{service}_webhook") @@ -89,6 +105,12 @@ def webhook_responder_alert(self, responder_ip): send_discord_message(self.webhooks["discord"], title=title, details=details) self.log.info(f"[+] Alert sent to Discord for {responder_ip}") self.responder_alerts[responder_ip] = datetime.now() + with self.state_lock: + with open("state/state.json", "r+") as state_file: + state = json.load(state_file) + state["responder_alerts"] = self.responder_alerts + state_file.seek(0) + json.dump(state, state_file) def webhook_sniffer_alert(self, protocol, requester_ip, requested_hostname): if requester_ip in self.vulnerable_alerts: @@ -107,7 +129,12 @@ def webhook_sniffer_alert(self, protocol, requester_ip, requested_hostname): self.vulnerable_alerts[requester_ip][protocol] = datetime.now() else: self.vulnerable_alerts[requester_ip] = {protocol: datetime.now()} - + with self.state_lock: + with open("state/state.json", "r+") as state_file: + state = json.load(state_file) + state["vulnerable_alerts"] = self.vulnerable_alerts + state_file.seek(0) + json.dump(state, state_file) def send_llmnr_request(self): # LLMNR uses the multicast IP 224.0.0.252 and UDP port 5355 @@ -180,6 +207,8 @@ def daemon(self): def responder_scan(self): self.log.info("[*] Responder scans started") + # Scapy setting -- multicast/broadcast responses won't come from dst IP + conf.checkIPaddr = False while True: if "llmnr" not in self.excluded_protocols: self.send_llmnr_request() From 7b3d1eeda6870c79dafd6572554891b038e14cea Mon Sep 17 00:00:00 2001 From: "C.J. May" Date: Wed, 17 Jul 2024 16:02:12 +0000 Subject: [PATCH 2/6] ignore state --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 59a4226..f4019f6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Project specific config.json config/ +state/ # Redis dump.rdp From baf675a6b6f22c04e543630583015b434b707805 Mon Sep 17 00:00:00 2001 From: "C.J. May" Date: Wed, 17 Jul 2024 16:07:03 +0000 Subject: [PATCH 3/6] make state folder if it doesn't exist --- respotter.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/respotter.py b/respotter.py index 9c0d69b..a9b9a5c 100644 --- a/respotter.py +++ b/respotter.py @@ -5,6 +5,7 @@ from ipaddress import ip_network import json from multiprocessing import Process, Lock +from pathlib import Path from scapy.all import * from scapy.layers.dns import DNS, DNSQR from scapy.layers.inet import IP, UDP @@ -71,6 +72,7 @@ def __init__(self, except FileNotFoundError: self.responder_alerts = {} self.vulnerable_alerts = {} + Path("state").mkdir(parents=True, exist_ok=True) with open("state/state.json", "w") as state_file: json.dump({"responder_alerts": {}, "vulnerable_alerts": {}}, state_file) # get broadcast IP for Netbios From 47f5667ecfba10b5573af74bcea6b822be5349b4 Mon Sep 17 00:00:00 2001 From: "C.J. May" Date: Wed, 17 Jul 2024 16:11:46 +0000 Subject: [PATCH 4/6] serialize timestamps in state file --- respotter.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/respotter.py b/respotter.py index a9b9a5c..7db7bf9 100644 --- a/respotter.py +++ b/respotter.py @@ -67,6 +67,11 @@ def __init__(self, previous_state = json.load(state_file) self.responder_alerts = previous_state["responder_alerts"] self.vulnerable_alerts = previous_state["vulnerable_alerts"] + for ip in self.responder_alerts: + self.responder_alerts[ip] = datetime.fromisoformat(self.responder_alerts[ip]) + for ip in self.vulnerable_alerts: + for protocol in self.vulnerable_alerts[ip]: + self.vulnerable_alerts[ip][protocol] = datetime.fromisoformat(self.vulnerable_alerts[ip][protocol]) except json.JSONDecodeError: raise FileNotFoundError except FileNotFoundError: @@ -110,7 +115,10 @@ def webhook_responder_alert(self, responder_ip): with self.state_lock: with open("state/state.json", "r+") as state_file: state = json.load(state_file) - state["responder_alerts"] = self.responder_alerts + new_state = self.responder_alerts + for ip in new_state: + new_state[ip] = new_state[ip].isoformat() + state["responder_alerts"] = new_state state_file.seek(0) json.dump(state, state_file) @@ -134,7 +142,11 @@ def webhook_sniffer_alert(self, protocol, requester_ip, requested_hostname): with self.state_lock: with open("state/state.json", "r+") as state_file: state = json.load(state_file) - state["vulnerable_alerts"] = self.vulnerable_alerts + new_state = self.vulnerable_alerts + for ip in new_state: + for protocol in new_state[ip]: + new_state[ip][protocol] = new_state[ip][protocol].isoformat() + state["vulnerable_alerts"] = new_state state_file.seek(0) json.dump(state, state_file) From 2e6794034d6c118e00246afd2f2ab3d7dee65d67 Mon Sep 17 00:00:00 2001 From: "C.J. May" Date: Wed, 17 Jul 2024 16:20:24 +0000 Subject: [PATCH 5/6] copy dict rather than reference --- respotter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/respotter.py b/respotter.py index 7db7bf9..85885a1 100644 --- a/respotter.py +++ b/respotter.py @@ -115,7 +115,7 @@ def webhook_responder_alert(self, responder_ip): with self.state_lock: with open("state/state.json", "r+") as state_file: state = json.load(state_file) - new_state = self.responder_alerts + new_state = self.responder_alerts.copy() for ip in new_state: new_state[ip] = new_state[ip].isoformat() state["responder_alerts"] = new_state @@ -142,7 +142,7 @@ def webhook_sniffer_alert(self, protocol, requester_ip, requested_hostname): with self.state_lock: with open("state/state.json", "r+") as state_file: state = json.load(state_file) - new_state = self.vulnerable_alerts + new_state = self.vulnerable_alerts.copy() for ip in new_state: for protocol in new_state[ip]: new_state[ip][protocol] = new_state[ip][protocol].isoformat() From 98beba6414e084ab87312a07532c1a21ca103d57 Mon Sep 17 00:00:00 2001 From: "C.J. May" Date: Wed, 17 Jul 2024 16:37:15 +0000 Subject: [PATCH 6/6] deep copy dictionary --- respotter.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/respotter.py b/respotter.py index 85885a1..7f5443b 100644 --- a/respotter.py +++ b/respotter.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import argparse +from copy import deepcopy from datetime import datetime, timedelta from ipaddress import ip_network import json @@ -115,7 +116,7 @@ def webhook_responder_alert(self, responder_ip): with self.state_lock: with open("state/state.json", "r+") as state_file: state = json.load(state_file) - new_state = self.responder_alerts.copy() + new_state = deepcopy(self.responder_alerts) for ip in new_state: new_state[ip] = new_state[ip].isoformat() state["responder_alerts"] = new_state @@ -142,7 +143,7 @@ def webhook_sniffer_alert(self, protocol, requester_ip, requested_hostname): with self.state_lock: with open("state/state.json", "r+") as state_file: state = json.load(state_file) - new_state = self.vulnerable_alerts.copy() + new_state = deepcopy(self.vulnerable_alerts) for ip in new_state: for protocol in new_state[ip]: new_state[ip][protocol] = new_state[ip][protocol].isoformat()