2 Commits 3ba1ac8ed8 ... ab51877602

Author SHA1 Message Date
  root ab51877602 concierge-permaudit: check running processes for permission issues 7 years ago
  guillaume 36c5c8ea8c concierge-permaudit: check permission of ruby search paths, and running executables 7 years ago
1 changed files with 94 additions and 3 deletions
  1. 94 3
      src/concierge-permaudit

+ 94 - 3
src/concierge-permaudit

@@ -1,14 +1,20 @@
 #!/usr/bin/python3
 #!/usr/bin/python3
 
 
+# Code python modules
+import argparse
 import os
 import os
 import pwd
 import pwd
 import re
 import re
+import shutil
 import subprocess
 import subprocess
 import sys
 import sys
 import glob
 import glob
 from pathlib import Path
 from pathlib import Path
 
 
-def get_perl_inc():
+# Third party modules
+import psutil
+
+def get_perl_searchpath():
   # perl -e "print join $/, values @INC"
   # perl -e "print join $/, values @INC"
   try:
   try:
     res = subprocess.check_output(['perl', '-e', 'print join $/, values @INC'])
     res = subprocess.check_output(['perl', '-e', 'print join $/, values @INC'])
@@ -16,6 +22,14 @@ def get_perl_inc():
   except FileNotFoundError:
   except FileNotFoundError:
     return []
     return []
 
 
+def get_ruby_searchpath():
+  # ruby -e 'puts $:'
+  try:
+    res = subprocess.check_output(['ruby', '-e', 'puts $:'])
+    return res.decode('utf-8').split("\n")
+  except FileNotFoundError:
+    return []
+
 disRules = list()
 disRules = list()
 disRules.append(('/etc/apache2/sites-available/*', 'SSLCertificateKeyFile\s+(\S+)'))
 disRules.append(('/etc/apache2/sites-available/*', 'SSLCertificateKeyFile\s+(\S+)'))
 disRules.append(('/etc/dovecot/conf.d/10-ssl.conf', 'ssl_key\s*=\s*<(\S+)'))
 disRules.append(('/etc/dovecot/conf.d/10-ssl.conf', 'ssl_key\s*=\s*<(\S+)'))
@@ -119,6 +133,18 @@ writePatterns = [
   '~/.logout' # tcsh shell
   '~/.logout' # tcsh shell
   ]
   ]
 
 
+interps = set([
+  '/bin/sh',
+  '/bin/bash',
+  '/usr/bin/perl',
+  '/usr/bin/python',
+  '/usr/bin/python3'
+  ])
+
+interpArgParse = argparse.ArgumentParser(description='Generic interpreter parser', add_help=False)
+interpArgParse.add_argument('interp', type=str)
+interpArgParse.add_argument('script', type=str)
+
 # Default values for Debian
 # Default values for Debian
 # TODO read values from /etc/login.defs
 # TODO read values from /etc/login.defs
 UID_MIN=1000
 UID_MIN=1000
@@ -129,6 +155,9 @@ for pw in pwd.getpwall():
   if pw.pw_uid in range(UID_MIN, UID_MAX):
   if pw.pw_uid in range(UID_MIN, UID_MAX):
     homePaths.append(pw.pw_dir)
     homePaths.append(pw.pw_dir)
 
 
+# Password database entry for root. By definition root uid = 0.
+rootPwe = pwd.getpwuid(0)
+
 def patternWalk(pattern):
 def patternWalk(pattern):
   if pattern == '~' or pattern[:2] == '~/':
   if pattern == '~' or pattern[:2] == '~/':
     for homePath in homePaths:
     for homePath in homePaths:
@@ -209,10 +238,54 @@ def logExceptions(message, pathList):
       exceptions += " * %s\n" % path.as_posix()
       exceptions += " * %s\n" % path.as_posix()
     exceptions += "\n"
     exceptions += "\n"
 
 
+def logException(description, path, context = None):
+  global exceptions
+  exceptions += "%s\n" % description
+  exceptions += "  Path: %s\n" % path.as_posix()
+  if context != None:
+    exceptions += "  Context: %s\n" % context
+  exceptions += "\n"
+
 def printExceptions():
 def printExceptions():
   global exceptions
   global exceptions
   print(exceptions, end='')
   print(exceptions, end='')
 
 
+def auditProcess(proc):
+  ruid = proc.uids()[0]
+  pid = proc.pid
+  exePathStr = proc.exe()
+  if len(exePathStr) > 0:
+    exePath = Path(exePathStr)
+    try:
+      if (exePath.stat().st_uid != rootPwe.pw_uid and
+          exePath.stat().st_uid != ruid):
+        logException('Executable is owned by another, non-root user', exePath.resolve(), 'Process %d' % proc.pid)
+    except:
+      pass
+    if isWorldWritable(exePath):
+      logException('Executable is world-writable', exePath.resolve(), 'Process %d' % proc.pid)
+
+def auditCommand(ruid, argList, cwd, env = {}, context = None):
+  if 'PATH' in env:
+    path = env['PATH']
+  else:
+    path = os.defpath
+  absArg0 = shutil.which(argList[0], path=path)
+  if absArg0 in interps and len(argList) > 1:
+    (args, remainining) = interpArgParse.parse_known_args(argList)
+    scriptPath = Path(args.script)
+    if not scriptPath.is_absolute():
+      scriptPath = Path(cwd, args.script)
+    try:
+      scriptPath = scriptPath.resolve()
+      if (scriptPath.stat().st_uid != rootPwe.pw_uid and
+          scriptPath.stat().st_uid != ruid):
+        logException('Script is owned by another, non-root user', scriptPath.resolve(), context)
+    except FileNotFoundError:
+      pass # warning('File not found')
+    if isWorldWritable(scriptPath):
+      logException('Script is world-writable', scriptPath.resolve(), context)
+
 readExceptions = []
 readExceptions = []
 for pattern in readPatterns:
 for pattern in readPatterns:
   for strPath in patternWalk(pattern):
   for strPath in patternWalk(pattern):
@@ -257,12 +330,30 @@ for strPath in sys.path:
 logExceptions('These python search paths are world-writable', pythonpathWriteExceptions)
 logExceptions('These python search paths are world-writable', pythonpathWriteExceptions)
 
 
 perlpathWriteExceptions = []
 perlpathWriteExceptions = []
-for strPath in get_perl_inc():
+for strPath in get_perl_searchpath():
   path = Path(strPath)
   path = Path(strPath)
   if isWorldWritable(path):
   if isWorldWritable(path):
     perlpathWriteExceptions.append(path)
     perlpathWriteExceptions.append(path)
 
 
-logExceptions('These perl include paths are world-writable', perlpathWriteExceptions)
+logExceptions('These perl search paths are world-writable', perlpathWriteExceptions)
+
+rubypathWriteExceptions = []
+for strPath in get_ruby_searchpath():
+  path = Path(strPath)
+  if isWorldWritable(path):
+    rubypathWriteExceptions.append(path)
+
+logExceptions('These ruby search paths are world-writable', rubypathWriteExceptions)
+
+for proc in psutil.process_iter():
+  pid = proc.pid
+  ruid = proc.uids()[0]
+  args = proc.cmdline()
+  cwd = proc.cwd()
+  env = proc.environ()
+  auditProcess(proc)
+  if len(args) > 0:
+    auditCommand(ruid, args, cwd, env, 'Process %d' % pid)
 
 
 # Passwords should be stored in /etc/shadow, not /etc/passwd
 # Passwords should be stored in /etc/shadow, not /etc/passwd
 contentExceptions = []
 contentExceptions = []