4
4
from datetime import datetime , timedelta
5
5
from ipaddress import ip_network
6
6
import json
7
- from multiprocessing import Process
7
+ from multiprocessing import Process , Lock
8
+ from pathlib import Path
8
9
from scapy .all import *
9
10
from scapy .layers .dns import DNS , DNSQR
10
11
from scapy .layers .inet import IP , UDP
@@ -39,6 +40,7 @@ def __init__(self,
39
40
teams_webhook = "" ,
40
41
syslog_address = "" ,
41
42
):
43
+ # initialize logger
42
44
self .log = logging .getLogger ('respotter' )
43
45
formatter = logging .Formatter ('' )
44
46
handler = logging .StreamHandler ()
@@ -50,15 +52,35 @@ def __init__(self,
50
52
formatter = logging .Formatter ('Respotter {processName}[{process}]: {message}' , style = '{' )
51
53
handler .setFormatter (formatter )
52
54
self .log .addHandler (handler )
53
- conf . checkIPaddr = False # multicast/broadcast responses won't come from dst IP
55
+ # import configuration
54
56
self .delay = delay
55
57
self .excluded_protocols = excluded_protocols
56
58
self .hostname = hostname
57
59
self .is_daemon = False
58
60
self .timeout = timeout
59
61
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
62
84
if subnet :
63
85
try :
64
86
network = ip_network (subnet )
@@ -68,6 +90,7 @@ def __init__(self,
68
90
elif "nbns" not in self .excluded_protocols :
69
91
self .log .error (f"[!] ERROR: subnet CIDR not configured. Netbios protocol will be disabled." )
70
92
self .excluded_protocols .append ("nbns" )
93
+ # setup webhooks
71
94
self .webhooks = {}
72
95
for service in ["teams" , "slack" , "discord" ]:
73
96
webhook = eval (f"{ service } _webhook" )
@@ -89,6 +112,15 @@ def webhook_responder_alert(self, responder_ip):
89
112
send_discord_message (self .webhooks ["discord" ], title = title , details = details )
90
113
self .log .info (f"[+] Alert sent to Discord for { responder_ip } " )
91
114
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 )
92
124
93
125
def webhook_sniffer_alert (self , protocol , requester_ip , requested_hostname ):
94
126
if requester_ip in self .vulnerable_alerts :
@@ -107,7 +139,16 @@ def webhook_sniffer_alert(self, protocol, requester_ip, requested_hostname):
107
139
self .vulnerable_alerts [requester_ip ][protocol ] = datetime .now ()
108
140
else :
109
141
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 )
111
152
112
153
def send_llmnr_request (self ):
113
154
# LLMNR uses the multicast IP 224.0.0.252 and UDP port 5355
@@ -180,6 +221,8 @@ def daemon(self):
180
221
181
222
def responder_scan (self ):
182
223
self .log .info ("[*] Responder scans started" )
224
+ # Scapy setting -- multicast/broadcast responses won't come from dst IP
225
+ conf .checkIPaddr = False
183
226
while True :
184
227
if "llmnr" not in self .excluded_protocols :
185
228
self .send_llmnr_request ()
0 commit comments