mirror of https://github.com/fail2ban/fail2ban
fixed grave vulnerability by wrong escape of tags by executing of shell actions
parent
e5c9f9ec1c
commit
4f1473724b
|
@ -539,6 +539,9 @@ class CommandAction(ActionBase):
|
||||||
#
|
#
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
ESCAPE_CRE = re.compile(r"""[\\#&;`|*?~<>\^\(\)\[\]{}$'"\n\r]""")
|
||||||
|
ESCAPE_VN_CRE = re.compile(r"\W")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def replaceDynamicTags(cls, realCmd, aInfo):
|
def replaceDynamicTags(cls, realCmd, aInfo):
|
||||||
"""Replaces dynamical tags in `query` with property values.
|
"""Replaces dynamical tags in `query` with property values.
|
||||||
|
@ -546,7 +549,7 @@ class CommandAction(ActionBase):
|
||||||
**Important**
|
**Important**
|
||||||
-------------
|
-------------
|
||||||
Because this tags are dynamic resp. foreign (user) input:
|
Because this tags are dynamic resp. foreign (user) input:
|
||||||
- values should be escaped
|
- values should be escaped (using "escape" as shell variable)
|
||||||
- no recursive substitution (no interpolation for <a<b>>)
|
- no recursive substitution (no interpolation for <a<b>>)
|
||||||
- don't use cache
|
- don't use cache
|
||||||
|
|
||||||
|
@ -562,19 +565,52 @@ class CommandAction(ActionBase):
|
||||||
str
|
str
|
||||||
shell script as string or array with tags replaced (direct or as variables).
|
shell script as string or array with tags replaced (direct or as variables).
|
||||||
"""
|
"""
|
||||||
realCmd = cls.replaceTag(realCmd, aInfo, conditional=False)
|
# array for escaped vars:
|
||||||
|
varsDict = dict()
|
||||||
|
|
||||||
|
def escapeVal(tag, value):
|
||||||
|
# if the value should be escaped:
|
||||||
|
if cls.ESCAPE_CRE.search(value):
|
||||||
|
# That one needs to be escaped since its content is
|
||||||
|
# out of our control
|
||||||
|
tag = 'f2bV_%s' % cls.ESCAPE_VN_CRE.sub('_', tag)
|
||||||
|
varsDict[tag] = value # add variable
|
||||||
|
value = '$'+tag # replacement as variable
|
||||||
|
# replacement for tag:
|
||||||
|
return value
|
||||||
|
|
||||||
|
# substitution callable, used by interpolation of each tag
|
||||||
|
def substVal(m):
|
||||||
|
tag = m.group(1) # tagname from match
|
||||||
|
try:
|
||||||
|
value = aInfo[tag]
|
||||||
|
except KeyError:
|
||||||
|
# fallback (no or default replacement)
|
||||||
|
return ADD_REPL_TAGS.get(tag, m.group())
|
||||||
|
value = str(value) # assure string
|
||||||
|
# replacement for tag:
|
||||||
|
return escapeVal(tag, value)
|
||||||
|
|
||||||
|
# Replace normally properties of aInfo non-recursive:
|
||||||
|
realCmd = TAG_CRE.sub(substVal, realCmd)
|
||||||
|
|
||||||
# Replace ticket options (filter capture groups) non-recursive:
|
# Replace ticket options (filter capture groups) non-recursive:
|
||||||
if '<' in realCmd:
|
if '<' in realCmd:
|
||||||
tickData = aInfo.get("F-*")
|
tickData = aInfo.get("F-*")
|
||||||
if not tickData: tickData = {}
|
if not tickData: tickData = {}
|
||||||
def substTag(m):
|
def substTag(m):
|
||||||
tn = mapTag2Opt(m.groups()[0])
|
tag = mapTag2Opt(m.groups()[0])
|
||||||
try:
|
try:
|
||||||
return str(tickData[tn])
|
value = str(tickData[tag])
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return ""
|
return ""
|
||||||
|
return escapeVal("F_"+tag, value)
|
||||||
|
|
||||||
realCmd = FCUSTAG_CRE.sub(substTag, realCmd)
|
realCmd = FCUSTAG_CRE.sub(substTag, realCmd)
|
||||||
|
|
||||||
|
# build command corresponding "escaped" variables:
|
||||||
|
if varsDict:
|
||||||
|
realCmd = Utils.buildShellCmd(realCmd, varsDict)
|
||||||
return realCmd
|
return realCmd
|
||||||
|
|
||||||
def _processCmd(self, cmd, aInfo=None, conditional=''):
|
def _processCmd(self, cmd, aInfo=None, conditional=''):
|
||||||
|
|
|
@ -60,6 +60,7 @@ class Utils():
|
||||||
DEFAULT_SLEEP_TIME = 2
|
DEFAULT_SLEEP_TIME = 2
|
||||||
DEFAULT_SLEEP_INTERVAL = 0.2
|
DEFAULT_SLEEP_INTERVAL = 0.2
|
||||||
DEFAULT_SHORT_INTERVAL = 0.001
|
DEFAULT_SHORT_INTERVAL = 0.001
|
||||||
|
DEFAULT_SHORTEST_INTERVAL = DEFAULT_SHORT_INTERVAL / 100
|
||||||
|
|
||||||
|
|
||||||
class Cache(object):
|
class Cache(object):
|
||||||
|
@ -183,8 +184,8 @@ class Utils():
|
||||||
def _popen_wait_end():
|
def _popen_wait_end():
|
||||||
retcode = popen.poll()
|
retcode = popen.poll()
|
||||||
return (True, retcode) if retcode is not None else None
|
return (True, retcode) if retcode is not None else None
|
||||||
# popen.poll is fast operation so we can put down the sleep interval:
|
# popen.poll is fast operation so we can use the shortest sleep interval:
|
||||||
retcode = Utils.wait_for(_popen_wait_end, timeout, Utils.DEFAULT_SHORT_INTERVAL / 100)
|
retcode = Utils.wait_for(_popen_wait_end, timeout, Utils.DEFAULT_SHORTEST_INTERVAL)
|
||||||
if retcode:
|
if retcode:
|
||||||
retcode = retcode[1]
|
retcode = retcode[1]
|
||||||
# if timeout:
|
# if timeout:
|
||||||
|
|
|
@ -394,7 +394,7 @@ class CommandActionTest(LogCaptureTestCase):
|
||||||
r'''printf %b "foreign input:\n'''
|
r'''printf %b "foreign input:\n'''
|
||||||
r''' -- $f2bV_A --\n'''
|
r''' -- $f2bV_A --\n'''
|
||||||
r''' -- $f2bV_B --\n'''
|
r''' -- $f2bV_B --\n'''
|
||||||
r''' -- $(echo $f2bV_C) --''' # echo just replaces \n to test it as single line
|
r''' -- $(echo -n $f2bV_C) --''' # echo just replaces \n to test it as single line
|
||||||
r'''"''',
|
r'''"''',
|
||||||
varsDict={
|
varsDict={
|
||||||
'f2bV_A': 'I\'m a hacker; && $(echo $f2bV_B)',
|
'f2bV_A': 'I\'m a hacker; && $(echo $f2bV_B)',
|
||||||
|
@ -406,6 +406,34 @@ class CommandActionTest(LogCaptureTestCase):
|
||||||
' -- I"m very bad hacker --',
|
' -- I"m very bad hacker --',
|
||||||
' -- `Very | very $(bad & worst hacker)` --', all=True)
|
' -- `Very | very $(bad & worst hacker)` --', all=True)
|
||||||
|
|
||||||
|
def testExecuteReplaceEscapeWithVars(self):
|
||||||
|
self.__action.actionban = 'echo "** ban <ip>, reason: <reason> ...\\n<matches>"'
|
||||||
|
self.__action.actionunban = 'echo "** unban <ip>"'
|
||||||
|
self.__action.actionstop = 'echo "** stop monitoring"'
|
||||||
|
matches = [
|
||||||
|
'<actionunban>',
|
||||||
|
'" Hooray! #',
|
||||||
|
'`I\'m cool script kiddy',
|
||||||
|
'`I`m very cool > /here-is-the-path/to/bin/.x-attempt.sh',
|
||||||
|
'<actionstop>',
|
||||||
|
]
|
||||||
|
aInfo = {
|
||||||
|
'ip': '192.0.2.1',
|
||||||
|
'reason': 'hacking attempt ( he thought he knows how f2b internally works ;)',
|
||||||
|
'matches': '\n'.join(matches)
|
||||||
|
}
|
||||||
|
self.pruneLog()
|
||||||
|
self.__action.ban(aInfo)
|
||||||
|
self.assertLogged(
|
||||||
|
'** ban %s' % aInfo['ip'], aInfo['reason'], *matches, all=True)
|
||||||
|
self.assertNotLogged(
|
||||||
|
'** unban %s' % aInfo['ip'], '** stop monitoring', all=True)
|
||||||
|
self.pruneLog()
|
||||||
|
self.__action.unban(aInfo)
|
||||||
|
self.__action.stop()
|
||||||
|
self.assertLogged(
|
||||||
|
'** unban %s' % aInfo['ip'], '** stop monitoring', all=True)
|
||||||
|
|
||||||
def testExecuteIncorrectCmd(self):
|
def testExecuteIncorrectCmd(self):
|
||||||
CommandAction.executeCmd('/bin/ls >/dev/null\nbogusXXX now 2>/dev/null')
|
CommandAction.executeCmd('/bin/ls >/dev/null\nbogusXXX now 2>/dev/null')
|
||||||
self.assertLogged('HINT on 127: "Command not found"')
|
self.assertLogged('HINT on 127: "Command not found"')
|
||||||
|
|
|
@ -1679,7 +1679,7 @@ class ServerConfigReaderTests(LogCaptureTestCase):
|
||||||
# complain --
|
# complain --
|
||||||
('j-complain-abuse',
|
('j-complain-abuse',
|
||||||
'complain['
|
'complain['
|
||||||
'name=%(__name__)s, grepopts="-m 1", grepmax=2, mailcmd="mail -s Hostname: <ip-host> - ",' +
|
'name=%(__name__)s, grepopts="-m 1", grepmax=2, mailcmd="mail -s \'Hostname: <ip-host>, family: <family>\' - ",' +
|
||||||
# test reverse ip:
|
# test reverse ip:
|
||||||
'debug=1,' +
|
'debug=1,' +
|
||||||
# 2 logs to test grep from multiple logs:
|
# 2 logs to test grep from multiple logs:
|
||||||
|
@ -1694,14 +1694,14 @@ class ServerConfigReaderTests(LogCaptureTestCase):
|
||||||
'testcase01.log:Dec 31 11:59:59 [sshd] error: PAM: Authentication failure for kevin from 87.142.124.10',
|
'testcase01.log:Dec 31 11:59:59 [sshd] error: PAM: Authentication failure for kevin from 87.142.124.10',
|
||||||
'testcase01a.log:Dec 31 11:55:01 [sshd] error: PAM: Authentication failure for test from 87.142.124.10',
|
'testcase01a.log:Dec 31 11:55:01 [sshd] error: PAM: Authentication failure for test from 87.142.124.10',
|
||||||
# both abuse mails should be separated with space:
|
# both abuse mails should be separated with space:
|
||||||
'mail -s Hostname: test-host - Abuse from 87.142.124.10 abuse-1@abuse-test-server abuse-2@abuse-test-server',
|
'mail -s Hostname: test-host, family: inet4 - Abuse from 87.142.124.10 abuse-1@abuse-test-server abuse-2@abuse-test-server',
|
||||||
),
|
),
|
||||||
'ip6-ban': (
|
'ip6-ban': (
|
||||||
# test reverse ip:
|
# test reverse ip:
|
||||||
'try to resolve 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.abuse-contacts.abusix.org',
|
'try to resolve 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.abuse-contacts.abusix.org',
|
||||||
'Lines containing failures of 2001:db8::1 (max 2)',
|
'Lines containing failures of 2001:db8::1 (max 2)',
|
||||||
# both abuse mails should be separated with space:
|
# both abuse mails should be separated with space:
|
||||||
'mail -s Hostname: test-host - Abuse from 2001:db8::1 abuse-1@abuse-test-server abuse-2@abuse-test-server',
|
'mail -s Hostname: test-host, family: inet6 - Abuse from 2001:db8::1 abuse-1@abuse-test-server abuse-2@abuse-test-server',
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue