-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathsipcheck.py
executable file
·443 lines (366 loc) · 16.6 KB
/
sipcheck.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
'''
sipCheck v.3.0
---------------------------
This application connects into the Asterisk Manager Interface (v11 or newer) and reads the Security Events received when some user try send
INVITE or REGISTER and when it happens SIPCheck classify the IP address into 'friends' or, if the user continues sending wrong passwords,
into 'attackers'.
SIPCheck counts the number of failed tries per IP Address so, if this address is not into a white list, and the tries are exceeded the limits
automatically will be baned during some time.
The new purpous of this application is ban attackers and automatically clean the IP past some time to avoid the firewall be bigger and bigger
If some IP address auth correctly, it is consider a "friend who trust" and it will be classified into white list to avoid be banned although
some SIP client send wrong passwords. (maybe this SIP users belongs a company with some phones and one of them are badly configured).
You can find more information at:
https://github.com/sinologicnet/sipcheck
If you have questions or suggestions, you can use this page:
https://github.com/sinologicnet/sipcheck/issues
Authors:
Elio Rojano, Sergio Cotelo, Javier Vidal, Tomás Sahagún
Thanks to:
Jose Luis Verdeguer (testing)
Aitor Martin (fix and corrections)
Email:
sipcheck@sinologic.net
'''
import time
import io
import os
import re
import sys
import socket
import logging
import asyncio
import configparser
from pygtail import Pygtail
from panoramisk import Manager
from threading import Thread
from threading import RLock
lock = RLock()
# We parser the config file
config = configparser.ConfigParser()
dir = os.path.dirname(os.path.abspath(__file__))
config.read(dir+'/sipcheck.conf')
if ('manager' in config):
managerHost = config['manager']['host']
managerPort = int(config['manager']['port'])
managerUser = config['manager']['username']
managerPass = config['manager']['password']
else:
managerHost = "127.0.0.1"
managerPort = 5038
managerUser = "manageruser"
managerPass = "SuPeR@p4ssw0rd123"
if ('log' in config):
logLevel = config['log']['level']
logFile = config['log']['file']
else:
logLevel = "DEBUG"
logFile = "/var/log/sipcheck.log"
if ('attacker' in config):
maxNumTries = int(config['attacker']['maxNumTries'])
maxNumInvitesWithoutAuth = int(config['attacker']['maxNumInvites'])
BLExpireTime = int(config['attacker']['BLExpireTime'])
WLExpireTime = int(config['attacker']['WLExpireTime'])
TLExpireTime = int(config['attacker']['TLExpireTime'])
iptablesChain = config['attacker']['iptablesChain']
else:
maxNumTries = 5
BLExpireTime = 86400
WLExpireTime = 21600
TLExpireTime = 3600
iptablesChain = "INPUT"
# We connect into a Asterisk Manager (Asterisk 11 or newer with Security permissions to read)
manager = Manager(loop=asyncio.get_event_loop(), host=managerHost, port=managerPort, username=managerUser, secret=managerPass)
# Set the logging system to dump everything we do
logging.basicConfig(filename=logFile,level=logging.DEBUG,format='%(asctime)s %(levelname)s: %(message)s')
Log = logging.getLogger()
level = logging.getLevelName(logLevel)
Log.setLevel(level)
logging.debug("Configured Blacklist expire time: "+str(BLExpireTime))
logging.debug("Configured Whitelist expire time: "+str(WLExpireTime))
logging.debug("Configured Templist expire time: "+str(TLExpireTime))
# We set the lists where we storage the addresses.
templist={} # Suspected addresses
whitelist={} # Trusted addresses
blacklist={} # Attackers addresses
invitelist={} # Invite control
## Function that counts the tries and insert the address into the suspected list.
def templist_counter(ip,score=1.0):
if (ip not in whitelist) and (ip not in blacklist):
if (ip in templist):
templist[ip]['intentos']=templist[ip]['intentos']+score
else:
templist[ip]={'intentos':score,'time':int(time.time())}
output=templist[ip]['intentos']
elif (ip in whitelist):
if (score < 1):
logging.warning("Detected INVITE from "+ip+" but this address is whitelisted.")
else:
logging.warning("Detected wrong password for "+ip+" but this address is whitelisted.")
output=0
else: # It shouldn't happen
logging.warning("Detected suspected behaviour for "+ip+" but this address is blacklisted.")
output=0
return output
## Function that counts the tries and insert the address into the suspected list.
def invitelist_counter(ip,score=1.0):
if (ip not in whitelist) and (ip not in blacklist):
if (ip in invitelist):
invitelist[ip]['veces']=invitelist[ip]['veces']+score
else:
invitelist[ip]={'veces':score,'time':int(time.time())}
output=invitelist[ip]['veces']
else: # It shouldn't happen
output=0
return output
## Function that insert the rule to drop everything from the ip into the iptables
def ban(ip):
if (not isbanned(ip)):
logging.info("Banned IP: "+ip)
os.popen("iptables -A "+iptablesChain+" -s "+ip+" -j DROP")
## Function that delete the rule to drop everything from the ip into the iptables
def unban(ip):
if (isbanned(ip)):
logging.info("Unbaned IP: "+ip)
os.popen("iptables -D "+iptablesChain+" -s "+ip+" -j DROP")
## Returns if an IP Address is the iptables list
def isbanned(ip):
salidaIptables=os.popen("iptables -L "+iptablesChain+" -n").read().replace(" ","#").replace("\n","")
out=False
if ("#"+ip+"#" in salidaIptables): # we uses '#' to sure that we don't confuse with pieces of others ip addresses
out=True
return out
def create_blackfile():
f = open("/tmp/blacklist.dat", "w")
f.write("# This file is generated automatically by SIPCheck 3, so please, dont modify it\n\n")
for t in blacklist:
f.write(t+","+str(blacklist[t])+"\n")
f.close()
## Function that add an IP address into a whitelist (and remove from list of suspected)
def insert_to_whitelist(ip,hastacuando=time.time()):
if ip not in [y for x in whitelist for y in x.split()]:
whitelist[ip]=int(hastacuando) # Insert into the whitelist
if (ip in templist): # Extract from suspected list (to save memory)
templist.pop(ip, None)
if (ip in invitelist):
invitelist.pop(ip, None)
# note: I know that we should haven't this ip address into the blacklist, but if it happen, we remove too. ;)
if (ip in blacklist):
blacklist.pop(ip, None)
## Function that add an IP address into a blacklist (and remove from list of suspected)
def insert_to_blacklist(ip,cuando=time.time()):
if ip not in [y for x in blacklist for y in x.split()]:
logging.info("BL: Detected attack from IP: '"+ip+"' (Banning address)")
blacklist[ip]=int(cuando); # Insert the address and the time into the blacklist
ban(ip)
create_blackfile()
if (ip in templist): # Remove from suspected list (to save memory)
templist.pop(ip, None)
if (ip in invitelist):
invitelist.pop(ip, None)
## Function that is executed when an 'invalidPassword' is received
def invalidPassword(evento):
logging.warning("Received wrong password for user "+evento['AccountID']+" from IP "+evento["RemoteAddress"])
# We check if the IP address is in the whitelist
# If it isn't into the whitelist, we increment the counter until the number of tries will be greater that the 'maxNumTries' constant
num = templist_counter(evento['RemoteAddress'],1.0)
if (num > maxNumTries):
insert_to_blacklist(evento['RemoteAddress'])
def inviteWithoutAuth(evento):
logging.warning("Received anonymous INVITE from IP "+evento["RemoteAddress"])
# We check if the IP address is in the whitelist
# If it isn't into the whitelist, we increment the counter until the number of tries will be greater that the 'maxNumTries' constant
num = templist_counter(evento['RemoteAddress'],1.0)
if (num > maxNumTries):
insert_to_blacklist(evento['RemoteAddress'])
## Function that is executed when a 'ChallengeSent' is received
def inviteSend(evento):
logging.debug("Received invite user "+evento['AccountID']+" from IP "+evento["RemoteAddress"])
# We check if the IP address is in the whitelist
# If it isn't into the whitelist, we increment the counter until the number of tries will be greater that the 'maxNumTries' constant
num = invitelist_counter(evento['RemoteAddress'],1.0)
if (num > maxNumInvitesWithoutAuth):
insert_to_blacklist(evento['RemoteAddress'])
## Function that is executed when a 'successfulAuth' is received
def successfulAuth(evento):
logging.debug("Received right password for user "+evento['AccountID']+" from IP "+evento["RemoteAddress"])
# We insert the IP address into the whitelist
insert_to_whitelist(evento['RemoteAddress'])
## Function to filter the string with IPv4 IP address from the manager object and returns just the IP address.
def getIP(stringip):
## We get the string "IPV4/UDP/X.X.X.X/5062" and we need only "X.X.X.X"
paramsIP=stringip.strip().split("/")
salida=""
if (len(paramsIP) > 3):
salida=paramsIP[2]
return salida
## Returns if a string is a valid IP address
def isValidIP(address):
try:
socket.inet_pton(socket.AF_INET, address)
except AttributeError:
try:
socket.inet_aton(address)
except socket.error:
return False
return address.count('.') == 3
except socket.error:
return False
return True
## Function that go through the lists checking the time when the addresses was inserted and if this time is greater than the Expiretime configured, it removes the addresses of these lists.
def expireRecord():
now=int(time.time())
# We search the elements with the time expired.
listaABorrar=[]
for t in blacklist:
if (now - blacklist[t] > BLExpireTime):
logging.info("BL: Expired time for "+t)
listaABorrar.append(t)
# ... and we remove the elements found
for t1 in listaABorrar:
blacklist.pop(t1, None)
unban(t1) # Lo extraemos del firewall
create_blackfile() # We update the blacklist file with actual records
# We search the elements with the time expired.
listaABorrar=[]
for t in whitelist:
if (now - whitelist[t] > WLExpireTime):
logging.info("WL: Expired time for "+t)
listaABorrar.append(t)
# ... and we remove the elements found
for t1 in listaABorrar:
whitelist.pop(t1, None)
# We search the elements with the time expired.
listaABorrar=[]
for t in templist:
if (now - templist[t]['time'] > TLExpireTime):
logging.info("TL: Expired time for "+t)
listaABorrar.append(t)
# ... and we remove the elements found
for t1 in listaABorrar:
templist.pop(t1, None)
# Uncomment this block to see updately the content of the lists (only for development)
if (logLevel == "DEBUG"):
print("-----------| "+time.strftime('%y-%m-%d %T')+" |---------------")
print("blacklist "+str(blacklist))
print("whitelist "+str(whitelist))
print("templist "+str(templist))
print("invitelist "+str(invitelist))
## Function that execute "expireRecord" function each 5 seconds
def expire():
while True:
#logging.debug("Executing expire process...")
expireRecord() # We process the lists to remove the expired records
time.sleep(5)
## It register the manager event that warning when the user send a right authentication
@manager.register_event('SuccessfulAuth')
def callback(manager, message):
message['RemoteAddress']=getIP(message.RemoteAddress.replace('"',''))
if (message['RemoteAddress'] != "127.0.0.1"):
logging.debug(message)
successfulAuth(message)
## It register the manager event that warning when the user send a wrong authentication
@manager.register_event('InvalidPassword')
def callback(manager, message):
message['RemoteAddress']=getIP(message.RemoteAddress.replace('"',''))
logging.debug(message)
invalidPassword(message)
'''
## It register the manager event that warning when the user send a wrong authentication
@manager.register_event('ChallengeSent')
def callback(manager, message):
message['RemoteAddress']=getIP(message.RemoteAddress.replace('"',''))
logging.debug(message)
inviteSend(message)
'''
## Function that insert the addresses located in whitelist.txt file, into the whitelist without expiretime.
def load_whitelist_file():
dir = os.path.dirname(os.path.abspath(__file__))
wlfile = dir+"/whitelist.txt"
logging.debug("Reading "+wlfile+" to insert IP address into Whitelist table...")
if (os.path.exists(wlfile)):
with io.open(wlfile) as fp:
line = fp.readline()
cnt = 1
while line:
content = line.strip()
if (content != "") and (content[0] != "#") and (isValidIP(content)):
logging.info("+ Added "+content+" into whitelist during one year")
insert_to_whitelist(content,time.time()+(60*60*24*365))
unban(content) # If this address has been banned sometime, we try to remove this ban
line = fp.readline()
cnt += 1
## Function that insert the addresses located in blacklist.txt file, into the blacklist (and ban them again if they wasn't on the iptables).
# If the time when they was banned is greater than BLExpireTime, the thread of ExpireTime will remove this address again.
def load_blacklist_file():
blfile="/tmp/blacklist.dat"
logging.debug("Reading "+blfile+" to insert IP address into Blacklist table...")
if (os.path.exists(blfile)):
with io.open(blfile) as fp:
line = fp.readline()
cnt = 1
while line:
content = line.strip()
if (content != "") and (content[0] != "#"):
registro = content.split(",")
if (len(registro) == 2) and (isValidIP(registro[0])):
if (content not in whitelist):
logging.info("+ Added "+content+" into blacklist again from the time: "+str(registro[1]))
insert_to_blacklist(registro[0],registro[1])
line = fp.readline()
cnt += 1
## System to parse the Asterisk message file searching INVITES without Authentication or trials of calls annoying.
def tailLogFile(filename):
while True:
for line in Pygtail(filename, offset_file="/tmp/sipcheck.tmp", read_from_end=True):
yield line
time.sleep(1.0)
def parseLog():
if (config['log']['asterisklog']):
filename=config['log']['asterisklog']
else:
filename="/var/log/asterisk/messages"
if (config['log']['level'] == "DEBUG"):
logging.info("Checking %s ..." % filename)
if (os.path.isfile(filename)):
generator = tailLogFile(filename)
for line in generator:
analizeLog(line)
else:
logging.error("!! Asterisk logfile %s not found. Please, check this file in sipcheck.conf" % filename)
def analizeLog(line):
# Maybe there are some future patters to detect others attacks
termsToSearch=[
"rejected because extension not found in context 'public'"
]
for term in termsToSearch:
if (term in line):
detectedAttack=True
try:
IP = re.search(r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', line).group()
except:
IP = re.search(r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', line)
if (IP):
if (logLevel == "DEBUG"):
print("Detected anonymous attack from %s" % str(IP))
event={'RemoteAddress':str(IP)}
inviteWithoutAuth(event)
## Main function
def main():
logging.info('-----------------------------------------------------')
logging.info('Starting SIPCheck 3 ...')
load_blacklist_file()
load_whitelist_file()
manager.connect()
try:
# We create an asyncronous thread that check the expiretime of the lists
Thread(name='expireRecord', target = expire).start()
# Thread for parsing Asterisk message log file
Thread(name='parseLog', target = parseLog).start()
# Run the manager loop
manager.loop.run_forever()
except KeyboardInterrupt:
manager.loop.close()
if __name__ == '__main__':
main()