concierge-permaudit 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. #!/usr/bin/python3
  2. # Code python modules
  3. import argparse
  4. import os
  5. import pwd
  6. import re
  7. import shutil
  8. import subprocess
  9. import sys
  10. import glob
  11. from pathlib import Path
  12. # Third party modules
  13. import psutil
  14. def get_perl_searchpath():
  15. # perl -e "print join $/, values @INC"
  16. try:
  17. res = subprocess.check_output(['perl', '-e', 'print join $/, values @INC'])
  18. return res.decode('utf-8').split("\n")
  19. except FileNotFoundError:
  20. return []
  21. def get_ruby_searchpath():
  22. # ruby -e 'puts $:'
  23. try:
  24. res = subprocess.check_output(['ruby', '-e', 'puts $:'])
  25. return res.decode('utf-8').split("\n")
  26. except FileNotFoundError:
  27. return []
  28. disRules = list()
  29. disRules.append(('/etc/apache2/sites-available/*', 'SSLCertificateKeyFile\s+(\S+)'))
  30. disRules.append(('/etc/dovecot/conf.d/10-ssl.conf', 'ssl_key\s*=\s*<(\S+)'))
  31. disRules.append(('/etc/nginx/sites-available/*', 'ssl_certificate_key\s+([^;]+);'))
  32. disRules.append(('/etc/postfix/main.cf', 'smtpd_tls_key_file\s*=\s*(\S+)'))
  33. readPatterns = [
  34. '/etc/shadow',
  35. '/etc/ssl/*_key',
  36. '~/.ssh/identity',
  37. '~/.ssh/id_dsa',
  38. '~/.ssh/id_rsa',
  39. '~/.ssh/id_ecdsa',
  40. '~/.ssh/id_ed25519',
  41. '~/.zsh_history', # zsh shell
  42. '~/.git-credentials',
  43. '~/.config/git/credentials',
  44. '~/.subversion/auth',
  45. '%APPDATA%/Subversion/auth/',
  46. '~/.hgrc',
  47. '~/.netrc',
  48. '~/.aws/credentials', # https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html
  49. '~/.docker/config.json', # https://docs.docker.com/engine/reference/commandline/login/
  50. '~/.config/gcloud/credentials.db', # https://cloud.google.com/storage/docs/gsutil/addlhelp/CredentialTypesSupportingVariousUseCases
  51. '~/.config/gcloud/legacy_credentials/*/*.json', # https://cloud.google.com/sdk/crypto
  52. '~/.config/gcloud/legacy_credentials/*/*.p12', # https://cloud.google.com/sdk/crypto
  53. '~/.config/filezilla/filezilla.xml',
  54. '~/.config/filezilla/sitemanager.xml',
  55. '~/.mozilla/firefox/*/key3.db', # mozilla firefox
  56. '~/.mozilla/firefox/*/logins.json', # mozilla firefox
  57. '~/.config/chromium/*/Default/Login Data', # chromium
  58. '~/.config/google-chrome/*/Default/Login Data', # google chrome
  59. '~/.opera/operaprefs.ini', # https://www.opera.com/docs/operafiles/
  60. '~/.icedove/*/key3.db',
  61. '~/.icedove/*/logins.json',
  62. '~/.thunderbird/*/key3.db',
  63. '~/.thunderbird/*/logins.json',
  64. '~/.purple/accounts.xml',
  65. '~/.python_history',
  66. '~/.config/hexchat/servlist.conf', # https://hexchat.readthedocs.io/fr/latest/faq.html
  67. '~/.config/hexchat/logs',
  68. '~/.irssi/config', # https://wiki.archlinux.org/index.php/Irssi#Configuration
  69. '~/.kde/share/config/konversationrc', # https://userbase.kde.org/Konversation/Tips_and_Tricks
  70. '~/.config/quassel-irc.org/quassel-storage.sqlite',
  71. '~/.weechat/irc.conf', # https://weechat.org/files/doc/stable/weechat_user.en.html#files_and_directories
  72. '~/.weechat/logs', # https://weechat.org/files/doc/stable/weechat_user.en.html#files_and_directories
  73. '~/.xchat2/servlist_.conf',
  74. '~/.xchat2/scrollback',
  75. '~/.xchat2/xchatlogs', # http://xchat.org/faq/
  76. '~/.bash_history',
  77. '~/.config/sonata/sonatarc',
  78. '/etc/graphite/local_settings.py',
  79. '/etc/roundcube/config.inc.php',
  80. '/etc/sympa/sympa.conf*',
  81. '/etc/dolibarr/conf.php*',
  82. '/etc/letsencrypt/archive/*/privkey*.pem',
  83. '/etc/letsencrypt/accounts/*/directory/*/private_key.json',
  84. '/etc/letsencrypt/keys/*.pem',
  85. '/etc/cups/ssl/*.key',
  86. '/etc/unbound/*.key',
  87. ]
  88. writePatterns = [
  89. '/etc/ld.so.conf',
  90. '/etc/ld.so.conf.d/*',
  91. '/etc/init.d/',
  92. '/etc/rc.local',
  93. '/etc/rd*.d/*',
  94. '/etc/cron.d/*',
  95. '/etc/cron.hourly/*',
  96. '/etc/cron.weekly/*',
  97. '/etc/cron.daily/*',
  98. '/etc/cron.monthly/*',
  99. '~/.aliases',
  100. '/etc/update-motd.d/*',
  101. '/etc/profile', # dash shell, bash shell
  102. '/etc/profile.d/*.sh', # dash shell, bash shell
  103. '~/.profile',
  104. '~/.config/autostart/*.desktop',
  105. '~/.inputrc',
  106. '/etc/bash.bashrc', # bash shell
  107. '~/.bashrc', # bash shell
  108. '~/.bash_aliases', # bash shell
  109. '/etc/bash_completion', # bash shell
  110. '/etc/bash_completion.d/*', # bash shell
  111. '~/.bash_completion', # bash shell
  112. '~/.bash_profile', # bash shell
  113. '~/.bash_login', # bash shell
  114. '~/.bash_logout', # bash shell
  115. '~/.config/fish/config.fish', # fish shell
  116. '~/.zprofile', # zsh shell
  117. '~/.zshrc', # zsh shell
  118. '~/.zlogin', # zsh shell
  119. '~/.zlogout', # zsh shell
  120. '/etc/csh.cshrc', # tcsh shell
  121. '/etc/csh.login', # tcsh shell
  122. '~/.tcshrc', # tcsh shell
  123. '~/.cshrc', # tcsh shell
  124. '~/.login', # tcsh shell
  125. '/etc/csh.logout', # tcsh shell
  126. '~/.logout' # tcsh shell
  127. ]
  128. interps = set([
  129. '/bin/sh',
  130. '/bin/bash',
  131. '/usr/bin/perl',
  132. '/usr/bin/python',
  133. '/usr/bin/python3'
  134. ])
  135. interpArgParse = argparse.ArgumentParser(description='Generic interpreter parser', add_help=False)
  136. interpArgParse.add_argument('interp', type=str)
  137. interpArgParse.add_argument('script', type=str)
  138. # Default values for Debian
  139. # TODO read values from /etc/login.defs
  140. UID_MIN=1000
  141. UID_MAX=60000
  142. homePaths = []
  143. for pw in pwd.getpwall():
  144. if pw.pw_uid in range(UID_MIN, UID_MAX):
  145. homePaths.append(pw.pw_dir)
  146. # Password database entry for root. By definition root uid = 0.
  147. rootPwe = pwd.getpwuid(0)
  148. def patternWalk(pattern):
  149. if pattern == '~' or pattern[:2] == '~/':
  150. for homePath in homePaths:
  151. yield from patternWalk(homePath + pattern[1:])
  152. else:
  153. yield from glob.glob(pattern)
  154. # Discover paths to file with sensible information
  155. for disRule in disRules:
  156. disPattern = disRule[0]
  157. disRe = re.compile(disRule[1])
  158. for disPath in patternWalk(disPattern):
  159. disFile = open(disPath, 'r')
  160. for match in re.finditer(disRe, disFile.read()):
  161. for group in match.groups():
  162. readPatterns.append(group)
  163. writePatternsParents = [
  164. ]
  165. for writePattern in writePatterns:
  166. path = Path(writePattern).parent
  167. homePath = Path('~')
  168. while path not in writePatternsParents:
  169. writePatternsParents.append(path)
  170. # Do not walk up past the home path for now
  171. if path != homePath:
  172. path = path.parent
  173. # Now, walk past the home path(s)
  174. for homePath in patternWalk('~'):
  175. path = Path(homePath).parent
  176. while path not in writePatternsParents:
  177. writePatternsParents.append(path)
  178. path = path.parent
  179. def hasMode(path, mode):
  180. try:
  181. return ( path.stat().st_mode & mode ) == mode
  182. except PermissionError:
  183. return ( 0o0000 & mode ) == mode
  184. def isWorldReadable(path):
  185. if hasMode(path, 0o0004):
  186. anchor = Path(path.anchor)
  187. path = path.parent
  188. while path != anchor:
  189. if hasMode(path, 0o0001):
  190. path = path.parent
  191. else:
  192. return False
  193. return True
  194. else:
  195. return False
  196. def isWorldWritable(path):
  197. anchor = Path(path.anchor)
  198. # If the path do not exist, find a parent that does
  199. while path != anchor and not path.exists():
  200. path = path.parent
  201. if hasMode(path, 0o0002):
  202. path = path.parent
  203. while path != anchor:
  204. if hasMode(path, 0o0001):
  205. path = path.parent
  206. else:
  207. return False
  208. return True
  209. else:
  210. return False
  211. exceptions = ''
  212. def logExceptions(message, pathList):
  213. global exceptions
  214. if len(pathList) > 0:
  215. exceptions += "%s: \n" % message
  216. for path in (pathList):
  217. exceptions += " * %s\n" % path.as_posix()
  218. exceptions += "\n"
  219. def logException(description, path, context = None):
  220. global exceptions
  221. exceptions += "%s\n" % description
  222. exceptions += " Path: %s\n" % path.as_posix()
  223. if context != None:
  224. exceptions += " Context: %s\n" % context
  225. exceptions += "\n"
  226. def printExceptions():
  227. global exceptions
  228. print(exceptions, end='')
  229. def auditProcess(proc):
  230. ruid = proc.uids()[0]
  231. pid = proc.pid
  232. exePathStr = proc.exe()
  233. if len(exePathStr) > 0:
  234. exePath = Path(exePathStr)
  235. try:
  236. if (exePath.stat().st_uid != rootPwe.pw_uid and
  237. exePath.stat().st_uid != ruid):
  238. logException('Executable is owned by another, non-root user', exePath.resolve(), 'Process %d' % proc.pid)
  239. except:
  240. pass
  241. if isWorldWritable(exePath):
  242. logException('Executable is world-writable', exePath.resolve(), 'Process %d' % proc.pid)
  243. def auditCommand(ruid, argList, cwd, env = {}, context = None):
  244. if 'PATH' in env:
  245. path = env['PATH']
  246. else:
  247. path = os.defpath
  248. absArg0 = shutil.which(argList[0], path=path)
  249. if absArg0 in interps and len(argList) > 1:
  250. (args, remainining) = interpArgParse.parse_known_args(argList)
  251. scriptPath = Path(args.script)
  252. if not scriptPath.is_absolute():
  253. scriptPath = Path(cwd, args.script)
  254. try:
  255. scriptPath = scriptPath.resolve()
  256. if (scriptPath.stat().st_uid != rootPwe.pw_uid and
  257. scriptPath.stat().st_uid != ruid):
  258. logException('Script is owned by another, non-root user', scriptPath.resolve(), context)
  259. except FileNotFoundError:
  260. pass # warning('File not found')
  261. if isWorldWritable(scriptPath):
  262. logException('Script is world-writable', scriptPath.resolve(), context)
  263. readExceptions = []
  264. for pattern in readPatterns:
  265. for strPath in patternWalk(pattern):
  266. path = Path(strPath)
  267. if isWorldReadable(path):
  268. readExceptions.append(path)
  269. logExceptions('These paths are world readable', readExceptions)
  270. writeExceptions = []
  271. for pattern in writePatterns:
  272. for strPath in patternWalk(pattern):
  273. path = Path(strPath)
  274. if isWorldWritable(path):
  275. writeExceptions.append(path)
  276. logExceptions('These paths are world-writable', writeExceptions)
  277. parentWriteExceptions = []
  278. for pattern in writePatternsParents:
  279. for strPath in patternWalk(str(pattern)):
  280. path = Path(strPath)
  281. if isWorldWritable(path):
  282. parentWriteExceptions.append(path)
  283. logExceptions('These paths are world-writable', parentWriteExceptions)
  284. execpathWriteExceptions = []
  285. for strPath in os.get_exec_path():
  286. path = Path(strPath)
  287. if isWorldWritable(path):
  288. execpathWriteExceptions.append(path)
  289. logExceptions('These executable search paths are world-writable', execpathWriteExceptions)
  290. pythonpathWriteExceptions = []
  291. for strPath in sys.path:
  292. path = Path(strPath)
  293. if isWorldWritable(path):
  294. pythonpathWriteExceptions.append(path)
  295. logExceptions('These python search paths are world-writable', pythonpathWriteExceptions)
  296. perlpathWriteExceptions = []
  297. for strPath in get_perl_searchpath():
  298. path = Path(strPath)
  299. if isWorldWritable(path):
  300. perlpathWriteExceptions.append(path)
  301. logExceptions('These perl search paths are world-writable', perlpathWriteExceptions)
  302. rubypathWriteExceptions = []
  303. for strPath in get_ruby_searchpath():
  304. path = Path(strPath)
  305. if isWorldWritable(path):
  306. rubypathWriteExceptions.append(path)
  307. logExceptions('These ruby search paths are world-writable', rubypathWriteExceptions)
  308. for proc in psutil.process_iter():
  309. pid = proc.pid
  310. ruid = proc.uids()[0]
  311. args = proc.cmdline()
  312. cwd = proc.cwd()
  313. env = proc.environ()
  314. auditProcess(proc)
  315. if len(args) > 0:
  316. auditCommand(ruid, args, cwd, env, 'Process %d' % pid)
  317. # Passwords should be stored in /etc/shadow, not /etc/passwd
  318. contentExceptions = []
  319. for pw in pwd.getpwall():
  320. if len(pw.pw_passwd) > 0 and pw.pw_passwd != 'x' and pw.pw_passwd != '*' and pw.pw_passwd != '!':
  321. contentExceptions.append(Path('/etc/passwd'))
  322. logExceptions('These files contains sensible information', contentExceptions)
  323. printExceptions()