Skip to content

Commit b80b838

Browse files
authored
Merge pull request #49 from lawndoc/save-state #minor
Save state
2 parents 01b9379 + 2e67940 commit b80b838

File tree

2 files changed

+49
-5
lines changed

2 files changed

+49
-5
lines changed

Diff for: .gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Project specific
22
config.json
33
config/
4+
state/
45

56
# Redis
67
dump.rdp

Diff for: respotter.py

+48-5
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
from datetime import datetime, timedelta
55
from ipaddress import ip_network
66
import json
7-
from multiprocessing import Process
7+
from multiprocessing import Process, Lock
8+
from pathlib import Path
89
from scapy.all import *
910
from scapy.layers.dns import DNS, DNSQR
1011
from scapy.layers.inet import IP, UDP
@@ -39,6 +40,7 @@ def __init__(self,
3940
teams_webhook="",
4041
syslog_address="",
4142
):
43+
# initialize logger
4244
self.log = logging.getLogger('respotter')
4345
formatter = logging.Formatter('')
4446
handler = logging.StreamHandler()
@@ -50,15 +52,35 @@ def __init__(self,
5052
formatter = logging.Formatter('Respotter {processName}[{process}]: {message}', style='{')
5153
handler.setFormatter(formatter)
5254
self.log.addHandler(handler)
53-
conf.checkIPaddr = False # multicast/broadcast responses won't come from dst IP
55+
# import configuration
5456
self.delay = delay
5557
self.excluded_protocols = excluded_protocols
5658
self.hostname = hostname
5759
self.is_daemon = False
5860
self.timeout = timeout
5961
self.verbosity = verbosity
60-
self.responder_alerts = {}
61-
self.vulnerable_alerts = {}
62+
# state persistence
63+
self.state_lock = Lock()
64+
try:
65+
with open("state/state.json", "r+") as state_file:
66+
try:
67+
previous_state = json.load(state_file)
68+
self.responder_alerts = previous_state["responder_alerts"]
69+
self.vulnerable_alerts = previous_state["vulnerable_alerts"]
70+
for ip in self.responder_alerts:
71+
self.responder_alerts[ip] = datetime.fromisoformat(self.responder_alerts[ip])
72+
for ip in self.vulnerable_alerts:
73+
for protocol in self.vulnerable_alerts[ip]:
74+
self.vulnerable_alerts[ip][protocol] = datetime.fromisoformat(self.vulnerable_alerts[ip][protocol])
75+
except json.JSONDecodeError:
76+
raise FileNotFoundError
77+
except FileNotFoundError:
78+
self.responder_alerts = {}
79+
self.vulnerable_alerts = {}
80+
Path("state").mkdir(parents=True, exist_ok=True)
81+
with open("state/state.json", "w") as state_file:
82+
json.dump({"responder_alerts": {}, "vulnerable_alerts": {}}, state_file)
83+
# get broadcast IP for Netbios
6284
if subnet:
6385
try:
6486
network = ip_network(subnet)
@@ -68,6 +90,7 @@ def __init__(self,
6890
elif "nbns" not in self.excluded_protocols:
6991
self.log.error(f"[!] ERROR: subnet CIDR not configured. Netbios protocol will be disabled.")
7092
self.excluded_protocols.append("nbns")
93+
# setup webhooks
7194
self.webhooks = {}
7295
for service in ["teams", "slack", "discord"]:
7396
webhook = eval(f"{service}_webhook")
@@ -89,6 +112,15 @@ def webhook_responder_alert(self, responder_ip):
89112
send_discord_message(self.webhooks["discord"], title=title, details=details)
90113
self.log.info(f"[+] Alert sent to Discord for {responder_ip}")
91114
self.responder_alerts[responder_ip] = datetime.now()
115+
with self.state_lock:
116+
with open("state/state.json", "r+") as state_file:
117+
state = json.load(state_file)
118+
new_state = self.responder_alerts.copy()
119+
for ip in new_state:
120+
new_state[ip] = new_state[ip].isoformat()
121+
state["responder_alerts"] = new_state
122+
state_file.seek(0)
123+
json.dump(state, state_file)
92124

93125
def webhook_sniffer_alert(self, protocol, requester_ip, requested_hostname):
94126
if requester_ip in self.vulnerable_alerts:
@@ -107,7 +139,16 @@ def webhook_sniffer_alert(self, protocol, requester_ip, requested_hostname):
107139
self.vulnerable_alerts[requester_ip][protocol] = datetime.now()
108140
else:
109141
self.vulnerable_alerts[requester_ip] = {protocol: datetime.now()}
110-
142+
with self.state_lock:
143+
with open("state/state.json", "r+") as state_file:
144+
state = json.load(state_file)
145+
new_state = self.vulnerable_alerts.copy()
146+
for ip in new_state:
147+
for protocol in new_state[ip]:
148+
new_state[ip][protocol] = new_state[ip][protocol].isoformat()
149+
state["vulnerable_alerts"] = new_state
150+
state_file.seek(0)
151+
json.dump(state, state_file)
111152

112153
def send_llmnr_request(self):
113154
# LLMNR uses the multicast IP 224.0.0.252 and UDP port 5355
@@ -180,6 +221,8 @@ def daemon(self):
180221

181222
def responder_scan(self):
182223
self.log.info("[*] Responder scans started")
224+
# Scapy setting -- multicast/broadcast responses won't come from dst IP
225+
conf.checkIPaddr = False
183226
while True:
184227
if "llmnr" not in self.excluded_protocols:
185228
self.send_llmnr_request()

0 commit comments

Comments
 (0)