Fail2Ban + MikroTikを使用した攻撃者の永続的な禁止

数日前、アスタリスクをインストールし、コールルーティングを使用して古い構成をダウンロードし、ローカルSIPプロバイダーに接続することを意図しました。 Asteriskを起動してからわずか数分後に、ログでサーバー上の認証の試みを見つけました。 このような画像は、インターネット上にあるすべてのアスタリスクで見られます。 私の最愛のMikrotikと同じくらい愛されているpythonでプレイし、これらの攻撃者をどうするかを考え出すのは強い意志でした。



だから私たちは持っています:





いくつかの記事( 1、2 )を読んだ後、次の概念が生まれました。

  1. Fail2Banを使用して攻撃者を一定期間禁止し、MySQLデータベースに自分のIPアドレスのレコードを追加します
  2. 一定数の禁止が発行された後、IPアドレスをルーターの禁止リストに追加します




そして今、ソリューションの実装に。

1.次の情報を含むデータベース/テーブルを作成します-IPアドレス、国コード、国名、発行禁止の数、攻撃/サービスのタイプ(Fail2Ban構成からの刑務所名)、最後の試行、最初の試行(将来のために、おそらくどういうわけかこのデータを使用します)。



スキーム
CREATE DATABASE fail2ban CHARACTER SET utf8; CREATE TABLE `ban_history` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `ip_address` char(15) NOT NULL DEFAULT '', `country_code` varchar(5) DEFAULT NULL, `country_name` varchar(30) DEFAULT NULL, `count` int(11) NOT NULL, `type` varchar(30) DEFAULT NULL, `last_attempt` datetime NOT NULL, `first_attempt` datetime NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
      
      









2.データベースにレコードを追加するスクリプトを作成します。 このスクリプトはpythonで記述されており、次の追加モジュールが必要です-pygeoipおよびMySQL-python 。 両方のモジュールは、pipパッケージマネージャーを使用して簡単にインストールできます。



 pip install pygeoip MySQL-python
      
      





