diff --git a/share/man/man4/Makefile b/share/man/man4/Makefile --- a/share/man/man4/Makefile +++ b/share/man/man4/Makefile @@ -288,6 +288,7 @@ mac_biba.4 \ mac_bsdextended.4 \ mac_ddb.4 \ + mac_do.4 \ mac_ifoff.4 \ mac_ipacl.4 \ mac_lomac.4 \ diff --git a/share/man/man4/mac_do.4 b/share/man/man4/mac_do.4 new file mode 100644 --- /dev/null +++ b/share/man/man4/mac_do.4 @@ -0,0 +1,62 @@ +.\" SPDX-License-Identifier: BSD-2-Clause +.\" +.\" Copyright (c) 2024 Baptiste Daroussin +.Dd May 9, 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 +.Pp +.Sh DESCRIPTION +The +.Nm +policy grants some user the ability to run a process as another user +according to 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 +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 elements is in the following form +.Dq [uid|gid]=: +Where +.Ar fid +is the uid or the gid, depending on the modifier specified for the users allowed +to become +.Ar tid . and +.Ar tid is the uid of the targetted user. +2 special forms are accepted for +.Ar tid : +.Va any +or +.Va * +which allows to target any users. +.Sh SEE ALSO +.Xr mac 4 , +.Xr mdo 1 diff --git a/sys/modules/Makefile b/sys/modules/Makefile --- a/sys/modules/Makefile +++ b/sys/modules/Makefile @@ -226,6 +226,7 @@ ${_mac_biba} \ ${_mac_bsdextended} \ ${_mac_ddb} \ + ${_mac_do} \ ${_mac_ifoff} \ ${_mac_ipacl} \ ${_mac_lomac} \ @@ -587,6 +588,7 @@ .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 diff --git a/sys/modules/mac_do/Makefile b/sys/modules/mac_do/Makefile new file mode 100644 --- /dev/null +++ b/sys/modules/mac_do/Makefile @@ -0,0 +1,6 @@ +.PATH: ${SRCTOP}/sys/security/mac_do + +KMOD= mac_do +SRCS= mac_do.c vnode_if.h + +.include diff --git a/sys/security/mac_do/mac_do.c b/sys/security/mac_do/mac_do.c new file mode 100644 --- /dev/null +++ b/sys/security/mac_do/mac_do.c @@ -0,0 +1,546 @@ +/* + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright(c) 2024 Baptiste Daroussin + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +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, *nrules; + void **rsv; + + rules = mac_do_rule_find(pr, &ppr); + if (ppr == pr) + goto done; + + mtx_unlock(&ppr->pr_mtx); + nrules = malloc(sizeof(struct mac_do_rule), 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(nrules, M_PRISON); + osd_free_reserved(rsv); + goto done; + } + mtx_lock(&pr->pr_mtx); + osd_jail_set_reserved(pr, mac_do_osd_jail_slot, rsv, nrules); + TAILQ_INIT(&nrules->head); + mtx_unlock(&ppr->pr_mtx); +done: + if (lrp != NULL) + *lrp = rules; + else + mtx_unlock(&pr->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); + mtx_unlock(&pr->pr_mtx); + 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) +{ + struct prison *pr; + 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, + }; + + mac_do_osd_jail_slot = osd_jail_register(mac_do_dealloc_prison, methods); + TAILQ_INIT(&rules0.head); + rules0.string[0] = '\0'; + 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); diff --git a/usr.bin/Makefile b/usr.bin/Makefile --- a/usr.bin/Makefile +++ b/usr.bin/Makefile @@ -87,6 +87,7 @@ lzmainfo \ m4 \ mandoc \ + mdo \ mesg \ ministat \ mkdep \ diff --git a/usr.bin/mdo/Makefile b/usr.bin/mdo/Makefile new file mode 100644 --- /dev/null +++ b/usr.bin/mdo/Makefile @@ -0,0 +1,4 @@ +PROG= mdo +SRCS= mdo.c + +.include diff --git a/usr.bin/mdo/mdo.1 b/usr.bin/mdo/mdo.1 new file mode 100644 --- /dev/null +++ b/usr.bin/mdo/mdo.1 @@ -0,0 +1,42 @@ +.\" SPDX-License-Identifier: BSD-2-Clause +.\" +.\" Copyright(c) 2024 Baptiste Daroussin +.Dd May 9, 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 diff --git a/usr.bin/mdo/mdo.c b/usr.bin/mdo/mdo.c new file mode 100644 --- /dev/null +++ b/usr.bin/mdo/mdo.c @@ -0,0 +1,76 @@ +/* + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright(c) 2024 Baptiste Daroussin + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +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, "Impossible to call initgroups"); + if (setgid(pw->pw_gid) == -1) + err(EXIT_FAILURE, "Impossible to call setgid"); + } + if (setuid(pw->pw_uid) == -1) + err(EXIT_FAILURE, "Impossible 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"); +}