|
@@ -4,11 +4,13 @@
|
|
|
import argparse
|
|
|
import os
|
|
|
import pwd
|
|
|
+import spwd
|
|
|
import re
|
|
|
import shutil
|
|
|
import subprocess
|
|
|
import sys
|
|
|
import glob
|
|
|
+from enum import IntEnum
|
|
|
from pathlib import Path
|
|
|
|
|
|
# Third party modules
|
|
@@ -31,10 +33,15 @@ def get_ruby_searchpath():
|
|
|
return []
|
|
|
|
|
|
disRules = list()
|
|
|
-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/nginx/sites-available/*', 'ssl_certificate_key\s+([^;]+);'))
|
|
|
-disRules.append(('/etc/postfix/main.cf', 'smtpd_tls_key_file\s*=\s*(\S+)'))
|
|
|
+disRules.append({'pathname': '/etc/apache2/sites-available/*', 're': 'SSLCertificateKeyFile\s+(\S+)'})
|
|
|
+disRules.append({'pathname': '/etc/dovecot/conf.d/10-ssl.conf', 're': 'ssl_key\s*=\s*<(\S+)'})
|
|
|
+disRules.append({'pathname': '/etc/nginx/sites-available/*', 'cwd': '/etc/nginx', 're': 'ssl_certificate_key\s+"?([^;]+)"?;'})
|
|
|
+disRules.append({'pathname': '/etc/nginx/sites-available/*', 'cwd': '/etc/nginx', 're': 'auth_basic_user_file\s+"?([^;]+)"?'})
|
|
|
+disRules.append({'pathname': '/etc/postfix/main.cf', 're': 'smtpd_tls_dkey_file\s*=\s*(\S+)'})
|
|
|
+disRules.append({'pathname': '/etc/postfix/main.cf', 're': 'smtpd_tls_key_file\s*=\s*(\S+)'})
|
|
|
+disRules.append({'pathname': '/etc/postfix/main.cf', 're': 'smtpd_tls_eckey_file\s*=\s*(\S+)'})
|
|
|
+disRules.append({'pathname': '/etc/postfix/main.cf', 're': 'smtpd_tls_dh512_param_file\s*=\s*(\S+)'})
|
|
|
+disRules.append({'pathname': '/etc/postfix/main.cf', 're': 'smtpd_tls_dh1024_param_file\s*=\s*(\S+)'})
|
|
|
|
|
|
readPatterns = [
|
|
|
'/etc/shadow',
|
|
@@ -90,6 +97,7 @@ readPatterns = [
|
|
|
'/etc/letsencrypt/keys/*.pem',
|
|
|
'/etc/cups/ssl/*.key',
|
|
|
'/etc/unbound/*.key',
|
|
|
+ '/etc/wireguard/*.conf',
|
|
|
]
|
|
|
|
|
|
writePatterns = [
|
|
@@ -133,13 +141,41 @@ writePatterns = [
|
|
|
'~/.logout' # tcsh shell
|
|
|
]
|
|
|
|
|
|
-interps = set([
|
|
|
- '/bin/sh',
|
|
|
- '/bin/bash',
|
|
|
- '/usr/bin/perl',
|
|
|
- '/usr/bin/python',
|
|
|
- '/usr/bin/python3'
|
|
|
- ])
|
|
|
+searchinterps = [
|
|
|
+ 'sh',
|
|
|
+ 'dash',
|
|
|
+ 'bash',
|
|
|
+ 'fish',
|
|
|
+ 'tcsh',
|
|
|
+ 'zsh',
|
|
|
+ 'node',
|
|
|
+ 'perl',
|
|
|
+ 'php',
|
|
|
+ 'php-cli',
|
|
|
+ 'python',
|
|
|
+ 'python2',
|
|
|
+ 'python2.7',
|
|
|
+ 'python3',
|
|
|
+ 'python3.4',
|
|
|
+ 'python3.5',
|
|
|
+ 'python3.6',
|
|
|
+ 'tclsh'
|
|
|
+ ]
|
|
|
+
|
|
|
+interps = set()
|
|
|
+
|
|
|
+for interpname in searchinterps:
|
|
|
+ interp = shutil.which(interpname)
|
|
|
+ if interp != None:
|
|
|
+ interp = Path(interp)
|
|
|
+ interps.add(str(interp))
|
|
|
+ realinterp = interp.resolve()
|
|
|
+ if realinterp != interp:
|
|
|
+ interps.add(str(realinterp))
|
|
|
+
|
|
|
+def is_interpreter(path):
|
|
|
+ resolvedPath = Path(path).resolve()
|
|
|
+ return str(resolvedPath) in interps
|
|
|
|
|
|
interpArgParse = argparse.ArgumentParser(description='Generic interpreter parser', add_help=False)
|
|
|
interpArgParse.add_argument('interp', type=str)
|
|
@@ -150,6 +186,20 @@ interpArgParse.add_argument('script', type=str)
|
|
|
UID_MIN=1000
|
|
|
UID_MAX=60000
|
|
|
|
|
|
+class Severity(IntEnum):
|
|
|
+ EMERG = 0
|
|
|
+ ALERT = 1
|
|
|
+ CRIT = 2
|
|
|
+ ERR = 3
|
|
|
+ WARNING = 4
|
|
|
+ NOTICE = 5
|
|
|
+ INFO = 6
|
|
|
+ DEBUG = 7
|
|
|
+
|
|
|
+argparser = argparse.ArgumentParser()
|
|
|
+argparser.add_argument('--severity', type = int, default = 3, choices = list(map(int, Severity)))
|
|
|
+args = argparser.parse_args()
|
|
|
+
|
|
|
homePaths = []
|
|
|
for pw in pwd.getpwall():
|
|
|
if pw.pw_uid in range(UID_MIN, UID_MAX):
|
|
@@ -167,13 +217,15 @@ def patternWalk(pattern):
|
|
|
|
|
|
# Discover paths to file with sensible information
|
|
|
for disRule in disRules:
|
|
|
- disPattern = disRule[0]
|
|
|
- disRe = re.compile(disRule[1])
|
|
|
+ disPattern = disRule['pathname']
|
|
|
+ disRe = re.compile(disRule['re'])
|
|
|
for disPath in patternWalk(disPattern):
|
|
|
+ cwd = disRule.get('cwd', Path(disPath).cwd())
|
|
|
disFile = open(disPath, 'r')
|
|
|
for match in re.finditer(disRe, disFile.read()):
|
|
|
for group in match.groups():
|
|
|
- readPatterns.append(group)
|
|
|
+ absPath = Path(cwd, group)
|
|
|
+ readPatterns.append(str(absPath))
|
|
|
|
|
|
writePatternsParents = [
|
|
|
]
|
|
@@ -230,40 +282,50 @@ def isWorldWritable(path):
|
|
|
|
|
|
exceptions = ''
|
|
|
|
|
|
-def logExceptions(message, pathList):
|
|
|
- global exceptions
|
|
|
- if len(pathList) > 0:
|
|
|
- exceptions += "%s: \n" % message
|
|
|
- for path in (pathList):
|
|
|
- exceptions += " * %s\n" % path.as_posix()
|
|
|
- exceptions += "\n"
|
|
|
-
|
|
|
-def logException(description, path, context = None):
|
|
|
+def logExceptions(description, paths = [], context = None, severity = 3):
|
|
|
global exceptions
|
|
|
+ if severity > args.severity:
|
|
|
+ return
|
|
|
exceptions += "%s\n" % description
|
|
|
- exceptions += " Path: %s\n" % path.as_posix()
|
|
|
if context != None:
|
|
|
exceptions += " Context: %s\n" % context
|
|
|
+ if len(paths) == 1:
|
|
|
+ exceptions += " Path: %s\n" % paths[0].as_posix()
|
|
|
+ elif len(paths) > 1:
|
|
|
+ exceptions += " Paths: \n"
|
|
|
+ for path in paths:
|
|
|
+ exceptions += " * %s\n" % path.as_posix()
|
|
|
exceptions += "\n"
|
|
|
|
|
|
+def logWarnings(description, paths = [], context = None):
|
|
|
+ return logExceptions(description, paths, context, 4)
|
|
|
+
|
|
|
def printExceptions():
|
|
|
global exceptions
|
|
|
print(exceptions, end='')
|
|
|
|
|
|
def auditProcess(proc):
|
|
|
ruid = proc.uids()[0]
|
|
|
- pid = proc.pid
|
|
|
exePathStr = proc.exe()
|
|
|
if len(exePathStr) > 0:
|
|
|
exePath = Path(exePathStr)
|
|
|
try:
|
|
|
+ exePath = exePath.resolve()
|
|
|
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
|
|
|
+ logExceptions('Executable is owned by another, non-root user', [exePath], 'Process %d' % proc.pid)
|
|
|
+ except FileNotFoundError:
|
|
|
+ logWarnings('Executable was moved or deleted', [exePath], 'Process %d' % proc.pid)
|
|
|
if isWorldWritable(exePath):
|
|
|
- logException('Executable is world-writable', exePath.resolve(), 'Process %d' % proc.pid)
|
|
|
+ logExceptions('Executable is world-writable', [exePath], 'Process %d' % proc.pid)
|
|
|
+ if ruid == rootPwe.pw_uid:
|
|
|
+ connlist = proc.connections(kind = 'inet')
|
|
|
+ connset = set(c for c in connlist if c.status == psutil.CONN_LISTEN)
|
|
|
+ if len(connset) > 0:
|
|
|
+ childlist = proc.children(recursive = True)
|
|
|
+ childset = set(p for p in childlist if p.uids()[0] != rootPwe.pw_uid)
|
|
|
+ if len(childset) == 0:
|
|
|
+ logExceptions('Root process listen for connections but has no unprivileged worker', [exePath], 'Process %s' % proc.pid, 5)
|
|
|
|
|
|
def auditCommand(ruid, argList, cwd, env = {}, context = None):
|
|
|
if 'PATH' in env:
|
|
@@ -271,20 +333,20 @@ def auditCommand(ruid, argList, cwd, env = {}, context = None):
|
|
|
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 absArg0 != None and is_interpreter(absArg0) and len(argList) > 1:
|
|
|
+ (interpargs, remainining) = interpArgParse.parse_known_args(argList)
|
|
|
+ scriptPath = Path(interpargs.script)
|
|
|
if not scriptPath.is_absolute():
|
|
|
- scriptPath = Path(cwd, args.script)
|
|
|
+ scriptPath = Path(cwd, interpargs.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)
|
|
|
+ logExceptions('Script is owned by another, non-root user', [scriptPath], context)
|
|
|
+ if isWorldWritable(scriptPath):
|
|
|
+ logExceptions('Script is world-writable', [scriptPath], context)
|
|
|
except FileNotFoundError:
|
|
|
- pass # warning('File not found')
|
|
|
- if isWorldWritable(scriptPath):
|
|
|
- logException('Script is world-writable', scriptPath.resolve(), context)
|
|
|
+ logWarnings('Script was moved or deleted', [scriptPath], context)
|
|
|
|
|
|
readExceptions = []
|
|
|
for pattern in readPatterns:
|
|
@@ -293,7 +355,8 @@ for pattern in readPatterns:
|
|
|
if isWorldReadable(path):
|
|
|
readExceptions.append(path)
|
|
|
|
|
|
-logExceptions('These paths are world readable', readExceptions)
|
|
|
+if len(readExceptions) > 0:
|
|
|
+ logExceptions('These paths are world readable', readExceptions)
|
|
|
|
|
|
writeExceptions = []
|
|
|
for pattern in writePatterns:
|
|
@@ -302,7 +365,8 @@ for pattern in writePatterns:
|
|
|
if isWorldWritable(path):
|
|
|
writeExceptions.append(path)
|
|
|
|
|
|
-logExceptions('These paths are world-writable', writeExceptions)
|
|
|
+if len(writeExceptions):
|
|
|
+ logExceptions('These paths are world-writable', writeExceptions)
|
|
|
|
|
|
parentWriteExceptions = []
|
|
|
for pattern in writePatternsParents:
|
|
@@ -311,7 +375,8 @@ for pattern in writePatternsParents:
|
|
|
if isWorldWritable(path):
|
|
|
parentWriteExceptions.append(path)
|
|
|
|
|
|
-logExceptions('These paths are world-writable', parentWriteExceptions)
|
|
|
+if len(parentWriteExceptions) > 0:
|
|
|
+ logExceptions('These paths are world-writable', parentWriteExceptions)
|
|
|
|
|
|
execpathWriteExceptions = []
|
|
|
for strPath in os.get_exec_path():
|
|
@@ -319,7 +384,8 @@ for strPath in os.get_exec_path():
|
|
|
if isWorldWritable(path):
|
|
|
execpathWriteExceptions.append(path)
|
|
|
|
|
|
-logExceptions('These executable search paths are world-writable', execpathWriteExceptions)
|
|
|
+if len(execpathWriteExceptions) > 0:
|
|
|
+ logExceptions('These executable search paths are world-writable', execpathWriteExceptions)
|
|
|
|
|
|
pythonpathWriteExceptions = []
|
|
|
for strPath in sys.path:
|
|
@@ -327,7 +393,8 @@ for strPath in sys.path:
|
|
|
if isWorldWritable(path):
|
|
|
pythonpathWriteExceptions.append(path)
|
|
|
|
|
|
-logExceptions('These python search paths are world-writable', pythonpathWriteExceptions)
|
|
|
+if len(pythonpathWriteExceptions) > 0:
|
|
|
+ logExceptions('These python search paths are world-writable', pythonpathWriteExceptions)
|
|
|
|
|
|
perlpathWriteExceptions = []
|
|
|
for strPath in get_perl_searchpath():
|
|
@@ -335,7 +402,8 @@ for strPath in get_perl_searchpath():
|
|
|
if isWorldWritable(path):
|
|
|
perlpathWriteExceptions.append(path)
|
|
|
|
|
|
-logExceptions('These perl search paths are world-writable', perlpathWriteExceptions)
|
|
|
+if len(perlpathWriteExceptions) > 0:
|
|
|
+ logExceptions('These perl search paths are world-writable', perlpathWriteExceptions)
|
|
|
|
|
|
rubypathWriteExceptions = []
|
|
|
for strPath in get_ruby_searchpath():
|
|
@@ -343,17 +411,18 @@ for strPath in get_ruby_searchpath():
|
|
|
if isWorldWritable(path):
|
|
|
rubypathWriteExceptions.append(path)
|
|
|
|
|
|
-logExceptions('These ruby search paths are world-writable', rubypathWriteExceptions)
|
|
|
+if len(rubypathWriteExceptions) > 0:
|
|
|
+ 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()
|
|
|
+ procargs = proc.cmdline()
|
|
|
cwd = proc.cwd()
|
|
|
env = proc.environ()
|
|
|
auditProcess(proc)
|
|
|
- if len(args) > 0:
|
|
|
- auditCommand(ruid, args, cwd, env, 'Process %d' % pid)
|
|
|
+ if len(procargs) > 0:
|
|
|
+ auditCommand(ruid, procargs, cwd, env, 'Process %d' % pid)
|
|
|
|
|
|
# Passwords should be stored in /etc/shadow, not /etc/passwd
|
|
|
contentExceptions = []
|
|
@@ -361,6 +430,12 @@ for pw in pwd.getpwall():
|
|
|
if len(pw.pw_passwd) > 0 and pw.pw_passwd != 'x' and pw.pw_passwd != '*' and pw.pw_passwd != '!':
|
|
|
contentExceptions.append(Path('/etc/passwd'))
|
|
|
|
|
|
-logExceptions('These files contains sensible information', contentExceptions)
|
|
|
+if len(contentExceptions) > 0:
|
|
|
+ logExceptions('These files contains sensible information', contentExceptions)
|
|
|
+
|
|
|
+credExceptions = []
|
|
|
+for sp in spwd.getspall():
|
|
|
+ if len(sp.sp_pwdp) == 0:
|
|
|
+ logExceptions('Password for unix user %s is empty' % (sp.sp_namp))
|
|
|
|
|
|
printExceptions()
|