integ/base/lshell/files/lshell-shell-escape-check.p...

122 lines
4.5 KiB
Diff

---
lshell/shellcmd.py | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 77 insertions(+), 3 deletions(-)
--- a/lshell/shellcmd.py
+++ b/lshell/shellcmd.py
@@ -30,7 +30,7 @@ import subprocess
from time import gmtime, strftime
from utils import get_aliases
-
+from distutils.spawn import find_executable
class ShellCmd(cmd.Cmd, object):
""" Main lshell CLI class
@@ -337,6 +337,44 @@ class ShellCmd(cmd.Cmd, object):
# strip all spaces/tabs
line = " ".join(line.split())
+ # Expand all variables
+ line = os.path.expandvars(line)
+
+ # *** AWK HOOK *** #
+ # Before we begin, check if user is trying
+ # to pass an awk script to the awk interpreter
+ # and disallow that option.
+ #
+ # Also disallow inline vars in awk since an attacker
+ # may use that to scramble a forbidden cmd
+ # such as the following shell escape:
+ # (awk -v X=ba -v Y=ash 'BEGIN { system("/bin/"X Y) }'
+ #
+ # In an ideal world we should parse the awk script
+ # and inline vars for forbidden paths and commands
+ # but that will require some gnarly regexes (esp for
+ # the inline vars). Deferring this as TODO
+ if re.match(r'\s*awk.*-f\s*[\w/~]+', line):
+ return self.warn_count('awk script option', oline, strict, ssh)
+ if re.match(r'\s*awk.*-v\s*\w+=', line):
+ return self.warn_count('awk inline variable option', oline, strict, ssh)
+
+
+ # process all quoted text seperately
+ # This logic is kept crudely simple on purpose.
+ # At most we might match the same stanza twice
+ # (for e.g. "'a'", 'a') but the converse would
+ # require detecting single quotation stanzas
+ # nested within double quotes and vice versa
+ relist = re.findall(r'[^=]\"(.+)\"',line)
+ relist2 = re.findall(r'[^=]\'(.+)\'',line)
+ relist = relist + relist2
+ for item in relist:
+ if self.check_secure(item, strict = strict):
+ return 1
+ if self.check_path(item, strict = strict):
+ return 1
+
# ignore quoted text
line = re.sub(r'\"(.+?)\"', '', line)
line = re.sub(r'\'(.+?)\'', '', line)
@@ -438,7 +476,8 @@ class ShellCmd(cmd.Cmd, object):
new_cmd_line = 'export ' + oline
self.g_line = new_cmd_line
self.check_secure(new_cmd_line, strict = strict)
- else:
+ # filter out macros, text or constructs that got picked up as commands
+ elif command.islower() and find_executable(command):
return self.warn_count('command', oline, strict, ssh, command)
return 0
@@ -499,6 +538,7 @@ class ShellCmd(cmd.Cmd, object):
%(self.conf['warning_counter']))
self.stderr.write('This incident has been reported.\n')
+
def check_path(self, line, completion=None, ssh=None, strict=None):
""" Check if a path is entered in the line. If so, it checks if user \
are allowed to see this path. If user is not allowed, it calls \
@@ -594,7 +634,41 @@ class ShellCmd(cmd.Cmd, object):
detect the new environment and then use that to update the \
environ of the lshell process.
"""
- pipe = subprocess.Popen("%s; env -0" % script,
+ try:
+ script_path = os.path.expanduser(script.\
+ strip("source").split()[0])
+ script_path = os.path.expandvars(script_path)
+ with open (script_path) as fd:
+ content = fd.readlines()
+ content = [line.strip('\n') for line in content]
+
+ # Although rare in a normal cases, an attacker
+ # may attempt to bypass line validation by
+ # scrambling commands via line continuations
+ partial_line = ""
+ for i,line in enumerate(content):
+ if line.startswith('#'):
+ continue
+ if len(line) > 1 and line.startswith('\\'):
+ # implying previous partial line
+ content[i] = line[:1].replace('\\', '', 1)
+ if partial_line:
+ content[i] = partial_line + line
+ if line.endswith('\\'):
+ # continuation character. First partial line.
+ # We shall expect the command to continue in
+ # a new line.
+ partial_line = content[i].strip('\\')
+ continue
+ partial_line = ""
+ if self.check_secure(content[i]):
+ return
+ if self.check_path(content[i]):
+ return
+ except:
+ pass
+
+ pipe = subprocess.Popen("%s; env -0" % script,
bufsize=1,
stdout=subprocess.PIPE,
shell=True)