diff --git a/ChangeLog b/ChangeLog index f5c65e33..f968164d 100644 --- a/ChangeLog +++ b/ChangeLog @@ -71,6 +71,7 @@ ver. 0.9.1 (2014/xx/xx) - better, faster, stronger - New filters: - monit Thanks Jason H Martin - directadmin Thanks niorg + - apache-shellshock Thanks Eugene Hopkinson (SlowRiot) - New actions: - symbiosis-blacklist-allports for Bytemark symbiosis firewall - fail2ban-client can fetch the running server version diff --git a/THANKS b/THANKS index 93dddde8..e8f628b8 100644 --- a/THANKS +++ b/THANKS @@ -34,6 +34,7 @@ David Nutter Derek Atkins Eric Gerbier Enrico Labedzki +Eugene Hopkinson (SlowRiot) ftoppi François Boulogne Frédéric diff --git a/config/filter.d/apache-shellshock.conf b/config/filter.d/apache-shellshock.conf new file mode 100644 index 00000000..39df1704 --- /dev/null +++ b/config/filter.d/apache-shellshock.conf @@ -0,0 +1,26 @@ +# Fail2Ban filter to block web requests containing custom headers attempting to exploit the shellshock bug +# +# + +[INCLUDES] + +# overwrite with apache-common.local if _apache_error_client is incorrect. +before = apache-common.conf + +[Definition] + +failregex = ^%(_apache_error_client)s (AH01215: )?/bin/(ba)?sh: warning: HTTP_.*?: ignoring function definition attempt(, referer: \S+)?\s*$ + ^%(_apache_error_client)s (AH01215: )?/bin/(ba)?sh: error importing function definition for `HTTP_.*?'(, referer: \S+)?\s*$ + +ignoreregex = + + +# DEV Notes: +# +# https://wiki.apache.org/httpd/ListOfErrors for apache error IDs +# +# example log lines: +# [Thu Sep 25 09:27:18.813902 2014] [cgi:error] [pid 16860] [client 89.207.132.76:59635] AH01215: /bin/bash: warning: HTTP_TEST: ignoring function definition attempt +# [Thu Sep 25 09:29:56.141832 2014] [cgi:error] [pid 16864] [client 162.247.73.206:41273] AH01215: /bin/bash: error importing function definition for `HTTP_TEST' +# +# Author: Eugene Hopkinson (riot@riot.so) diff --git a/config/jail.conf b/config/jail.conf index 2305c006..db7f7c86 100644 --- a/config/jail.conf +++ b/config/jail.conf @@ -321,6 +321,11 @@ port = http,https logpath = %(apache_error_log)s maxretry = 2 +[apache-shellshock] + +port = http,https +logpath = $(apache_error_log)s +maxretry = 1 [nginx-http-auth] diff --git a/fail2ban/client/configparserinc.py b/fail2ban/client/configparserinc.py index ad0a255c..31945849 100644 --- a/fail2ban/client/configparserinc.py +++ b/fail2ban/client/configparserinc.py @@ -124,7 +124,7 @@ class SafeConfigParserWithIncludes(object): else: fileNamesFull = resource # check cache - hashv = '///'.join(fileNamesFull) + hashv = '\x01'.join(fileNamesFull) cr, ret, mtime = SCPWI.CFG_CACHE.get(hashv, (None, False, 0)) curmt = SCPWI._resource_mtime(fileNamesFull) if cr is not None and mtime == curmt: @@ -167,7 +167,7 @@ class SafeConfigParserWithIncludes(object): parser = SCPWI() try: # read without includes - parser.read(resource, get_includes = False) + parser.read(resource, get_includes=False) except UnicodeDecodeError, e: logSys.error("Error decoding config file '%s': %s" % (resource, e)) return [] @@ -232,11 +232,42 @@ after = 1.conf super(_SafeConfigParserWithIncludes, self).__init__( *args, **kwargs) + def get_defaults(self): + return self._defaults + + def get_sections(self): + return self._sections def read(self, filenames): if not isinstance(filenames, list): filenames = [ filenames ] - logSys.debug("Reading files: %s", filenames) + if len(filenames) > 1: + # read multiple configs: + ret = [] + alld = self.get_defaults() + alls = self.get_sections() + for filename in filenames: + # read single one, add to return list: + cfg = SafeConfigParserWithIncludes() + i = cfg.read(filename, get_includes=False) + if i: + ret += i + # merge defaults and all sections to self: + alld.update(cfg.get_defaults()) + for n, s in cfg.get_sections().iteritems(): + if isinstance(s, dict): + s2 = alls.get(n) + if isinstance(s2, dict): + s2.update(s) + else: + alls[n] = s.copy() + else: + alls[n] = s + + return ret + + # read one config : + logSys.debug("Reading file: %s", filenames[0]) if sys.version_info >= (3,2): # pragma: no cover return SafeConfigParser.read(self, filenames, encoding='utf-8') else: diff --git a/fail2ban/tests/action_d/test_badips.py b/fail2ban/tests/action_d/test_badips.py index 30e016ea..a7f148b1 100644 --- a/fail2ban/tests/action_d/test_badips.py +++ b/fail2ban/tests/action_d/test_badips.py @@ -22,11 +22,7 @@ import unittest import sys from ..dummyjail import DummyJail - -if os.path.exists('config/fail2ban.conf'): - CONFIG_DIR = "config" -else: - CONFIG_DIR='/etc/fail2ban' +from ..utils import CONFIG_DIR if sys.version_info >= (2,7): class BadIPsActionTest(unittest.TestCase): diff --git a/fail2ban/tests/action_d/test_smtp.py b/fail2ban/tests/action_d/test_smtp.py index a8a8a1a9..440db55c 100644 --- a/fail2ban/tests/action_d/test_smtp.py +++ b/fail2ban/tests/action_d/test_smtp.py @@ -30,10 +30,7 @@ else: from ..dummyjail import DummyJail -if os.path.exists('config/fail2ban.conf'): - CONFIG_DIR = "config" -else: - CONFIG_DIR='/etc/fail2ban' +from ..utils import CONFIG_DIR class TestSMTPServer(smtpd.SMTPServer): diff --git a/fail2ban/tests/clientreadertestcase.py b/fail2ban/tests/clientreadertestcase.py index 7e343954..438c0c85 100644 --- a/fail2ban/tests/clientreadertestcase.py +++ b/fail2ban/tests/clientreadertestcase.py @@ -32,8 +32,10 @@ from ..client.configurator import Configurator from .utils import LogCaptureTestCase TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files") + +from .utils import CONFIG_DIR + STOCK = os.path.exists(os.path.join('config','fail2ban.conf')) -CONFIG_DIR='config' if STOCK else '/etc/fail2ban' IMPERFECT_CONFIG = os.path.join(os.path.dirname(__file__), 'config') @@ -363,12 +365,22 @@ class JailsReaderTestCache(LogCaptureTestCase): self.assertTrue(configurator.getOptions(None)) cnt = 0 for s in self.getLog().rsplit('\n'): - if re.match(r"^Reading files: .*jail.local", s): + if re.match(r"^Reading files?: .*jail.local", s): cnt += 1 - # if cnt > 2: + # if cnt > 1: # self.printLog() - self.assertFalse(cnt > 2, "Too many times reading of config files, cnt = %s" % cnt) - self.assertFalse(cnt <= 0) + self.assertFalse(cnt > 1, "Too many times reading of config files, cnt = %s" % cnt) + self.assertFalse(cnt == 0) + + # read whole configuration like a file2ban-client again ... + configurator = Configurator() + configurator.setBaseDir(basedir) + configurator.readEarly() + configurator.getEarlyOptions() + configurator.readAll() + self.assertTrue(configurator.getOptions(None)) + self.assertFalse(cnt == 0) + finally: shutil.rmtree(basedir) diff --git a/fail2ban/tests/files/logs/apache-shellshock b/fail2ban/tests/files/logs/apache-shellshock new file mode 100644 index 00000000..0acf4546 --- /dev/null +++ b/fail2ban/tests/files/logs/apache-shellshock @@ -0,0 +1,4 @@ +# failJSON: { "time": "2014-09-25T09:27:18", "match": true , "host": "89.207.132.76" } +[Thu Sep 25 09:27:18.813902 2014] [cgi:error] [pid 16860] [client 89.207.132.76:59635] AH01215: /bin/bash: warning: HTTP_TEST: ignoring function definition attempt +# failJSON: { "time": "2014-09-25T09:29:56", "match": true , "host": "162.247.73.206" } +[Thu Sep 25 09:29:56.141832 2014] [cgi:error] [pid 16864] [client 162.247.73.206:41273] AH01215: /bin/bash: error importing function definition for `HTTP_TEST' diff --git a/fail2ban/tests/samplestestcase.py b/fail2ban/tests/samplestestcase.py index 132ade7b..e0831184 100644 --- a/fail2ban/tests/samplestestcase.py +++ b/fail2ban/tests/samplestestcase.py @@ -32,13 +32,9 @@ else: from ..server.filter import Filter from ..client.filterreader import FilterReader -from .utils import setUpMyTime, tearDownMyTime +from .utils import setUpMyTime, tearDownMyTime, CONFIG_DIR TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files") -if os.path.exists('config/fail2ban.conf'): - CONFIG_DIR = "config" -else: - CONFIG_DIR='/etc/fail2ban' class FilterSamplesRegex(unittest.TestCase): diff --git a/fail2ban/tests/utils.py b/fail2ban/tests/utils.py index 55b63bd3..56e35094 100644 --- a/fail2ban/tests/utils.py +++ b/fail2ban/tests/utils.py @@ -34,6 +34,15 @@ from ..helpers import getLogger logSys = getLogger(__name__) +CONFIG_DIR = os.environ.get('FAIL2BAN_CONFIG_DIR', None) + +if not CONFIG_DIR: +# Use heuristic to figure out where configuration files are + if os.path.exists(os.path.join('config','fail2ban.conf')): + CONFIG_DIR = 'config' + else: + CONFIG_DIR = '/etc/fail2ban' + def mtimesleep(): # no sleep now should be necessary since polling tracks now not only # mtime but also ino and size