スクリプト
 #!/usr/bin/env python2 # -*- coding: utf-8 -*- import os import urllib import gzip import StringIO import logging import logging.handlers import MySQLdb import MySQLdb.cursors import ConfigParser import pygeoip from datetime import datetime from sys import exit from optparse import OptionParser def main(config, logger, ip_addr, attack_type, GEOIP_DAT): url = urllib.urlopen('http://geolite.maxmind.com/download/geoip/database/GeoLiteCountry/GeoIP.dat.gz') url_f = StringIO.StringIO(url.read()) handle = gzip.GzipFile(fileobj=url_f) with open(GEOIP_DAT, 'w') as out: for line in handle: out.write(line) if config.has_option('general', 'mysql_ip') and config.has_option('general', 'mysql_user') and config.has_option('general', 'mysql_password') and config.has_option('general', 'mysql_db'): try: logger.info("Connecting to MySQL host: %s" % config.get('general', 'mysql_ip')) db = MySQLdb.connect( host=config.get('general', 'mysql_ip'), user=config.get('general', 'mysql_user'), passwd=config.get('general', 'mysql_password'), db=config.get('general', 'mysql_db'), cursorclass=MySQLdb.cursors.DictCursor ) cursor = db.cursor() logger.debug("Connected") except MySQLdb.Error, e: logger.error("Error %d: %s" % (e.args[0], e.args[1])) exit(2) else: query = """select * from ban_history where ip_address='%s' and type='%s'""" % (ip_addr, attack_type) result = run_query(cursor, query, logger) result = cursor.fetchall() now = datetime.now() gi = pygeoip.GeoIP(GEOIP_DAT, flags=pygeoip.const.MEMORY_CACHE) country_code = gi.country_code_by_addr(ip_addr) country_name = gi.country_name_by_addr(ip_addr) if len(result) > 0: logger.info("Updating blacklist DB record for IP-address %s" % ip_addr) result = result[0] count = result['count'] + 1 query = """update ban_history set count=%s, last_attempt='%s', country_code='%s', country_name='%s' where id=%s""" % (count, now, country_code, country_name, result['id']) result = run_query(cursor, query, logger) db.commit() else: logger.info("Adding IP-address %s into blacklist DB" % ip_addr) count = 1 query = """insert into ban_history (ip_address, country_code, country_name, count, type, last_attempt, first_attempt) values('%s', '%s', '%s', %s, '%s', '%s', '%s')""" % (ip_addr, country_code, country_name, count, attack_type, now, now) result = run_query(cursor, query, logger) db.commit() else: logger.error("Configuration incomplete") exit(3) def run_query(cursor, query, logger): try: logger.debug("Running query \'%s\'" % query) cursor.execute(query) except MySQLdb.Error, e: logger.error("Error %d: %s" % (e.args[0], e.args[1])) exit(2) else: return True if __name__ == '__main__': try: ROOT_PATH = os.path.dirname(os.path.realpath(__file__)) GEOIP_DAT = os.path.join(ROOT_PATH, 'GeoIP.dat') parser = OptionParser(usage="usage: %prog [-c <configuration_file>] [-v] --ip IP-ADDRESS --type TYPE") parser.add_option("-v", "--verbose", action="store_true", default=False, dest="verbose", help="Verbose output") parser.add_option("-c", "--config", action="store", default=False, dest="cfg_file", help="Full path to configuration file") parser.add_option("--ip", action="store", default=False, dest="ip_addr", help="Attacker IP address") parser.add_option("--type", action="store", default=False, dest="attack_type", help="Type of attack (service)") (options, args) = parser.parse_args() verbose = options.verbose ip_addr = options.ip_addr attack_type = options.attack_type # Reading configuration file cfg_file = options.cfg_file if not cfg_file: cfg_file = os.path.join(ROOT_PATH, 'blacklist_db.cfg') config = ConfigParser.RawConfigParser() config.read(cfg_file) # Logging if config.get('general', 'log_file'): LOGFILE = config.get('general', 'log_file') else: LOGFILE = '/tmp/blacklist_db.log' FORMAT = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S') try: rotatetime = logging.handlers.TimedRotatingFileHandler(LOGFILE, when="midnight", interval=1, backupCount=14) except IOError, e: print "ERROR %s: Can not open log file - %s" % (e[0], e[1]) exit(1) except Exception, e: print "Can not configure logger - %s" % e exit(1) formatter = logging.Formatter('%(asctime)s: %(message)s','%y-%m-%d %H:%M:%S') rotatetime.setFormatter(FORMAT) logger = logging.getLogger('BLACKLIST-DB') logger.addHandler(rotatetime) if verbose: lvl = logging.DEBUG console = logging.StreamHandler() formatter = logging.Formatter('%(asctime)s: %(message)s','%y-%m-%d %H:%M:%S') console.setFormatter(formatter) logger.addHandler(console) else: lvl = logging.INFO logger.setLevel(lvl) if ip_addr and attack_type: main(config, logger, ip_addr, attack_type, GEOIP_DAT) else: logger.error("IP address and attack type are needed but not specified") exit(1) except (KeyboardInterrupt): logger.info("CTRL-C... exit") exit(0) except (SystemExit): logger.info("Exit") exit(0)
      
      









スクリプトは、構成ファイルからデータベースに接続するためのデータを取得します。デフォルトでは、同じディレクトリ内で検索を試みますが、「-c」キーを使用してパスを指定することもできます。



構成ファイルの例
[一般]

log_file = /var/log/blacklist_db.log

mysql_ip = localhost

mysql_user = db_user

mysql_password = db_pass

mysql_db = fail2ban



#禁止の数。その後、IPアドレスをブラックリストに追加します。デフォルトは10です

#ban_count = 10





重要な点は、iptablesにルールを追加するとともにスクリプトが実行されることです。そのため、次のファイルを編集しました。

/etc/fail2ban/action.d/iptables-allports.conf
 #   actionban = iptables -I fail2ban-<name> 1 -s <ip> -j <blocktype> #   actionban = iptables -I fail2ban-<name> 1 -s <ip> -j <blocktype> /// -v --ip <ip> --type <name>
      
      







