From 1492ab2247bc02deaa65b80479aa4070e5404b39 Mon Sep 17 00:00:00 2001 From: sebres Date: Tue, 11 Feb 2020 18:44:36 +0100 Subject: [PATCH] improve processing of pending failures (lines without ID/IP) - fail2ban-regex would show those in matched lines now (as well as increase count of matched RE); avoid overwrite of data with empty tags by ticket constructed from multi-line failures; amend to d1b7e2b5fb2b389d04845369d7d29db65425dcf2: better output (as well as ignoring of pending lines) using `--out msg`; filter.d/sshd.conf: don't forget mlf-cache on "disconnecting: too many authentication failures" - message does not have IP (must be followed by "closed [preauth]" to obtain host-IP). --- config/filter.d/sshd.conf | 2 +- fail2ban/client/fail2banregex.py | 8 +++--- fail2ban/server/filter.py | 34 +++++++++++--------------- fail2ban/tests/files/logs/sshd | 2 +- fail2ban/tests/files/logs/sshd-journal | 2 +- 5 files changed, 22 insertions(+), 26 deletions(-) diff --git a/config/filter.d/sshd.conf b/config/filter.d/sshd.conf index d764a076..b382ffc1 100644 --- a/config/filter.d/sshd.conf +++ b/config/filter.d/sshd.conf @@ -55,7 +55,7 @@ cmnfailre = ^[aA]uthentication (?:failure|error|failed) for .* ^(error: )?maximum authentication attempts exceeded for .* from %(__on_port_opt)s(?: ssh\d*)?%(__suff)s$ ^User .+ not allowed because account is locked%(__suff)s ^Disconnecting(?: from)?(?: (?:invalid|authenticating)) user \S+ %(__on_port_opt)s:\s*Change of username or service not allowed:\s*.*\[preauth\]\s*$ - ^Disconnecting: Too many authentication failures(?: for .+?)?%(__suff)s$ + ^Disconnecting: Too many authentication failures(?: for .+?)?%(__suff)s$ ^Received disconnect from %(__on_port_opt)s:\s*11: -other> ^Accepted \w+ for \S+ from (?:\s|$) diff --git a/fail2ban/client/fail2banregex.py b/fail2ban/client/fail2banregex.py index 513b765d..afcb4282 100644 --- a/fail2ban/client/fail2banregex.py +++ b/fail2ban/client/fail2banregex.py @@ -272,6 +272,8 @@ class Fail2banRegex(object): self._filter.returnRawHost = opts.raw self._filter.checkFindTime = False self._filter.checkAllRegex = opts.checkAllRegex and not opts.out + # ignore pending (without ID/IP), added to matches if it hits later (if ID/IP can be retreved) + self._filter.ignorePending = opts.out; self._backend = 'auto' def output(self, line): @@ -452,7 +454,6 @@ class Fail2banRegex(object): try: found = self._filter.processLine(line, date) lines = [] - line = self._filter.processedLine() ret = [] for match in found: # Append True/False flag depending if line was matched by @@ -488,7 +489,7 @@ class Fail2banRegex(object): self._line_stats.matched += 1 self._line_stats.missed -= 1 if lines: # pre-lines parsed in multiline mode (buffering) - lines.append(line) + lines.append(self._filter.processedLine()) line = "\n".join(lines) return line, ret, is_ignored @@ -523,7 +524,8 @@ class Fail2banRegex(object): output(ret[1]) elif self._opts.out == 'msg': for ret in ret: - output('\n'.join(map(lambda v:''.join(v for v in v), ret[3].get('matches')))) + for ret in ret[3].get('matches'): + output(''.join(v for v in ret)) elif self._opts.out == 'row': for ret in ret: output('[%r,\t%r,\t%r],' % (ret[1],ret[2],dict((k,v) for k, v in ret[3].iteritems() if k != 'matches'))) diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index 0c44c7ac..3a100fbd 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -105,6 +105,8 @@ class Filter(JailThread): self.returnRawHost = False ## check each regex (used for test purposes): self.checkAllRegex = False + ## avoid finding of pending failures (without ID/IP, used in fail2ban-regex): + self.ignorePending = True ## if true ignores obsolete failures (failure time < now - findTime): self.checkFindTime = True ## Ticks counter @@ -651,7 +653,7 @@ class Filter(JailThread): fail['users'] = users = set() users.add(user) return users - return None + return users # # ATM incremental (non-empty only) merge deactivated ... # @staticmethod @@ -680,25 +682,22 @@ class Filter(JailThread): if not fail.get('nofail'): fail['nofail'] = fail["mlfgained"] elif fail.get('nofail'): nfflgs |= 1 - if fail.get('mlfforget'): nfflgs |= 2 + if fail.pop('mlfforget', None): nfflgs |= 2 # if multi-line failure id (connection id) known: if mlfidFail: mlfidGroups = mlfidFail[1] # update users set (hold all users of connect): users = self._updateUsers(mlfidGroups, fail.get('user')) # be sure we've correct current state ('nofail' and 'mlfgained' only from last failure) - try: - del mlfidGroups['nofail'] - del mlfidGroups['mlfgained'] - except KeyError: - pass + mlfidGroups.pop('nofail', None) + mlfidGroups.pop('mlfgained', None) # # ATM incremental (non-empty only) merge deactivated (for future version only), # # it can be simulated using alternate value tags, like ..., # # so previous value 'val' will be overwritten only if 'alt_val' is not empty... # _updateFailure(mlfidGroups, fail) # # overwrite multi-line failure with all values, available in fail: - mlfidGroups.update(fail) + mlfidGroups.update(((k,v) for k,v in fail.iteritems() if v is not None)) # new merged failure data: fail = mlfidGroups # if forget (disconnect/reset) - remove cached entry: @@ -709,20 +708,14 @@ class Filter(JailThread): mlfidFail = [self.__lastDate, fail] self.mlfidCache.set(mlfid, mlfidFail) # check users in order to avoid reset failure by multiple logon-attempts: - if users and len(users) > 1: + if fail.pop('mlfpending', 0) or users and len(users) > 1: # we've new user, reset 'nofail' because of multiple users attempts: - try: - del fail['nofail'] - nfflgs &= ~1 # reset nofail - except KeyError: - pass + fail.pop('nofail', None) + nfflgs &= ~1 # reset nofail # merge matches: if not (nfflgs & 1): # current nofail state (corresponding users) - try: - m = fail.pop("nofail-matches") - m += fail.get("matches", []) - except KeyError: - m = fail.get("matches", []) + m = fail.pop("nofail-matches", []) + m += fail.get("matches", []) if not (nfflgs & 8): # no gain signaled m += failRegex.getMatchedTupleLines() fail["matches"] = m @@ -888,7 +881,8 @@ class Filter(JailThread): if host is None: if ll <= 7: logSys.log(7, "No failure-id by mlfid %r in regex %s: %s", mlfid, failRegexIndex, fail.get('mlfforget', "waiting for identifier")) - if not self.checkAllRegex: return failList + fail['mlfpending'] = 1; # mark failure is pending + if not self.checkAllRegex and self.ignorePending: return failList ips = [None] # if raw - add single ip or failure-id, # otherwise expand host to multiple ips using dns (or ignore it if not valid): diff --git a/fail2ban/tests/files/logs/sshd b/fail2ban/tests/files/logs/sshd index a5f64939..2b8e6621 100644 --- a/fail2ban/tests/files/logs/sshd +++ b/fail2ban/tests/files/logs/sshd @@ -134,7 +134,7 @@ Sep 29 17:15:02 spaceman sshd[12946]: Failed password for user from 127.0.0.1 po # failJSON: { "time": "2004-09-29T17:15:02", "match": true , "host": "127.0.0.1", "desc": "Injecting while exhausting initially present {0,100} match length limits set for ruser etc" } Sep 29 17:15:02 spaceman sshd[12946]: Failed password for user from 127.0.0.1 port 20000 ssh1: ruser XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX from 1.2.3.4 # failJSON: { "time": "2004-09-29T17:15:03", "match": true , "host": "aaaa:bbbb:cccc:1234::1:1", "desc": "Injecting while exhausting initially present {0,100} match length limits set for ruser etc" } -Sep 29 17:15:03 spaceman sshd[12946]: Failed password for user from aaaa:bbbb:cccc:1234::1:1 port 20000 ssh1: ruser XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX from 1.2.3.4 +Sep 29 17:15:03 spaceman sshd[12947]: Failed password for user from aaaa:bbbb:cccc:1234::1:1 port 20000 ssh1: ruser XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX from 1.2.3.4 # failJSON: { "time": "2004-11-11T08:04:51", "match": true , "host": "127.0.0.1", "desc": "Injecting on username ssh 'from 10.10.1.1'@localhost" } Nov 11 08:04:51 redbamboo sshd[2737]: Failed password for invalid user from 10.10.1.1 from 127.0.0.1 port 58946 ssh2 diff --git a/fail2ban/tests/files/logs/sshd-journal b/fail2ban/tests/files/logs/sshd-journal index 07e34efe..ba755645 100644 --- a/fail2ban/tests/files/logs/sshd-journal +++ b/fail2ban/tests/files/logs/sshd-journal @@ -135,7 +135,7 @@ srv sshd[12946]: Failed password for user from 127.0.0.1 port 20000 ssh1: ruser # failJSON: { "match": true , "host": "127.0.0.1", "desc": "Injecting while exhausting initially present {0,100} match length limits set for ruser etc" } srv sshd[12946]: Failed password for user from 127.0.0.1 port 20000 ssh1: ruser XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX from 1.2.3.4 # failJSON: { "match": true , "host": "aaaa:bbbb:cccc:1234::1:1", "desc": "Injecting while exhausting initially present {0,100} match length limits set for ruser etc" } -srv sshd[12946]: Failed password for user from aaaa:bbbb:cccc:1234::1:1 port 20000 ssh1: ruser XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX from 1.2.3.4 +srv sshd[12947]: Failed password for user from aaaa:bbbb:cccc:1234::1:1 port 20000 ssh1: ruser XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX from 1.2.3.4 # failJSON: { "match": true , "host": "127.0.0.1", "desc": "Injecting on username ssh 'from 10.10.1.1'@localhost" } srv sshd[2737]: Failed password for invalid user from 10.10.1.1 from 127.0.0.1 port 58946 ssh2