?
# coding=utf-8
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2019 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENSE.TXT
"""
clsudoers unix group management for lvemanager.
Operator upgrade note (CLOS-4576, F-37): ``add_unix_user_to_sudoers()`` now
fails closed when the ``clsudoers`` unix group has pre-existing members
outside the legitimate set (``admins()`` + the user being added +
package-owned members ``root`` / ``lvemanager``). Previously,
``groupadd -f`` was silently idempotent and any stowaway primary-GID
owners or ``gr_mem`` entries (left by manual admin steps or another
RPM's post-install) were quietly elevated to passwordless sudo for
selectorctl / cloudlinux-selector.
The allowlist explicitly includes ``root`` (implicit group membership in
many tools) and ``lvemanager`` (the package's runtime user, placed in
``clsudoers`` by the lvemanager RPM's ``%post`` for SPA's clsudo flow on
DirectAdmin/Plesk) — so the gate fires only on the F-37 attack signature
(a stowaway secondary member or a primary-GID-reuse owner), not on
legitimate package-installed state.
After upgrading to a build that ships this change, the first admin-add
operation on an AFFECTED host (one with non-package stowaways in the
group) raises an exception from ``_assert_sudoers_group_clean()``. To
recover, operators must either:
* remove the stowaway entries from the ``clsudoers`` group (audit with
``getent group clsudoers`` and ``awk -F: '$4==<gid> {print $1}' /etc/passwd``
for primary-GID owners), OR
* add those users via the panel's ``admins()`` interface (panel admin user
list) so they are recognised as legitimate members before retrying.
This is intentional — silent elevation of unaudited accounts is the
vulnerability being closed; reverting to ``groupadd -f`` idempotence is not
an acceptable workaround.
"""
from __future__ import print_function
from __future__ import division
from __future__ import absolute_import
import grp
import pwd
import subprocess
from cl_proc_hidepid import remount_proc
from clcommon.cpapi import admins, getCPName
from clcommon.sysctl import SysCtlConf, SYSCTL_CL_CONF_FILE
from clcommon.const import Feature
from clcommon.cpapi import is_panel_feature_supported
from clsudo import Clsudo
# Default admins group
DEFAULT_GROUP_NAME = "admin"
# Group name for fs.proc_super_gid
SUPER_GROUP_NAME = "clsupergid"
# Groupname for sudoers
SUDOERS_GROUP_NAME = "clsudoers"
# System-UID ceiling: an upper bound on the UID an account may have to be
# considered "package-installed" (`useradd --system`). The allowlist below
# additionally requires the account name to belong to the lvemanager
# package family — UID range alone is NOT the gate; it is the SECOND
# precondition. Hard-coding 999 is safe across CL7/8/9/10 and ubuntu22.
_SYSTEM_UID_MAX = 999
# Always-allowed package-owned member names. The lvemanager package's
# canonical SPA runtime user is `lvemanager`. `root` is implicit / always
# benign. DirectAdmin installs sometimes append a UUID suffix
# (`lvemanager_<uuid>`); the helper below matches that family by name
# prefix AND requires the matched account to live in the system-UID
# range. Anything else (a `daemon`/`bin`/`mail` system account, a
# regular user, a stowaway with a fabricated name) fails closed.
_PACKAGE_OWNED_CLSUDOERS_MEMBERS = frozenset({"root", "lvemanager"})
_PACKAGE_OWNED_CLSUDOERS_NAME_PREFIXES = ("lvemanager_",)
def _is_system_owned_user(name):
"""Return True if `name` is a known package-owned `clsudoers` member
(always-allowed name OR an lvemanager-package-family name) AND lives
in the system-UID range. Both conditions are required — a regular
user named `lvemanager_evil` (UID 5000) is NOT package-owned; an
unrelated system account named `daemon` (UID 2) is NOT package-owned
either. The gate exists to fire on the F-37 priv_group_reuse
signature; we only want to exempt the RPM's own post-install state.
"""
if name == "root":
# Implicit group membership in many tools; never a stowaway.
return True
is_known_name = (
name in _PACKAGE_OWNED_CLSUDOERS_MEMBERS or
any(name.startswith(p) for p in _PACKAGE_OWNED_CLSUDOERS_NAME_PREFIXES)
)
if not is_known_name:
return False
try:
return pwd.getpwnam(name).pw_uid <= _SYSTEM_UID_MAX
except KeyError:
# gr_mem references a non-existent user — that itself is suspicious
# (stowaway entry); let the gate flag it.
return False
def _add_user_to_group(user_name, group_name):
"""Add user to given unix group"""
retcode = subprocess.call(["/usr/bin/gpasswd", "-a", user_name, group_name])
if retcode != 0:
return False
return True
# Remove user from group
def _remove_user_from_group(user_name, group_name):
retcode = subprocess.call(["/usr/bin/gpasswd", "-d", user_name, group_name])
if retcode != 0:
return False
return True
def _add_admins_into_group(group_name, new_admin_name):
"""
Add all present DA admins (plus new_admin_name admin) to supplied group
:param new_admin_name: new admin name to add
:return:
"""
# Get admin list from DA and append new admin name to it
admin_list = list(admins())
if new_admin_name not in admin_list:
admin_list.append(new_admin_name)
for admin in admin_list:
_add_user_to_group(admin, group_name)
def _create_group(group_name):
"""Create group with given name"""
retcode = subprocess.call(["/usr/sbin/groupadd", "-f", group_name])
if retcode != 0:
return False
return True
def _add_admins_into_supergid_grp(new_admin_name):
"""
Add all present DA admins (plus new_admin_name admin) to current supergid group
:param new_admin_name: new admin name to add
:return:
"""
# Determine SUPER_GROUP_NAME gid
super_gid = str(grp.getgrnam(SUPER_GROUP_NAME).gr_gid)
sysctl_cfg = SysCtlConf(config_file=SYSCTL_CL_CONF_FILE)
# returns set gid from sysctl.conf or kernel default
proc_super_gid = sysctl_cfg.get('fs.proc_super_gid')
# set fs.proc_super_gid and add admins to group with this gid if:
# 1. it was not found in sysctl.conf;
if not sysctl_cfg.has_parameter('fs.proc_super_gid'):
sysctl_cfg.set('fs.proc_super_gid', super_gid)
_add_admins_into_group(SUPER_GROUP_NAME, new_admin_name)
return
elif getCPName() == 'DirectAdmin':
# Only for DA
try:
admin_gid = str(grp.getgrnam(DEFAULT_GROUP_NAME).gr_gid)
except KeyError:
admin_gid = None
if proc_super_gid == admin_gid:
sysctl_cfg.set('fs.proc_super_gid', super_gid)
_add_admins_into_group(SUPER_GROUP_NAME, new_admin_name)
return
# otherwise read fs.proc_super_gid and add admins to group with this gid
try:
proc_super_gid = int(proc_super_gid)
except ValueError:
raise RuntimeError("Bad fs.proc_super_gid option value in /etc/sysctl.conf")
# add all panel admins into custom proc_super_gid group
proc_super_name = grp.getgrgid(proc_super_gid).gr_name
_add_admins_into_group(proc_super_name, new_admin_name)
def _assert_sudoers_group_clean(group_name, expected_members):
"""
Refuse to grant %group_name passwordless sudo if the unix group already
exists with members or primary-GID owners that the lvemanager flow did
not place there. `groupadd -f` is silently idempotent and would otherwise
elevate any pre-existing member on the first call.
:param group_name: target group (clsudoers)
:param expected_members: iterable of usernames the lvemanager flow is
about to authorize (panel admins + the user being added).
:raises Exception: if any unexpected secondary member or primary-GID
owner is found.
"""
try:
gr = grp.getgrnam(group_name)
except KeyError:
# Group does not exist yet — _create_group will create it empty.
return
expected = set(expected_members)
# Secondary `gr_mem` entries that are neither expected (panel admins +
# the user being added) nor package-installed system accounts are the
# F-37 stowaway signature. The system-account allowlist covers
# legitimate runtime users the lvemanager RPM (and DA-side installers)
# place in the group at install time, including DA variants like
# `lvemanager_<uuid>` which still land in the UID system range.
unexpected_members = [
m for m in gr.gr_mem
if m not in expected and not _is_system_owned_user(m)
]
# Primary-GID owners are the canonical F-37 attack: a user account
# whose primary group landed on clsudoers.gr_gid via GID-reuse or a
# buggy installer. Only the lvemanager-package family of system
# accounts is legitimately allowed to have clsudoers as a primary
# group; an unrelated system user with the same primary GID is the
# priv_group_reuse signature and must trip the gate. Reuse the
# `_is_system_owned_user` predicate so the name + UID criteria stay
# in lockstep with the gr_mem path above.
unexpected_primary = [
p.pw_name for p in pwd.getpwall()
if p.pw_gid == gr.gr_gid
and p.pw_name not in expected
and not _is_system_owned_user(p.pw_name)
]
if unexpected_members or unexpected_primary:
raise Exception(
"ERROR: refusing to grant %%%s passwordless sudo: group already "
"has unexpected members %r and primary-GID owners %r" % (
group_name, unexpected_members, unexpected_primary))
def add_unix_user_to_sudoers(name):
# create all supergid stuff only if regular CL edition
if is_panel_feature_supported(Feature.LVE):
if not _create_group(SUPER_GROUP_NAME):
raise Exception("ERROR: Can't create %s group\n" % SUPER_GROUP_NAME)
_add_admins_into_supergid_grp(name)
if not _add_user_to_group(name, SUPER_GROUP_NAME):
raise Exception("ERROR: Can't add user %s to %s group\n" % (
name, SUPER_GROUP_NAME))
# Gate: if clsudoers already exists with non-lvemanager members, fail
# closed rather than silently elevate them via Clsudo.add_lvemanager_group.
# Allowlist members the lvemanager RPM itself places in the group at
# package install time (see _PACKAGE_OWNED_CLSUDOERS_MEMBERS) so the
# gate fires only on the F-37 attack signature, not on legitimate
# package-installed state.
expected_clsudoers = set(admins())
expected_clsudoers.add(name)
expected_clsudoers.update(_PACKAGE_OWNED_CLSUDOERS_MEMBERS)
_assert_sudoers_group_clean(SUDOERS_GROUP_NAME, expected_clsudoers)
if not _create_group(SUDOERS_GROUP_NAME):
raise Exception("ERROR: Can't create %s group\n" % SUDOERS_GROUP_NAME)
if not _add_user_to_group(name, SUDOERS_GROUP_NAME):
raise Exception("ERROR: Can't add user %s to %s group\n" % (
name, SUDOERS_GROUP_NAME))
# Add SUDOERS_GROUP_NAME group to /etc/sudoers
sudo = Clsudo()
sudo.add_lvemanager_group(SUDOERS_GROUP_NAME)
# CAG-796: use hidepid=2 when mounting /proc
remount_proc()
def remove_unix_user_from_sudoers(name):
# Remove user from all groups
_remove_user_from_group(name, SUPER_GROUP_NAME)
_remove_user_from_group(name, SUDOERS_GROUP_NAME)