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,60 @@ +.\" 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 mac_do 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 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 PROV_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 in a list of element 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 specicied of the users allowed +to become +.Ar tid . and +.Ar tid is the uid of the targetted user. +2 special form 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,335 @@ +/* + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright(c) 2024 Baptiste Daroussin +#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 struct mtx rule_mtx; +static char rule_string[MAC_RULE_STRING_LEN]; + +#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; +}; + +static TAILQ_HEAD(rulehead, rule) rule_head; + +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 int +sysctl_rules(SYSCTL_HANDLER_ARGS) +{ + char *copy_string, *new_string; + struct rulehead head, saved_head; + int error; + + if (req->newptr == NULL) + return (sysctl_handle_string(oidp, rule_string, MAC_RULE_STRING_LEN, req)); + + new_string = malloc(MAC_RULE_STRING_LEN, M_DO, + M_WAITOK|M_ZERO); + mtx_lock(&rule_mtx); + strcpy(new_string, rule_string); + mtx_unlock(&rule_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(&rule_mtx); + TAILQ_CONCAT(&saved_head, &rule_head, r_entries); + TAILQ_CONCAT(&rule_head, &head, r_entries); + strcpy(rule_string, new_string); + mtx_unlock(&rule_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) +{ + mtx_destroy(&rule_mtx); + toast_rules(&rule_head); +} + +static void +init(struct mac_policy_conf *mpc) +{ + mtx_init(&rule_mtx, "rule_mtx", NULL, MTX_DEF); + TAILQ_INIT(&rule_head); +} + +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; + + if (do_enabled == 0) + return (EPERM); + + + mtx_lock(&rule_mtx); + 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(&rule_mtx); + return (0); + default: + break; + } + } + } + mtx_unlock(&rule_mtx); + return (EPERM); +} + +static int +check_setgroups(struct ucred *cred, int ngrp, gid_t *groups) +{ + struct rule *r; + char *fullpath = NULL; + char *freebuf = NULL; + + 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); + + mtx_lock(&rule_mtx); + TAILQ_FOREACH(r, &rule_head, r_entries) { + if (rule_is_valid(cred, r)) { + mtx_unlock(&rule_mtx); + return (0); + } + } + mtx_unlock(&rule_mtx); + + return (EPERM); +} + +static int +check_setuid(struct ucred *cred, uid_t uid) +{ + struct rule *r; + int error; + char *fullpath = NULL; + char *freebuf = NULL; + + 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; + mtx_lock(&rule_mtx); + 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(&rule_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 + +#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"); +}