/etc/fail2ban/action.d/iptables-multiport.conf
 #   actionban = iptables -I fail2ban-<name> 1 -s <ip> -j <blocktype> #   actionban = iptables -I fail2ban-<name> 1 -s <ip> -j <blocktype> /// -v --ip <ip> --type <name>
      
      







/etc/fail2ban/action.d/iptables-new.conf
(このアクションの用途がわからない、忠実度を変更する)

 #   actionban = iptables -I fail2ban-<name> 1 -s <ip> -j <blocktype> #   actionban = iptables -I fail2ban-<name> 1 -s <ip> -j <blocktype> /// -v --ip <ip> --type <name>
      
      







したがって、適切なルールをiptablesに追加した後、スクリプトが実行され、データベース内のデータが追加または更新されます。



3.ブラックリストを生成するスクリプトを作成します。これは、その後マイクロティックにインポートされます。 スクリプトは同じ構成ファイルを使用してデータベースへの接続に必要な設定を取得し、そのルートディレクトリで検索します。ここでも、「-c」キーを使用してパスを指定できます。 出力では、Mikrotikにインポートするためのスクリプト/アドレスリストが作成されます。これも同じディレクトリにあり、「-o」キーを使用して代替パスを指定できます。



スクリプト
 #!/usr/bin/env python2 # -*- coding: utf-8 -*- import os import logging import logging.handlers import MySQLdb import MySQLdb.cursors import ConfigParser from sys import exit from optparse import OptionParser def main(config, logger, output): if config.has_option('general', 'ban_count'): ban_count = config.getint('general', 'ban_count') else: ban_count = 10 if config.has_option('general', 'mysql_ip') and config.has_option('general', 'mysql_user') and config.has_option('general', 'mysql_password') and config.has_option('general', 'mysql_db'): try: logger.info("Connecting to MySQL host: %s" % config.get('general', 'mysql_ip')) db = MySQLdb.connect( host=config.get('general', 'mysql_ip'), user=config.get('general', 'mysql_user'), passwd=config.get('general', 'mysql_password'), db=config.get('general', 'mysql_db'), cursorclass=MySQLdb.cursors.DictCursor ) cursor = db.cursor() logger.debug("Connected") except MySQLdb.Error, e: logger.error("Error %d: %s" % (e.args[0], e.args[1])) exit(2) else: contents = ['/ip firewall address-list'] logger.info('Fetching adresses from the blacklist DB') query = """select * from ban_history""" result = run_query(cursor, query, logger) result = cursor.fetchall() for ip in result: if ip['count'] >= ban_count: list_name = '%s_BLC' % ip['type'].upper() logger.info('Adding IP %s into \'%s\' list' % (ip['ip_address'], list_name)) list_line = 'add address=%s list=%s comment=BLACKLIST' % (ip['ip_address'], list_name) contents.append(list_line) if len(contents) > 1: logger.info('Generating mikrotik rsc script...') script_file = open(output, 'w') for item in contents: script_file.write("%s\r\n" % item) script_file.close() logger.info('Done') else: logger.error("Configuration incomplete") exit(3) def run_query(cursor, query, logger): try: logger.debug("Running query \'%s\'" % query) cursor.execute(query) except MySQLdb.Error, e: logger.error("Error %d: %s" % (e.args[0], e.args[1])) exit(2) else: return True if __name__ == '__main__': try: ROOT_PATH = os.path.dirname(os.path.realpath(__file__)) parser = OptionParser(usage="usage: %prog [-c <configuration_file>] [-v] [-o <output_file_path>]") parser.add_option("-v", "--verbose", action="store_true", default=False, dest="verbose", help="Verbose output") parser.add_option("-c", "--config", action="store", default=False, dest="cfg_file", help="Full path to configuration file") parser.add_option("-o", action="store", default=False, dest="output", help="Full path for the generated script file") (options, args) = parser.parse_args() verbose = options.verbose output = options.output if not output: output = os.path.join(ROOT_PATH, 'blacklists.rsc') # Reading configuration file cfg_file = options.cfg_file if not cfg_file: cfg_file = os.path.join(ROOT_PATH, 'blacklist_db.cfg') config = ConfigParser.RawConfigParser() config.read(cfg_file) # Logging if config.get('general', 'log_file'): LOGFILE = config.get('general', 'log_file') else: LOGFILE = '/tmp/blacklist_db.log' FORMAT = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S') try: rotatetime = logging.handlers.TimedRotatingFileHandler(LOGFILE, when="midnight", interval=1, backupCount=14) except IOError, e: print "ERROR %s: Can not open log file - %s" % (e[0], e[1]) exit(1) except Exception, e: print "Can not configure logger - %s" % e exit(1) formatter = logging.Formatter('%(asctime)s: %(message)s','%y-%m-%d %H:%M:%S') rotatetime.setFormatter(FORMAT) logger = logging.getLogger('BLACKLIST-DB') logger.addHandler(rotatetime) if verbose: lvl = logging.DEBUG console = logging.StreamHandler() formatter = logging.Formatter('%(asctime)s: %(message)s','%y-%m-%d %H:%M:%S') console.setFormatter(formatter) logger.addHandler(console) else: lvl = logging.INFO logger.setLevel(lvl) main(config, logger, output) except (KeyboardInterrupt): logger.info("CTRL-C... exit") exit(0) except (SystemExit): logger.info("Exit") exit(0)
      
      









