123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366 |
- #!/usr/bin/python3
- # Code python modules
- import argparse
- import os
- import pwd
- import re
- import shutil
- import subprocess
- import sys
- import glob
- from pathlib import Path
- # Third party modules
- import psutil
- def get_perl_searchpath():
- # perl -e "print join $/, values @INC"
- try:
- res = subprocess.check_output(['perl', '-e', 'print join $/, values @INC'])
- return res.decode('utf-8').split("\n")
- except FileNotFoundError:
- 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.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+)'))
- readPatterns = [
- '/etc/shadow',
- '/etc/ssl/*_key',
- '~/.ssh/identity',
- '~/.ssh/id_dsa',
- '~/.ssh/id_rsa',
- '~/.ssh/id_ecdsa',
- '~/.ssh/id_ed25519',
- '~/.zsh_history', # zsh shell
- '~/.git-credentials',
- '~/.config/git/credentials',
- '~/.subversion/auth',
- '%APPDATA%/Subversion/auth/',
- '~/.hgrc',
- '~/.netrc',
- '~/.aws/credentials', # https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html
- '~/.docker/config.json', # https://docs.docker.com/engine/reference/commandline/login/
- '~/.config/gcloud/credentials.db', # https://cloud.google.com/storage/docs/gsutil/addlhelp/CredentialTypesSupportingVariousUseCases
- '~/.config/gcloud/legacy_credentials/*/*.json', # https://cloud.google.com/sdk/crypto
- '~/.config/gcloud/legacy_credentials/*/*.p12', # https://cloud.google.com/sdk/crypto
- '~/.config/filezilla/filezilla.xml',
- '~/.config/filezilla/sitemanager.xml',
- '~/.mozilla/firefox/*/key3.db', # mozilla firefox
- '~/.mozilla/firefox/*/logins.json', # mozilla firefox
- '~/.config/chromium/*/Default/Login Data', # chromium
- '~/.config/google-chrome/*/Default/Login Data', # google chrome
- '~/.opera/operaprefs.ini', # https://www.opera.com/docs/operafiles/
- '~/.icedove/*/key3.db',
- '~/.icedove/*/logins.json',
- '~/.thunderbird/*/key3.db',
- '~/.thunderbird/*/logins.json',
- '~/.purple/accounts.xml',
- '~/.python_history',
- '~/.config/hexchat/servlist.conf', # https://hexchat.readthedocs.io/fr/latest/faq.html
- '~/.config/hexchat/logs',
- '~/.irssi/config', # https://wiki.archlinux.org/index.php/Irssi#Configuration
- '~/.kde/share/config/konversationrc', # https://userbase.kde.org/Konversation/Tips_and_Tricks
- '~/.config/quassel-irc.org/quassel-storage.sqlite',
- '~/.weechat/irc.conf', # https://weechat.org/files/doc/stable/weechat_user.en.html#files_and_directories
- '~/.weechat/logs', # https://weechat.org/files/doc/stable/weechat_user.en.html#files_and_directories
- '~/.xchat2/servlist_.conf',
- '~/.xchat2/scrollback',
- '~/.xchat2/xchatlogs', # http://xchat.org/faq/
- '~/.bash_history',
- '~/.config/sonata/sonatarc',
- '/etc/graphite/local_settings.py',
- '/etc/roundcube/config.inc.php',
- '/etc/sympa/sympa.conf*',
- '/etc/dolibarr/conf.php*',
- '/etc/letsencrypt/archive/*/privkey*.pem',
- '/etc/letsencrypt/accounts/*/directory/*/private_key.json',
- '/etc/letsencrypt/keys/*.pem',
- '/etc/cups/ssl/*.key',
- '/etc/unbound/*.key',
- ]
- writePatterns = [
- '/etc/ld.so.conf',
- '/etc/ld.so.conf.d/*',
- '/etc/init.d/',
- '/etc/rc.local',
- '/etc/rd*.d/*',
- '/etc/cron.d/*',
- '/etc/cron.hourly/*',
- '/etc/cron.weekly/*',
- '/etc/cron.daily/*',
- '/etc/cron.monthly/*',
- '~/.aliases',
- '/etc/update-motd.d/*',
- '/etc/profile', # dash shell, bash shell
- '/etc/profile.d/*.sh', # dash shell, bash shell
- '~/.profile',
- '~/.config/autostart/*.desktop',
- '~/.inputrc',
- '/etc/bash.bashrc', # bash shell
- '~/.bashrc', # bash shell
- '~/.bash_aliases', # bash shell
- '/etc/bash_completion', # bash shell
- '/etc/bash_completion.d/*', # bash shell
- '~/.bash_completion', # bash shell
- '~/.bash_profile', # bash shell
- '~/.bash_login', # bash shell
- '~/.bash_logout', # bash shell
- '~/.config/fish/config.fish', # fish shell
- '~/.zprofile', # zsh shell
- '~/.zshrc', # zsh shell
- '~/.zlogin', # zsh shell
- '~/.zlogout', # zsh shell
- '/etc/csh.cshrc', # tcsh shell
- '/etc/csh.login', # tcsh shell
- '~/.tcshrc', # tcsh shell
- '~/.cshrc', # tcsh shell
- '~/.login', # tcsh shell
- '/etc/csh.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
- # TODO read values from /etc/login.defs
- UID_MIN=1000
- UID_MAX=60000
- homePaths = []
- for pw in pwd.getpwall():
- if pw.pw_uid in range(UID_MIN, UID_MAX):
- homePaths.append(pw.pw_dir)
- # Password database entry for root. By definition root uid = 0.
- rootPwe = pwd.getpwuid(0)
- def patternWalk(pattern):
- if pattern == '~' or pattern[:2] == '~/':
- for homePath in homePaths:
- yield from patternWalk(homePath + pattern[1:])
- else:
- yield from glob.glob(pattern)
- # Discover paths to file with sensible information
- for disRule in disRules:
- disPattern = disRule[0]
- disRe = re.compile(disRule[1])
- for disPath in patternWalk(disPattern):
- disFile = open(disPath, 'r')
- for match in re.finditer(disRe, disFile.read()):
- for group in match.groups():
- readPatterns.append(group)
- writePatternsParents = [
- ]
- for writePattern in writePatterns:
- path = Path(writePattern).parent
- homePath = Path('~')
- while path not in writePatternsParents:
- writePatternsParents.append(path)
- # Do not walk up past the home path for now
- if path != homePath:
- path = path.parent
- # Now, walk past the home path(s)
- for homePath in patternWalk('~'):
- path = Path(homePath).parent
- while path not in writePatternsParents:
- writePatternsParents.append(path)
- path = path.parent
- def hasMode(path, mode):
- try:
- return ( path.stat().st_mode & mode ) == mode
- except PermissionError:
- return ( 0o0000 & mode ) == mode
- def isWorldReadable(path):
- if hasMode(path, 0o0004):
- anchor = Path(path.anchor)
- path = path.parent
- while path != anchor:
- if hasMode(path, 0o0001):
- path = path.parent
- else:
- return False
- return True
- else:
- return False
- def isWorldWritable(path):
- anchor = Path(path.anchor)
- # If the path do not exist, find a parent that does
- while path != anchor and not path.exists():
- path = path.parent
- if hasMode(path, 0o0002):
- path = path.parent
- while path != anchor:
- if hasMode(path, 0o0001):
- path = path.parent
- else:
- return False
- return True
- else:
- return False
- 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):
- 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():
- 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:
- 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 = []
- for pattern in readPatterns:
- for strPath in patternWalk(pattern):
- path = Path(strPath)
- if isWorldReadable(path):
- readExceptions.append(path)
- logExceptions('These paths are world readable', readExceptions)
- writeExceptions = []
- for pattern in writePatterns:
- for strPath in patternWalk(pattern):
- path = Path(strPath)
- if isWorldWritable(path):
- writeExceptions.append(path)
- logExceptions('These paths are world-writable', writeExceptions)
- parentWriteExceptions = []
- for pattern in writePatternsParents:
- for strPath in patternWalk(str(pattern)):
- path = Path(strPath)
- if isWorldWritable(path):
- parentWriteExceptions.append(path)
- logExceptions('These paths are world-writable', parentWriteExceptions)
- execpathWriteExceptions = []
- for strPath in os.get_exec_path():
- path = Path(strPath)
- if isWorldWritable(path):
- execpathWriteExceptions.append(path)
- logExceptions('These executable search paths are world-writable', execpathWriteExceptions)
- pythonpathWriteExceptions = []
- for strPath in sys.path:
- path = Path(strPath)
- if isWorldWritable(path):
- pythonpathWriteExceptions.append(path)
- logExceptions('These python search paths are world-writable', pythonpathWriteExceptions)
- perlpathWriteExceptions = []
- for strPath in get_perl_searchpath():
- path = Path(strPath)
- if isWorldWritable(path):
- perlpathWriteExceptions.append(path)
- 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
- contentExceptions = []
- 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)
- printExceptions()
|