--- 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)