mac_do: add a new MAC/do policy and mdo(1) utility

This policy enables a user to become another user without having to be
root (hence no setuid binary). it is configured via rules using sysctl
security.mac.do.rules

For example:
security.mac.do.rules=uid=1001:80,gid=0:any

The above rule means the user identifier by the uid 1001 is able to
become user 80
Any user of the group 0 are allowed to become any user on the system.

The mdo(1) utility expects the MAC/do policy to be installed and its
rules defined.

Reviewed by:	des
Differential Revision:	https://reviews.freebsd.org/D45145
This commit is contained in:
Baptiste Daroussin 2024-05-10 00:03:28 +02:00
parent ac4ddc467b
commit 8aac90f18a
9 changed files with 757 additions and 0 deletions

View File

@ -297,6 +297,7 @@ MAN= aac.4 \
mac_biba.4 \
mac_bsdextended.4 \
mac_ddb.4 \
mac_do.4 \
mac_ifoff.4 \
mac_ipacl.4 \
mac_lomac.4 \

78
share/man/man4/mac_do.4 Normal file
View File

@ -0,0 +1,78 @@
.\"-
.\" Copyright (c) 2024 Baptiste Daroussin <bapt@FreeBSD.org>
.\"
.\" SPDX-License-Identifier: BSD-2-Clause
.\"
.Dd May 22, 2024
.Dt MAC_DO 4
.Os
.Sh NAME
.Nm mac_do
.Nd "policy allowing user to execute program as another user"
.Sh SYNOPSIS
To compile the
.Nm
policy into your kernel, place the following lines
in your kernel configruation file:
.Bd -ragged -offset indent
.Cd "options MAC"
.Cd "options MAC_DO"
.Ed
.Sh DESCRIPTION
The
.Nm
policy grants users the ability to run processs as other users
according to predefined rules.
.Pp
The exact set of kernel privileges granted are:
.Bl -inset -compact -offset indent
.It Dv PRIV_CRED_SETGROUPS
.It Dv PRIV_CRET_SETUID
.El
.Pp
The following
.Xr sysctl 8
MIBs are available:
.Bl -tag -width indent
.It Va security.mac.do.enabled
Enable the
.Nm
policy.
(Default: 1).
.It Va security.mac.do.rules
The set of rules.
.El
.Pp
The rules consist of a list of elements separated by
.So , Sc .
Each element is of the form
.Sm off
.Do
.Op Cm uid | Cm gid
.Li =
.Ar fid
.Li :
.Ar tid
.Dc
.Sm on .
Where
.Ar fid
is the uid or gid of the user or group the rule applies to, and
.Ar tid
is the uid of the targetted user.
Two special forms are accepted for
.Ar tid :
.Va any
or
.Va * ,
which allow to target any user.
.Sh EXAMPLES
The following rule:
.Pp
.Dl security.mac.do.rules=uid=1001:80,gid=0:any
.Pp
means the user with the uid 1001 can execute processes as user with uid 80,
all the users which belongs to the group gid 0 can execute processes as any user.
.Sh SEE ALSO
.Xr mac 4 ,
.Xr mdo 1

View File

@ -226,6 +226,7 @@ SUBDIR= \
${_mac_biba} \
${_mac_bsdextended} \
${_mac_ddb} \
${_mac_do} \
${_mac_ifoff} \
${_mac_ipacl} \
${_mac_lomac} \
@ -587,6 +588,7 @@ _mac_bsdextended= mac_bsdextended
.if ${KERN_OPTS:MDDB} || defined(ALL_MODULES)
_mac_ddb= mac_ddb
.endif
_mac_do= mac_do
_mac_ifoff= mac_ifoff
_mac_ipacl= mac_ipacl
_mac_lomac= mac_lomac

View File

@ -0,0 +1,6 @@
.PATH: ${SRCTOP}/sys/security/mac_do
KMOD= mac_do
SRCS= mac_do.c vnode_if.h
.include <bsd.kmod.mk>

View File

