Advanced Web Server Manager
Complete File Manager & Terminal - Standalone Version
By Sid Gifari | Gifari Industries
Current path:
/
/
opt
/
cloudlinux
/
venv
/
lib
/
python3.11
/
site-packages
✏️
Editing: clsudo.py
# coding=utf-8 # Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2018 All Rights Reserved # # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENSE.TXT import logging import os import pwd import grp import re import subprocess import tempfile from stat import S_IRUSR, S_IRGRP logger = logging.getLogger(__name__) class NoSuchUser(Exception): def __init__(self, user): Exception.__init__(self, f'No such user ({user})') class NoSuchGroup(Exception): def __init__(self, group): Exception.__init__(self, f'No such group ({group})') class UnableToReadFile(Exception): def __init__(self): Exception.__init__(self, 'Cannot read sudoers file') class UnableToWriteFile(Exception): def __init__(self): Exception.__init__(self, 'Cannot modify sudoers file') SUDOERS_FILE = '/etc/sudoers' # argv-restricted: bare command paths grant arbitrary argv as root (sudoers # semantics). ps/grep/service have no in-tree sudo caller -> pin to no args (""); # lvectl's only sudo caller is `lvectl lve-version`. ALIAS_LVECTL_CMDS = ['/bin/ps ""', '/bin/grep ""', '/sbin/service ""', "/usr/bin/getcontrolpaneluserspackages", "/usr/sbin/lvectl lve-version", "/usr/local/directadmin/plugins/new_lvemanager/admin/GetDomains", "/usr/share/l.v.e-manager/utils/cloudlinux-cli.py"] ALIAS_LVECTL_USER_CMDS = ["/usr/share/l.v.e-manager/utils/cloudlinux-cli-user.py"] # argv-restricted (same rationale as ALIAS_LVECTL_CMDS): no in-tree consumer # invokes these via sudo, so pin to no args ("") rather than arbitrary argv as root. ALIAS_CAGEFS_CMDS = ['/usr/sbin/cagefsctl ""', '/bin/ps ""', '/bin/grep ""', '/sbin/service ""'] # Detect a CAGEFS_CMDS / SELECTOR_CMDS alias definition regardless of the # (visudo-legal) whitespace around Cmnd_Alias, the alias name, and `=`. A # substring match on a single-space form misses a valid non-canonical line, so # the upgrade-rewrite would skip it and append a duplicate alias that # `visudo -c` then rejects. CAGEFS_ALIAS_RE = re.compile(r'^\s*Cmnd_Alias\s+CAGEFS_CMDS\s*=') SELECTOR_ALIAS_RE = re.compile(r'^\s*Cmnd_Alias\s+SELECTOR_CMDS\s*=') LVECTL_ALIAS_RE = re.compile(r'^\s*Cmnd_Alias\s+LVECTL_CMDS\s*=') # argv-restricted (same rationale as ALIAS_LVECTL_CMDS/ALIAS_CAGEFS_CMDS): the # only in-tree sudo caller named cl_selector.py runs /usr/sbin/cloudlinux-selector # (a different path) as the target user, not these paths as root; lveps runs from # the lve-stats root daemon without sudo; piniset has no caller (deprecated). No # consumer invokes these via the NOPASSWD-root grant, so pin to no args ("") # rather than arbitrary argv as root. ALIAS_SELECTOR_CMDS = ['/usr/bin/cl-selector ""', '/usr/bin/piniset ""', '/usr/sbin/lveps ""', '/usr/bin/selectorctl ""'] DEFAULTS_REQUIRETTY = 'Defaults:%s !requiretty' # Patterns for group GROUP_LVECTL_SELECTOR = '%%%s ALL=NOPASSWD: LVECTL_CMDS, SELECTOR_CMDS' GROUP_DEFAULTS_REQUIRETTY = 'Defaults:%%%s !requiretty' class Clsudo: """ Adds CloudLinux users to sudoers file """ # Fail-closed allowlist for the write destination. `sudoers_file` is a # public parameter that flows into `filepath`; the only path the product # ever writes is /etc/sudoers. Restricting the destination here ensures a # caller that forwarded an attacker-influenced path is refused rather than # dropping a sudoers file at an arbitrary location. Tests override this. _ALLOWED_SUDOERS_PATHS = (SUDOERS_FILE,) filepath = None sudoers_list = [] has_action = False has_group_action = False has_alias = False has_user_alias = False has_rights = False has_user_rights = False has_selector_alias = False has_selector_rights = False has_cagefs_alias = False has_cagefs_rights = False @staticmethod def add_user(user, sudoers_file=SUDOERS_FILE): """ Adds username to sudoers file (for lvemanager) """ logger.info("add_user: adding user '%s' to sudoers file '%s' " "(uid=%d, pid=%d)", user, sudoers_file, os.getuid(), os.getpid()) # Update command lists for lvemanager Clsudo.update_commands_list(sudoers_file) Clsudo._check_user(user) Clsudo._get_contents(user) if not Clsudo.has_alias: Clsudo.sudoers_list.append('Cmnd_Alias LVECTL_CMDS = ' + ", ".join(ALIAS_LVECTL_CMDS)) if not Clsudo.has_user_alias: Clsudo.sudoers_list.append('Cmnd_Alias LVECTL_USER_CMDS = ' + ", ".join(ALIAS_LVECTL_USER_CMDS)) if not Clsudo.has_selector_alias: Clsudo.sudoers_list.append('Cmnd_Alias SELECTOR_CMDS = ' + ", ".join(ALIAS_SELECTOR_CMDS)) if not Clsudo.has_rights: Clsudo.sudoers_list.append(f'{user} ALL=NOPASSWD: LVECTL_CMDS') if not Clsudo.has_user_rights: Clsudo.sudoers_list.append(f'{user} ALL=(ALL) NOPASSWD: LVECTL_USER_CMDS') if not Clsudo.has_selector_rights: Clsudo.sudoers_list.append(f'{user} ALL=NOPASSWD: SELECTOR_CMDS') if not Clsudo.has_action: Clsudo.sudoers_list.append(DEFAULTS_REQUIRETTY % (user,)) Clsudo._write_contents() logger.info("add_user: successfully added user '%s' to '%s'", user, sudoers_file) @staticmethod def add_cagefs_user(user, sudoers_file=SUDOERS_FILE): """ Adds username to sudoers file (for cagefs) """ logger.info("add_cagefs_user: adding user '%s' to sudoers file '%s' " "(uid=%d, pid=%d)", user, sudoers_file, os.getuid(), os.getpid()) Clsudo.filepath = sudoers_file Clsudo._check_user(user) Clsudo._get_contents(user) if not Clsudo.has_cagefs_alias: Clsudo.sudoers_list.append('Cmnd_Alias CAGEFS_CMDS = ' + ", ".join(ALIAS_CAGEFS_CMDS)) if not Clsudo.has_cagefs_rights: Clsudo.sudoers_list.append(f'{user} ALL=NOPASSWD: CAGEFS_CMDS') if not Clsudo.has_action: Clsudo.sudoers_list.append(DEFAULTS_REQUIRETTY % (user,)) Clsudo._write_contents() logger.info("add_cagefs_user: successfully added user '%s' to '%s'", user, sudoers_file) @staticmethod def add_lvemanager_group(group_name, sudoers_file=SUDOERS_FILE): """ Adds group to sudoers file, grants access to LVE Manager """ logger.info("add_lvemanager_group: adding group '%s' to sudoers file '%s' " "(uid=%d, pid=%d)", group_name, sudoers_file, os.getuid(), os.getpid()) # Update command lists for lvemanager Clsudo.update_commands_list(sudoers_file) Clsudo._check_group(group_name) Clsudo._get_contents_group(group_name) if not Clsudo.has_alias: Clsudo.sudoers_list.append('Cmnd_Alias LVECTL_CMDS = ' + ", ".join(ALIAS_LVECTL_CMDS)) if not Clsudo.has_selector_alias: Clsudo.sudoers_list.append('Cmnd_Alias SELECTOR_CMDS = ' + ", ".join(ALIAS_SELECTOR_CMDS)) if not Clsudo.has_action: Clsudo.sudoers_list.append(GROUP_LVECTL_SELECTOR % (group_name,)) if not Clsudo.has_group_action: Clsudo.sudoers_list.append(GROUP_DEFAULTS_REQUIRETTY % (group_name,)) # writes file Clsudo._write_contents() logger.info("add_lvemanager_group: successfully added group '%s' to '%s'", group_name, sudoers_file) @staticmethod def remove_user(user, sudoers_file=SUDOERS_FILE): """ Removes username from sudoers file """ logger.info("remove_user: removing user '%s' from sudoers file '%s' " "(uid=%d, pid=%d)", user, sudoers_file, os.getuid(), os.getpid()) Clsudo.filepath = sudoers_file try: with open(Clsudo.filepath, encoding='utf-8') as f: Clsudo.sudoers_list = f.read().splitlines() idx = 0 removed = False while idx < len(Clsudo.sudoers_list): line = Clsudo.sudoers_list[idx] # Anchor matches at line start: a substring match would let # remove_user(<user>) also strip the rules of a different enrolled # admin whose name ends with <user> (remove_user('alice') must not # touch 'malice'). add_user writes both the plain `<user> ALL=NOPASSWD:` # rules and the `<user> ALL=(ALL) NOPASSWD: LVECTL_USER_CMDS` rule # (line 94); match the `(ALL) ` form too so it is not orphaned (CLOS-4593 [28]). if (line.startswith(f'{user} ALL=NOPASSWD:') or line.startswith(f'{user} ALL=(ALL) NOPASSWD:') or line.startswith(DEFAULTS_REQUIRETTY % (user,))): logger.info("remove_user: removing sudoers rule for user '%s': %s", user, line) Clsudo.sudoers_list.remove(line) removed = True continue idx += 1 if removed: Clsudo._write_contents() logger.info("remove_user: successfully removed user '%s' from '%s'", user, sudoers_file) else: logger.info("remove_user: no rules found for user '%s' in '%s'", user, sudoers_file) except (IOError, OSError) as e: raise UnableToReadFile() from e @staticmethod def update_user(user, sudoers_file=SUDOERS_FILE): """ updates username in sudoers file :param user: username for caching :param sudoers_file: path to /etc/sudoers (only for tests) :return: None """ logger.info("update_user: updating user '%s' in sudoers file '%s' " "(uid=%d, pid=%d)", user, sudoers_file, os.getuid(), os.getpid()) # Update command lists Clsudo.update_commands_list(sudoers_file) # For backward compatibility # Check user presence in system Clsudo._check_user(user) Clsudo._get_contents(user) @staticmethod def update_commands_list(sudoers_file=SUDOERS_FILE): """ Update command lists for lvemanager plugin If any required command absent in file, add it :param sudoers_file: path to /etc/sudoers :return: None """ # Read /etc/sudoers Clsudo.filepath = sudoers_file Clsudo.temp_dir = os.path.dirname(Clsudo.filepath) Clsudo._read_sudoers() # (regex, alias name, restricted command list) per upgradeable alias. # The anchored regexes tolerate any visudo-legal whitespace around # Cmnd_Alias / the alias name / `=`; _rewrite_alias collapses a # backslash line-continuation span and no-ops on an unrecognised form, # so an existing lax (or continuation) definition is upgraded in place # without duplication or corruption (same machinery as the CAGEFS_CMDS # upgrade in _get_contents). alias_specs = ((LVECTL_ALIAS_RE, 'LVECTL_CMDS', ALIAS_LVECTL_CMDS), (SELECTOR_ALIAS_RE, 'SELECTOR_CMDS', ALIAS_SELECTOR_CMDS)) is_sudoer_change = False for idx, command_string in enumerate(Clsudo.sudoers_list): for alias_re, alias_name, alias_list in alias_specs: if alias_re.match(command_string): before = Clsudo.sudoers_list[idx] Clsudo._rewrite_alias(idx, alias_name, alias_list) if Clsudo.sudoers_list[idx] != before: is_sudoer_change = True break if is_sudoer_change: logger.info("update_commands_list: updating command aliases in '%s' " "(uid=%d, pid=%d)", sudoers_file, os.getuid(), os.getpid()) Clsudo._write_contents() @staticmethod def _check_user(user): """ Checks passwd database for username presence @param user: string """ try: pwd.getpwnam(user) except KeyError as e: raise NoSuchUser(user) from e @staticmethod def _check_group(group_name): """ Checks grp database for group_name presence @param group_name: string """ try: grp.getgrnam(group_name) except KeyError as e: raise NoSuchGroup(group_name) from e @staticmethod def _read_sudoers(): with open(Clsudo.filepath, encoding='utf-8') as f: Clsudo.sudoers_list = f.read().splitlines() @staticmethod def _rewrite_alias(idx, alias_name, alias_cmds): """Rewrite the existing `alias_name` Cmnd_Alias definition at sudoers_list[idx] to the single canonical argv-restricted line, in place. Shared by the CAGEFS_CMDS and SELECTOR_CMDS upgrade rewrites - both are lax bare-command aliases whose pre-fix on-disk form must be tightened to the argv-restricted `alias_cmds` join without duplicating or corrupting the definition. The definition may span several physical lines via visudo-legal backslash line-continuation (_read_sudoers splits on physical lines, so each continuation line is its own list element). Consume the whole span - the matched line plus every following line the preceding line continued onto - and replace it with one restricted line. Never-corrupt invariant: if the definition cannot be confidently and safely collapsed (a continuation whose trailing backslash runs off the end of the file - i.e. the file is already malformed), leave it exactly as-is. The worst case of the upgrade rewrite is a safe no-op (the file stays as valid as it was), never a corrupted sudoers file. The caller still sets the has_*_alias flag, so no duplicate definition is appended. """ lines = Clsudo.sudoers_list end = idx # Walk every physical line the preceding line continued onto (trailing # backslash). A dangling continuation (last line of the span ends in a # backslash with no successor) is an already-malformed form we do not # recognise -> bail out, leaving the definition untouched. while lines[end].endswith('\\'): if end + 1 >= len(lines): return end += 1 restricted = f'Cmnd_Alias {alias_name} = ' + ", ".join(alias_cmds) if end == idx and lines[idx].strip() == restricted: return # Collapse [idx, end] (matched line + its continuation lines) to the # single restricted line. Only elements after idx are removed, so the # enumerate() in the caller resumes correctly at the next real line. lines[idx:end + 1] = [restricted] @staticmethod def _get_contents(user): """ Reads file into list of strings @param user: string """ # Clear all status flags Clsudo.has_action = False Clsudo.has_group_action = False Clsudo.has_alias = False Clsudo.has_user_alias = False Clsudo.has_rights = False Clsudo.has_user_rights = False Clsudo.has_selector_alias = False Clsudo.has_selector_rights = False Clsudo.has_cagefs_alias = False Clsudo.has_cagefs_rights = False require_tty_pattern = re.compile(rf'Defaults:\s*{user}\s*!requiretty') try: # Read sudoers file Clsudo._read_sudoers() for idx, command_string in enumerate(Clsudo.sudoers_list): if "Cmnd_Alias LVECTL_CMDS" in command_string: Clsudo.has_alias = True continue if "Cmnd_Alias LVECTL_USER_CMDS" in command_string: Clsudo.has_user_alias = True continue if CAGEFS_ALIAS_RE.match(command_string): # Upgrade: rewrite a pre-existing lax CAGEFS_CMDS definition # (bare command paths grant arbitrary argv as root) to the # argv-restricted form in place. Matched whitespace-tolerantly # and continuation-aware so a valid non-canonical or multi-line # definition is rewritten, not duplicated or corrupted. Clsudo._rewrite_alias(idx, 'CAGEFS_CMDS', ALIAS_CAGEFS_CMDS) Clsudo.has_cagefs_alias = True continue if f"{user} ALL=NOPASSWD: LVECTL_CMDS" in command_string: Clsudo.has_rights = True continue if f"{user} ALL=(ALL) NOPASSWD: LVECTL_USER_CMDS" in command_string: Clsudo.has_user_rights = True continue if f"{user} ALL=NOPASSWD: CAGEFS_CMDS" in command_string: Clsudo.has_cagefs_rights = True continue if "requiretty" in command_string: pattern_match = require_tty_pattern.search(command_string) if pattern_match: Clsudo.has_action = True continue if SELECTOR_ALIAS_RE.match(command_string): # Upgrade: rewrite a pre-existing lax SELECTOR_CMDS definition # (bare command paths grant arbitrary argv as root) to the # argv-restricted form in place, same whitespace-tolerant, # continuation-aware, never-corrupt rewrite as CAGEFS_CMDS. Clsudo._rewrite_alias(idx, 'SELECTOR_CMDS', ALIAS_SELECTOR_CMDS) Clsudo.has_selector_alias = True continue if f"{user} ALL=NOPASSWD: SELECTOR_CMDS" in command_string: Clsudo.has_selector_rights = True continue except (IOError, OSError) as e: raise UnableToReadFile() from e @staticmethod def _get_contents_group(group_name): """ Reads file into list of strings @param group_name: string """ # Clear all status flags Clsudo.has_action = False Clsudo.has_group_action = False Clsudo.has_alias = False Clsudo.has_rights = False Clsudo.has_selector_alias = False Clsudo.has_selector_rights = False Clsudo.has_cagefs_alias = False Clsudo.has_cagefs_rights = False group_prefix = f"%{group_name}" group_action = f"Defaults:%{group_name}" group_pattern = re.compile(rf'{group_name}\s*ALL=NOPASSWD:\s*LVECTL_CMDS,\s*SELECTOR_CMDS') try: # Read sudoers file Clsudo._read_sudoers() for idx, command_string in enumerate(Clsudo.sudoers_list): if SELECTOR_ALIAS_RE.match(command_string): # Upgrade: rewrite a pre-existing lax SELECTOR_CMDS definition # to the argv-restricted form in place (see _get_contents). Clsudo._rewrite_alias(idx, 'SELECTOR_CMDS', ALIAS_SELECTOR_CMDS) Clsudo.has_selector_alias = True continue if "Cmnd_Alias LVECTL_CMDS" in command_string: Clsudo.has_alias = True continue if CAGEFS_ALIAS_RE.match(command_string): # Upgrade: rewrite a pre-existing lax CAGEFS_CMDS definition # to the argv-restricted form in place (see _get_contents). Clsudo._rewrite_alias(idx, 'CAGEFS_CMDS', ALIAS_CAGEFS_CMDS) Clsudo.has_cagefs_alias = True continue if command_string.startswith(group_prefix): pattern_match = group_pattern.search(command_string) if pattern_match: Clsudo.has_action = True if command_string.startswith(group_action): Clsudo.has_group_action = True except (IOError, OSError) as e: raise UnableToReadFile() from e @staticmethod def _write_contents(): """ Writes data to temporary file then checks it and rewrites sudoers file """ # Fail-closed destination allowlist: visudo -c (below) validates sudoers # SYNTAX, not LOCATION, so the write target itself must be constrained. # Refuse any destination not on the approved list before creating or # renaming any file. allowed = {os.path.realpath(p) for p in Clsudo._ALLOWED_SUDOERS_PATHS} if os.path.realpath(Clsudo.filepath) not in allowed: logger.warning("_write_contents: refusing to write sudoers to " "disallowed destination '%s' (uid=%d, pid=%d)", Clsudo.filepath, os.getuid(), os.getpid()) raise UnableToWriteFile() try: temp_dir = os.path.dirname(Clsudo.filepath) temp_prefix = 'lve_sudoers_' fd, temp_path = tempfile.mkstemp(prefix=temp_prefix, dir=temp_dir) fo = os.fdopen(fd, 'w') fo.write('\n'.join(Clsudo.sudoers_list) + '\n') fo.close() mask = S_IRUSR | S_IRGRP os.chmod(temp_path, mask) if not Clsudo._is_file_valid(temp_path): raise IOError except (IOError, OSError) as e: logger.warning("_write_contents: failed to write sudoers file '%s' " "(uid=%d, pid=%d)", Clsudo.filepath, os.getuid(), os.getpid()) try: if os.path.exists(temp_path): os.unlink(temp_path) except Exception: pass raise UnableToWriteFile() from e try: os.rename(temp_path, Clsudo.filepath) except OSError as e: logger.warning("_write_contents: failed to rename temp file to '%s' " "(uid=%d, pid=%d)", Clsudo.filepath, os.getuid(), os.getpid()) raise UnableToWriteFile() from e @staticmethod def _is_file_valid(filename): cmd = [ '/usr/sbin/visudo', '-c', '-f', filename ] with subprocess.Popen( cmd, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) as proc: proc.communicate() if proc.returncode != 0: return False return True
💾 Save Changes
❌ Cancel