mirror of https://github.com/fail2ban/fail2ban
hold all user names affected by interim attempts in order to avoid forget a failures after success login:
intruder (as legitimate user) firstly tries to login with another user-name (brute-force), so hopes to reset failure counter by succeeded login; this is fixed and covered in tests now; sshd-filter extended to cover multiple-login attempts (also fully implements gh-2070);pull/2090/head
parent
a9c94686b6
commit
25cc42129a
|
@ -29,6 +29,9 @@ __on_port_opt = (?: (?:port \d+|on \S+)){0,2}
|
|||
# see ssherr.c for all possible SSH_ERR_..._ALG_MATCH errors.
|
||||
__alg_match = (?:(?:\w+ (?!found\b)){0,2}\w+)
|
||||
|
||||
# PAM authentication mechanism, can be overridden, e. g. `filter = sshd[__pam_auth='pam_ldap']`:
|
||||
__pam_auth = pam_[a-z]+
|
||||
|
||||
[Definition]
|
||||
|
||||
prefregex = ^<F-MLFID>%(__prefix_line)s</F-MLFID>%(__pref)s<F-CONTENT>.+</F-CONTENT>$
|
||||
|
@ -46,13 +49,13 @@ cmnfailre = ^[aA]uthentication (?:failure|error|failed) for <F-USER>.*</F-USER>
|
|||
^Received <F-MLFFORGET>disconnect</F-MLFFORGET> from <HOST>%(__on_port_opt)s:\s*3: .*: Auth fail%(__suff)s$
|
||||
^User <F-USER>.+</F-USER> from <HOST> not allowed because a group is listed in DenyGroups%(__suff)s$
|
||||
^User <F-USER>.+</F-USER> from <HOST> not allowed because none of user's groups are listed in AllowGroups%(__suff)s$
|
||||
^pam_unix\(sshd:auth\):\s+authentication failure;\s*logname=\S*\s*uid=\d*\s*euid=\d*\s*tty=\S*\s*ruser=<F-USER>\S*</F-USER>\s*rhost=<HOST>\s.*%(__suff)s$
|
||||
^<F-NOFAIL>%(__pam_auth)s\(sshd:auth\):\s+authentication failure;</F-NOFAIL>(?:\s+(?:(?:logname|e?uid|tty)=\S*)){0,4}\s+ruser=<F-ALT_USER>\S*</F-ALT_USER>\s+rhost=<HOST>(?:\s+user=<F-USER>\S*</F-USER>)?%(__suff)s$
|
||||
^(error: )?maximum authentication attempts exceeded for <F-USER>.*</F-USER> from <HOST>%(__on_port_opt)s(?: ssh\d*)?%(__suff)s$
|
||||
^User <F-USER>.+</F-USER> not allowed because account is locked%(__suff)s
|
||||
^<F-MLFFORGET>Disconnecting</F-MLFFORGET>: Too many authentication failures(?: for <F-USER>.+?</F-USER>)?%(__suff)s
|
||||
^<F-NOFAIL>Received <F-MLFFORGET>disconnect</F-MLFFORGET></F-NOFAIL> from <HOST>%(__on_port_opt)s:\s*11:
|
||||
^<F-NOFAIL>Connection <F-MLFFORGET>closed</F-MLFFORGET></F-NOFAIL> by <HOST>%(__suff)s$
|
||||
^<F-MLFFORGET><F-NOFAIL>Accepted publickey</F-NOFAIL></F-MLFFORGET> for \S+ from <HOST>(?:\s|$)
|
||||
^<F-MLFFORGET><F-NOFAIL>Accepted \w+</F-NOFAIL></F-MLFFORGET> for <F-USER>\S+</F-USER> from <HOST>(?:\s|$)
|
||||
|
||||
mdre-normal =
|
||||
|
||||
|
|
|
@ -588,31 +588,62 @@ class Filter(JailThread):
|
|||
return ignoreRegexIndex
|
||||
return None
|
||||
|
||||
def _updateUsers(self, fail, user=()):
|
||||
users = fail.get('users')
|
||||
# only for regex contains user:
|
||||
if user:
|
||||
if not users:
|
||||
fail['users'] = users = set()
|
||||
users.add(user)
|
||||
return users
|
||||
return None
|
||||
|
||||
def _mergeFailure(self, mlfid, fail, failRegex):
|
||||
mlfidFail = self.mlfidCache.get(mlfid) if self.__mlfidCache else None
|
||||
users = None
|
||||
nfflgs = 0
|
||||
if fail.get('nofail'): nfflgs |= 1
|
||||
if fail.get('mlfforget'): nfflgs |= 2
|
||||
# if multi-line failure id (connection id) known:
|
||||
if mlfidFail:
|
||||
mlfidGroups = mlfidFail[1]
|
||||
# update - if not forget (disconnect/reset):
|
||||
if not fail.get('mlfforget'):
|
||||
mlfidGroups.update(fail)
|
||||
else:
|
||||
self.mlfidCache.unset(mlfid) # remove cached entry
|
||||
# merge with previous info:
|
||||
fail2 = mlfidGroups.copy()
|
||||
fail2.update(fail)
|
||||
if not fail.get('nofail'): # be sure we've correct current state
|
||||
try:
|
||||
del fail2['nofail']
|
||||
except KeyError:
|
||||
pass
|
||||
fail2["matches"] = fail.get("matches", []) + failRegex.getMatchedTupleLines()
|
||||
fail = fail2
|
||||
elif not fail.get('mlfforget'):
|
||||
# update users set (hold all users of connect):
|
||||
users = self._updateUsers(mlfidGroups, fail.get('user'))
|
||||
# be sure we've correct current state ('nofail' only from last failure)
|
||||
try:
|
||||
del mlfidGroups['nofail']
|
||||
except KeyError:
|
||||
pass
|
||||
# update not empty values:
|
||||
mlfidGroups.update(((k,v) for k,v in fail.iteritems() if v))
|
||||
fail = mlfidGroups
|
||||
# if forget (disconnect/reset) - remove cached entry:
|
||||
if nfflgs & 2:
|
||||
self.mlfidCache.unset(mlfid)
|
||||
elif not (nfflgs & 2): # not mlfforget
|
||||
users = self._updateUsers(fail, fail.get('user'))
|
||||
mlfidFail = [self.__lastDate, fail]
|
||||
self.mlfidCache.set(mlfid, mlfidFail)
|
||||
if fail.get('nofail'):
|
||||
fail["matches"] = failRegex.getMatchedTupleLines()
|
||||
# check users in order to avoid reset failure by multiple logon-attempts:
|
||||
if users and len(users) > 1:
|
||||
# we've new user, reset 'nofail' because of multiple users attempts:
|
||||
try:
|
||||
del fail['nofail']
|
||||
except KeyError:
|
||||
pass
|
||||
# merge matches:
|
||||
if not fail.get('nofail'): # current state (corresponding users)
|
||||
try:
|
||||
m = fail.pop("nofail-matches")
|
||||
m += fail.get("matches", [])
|
||||
except KeyError:
|
||||
m = fail.get("matches", [])
|
||||
if not (nfflgs & 2): # not mlfforget:
|
||||
m += failRegex.getMatchedTupleLines()
|
||||
fail["matches"] = m
|
||||
elif not (nfflgs & 2) and (nfflgs & 1): # not mlfforget and nofail:
|
||||
fail["nofail-matches"] = fail.get("nofail-matches", []) + failRegex.getMatchedTupleLines()
|
||||
# return merged:
|
||||
return fail
|
||||
|
||||
|
||||
|
@ -738,6 +769,7 @@ class Filter(JailThread):
|
|||
fail = self._mergeFailure(mlfid, fail, failRegex)
|
||||
# bypass if no-failure case:
|
||||
if fail.get('nofail'):
|
||||
# if not users or len(users) <= 1:
|
||||
logSys.log(7, "Nofail by mlfid %r in regex %s: %s",
|
||||
mlfid, failRegexIndex, fail.get('mlfforget', "waiting for failure"))
|
||||
if not self.checkAllRegex: return failList
|
||||
|
|
|
@ -27,6 +27,9 @@ __prefix_line_ml2 = %(__suff)s$<SKIPLINES>^(?P=__prefix)%(__pref)s
|
|||
# see ssherr.c for all possible SSH_ERR_..._ALG_MATCH errors.
|
||||
__alg_match = (?:(?:\w+ (?!found\b)){0,2}\w+)
|
||||
|
||||
# PAM authentication mechanism, can be overridden, e. g. `filter = sshd[__pam_auth='pam_ldap']`:
|
||||
__pam_auth = pam_[a-z]+
|
||||
|
||||
[Definition]
|
||||
|
||||
cmnfailre = ^%(__prefix_line_sl)s[aA]uthentication (?:failure|error|failed) for .* from <HOST>( via \S+)?\s*%(__suff)s$
|
||||
|
@ -42,7 +45,7 @@ cmnfailre = ^%(__prefix_line_sl)s[aA]uthentication (?:failure|error|failed) for
|
|||
^%(__prefix_line_sl)sReceived disconnect from <HOST>%(__on_port_opt)s:\s*3: .*: Auth fail%(__suff)s$
|
||||
^%(__prefix_line_sl)sUser .+ from <HOST> not allowed because a group is listed in DenyGroups\s*%(__suff)s$
|
||||
^%(__prefix_line_sl)sUser .+ from <HOST> not allowed because none of user's groups are listed in AllowGroups\s*%(__suff)s$
|
||||
^%(__prefix_line_sl)spam_unix\(sshd:auth\):\s+authentication failure;\s*logname=\S*\s*uid=\d*\s*euid=\d*\s*tty=\S*\s*ruser=\S*\s*rhost=<HOST>\s.*%(__suff)s$
|
||||
^%(__prefix_line_ml1)s%(__pam_auth)s\(sshd:auth\):\s+authentication failure;\s*logname=\S*\s*uid=\d*\s*euid=\d*\s*tty=\S*\s*ruser=\S*\s*rhost=<HOST>\s.*%(__suff)s$%(__prefix_line_ml2)sConnection closed
|
||||
^%(__prefix_line_sl)s(error: )?maximum authentication attempts exceeded for .* from <HOST>%(__on_port_opt)s(?: ssh\d*)? \[preauth\]$
|
||||
^%(__prefix_line_ml1)sUser .+ not allowed because account is locked%(__prefix_line_ml2)sReceived disconnect from <HOST>%(__on_port_opt)s:\s*11: .+%(__suff)s$
|
||||
^%(__prefix_line_ml1)sDisconnecting: Too many authentication failures(?: for .+?)?%(__suff)s%(__prefix_line_ml2)sConnection closed by <HOST>%(__suff)s$
|
||||
|
|
|
@ -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
|
||||
|
@ -212,9 +212,46 @@ Apr 27 13:02:04 host sshd[29116]: input_userauth_request: invalid user root [pre
|
|||
# failJSON: { "time": "2005-04-27T13:02:04", "match": true , "host": "1.2.3.4", "desc": "No Bye-Bye" }
|
||||
Apr 27 13:02:04 host sshd[29116]: Received disconnect from 1.2.3.4: 11: Normal Shutdown, Thank you for playing [preauth]
|
||||
|
||||
# Match sshd auth errors on OpenSUSE systems
|
||||
# failJSON: { "time": "2015-04-16T20:02:50", "match": true , "host": "222.186.21.217", "desc": "Authentication for user failed" }
|
||||
2015-04-16T18:02:50.321974+00:00 host sshd[2716]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=222.186.21.217 user=root
|
||||
# Match sshd auth errors on OpenSUSE systems (gh-1024)
|
||||
# failJSON: { "match": false, "desc": "No failure until closed or another fail (e. g. F-MLFFORGET by success/accepted password can avoid failure, see gh-2070)" }
|
||||
2015-04-16T18:02:50.321974+00:00 host sshd[2716]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=192.0.2.112 user=root
|
||||
# failJSON: { "time": "2015-04-16T20:02:50", "match": true , "host": "192.0.2.112", "desc": "Should catch failure - no success/no accepted password" }
|
||||
2015-04-16T18:02:50.568798+00:00 host sshd[2716]: Connection closed by 192.0.2.112
|
||||
|
||||
# disable this case for obsolete multi-line filter (zzz-sshd-obsolete...):
|
||||
# filterOptions: [{"test.condition": "name == 'sshd'"}]
|
||||
|
||||
# 2 methods auth: pam_unix and pam_ldap are used in combination (gh-2070), succeeded after "failure" in first method:
|
||||
# failJSON: { "match": false , "desc": "No failure" }
|
||||
Mar 7 18:53:20 bar sshd[1556]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=192.0.2.113 user=rda
|
||||
# failJSON: { "match": false , "desc": "No failure" }
|
||||
Mar 7 18:53:20 bar sshd[1556]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser=rda rhost=192.0.2.113 [preauth]
|
||||
# failJSON: { "match": false , "desc": "No failure" }
|
||||
Mar 7 18:53:20 bar sshd[1556]: Accepted password for rda from 192.0.2.113 port 52100 ssh2
|
||||
# failJSON: { "match": false , "desc": "No failure" }
|
||||
Mar 7 18:53:20 bar sshd[1556]: pam_unix(sshd:session): session opened for user rda by (uid=0)
|
||||
# failJSON: { "match": false , "desc": "No failure" }
|
||||
Mar 7 18:53:20 bar sshd[1556]: Connection closed by 192.0.2.113
|
||||
|
||||
# several attempts, intruder tries to "forget" failed attempts by success login (all 3 attempts with different users):
|
||||
# failJSON: { "match": false , "desc": "Still no failure (first try)" }
|
||||
Mar 7 18:53:22 bar sshd[1558]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser=root rhost=192.0.2.114
|
||||
# failJSON: { "time": "2005-03-07T18:53:23", "match": true , "attempts": 2, "users": ["root", "sudoer"], "host": "192.0.2.114", "desc": "Failure: attempt 2nd user" }
|
||||
Mar 7 18:53:23 bar sshd[1558]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser=sudoer rhost=192.0.2.114
|
||||
# failJSON: { "time": "2005-03-07T18:53:24", "match": true , "attempts": 2, "users": ["root", "sudoer", "known"], "host": "192.0.2.114", "desc": "Failure: attempt 3rd user" }
|
||||
Mar 7 18:53:24 bar sshd[1558]: Accepted password for known from 192.0.2.114 port 52100 ssh2
|
||||
# failJSON: { "match": false , "desc": "No failure" }
|
||||
Mar 7 18:53:24 bar sshd[1558]: pam_unix(sshd:session): session opened for user known by (uid=0)
|
||||
|
||||
# several attempts, intruder tries to "forget" failed attempts by success login (accepted for other user as in first failed attempt):
|
||||
# failJSON: { "match": false , "desc": "Still no failure (first try)" }
|
||||
Mar 7 18:53:32 bar sshd[1559]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser=root rhost=192.0.2.116
|
||||
# failJSON: { "match": false , "desc": "Still no failure (second try, same user)" }
|
||||
Mar 7 18:53:32 bar sshd[1559]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser=root rhost=192.0.2.116
|
||||
# failJSON: { "time": "2005-03-07T18:53:34", "match": true , "attempts": 2, "users": ["root", "known"], "host": "192.0.2.116", "desc": "Failure: attempt 2nd user" }
|
||||
Mar 7 18:53:34 bar sshd[1559]: Accepted password for known from 192.0.2.116 port 52100 ssh2
|
||||
# failJSON: { "match": false , "desc": "No failure" }
|
||||
Mar 7 18:53:38 bar sshd[1559]: Connection closed by 192.0.2.116
|
||||
|
||||
# filterOptions: [{"mode": "ddos"}, {"mode": "aggressive"}]
|
||||
|
||||
|
|
|
@ -151,6 +151,7 @@ def testSampleRegexsFactory(name, basedir):
|
|||
logFile = fileinput.FileInput(os.path.join(TEST_FILES_DIR, "logs",
|
||||
filename))
|
||||
|
||||
ignoreBlock = False
|
||||
for line in logFile:
|
||||
jsonREMatch = re.match("^#+ ?(failJSON|filterOptions|addFILE):(.+)$", line)
|
||||
if jsonREMatch:
|
||||
|
@ -160,9 +161,13 @@ def testSampleRegexsFactory(name, basedir):
|
|||
if jsonREMatch.group(1) == 'filterOptions':
|
||||
# following lines with another filter options:
|
||||
self._filterTests = []
|
||||
ignoreBlock = False
|
||||
for opts in (faildata if isinstance(faildata, list) else [faildata]):
|
||||
# unique filter name (using options combination):
|
||||
self.assertTrue(isinstance(opts, dict))
|
||||
if opts.get('test.condition'):
|
||||
ignoreBlock = not eval(opts.get('test.condition'))
|
||||
del opts['test.condition']
|
||||
fltName = opts.get('filterName')
|
||||
if not fltName: fltName = str(opts) if opts else ''
|
||||
fltName = name + fltName
|
||||
|
@ -179,10 +184,11 @@ def testSampleRegexsFactory(name, basedir):
|
|||
raise ValueError("%s: %s:%i" %
|
||||
(e, logFile.filename(), logFile.filelineno()))
|
||||
line = next(logFile)
|
||||
elif line.startswith("#") or not line.strip():
|
||||
elif ignoreBlock or line.startswith("#") or not line.strip():
|
||||
continue
|
||||
else: # pragma: no cover - normally unreachable
|
||||
faildata = {}
|
||||
if ignoreBlock: continue
|
||||
|
||||
# if filter options was not yet specified:
|
||||
if not self._filterTests:
|
||||
|
@ -224,9 +230,17 @@ def testSampleRegexsFactory(name, basedir):
|
|||
for k, v in faildata.iteritems():
|
||||
if k not in ("time", "match", "desc", "filter"):
|
||||
fv = fail.get(k, None)
|
||||
# Fallback for backwards compatibility (previously no fid, was host only):
|
||||
if k == "host" and fv is None:
|
||||
fv = fid
|
||||
if fv is None:
|
||||
# Fallback for backwards compatibility (previously no fid, was host only):
|
||||
if k == "host":
|
||||
fv = fid
|
||||
# special case for attempts counter:
|
||||
if k == "attempts":
|
||||
fv = len(fail.get('matches', {}))
|
||||
# compare sorted (if set)
|
||||
if isinstance(fv, (set, list, dict)):
|
||||
self.assertSortedEqual(fv, v)
|
||||
continue
|
||||
self.assertEqual(fv, v)
|
||||
|
||||
t = faildata.get("time", None)
|
||||
|
|
Loading…
Reference in New Issue