このスクリプトはクラウンを使用して実行され、起動頻度を15分に設定します。

 */15 * * * * /// > /dev/null 2>&1
      
      







4.受信したリストをルーターにインポートします。



Mikrotikの結果のスクリプトは、Webサーバーのディレクトリに配置する必要があります。次の行を追加した構成にnginxがあります。

  location /blacklists.rsc { root /////; }
      
      





Webサーバーの代わりにftpまたはtftpを使用できますが 、それはすべて好みに依存します。



この部分は、 2番目の記事からほぼ完全に「盗まれる」。



1時間に1回、ファイルは次のスクリプトを使用してHTTP経由でサーバーからダウンロードされます(以下はMikrotikのスクリプトとスケジューラルールです)。

 #    ,  example.com   ,  IP    /system script add name="Download_blacklists" source={ /tool fetch url="http://example.com/blacklists.rsc" mode=http; :log info "Downloaded blacklists.rsc"; } #      /system scheduler add comment="Download blacklists" interval=1h name="DownloadBlackLists" on-event=Download_blacklists start-date=jan/01/1970 start-time=01:05:00
      
      







ブラックリストをインポートするスクリプト:

 #  /system script add name="Update_blacklists" source={ /ip firewall address-list remove [/ip firewall address-list find comment="BLACKLIST"]; /import file-name=blacklists.rsc; :log info "Removal old blacklists and add new"; } #   /system scheduler add comment="Update BlackList" interval=1h name="InstallBlackLists" on-event=Update_blacklists start-date=jan/01/1970 start-time=01:15:00
      
      







このリストを使用するには、禁止ルールが作成され、許容ルールの前に配置されます(ルールは順番に処理されるため)。この例では、SSH接続とSIPに対して2つのルールが作成されます。

 /ip firewall filter add action=reject chain=forward comment="SIP: Reject Blacklisted IP addresses" dst-port=5060-5061 in-interface=ID-Net protocol=udp src-address-list=ASTERISK_BLC add action=reject chain=forward comment="SSH: Reject Blacklisted IP addresses" dst-port=22 in-interface=ID-Net protocol=tcp src-address-list=SSH_BLC
      
      







ID-Netは私のフロントエンドの名前です。



この「バイク」は何のふりもせず、数時間「膝の上」で組み立てられました。

ハブロフスク市民に対する建設的な批判と、改善の可能性についての提案を期待しています。



送信されたすべてのスクリプトとデータベーススキーマでアーカイブします。

GitHubのリポジトリ



All Articles