From 54613da01ddf0f14527822ced5b9bd750b825fb6 Mon Sep 17 00:00:00 2001 From: Thuban <thuban@yeuxdelibad.net> Date: Sat, 01 Dec 2018 09:17:24 +0000 Subject: [PATCH] exemples en + --- vilain.conf | 29 +++++ vilain.py | 64 +++++++----- Makefile | 12 + vilainreport.py | 131 ++++++++++++++++++++++++++ vilain | 2 LICENSE | 21 ++++ vilainreport | 11 ++ 7 files changed, 236 insertions(+), 34 deletions(-) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b663197 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Thuban + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile index e153e0b..92f620b 100644 --- a/Makefile +++ b/Makefile @@ -2,21 +2,25 @@ # See LICENSE file for copyright and license details. # # vilain version -VERSION = 0.3 +VERSION = 0.7 # Customize below to fit your system # paths PREFIX = /usr/local MANPREFIX = ${PREFIX}/man/man1/ -install: +install: @echo installing executable file to ${DESTDIR}${PREFIX}/bin @mkdir -p ${DESTDIR}${PREFIX}/bin @cp -f vilain ${DESTDIR}${PREFIX}/bin + @cp -f vilainreport ${DESTDIR}${PREFIX}/bin @echo installing script file to ${DESTDIR}${PREFIX}/sbin @cp -f vilain.py ${DESTDIR}${PREFIX}/sbin + @cp -f vilainreport.py ${DESTDIR}${PREFIX}/sbin @chmod 755 ${DESTDIR}${PREFIX}/bin/vilain + @chmod 755 ${DESTDIR}${PREFIX}/bin/vilainreport @chmod 644 ${DESTDIR}${PREFIX}/sbin/vilain.py + @chmod 644 ${DESTDIR}${PREFIX}/sbin/vilainreport.py @echo installing init script in /etc/rc.d @cp -f vilain.rc /etc/rc.d/vilain @chmod 755 /etc/rc.d/vilain @@ -29,8 +33,10 @@ uninstall: @echo removing executable file from ${DESTDIR}${PREFIX}/bin @rm -f ${DESTDIR}${PREFIX}/bin/vilain + @rm -f ${DESTDIR}${PREFIX}/bin/vilainreport @rm -f ${DESTDIR}${PREFIX}/sbin/vilain.py + @rm -f ${DESTDIR}${PREFIX}/sbin/vilainreport.py @echo removing manual page to ${DESTDIR}${MANPREFIX}/ @rm -f ${DESTDIR}${MANPREFIX}/vilain.1 -.PHONY: install uninstall +.PHONY: install uninstall diff --git a/vilain b/vilain index d385066..9c2841c 100755 --- a/vilain +++ b/vilain @@ -1,5 +1,5 @@ #!/bin/sh -# script to launch vilain with the latest python3 version avaiable +# script to launch vilain with the latest python3 version available PYTHONVERSION=$(ls -l /usr/local/bin/python3.* |grep -Eo "3\.[0-9]" |tail -n1) PYTHON="/usr/local/bin/python$PYTHONVERSION" diff --git a/vilain.conf b/vilain.conf index ecfec45..1e9a7e5 100644 --- a/vilain.conf +++ b/vilain.conf @@ -1,13 +1,16 @@ [DEFAULT] # 24h + 5min # Time to keep banned a bad ip -watch_while = 86700 +watch_while = 86700 # Max tries before being banned maxtries = 3 # pf table to keep bad IP. # remember to clean the table with this command in a cron job : # /sbin/pfctl -t vilain_bruteforce -T expire 86400 vilain_table = vilain_bruteforce + +# vilain log file +vilain_log = /var/log/daemon # duration before each checks on the different log files sleeptime = 3.0 @@ -30,6 +33,14 @@ [ssh2] logfile = /var/log/authlog regex = .* Connection closed by ([\S]+) .* + +[ssh3] +logfile = /var/log/authlog +regex = .* Invalid user \w+ ([\S]+) .* + +[ssh4] +logfile = /var/log/authlog +regex = .* Disconnected from authenticating user root ([\S]+) .* #[http404] #logfile = /var/www/logs/access.log @@ -56,3 +67,19 @@ logfile = /var/www/logs/access.log regex = (?:\S+\s){1}(\S+).*wp-login.php.* +# Nextcloud: login page +# Nextcloud 12 brings protection against brute-force attacks +# but 1/ not yet tested so far 2/ system protection is probably more efficient +[nextcloud] +logfile = /var/www/htdocs/datacloud/nextcloud.log +regex = .*Bruteforce attempt from \\"(.*)\\" detected + +# Nextcloud: public shares protected by password +# regex is compliant with NginX log format: +# /etc/nginx/nginx.conf: +# log_format main '$remote_addr - $remote_user [$time_local] "$request" ' +# '$status $body_bytes_sent "$http_referer" ' +# '"$http_user_agent" "$http_x_forwarded_for"'; +[nextcloud-share] +logfile = /var/www/logs/access-nextcloud.log +regex = (\d+\.\d+\.\d+\.\d+) \-.*POST /s/\w+/authenticate HTTP/1.1\" 200 diff --git a/vilain.py b/vilain.py index d36c59f..082b15f 100755 --- a/vilain.py +++ b/vilain.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 -# -*- coding:Utf-8 -*- +# -*- coding:Utf-8 -*- """ -Author : thuban <thuban@yeuxdelibad.net> +Author : thuban <thuban@yeuxdelibad.net> Vincent <vincent.delft@gmail.com> Licence : MIT Require : python >= 3.5 @@ -11,11 +11,11 @@ Description : Mimic fail2ban with pf for OpenBSD. Inspired from http://www.vincentdelft.be/post/post_20161106 - In pf.conf, add : + In pf.conf, add : table <vilain_bruteforce> persist - block quick from <vilain_bruteforce> + block quick from <vilain_bruteforce> - To see banned IP : + To see banned IP : pfctl -t vilain_bruteforce -T show """ @@ -30,7 +30,7 @@ import time CONFIGFILE = "/etc/vilain.conf" -VERSION = "0.5" +VERSION = "0.7" vilain_table = "vilain_bruteforce" LOGFILE = "/var/log/daemon" @@ -38,15 +38,19 @@ print("Only root can use this tool") sys.exit(1) -# Configure logging -log_handler = logging.handlers.WatchedFileHandler(LOGFILE) -formatter = logging.Formatter( - '%(asctime)s %(module)s:%(funcName)s:%(message)s', - '%b %d %H:%M:%S') -log_handler.setFormatter(formatter) +# declare logger logger = logging.getLogger(__name__) -logger.addHandler(log_handler) -logger.setLevel(logging.INFO) + +def configure_logging(): + print('Log file : {}'.format(LOGFILE)) + log_handler = logging.handlers.WatchedFileHandler(LOGFILE) + formatter = logging.Formatter( + '%(asctime)s %(module)s:%(funcName)s:%(message)s', + '%Y-%m-%d %H:%M:%S') + log_handler.setFormatter(formatter) + logger.addHandler(log_handler) + logger.setLevel(logging.INFO) + # functions def readconfig(): @@ -57,11 +61,9 @@ config = configparser.ConfigParser() config.read(CONFIGFILE) - return(config) + return (config, config.defaults()) -def load_config(): - c = readconfig() - d = c.defaults() +def load_config(c, d): watch_while = int(d['watch_while']) VILAIN_TABLE = d['vilain_table'] default_maxtries = int(d['maxtries']) @@ -72,8 +74,7 @@ ignoreips = [ i[1] for i in c.items('ignoreip') if i[0] not in c.defaults()] return(watch_while, default_maxtries, vilain_table, ignoreips, sleeptime) -def load_sections(): - c = readconfig() +def load_sections(c): for s in c.sections(): if c.has_option(s,'logfile'): LOGFILE = c.get(s,'logfile') @@ -87,15 +88,15 @@ yield d class Vilain(): - def __init__(self): + def __init__(self, config, config_dict): logger.info('Start vilain version {}'.format(VERSION)) self.loop = asyncio.get_event_loop() - self.watch_while, self.default_maxtries, self.vilain_table, self.ignore_ips, self.sleeptime = load_config() + self.watch_while, self.default_maxtries, self.vilain_table, self.ignore_ips, self.sleeptime = load_config(config, config_dict) self.ip_seen_at = {} self.load_bad_ips() self.bad_ip_queue = asyncio.Queue(loop=self.loop) - for entry in load_sections(): + for entry in load_sections(config): logger.info("Start vilain for {}".format(entry)) asyncio.ensure_future(self.check_logs(entry['logfile'], entry['maxtries'], entry['regex'], entry['name'])) @@ -111,7 +112,7 @@ for res in ret.split(): ip = res.strip().decode('utf-8') logger.info('Add existing banned IPs in your pf table: {}'.format(ip)) - #we assign the counter to 1, but for sure we don't know the real value + #we assign the counter to 1, but for sure we don't know the real value self.ip_seen_at[ip]={'time':time.time(),'count':1} @@ -181,7 +182,7 @@ logger.info("{} detected, reason {}, count: {}, maxtries: {}".format(ip, reason, n_ip, maxtries)) if n_ip >= maxtries: ret = subprocess.call(["pfctl", "-t", self.vilain_table, "-T", "add", ip]) - logger.info("Blacklisting {}, return code:{}".format(ip, ret)) + logger.info("Blacklisting {}, reason {}, return code:{}".format(ip, reason, ret)) #for debugging, this line allow us to see if the script run until here logger.debug('ban_ips end:{}'.format(self.ip_seen_at)) @@ -206,9 +207,9 @@ -def main(): +def main(config, config_dict): os.chdir(os.path.dirname(os.path.abspath(__file__))) - v = Vilain() + v = Vilain(config, config_dict) v.start() return 0 @@ -229,11 +230,16 @@ if args.version: print("Version: ", VERSION) sys.exit(0) - main() + # read config + config, config_dict = readconfig() + logfile = config_dict.get('vilain_log', None) + if logfile: + LOGFILE = logfile + configure_logging() + main(config, config_dict) # vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 # vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 - diff --git a/vilainreport b/vilainreport new file mode 100644 index 0000000..7d6e7a8 --- /dev/null +++ b/vilainreport @@ -0,0 +1,11 @@ +#!/bin/sh +# script to launch vilainreport with the latest python3 version available + +PYTHONVERSION=$(ls -l /usr/local/bin/python3.* |grep -Eo "3\.[0-9]" |tail -n1) +PYTHON="/usr/local/bin/python$PYTHONVERSION" +if [ -x $PYTHON ]; then + $PYTHON /usr/local/sbin/vilainreport.py +else + echo "Error : no python3 executable found" +fi +exit diff --git a/vilainreport.py b/vilainreport.py new file mode 100644 index 0000000..11a974f --- /dev/null +++ b/vilainreport.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +# -*- coding:Utf-8 -*- +""" +Author : Yax https://blogduyax.madyanne.fr/ +""" + +import re +import sys +import socket + +pattern = '(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+).*Blacklisting (\d+\.\d+\.\d+\.\d+), reason (.*), return' +regex = re.compile(pattern) + + +class CounterDict: + + def __init__(self): + self._counters = dict() + + def inc(self, k): + v = self._counters.get(k, 0) + 1 + self._counters[k] = v + + def get(self, k): + return self._counters.get(k, 0) + + def keys(self): + return self._counters.keys() + + def reset(self): + self._counters = dict() + + def topitems(self): + return sorted(self._counters.items(), key=lambda x: x[1], reverse=True) + + +class Value: + + def __init__(self): + self._value = "" + + def __str__(self): + return self._value + + def __eq__(self, other): + return str(self._value) == str(other) + + def set(self, value): + self._value = value + + +last_day = Value() + +# daily counters: key is reason +dcounters = CounterDict() + +# global counters: key is reason +gcounters = CounterDict() + +# hourly counters: key is hour +hcounters = CounterDict() + +# top counters: key is IP +tcounters = CounterDict() + + +def plural(noun, count): + if count > 1: + return noun + "s" + else: + return noun + + +def process(m): + current_day = m.group(1) + "-" + m.group(2) + "-" + m.group(3) + current_hour = m.group(4) + full_time = m.group(4) + ":" + m.group(5) + ":" + m.group(6) + ip = m.group(7) + reason = m.group(8) + + # new day + #print("({})-({}) => {}".format(last_day, current_day, last_day == current_day)) + if last_day != current_day: + # display day counters + sys.stdout.write("\n") + for reason in dcounters.keys(): + count = dcounters.get(reason) + sys.stdout.write("Probe '{}': {} {}\n".format(reason, count, plural("attack", count))) + last_day.set(current_day) + dcounters.reset() + sys.stdout.write("\n### Date {}\n".format(current_day)) + + # output current line + sys.stdout.write("{} blacklist IP {} ({})\n".format(full_time, ip, reason)) + + # increment counters + dcounters.inc(reason) + gcounters.inc(reason) + hcounters.inc(current_hour) + tcounters.inc(ip) + + +# parse stdin +for line in sys.stdin: + match = regex.match(line) + if match: + process(match) + +# output counters +sys.stdout.write("\n") +for reason in dcounters.keys(): + sys.stdout.write("Probe '{}' : {} attacks\n".format(reason, dcounters.get(reason))) + +sys.stdout.write("\n### Attacks per probe\n") +for k in gcounters.keys(): + count = gcounters.get(k) + sys.stdout.write("Probe '{}': {} {} \n".format(k, count, plural("attack", count))) + +sys.stdout.write("\n### Hourly repartition\n") +for k in sorted(hcounters.keys()): + sys.stdout.write("Hour {} - {:02d}: {}\n".format(k, int(k) + 1, hcounters.get(k))) + +sys.stdout.write("\n### Top attackers\n") +for k, v in tcounters.topitems(): + if v < 2: + break + try: + ns = socket.gethostbyaddr(k.strip())[0] + except: + ns = '?' + sys.stdout.write("IP {:16}: {} - {}\n".format(k, v, ns)) -- Gitblit v1.9.3