diff options
author | Baptiste Daroussin <bapt@FreeBSD.org> | 2024-05-09 22:03:28 +0000 |
---|---|---|
committer | Baptiste Daroussin <bapt@FreeBSD.org> | 2024-05-22 12:01:41 +0000 |
commit | 8aac90f18aef7c9eea906c3ff9a001ca7b94f375 (patch) | |
tree | ffb10f6d22183188b199cbc09d074a64e8ed28d9 /sys/security | |
parent | ac4ddc467bfe895e965ed18e8b7bbbb4d7bf964d (diff) | |
download | src-8aac90f18aef7c9eea906c3ff9a001ca7b94f375.tar.gz src-8aac90f18aef7c9eea906c3ff9a001ca7b94f375.zip |
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
Diffstat (limited to 'sys/security')
-rw-r--r-- | sys/security/mac_do/mac_do.c | 545 |
1 files changed, 545 insertions, 0 deletions
diff --git a/sys/security/mac_do/mac_do.c b/sys/security/mac_do/mac_do.c new file mode 100644 index 000000000000..8685954b7db6 --- /dev/null +++ b/sys/security/mac_do/mac_do.c @@ -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); |