@ -0,0 +1,545 @@
/*-
* Copyright(c) 2024 Baptiste Daroussin <bapt@FreeBSD.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <sys/param.h>
#include <sys/malloc.h>
#include <sys/jail.h>
#include <sys/kernel.h>
#include <sys/lock.h>
#include <sys/module.h>
#include <sys/mount.h>
#include <sys/mutex.h>
#include <sys/priv.h>
#include <sys/proc.h>
#include <sys/socket.h>
#include <sys/sx.h>
#include <sys/sysctl.h>
#include <sys/systm.h>
#include <sys/ucred.h>
#include <sys/vnode.h>
#include <security/mac/mac_policy.h>
SYSCTL_DECL(_security_mac);
static SYSCTL_NODE(_security_mac, OID_AUTO, do,
CTLFLAG_RW|CTLFLAG_MPSAFE, 0, "mac_do policy controls");
static int do_enabled = 1;
SYSCTL_INT(_security_mac_do, OID_AUTO, enabled, CTLFLAG_RWTUN,
&do_enabled, 0, "Enforce do policy");
static MALLOC_DEFINE(M_DO, "do_rule", "Rules for mac_do");
#define MAC_RULE_STRING_LEN 1024
static unsigned mac_do_osd_jail_slot;
#define RULE_UID 1
#define RULE_GID 2
#define RULE_ANY 3
struct rule {
int from_type;
union {
uid_t f_uid;
gid_t f_gid;
};
int to_type;
uid_t t_uid;
TAILQ_ENTRY(rule) r_entries;
};
struct mac_do_rule {
char string[MAC_RULE_STRING_LEN];
TAILQ_HEAD(rulehead, rule) head;
};
static struct mac_do_rule rules0;
static void
toast_rules(struct rulehead *head)
{
struct rule *r;
while ((r = TAILQ_FIRST(head)) != NULL) {
TAILQ_REMOVE(head, r, r_entries);
free(r, M_DO);
}
}
static int
parse_rule_element(char *element, struct rule **rule)
{
int error = 0;
char *type, *id, *p;
struct rule *new;
new = malloc(sizeof(*new), M_DO, M_ZERO|M_WAITOK);
type = strsep(&element, "=");
if (type == NULL) {
error = EINVAL;
goto out;
}
if (strcmp(type, "uid") == 0) {
new->from_type = RULE_UID;
} else if (strcmp(type, "gid") == 0) {
new->from_type = RULE_GID;
} else {
error = EINVAL;
goto out;
}
id = strsep(&element, ":");
if (id == NULL) {
error = EINVAL;
goto out;
}
if (new->from_type == RULE_UID)
new->f_uid = strtol(id, &p, 10);
if (new->from_type == RULE_GID)
new->f_gid = strtol(id, &p, 10);
if (*p != '\0') {
error = EINVAL;
goto out;
}
if (*element == '\0') {
error = EINVAL;
goto out;
}
if (strcmp(element, "any") == 0 || strcmp(element, "*") == 0) {
new->to_type = RULE_ANY;
} else {
new->to_type = RULE_UID;
new->t_uid = strtol(element, &p, 10);
if (*p != '\0') {
error = EINVAL;
goto out;
}
}
out:
if (error != 0) {
free(new, M_DO);
*rule = NULL;
} else
*rule = new;
return (error);
}
static int
parse_rules(char *string, struct rulehead *head)
{
struct rule *new;
char *element;
int error = 0;
while ((element = strsep(&string, ",")) != NULL) {
if (strlen(element) == 0)
continue;
error = parse_rule_element(element, &new);
if (error)
goto out;
TAILQ_INSERT_TAIL(head, new, r_entries);
}
out:
if (error != 0)
toast_rules(head);
return (error);
}
static struct mac_do_rule *
mac_do_rule_find(struct prison *spr, struct prison **prp)
{
struct prison *pr;
struct mac_do_rule *rules;
for (pr = spr;; pr = pr->pr_parent) {
mtx_lock(&pr->pr_mtx);
if (pr == &prison0) {
rules = &rules0;
break;
}
rules = osd_jail_get(pr, mac_do_osd_jail_slot);
if (rules != NULL)
break;
mtx_unlock(&pr->pr_mtx);
}
*prp = pr;
return (rules);
}
static int
sysctl_rules(SYSCTL_HANDLER_ARGS)
{
char *copy_string, *new_string;
struct rulehead head, saved_head;
struct prison *pr;
struct mac_do_rule *rules;
int error;
rules = mac_do_rule_find(req->td->td_ucred->cr_prison, &pr);
mtx_unlock(&pr->pr_mtx);
if (req->newptr == NULL)
return (sysctl_handle_string(oidp, rules->string, MAC_RULE_STRING_LEN, req));
new_string = malloc(MAC_RULE_STRING_LEN, M_DO,
M_WAITOK|M_ZERO);
mtx_lock(&pr->pr_mtx);
strlcpy(new_string, rules->string, MAC_RULE_STRING_LEN);
mtx_unlock(&pr->pr_mtx);
error = sysctl_handle_string(oidp, new_string, MAC_RULE_STRING_LEN, req);
if (error)
goto out;
copy_string = strdup(new_string, M_DO);
TAILQ_INIT(&head);
error = parse_rules(copy_string, &head);
free(copy_string, M_DO);
if (error)
goto out;
TAILQ_INIT(&saved_head);
mtx_lock(&pr->pr_mtx);
TAILQ_CONCAT(&saved_head, &rules->head, r_entries);
TAILQ_CONCAT(&rules->head, &head, r_entries);
strlcpy(rules->string, new_string, MAC_RULE_STRING_LEN);
mtx_unlock(&pr->pr_mtx);
toast_rules(&saved_head);
out:
free(new_string, M_DO);
return (error);
}
SYSCTL_PROC(_security_mac_do, OID_AUTO, rules,
CTLTYPE_STRING|CTLFLAG_RW|CTLFLAG_MPSAFE,
0, 0, sysctl_rules, "A",
"Rules");
static void
destroy(struct mac_policy_conf *mpc)
{
osd_jail_deregister(mac_do_osd_jail_slot);
toast_rules(&rules0.head);
}
static void
mac_do_alloc_prison(struct prison *pr, struct mac_do_rule **lrp)
{
struct prison *ppr;
struct mac_do_rule *rules, *new_rules;
void **rsv;
rules = mac_do_rule_find(pr, &ppr);
if (ppr == pr)
goto done;
mtx_unlock(&ppr->pr_mtx);
new_rules = malloc(sizeof(*new_rules), M_PRISON, M_WAITOK|M_ZERO);
rsv = osd_reserve(mac_do_osd_jail_slot);
rules = mac_do_rule_find(pr, &ppr);
if (ppr == pr) {
free(new_rules, M_PRISON);
osd_free_reserved(rsv);
goto done;
}
mtx_lock(&pr->pr_mtx);
osd_jail_set_reserved(pr, mac_do_osd_jail_slot, rsv, new_rules);
TAILQ_INIT(&new_rules->head);
done:
if (lrp != NULL)
*lrp = rules;
mtx_unlock(&pr->pr_mtx);
mtx_unlock(&ppr->pr_mtx);
}
static void
mac_do_dealloc_prison(void *data)
{
struct mac_do_rule *r = data;
toast_rules(&r->head);
}
static int
mac_do_prison_set(void *obj, void *data)
{
struct prison *pr = obj;
struct vfsoptlist *opts = data;
struct rulehead head, saved_head;
struct mac_do_rule *rules;
char *rules_string, *copy_string;
int error, jsys, len;
error = vfs_copyopt(opts, "mdo", &jsys, sizeof(jsys));
if (error == ENOENT)
jsys = -1;
error = vfs_getopt(opts, "mdo.rules", (void **)&rules_string, &len);
if (error == ENOENT)
rules = NULL;
else
jsys = JAIL_SYS_NEW;
switch (jsys) {
case JAIL_SYS_INHERIT:
mtx_lock(&pr->pr_mtx);
osd_jail_del(pr, mac_do_osd_jail_slot);
mtx_unlock(&pr->pr_mtx);
break;
case JAIL_SYS_NEW:
mac_do_alloc_prison(pr, &rules);
if (rules_string == NULL)
break;
copy_string = strdup(rules_string, M_DO);
TAILQ_INIT(&head);
error = parse_rules(copy_string, &head);
free(copy_string, M_DO);
if (error)
return (1);
TAILQ_INIT(&saved_head);
mtx_lock(&pr->pr_mtx);
TAILQ_CONCAT(&saved_head, &rules->head, r_entries);
TAILQ_CONCAT(&rules->head, &head, r_entries);
strlcpy(rules->string, rules_string, MAC_RULE_STRING_LEN);
mtx_unlock(&pr->pr_mtx);
toast_rules(&saved_head);
break;
}
return (0);
}
SYSCTL_JAIL_PARAM_SYS_NODE(mdo, CTLFLAG_RW, "Jail MAC/do parameters");
SYSCTL_JAIL_PARAM_STRING(_mdo, rules, CTLFLAG_RW, MAC_RULE_STRING_LEN,
"Jail MAC/do rules");
static int
mac_do_prison_get(void *obj, void *data)
{
struct prison *ppr, *pr = obj;
struct vfsoptlist *opts = data;
struct mac_do_rule *rules;
int jsys, error;
rules = mac_do_rule_find(pr, &ppr);
error = vfs_setopt(opts, "mdo", &jsys, sizeof(jsys));
if (error != 0 && error != ENOENT)
goto done;
error = vfs_setopts(opts, "mdo.rules", rules->string);
if (error != 0 && error != ENOENT)
goto done;
mtx_unlock(&ppr->pr_mtx);
error = 0;
done:
return (0);
}
static int
mac_do_prison_create(void *obj, void *data __unused)
{
struct prison *pr = obj;
mac_do_alloc_prison(pr, NULL);
return (0);
}
static int
mac_do_prison_remove(void *obj, void *data __unused)
{
struct prison *pr = obj;
struct mac_do_rule *r;
mtx_lock(&pr->pr_mtx);
r = osd_jail_get(pr, mac_do_osd_jail_slot);
mtx_unlock(&pr->pr_mtx);
toast_rules(&r->head);
return (0);
}
static int
mac_do_prison_check(void *obj, void *data)
{
struct vfsoptlist *opts = data;
char *rules_string;
int error, jsys, len;
error = vfs_copyopt(opts, "mdo", &jsys, sizeof(jsys));
if (error != ENOENT) {
if (error != 0)
return (error);
if (jsys != JAIL_SYS_NEW && jsys != JAIL_SYS_INHERIT)
return (EINVAL);
}
error = vfs_getopt(opts, "mdo.rules", (void **)&rules_string, &len);
if (error != ENOENT) {
if (error != 0)
return (error);
if (len > MAC_RULE_STRING_LEN) {
vfs_opterror(opts, "mdo.rules too long");
return (ENAMETOOLONG);
}
}
if (error == ENOENT)
error = 0;
return (error);
}
static void
init(struct mac_policy_conf *mpc)
{
static osd_method_t methods[PR_MAXMETHOD] = {
[PR_METHOD_CREATE] = mac_do_prison_create,
[PR_METHOD_GET] = mac_do_prison_get,
[PR_METHOD_SET] = mac_do_prison_set,
[PR_METHOD_CHECK] = mac_do_prison_check,
[PR_METHOD_REMOVE] = mac_do_prison_remove,
};
struct prison *pr;
mac_do_osd_jail_slot = osd_jail_register(mac_do_dealloc_prison, methods);
TAILQ_INIT(&rules0.head);
sx_slock(&allprison_lock);
TAILQ_FOREACH(pr, &allprison, pr_list)
mac_do_alloc_prison(pr, NULL);
sx_sunlock(&allprison_lock);
}
static bool
rule_is_valid(struct ucred *cred, struct rule *r)
{
if (r->from_type == RULE_UID && r->f_uid == cred->cr_uid)
return (true);
if (r->from_type == RULE_GID && r->f_gid == cred->cr_gid)
return (true);
return (false);
}
static int
priv_grant(struct ucred *cred, int priv)
{
struct rule *r;
struct prison *pr;
struct mac_do_rule *rule;
if (do_enabled == 0)
return (EPERM);
rule = mac_do_rule_find(cred->cr_prison, &pr);
TAILQ_FOREACH(r, &rule->head, r_entries) {
if (rule_is_valid(cred, r)) {
switch (priv) {
case PRIV_CRED_SETGROUPS:
case PRIV_CRED_SETUID:
mtx_unlock(&pr->pr_mtx);
return (0);
default:
break;
}
}
}
mtx_unlock(&pr->pr_mtx);
return (EPERM);
}
static int
check_setgroups(struct ucred *cred, int ngrp, gid_t *groups)
{
struct rule *r;
char *fullpath = NULL;
char *freebuf = NULL;
struct prison *pr;
struct mac_do_rule *rule;
if (do_enabled == 0)
return (0);
if (cred->cr_uid == 0)
return (0);
if (vn_fullpath(curproc->p_textvp, &fullpath, &freebuf) != 0)
return (EPERM);
if (strcmp(fullpath, "/usr/bin/mdo") != 0) {
free(freebuf, M_TEMP);
return (EPERM);
}
free(freebuf, M_TEMP);
rule = mac_do_rule_find(cred->cr_prison, &pr);
TAILQ_FOREACH(r, &rule->head, r_entries) {
if (rule_is_valid(cred, r)) {
mtx_unlock(&pr->pr_mtx);
return (0);
}
}
mtx_unlock(&pr->pr_mtx);
return (EPERM);
}
static int
check_setuid(struct ucred *cred, uid_t uid)
{
struct rule *r;
int error;
char *fullpath = NULL;
char *freebuf = NULL;
struct prison *pr;
struct mac_do_rule *rule;
if (do_enabled == 0)
return (0);
if (cred->cr_uid == uid || cred->cr_uid == 0)
return (0);
if (vn_fullpath(curproc->p_textvp, &fullpath, &freebuf) != 0)
return (EPERM);
if (strcmp(fullpath, "/usr/bin/mdo") != 0) {
free(freebuf, M_TEMP);
return (EPERM);
}
free(freebuf, M_TEMP);
error = EPERM;
rule = mac_do_rule_find(cred->cr_prison, &pr);
TAILQ_FOREACH(r, &rule->head, r_entries) {
if (r->from_type == RULE_UID) {
if (cred->cr_uid != r->f_uid)
continue;
if (r->to_type == RULE_ANY) {
error = 0;
break;
}
if (r->to_type == RULE_UID && uid == r->t_uid) {
error = 0;
break;
}
}
if (r->from_type == RULE_GID) {
if (cred->cr_gid != r->f_gid)
continue;
if (r->to_type == RULE_ANY) {
error = 0;
break;
}
if (r->to_type == RULE_UID && uid == r->t_uid) {
error = 0;
break;
}
}
}
mtx_unlock(&pr->pr_mtx);
return (error);
}
static struct mac_policy_ops do_ops = {
.mpo_destroy = destroy,
.mpo_init = init,
.mpo_cred_check_setuid = check_setuid,
.mpo_cred_check_setgroups = check_setgroups,
.mpo_priv_grant = priv_grant,
};
MAC_POLICY_SET(&do_ops, mac_do, "MAC/do",
MPC_LOADTIME_FLAG_UNLOADOK, NULL);
MODULE_VERSION(mac_do, 1);

View File

@ -87,6 +87,7 @@ SUBDIR= alias \
lzmainfo \
m4 \
mandoc \
mdo \
mesg \
ministat \
mkdep \

4
usr.bin/mdo/Makefile Normal file
View File

@ -0,0 +1,4 @@
PROG= mdo
SRCS= mdo.c
.include <bsd.prog.mk>

44
usr.bin/mdo/mdo.1 Normal file
View File

@ -0,0 +1,44 @@
.\"-
.\" Copyright(c) 2024 Baptiste Daroussin <bapt@FreeBSD.org>
.\"
.\" SPDX-License-Identifier: BSD-2-Clause
.\"
.Dd May 22, 2024
.Dt MDO 1
.Os
.Sh NAME
.Nm mdo
.Nd execute commands as another user
.Sh SYNOPSIS
.Nm
.Op Fl u Ar username
.Op Fl i
.Op command Op args
.Sh DESCRIPTION
The
.Nm
utility executes the specified
.Ar command
as user
.Ar username .
.Pp
If no
.Ar username
is provided it defaults to the
.Va root
user.
If no
.Ar command
is specified, it will execute the shell specified as
.Va SHELL
environnement variable, falling back on
.Pa /bin/sh .
.Pp
The
.Fl i
option can be used to only call
.Fn setuid
and keep the group from the calling user.
.Sh SEE ALSO
.Xr su 1
.Xr mac_do 4

76
usr.bin/mdo/mdo.c Normal file
View File

@ -0,0 +1,76 @@
/*-
* Copyright(c) 2024 Baptiste Daroussin <bapt@FreeBSD.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <sys/limits.h>
#include <err.h>
#include <paths.h>
#include <pwd.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static void
usage(void)
{
fprintf(stderr, "usage: mdo [-u username] [-i] [--] [command [args]]\n");
exit(EXIT_FAILURE);
}
int
main(int argc, char **argv)
{
struct passwd *pw;
const char *username = "root";
bool uidonly = false;
int ch;
while ((ch = getopt(argc, argv, "u:i")) != -1) {
switch (ch) {
case 'u':
username = optarg;
break;
case 'i':
uidonly = true;
break;
default:
usage();
}
}
argc -= optind;
argv += optind;
if ((pw = getpwnam(username)) == NULL) {
if (strspn(username, "0123456789") == strlen(username)) {
const char *errp = NULL;
uid_t uid = strtonum(username, 0, UID_MAX, &errp);
if (errp != NULL)
err(EXIT_FAILURE, "%s", errp);
pw = getpwuid(uid);
}
if (pw == NULL)
err(EXIT_FAILURE, "invalid username '%s'", username);
}
if (!uidonly) {
if (initgroups(pw->pw_name, pw->pw_gid) == -1)
err(EXIT_FAILURE, "failed to call initgroups");
if (setgid(pw->pw_gid) == -1)
err(EXIT_FAILURE, "failed to call setgid");
}
if (setuid(pw->pw_uid) == -1)
err(EXIT_FAILURE, "failed to call setuid");
if (*argv == NULL) {
const char *sh = getenv("SHELL");
if (sh == NULL)
sh = _PATH_BSHELL;
execlp(sh, sh, "-i", NULL);
} else {
execvp(argv[0], argv);
}
err(EXIT_FAILURE, "exec failed");
}