|
@@ -1,13 +1,19 @@
|
|
#!/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
|
|
|
|
|
|
|
|
+# Third party modules
|
|
|
|
+import psutil
|
|
|
|
+
|
|
def get_perl_searchpath():
|
|
def get_perl_searchpath():
|
|
# perl -e "print join $/, values @INC"
|
|
# perl -e "print join $/, values @INC"
|
|
try:
|
|
try:
|
|
@@ -127,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
|
|
@@ -137,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:
|
|
@@ -217,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):
|
|
@@ -280,13 +345,15 @@ for strPath in get_ruby_searchpath():
|
|
|
|
|
|
logExceptions('These ruby search paths are world-writable', rubypathWriteExceptions)
|
|
logExceptions('These ruby search paths are world-writable', rubypathWriteExceptions)
|
|
|
|
|
|
-processWriteExceptions = []
|
|
|
|
-for strPath in patternWalk('/proc/*/exe'):
|
|
|
|
- path = Path(strPath)
|
|
|
|
- if isWorldWritable(path):
|
|
|
|
- processWriteExceptions.append(path.resolve())
|
|
|
|
-
|
|
|
|
-logExceptions('Running processes use world-writable executables', processWriteExceptions)
|
|
|
|
|
|
+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 = []
|