Files
sudo/plugins/sudoers/testsudoers.c
Todd C. Miller f6a9bb2e23 Use a single callback for sudoers_lookup() and add a closure pointer.
The single callback now receives all the match info (or UNSPEC if
no match was attempted).  This makes it possible to use the callback
for more than just printing testsudoers output.
2023-08-07 15:06:19 -06:00

778 lines
19 KiB
C

/*
* SPDX-License-Identifier: ISC
*
* Copyright (c) 1996, 1998-2005, 2007-2023
* Todd C. Miller <Todd.Miller@sudo.ws>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
* Sponsored in part by the Defense Advanced Research Projects
* Agency (DARPA) and Air Force Research Laboratory, Air Force
* Materiel Command, USAF, under agreement number F39502-99-1-0512.
*/
/*
* This is an open source non-commercial project. Dear PVS-Studio, please check it.
* PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
*/
#include <config.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#ifdef HAVE_STDBOOL_H
# include <stdbool.h>
#else
# include "compat/stdbool.h"
#endif /* HAVE_STDBOOL_H */
#include <string.h>
#ifdef HAVE_STRINGS_H
# include <strings.h>
#endif /* HAVE_STRINGS_H */
#include <unistd.h>
#include <errno.h>
#include "testsudoers_pwutil.h"
#include "toke.h"
#include "tsgetgrpw.h"
#include "sudoers.h"
#include "interfaces.h"
#include "sudo_conf.h"
#include "sudo_lbuf.h"
#include <gram.h>
#ifndef YYDEBUG
# define YYDEBUG 0
#endif
enum sudoers_formats {
format_ldif,
format_sudoers
};
/*
* Function Prototypes
*/
static void dump_sudoers(void);
static void set_runaspw(const char *);
static void set_runasgr(const char *);
static bool cb_runas_default(const char *file, int line, int column, const union sudo_defs_val *, int);
static int testsudoers_error(const char * restrict buf);
static int testsudoers_output(const char * restrict buf);
sudo_noreturn static void usage(void);
static void cb_lookup(struct userspec *us, int user_match, struct privilege *priv, int host_match, struct cmndspec *cs, int date_match, int runas_match, int cmnd_match, void *closure);
static int testsudoers_query(const struct sudo_nss *nss, struct passwd *pw);
/*
* Globals
*/
struct sudo_user sudo_user;
struct passwd *list_pw;
static const char *orig_cmnd;
static char *runas_group, *runas_user;
unsigned int sudo_mode = MODE_RUN;
#if defined(SUDO_DEVEL) && defined(__OpenBSD__)
extern char *malloc_options;
#endif
sudo_dso_public int main(int argc, char *argv[]);
int
main(int argc, char *argv[])
{
struct sudoers_parser_config sudoers_conf = SUDOERS_PARSER_CONFIG_INITIALIZER;
struct sudo_nss_list snl = TAILQ_HEAD_INITIALIZER(snl);
enum sudoers_formats input_format = format_sudoers;
struct sudo_nss testsudoers_nss;
char *p, *grfile, *pwfile;
const char *errstr;
int ch, dflag, exitcode = EXIT_FAILURE;
unsigned int validated;
int status = FOUND;
int pwflag = 0;
char cwdbuf[PATH_MAX];
time_t now;
id_t id;
debug_decl(main, SUDOERS_DEBUG_MAIN);
#if defined(SUDO_DEVEL) && defined(__OpenBSD__)
malloc_options = "S";
#endif
#if YYDEBUG
sudoersdebug = 1;
#endif
initprogname(argc > 0 ? argv[0] : "testsudoers");
if (!sudoers_initlocale(setlocale(LC_ALL, ""), def_sudoers_locale))
sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
sudo_warn_set_locale_func(sudoers_warn_setlocale);
bindtextdomain("sudoers", LOCALEDIR); /* XXX - should have own domain */
textdomain("sudoers");
time(&now);
/* Initialize the debug subsystem. */
if (sudo_conf_read(NULL, SUDO_CONF_DEBUG) == -1)
goto done;
if (!sudoers_debug_register(getprogname(), sudo_conf_debug_files(getprogname())))
goto done;
dflag = 0;
grfile = pwfile = NULL;
while ((ch = getopt(argc, argv, "+D:dg:G:h:i:L:lP:p:R:T:tu:U:v")) != -1) {
switch (ch) {
case 'D':
user_runcwd = optarg;
break;
case 'd':
dflag = 1;
break;
case 'G':
id = sudo_strtoid(optarg, &errstr);
if (errstr != NULL)
sudo_fatalx("group-ID %s: %s", optarg, errstr);
sudoers_conf.sudoers_gid = (gid_t)id;
break;
case 'g':
runas_group = optarg;
SET(sudo_user.flags, RUNAS_GROUP_SPECIFIED);
break;
case 'h':
user_host = optarg;
break;
case 'i':
if (strcasecmp(optarg, "ldif") == 0) {
input_format = format_ldif;
} else if (strcasecmp(optarg, "sudoers") == 0) {
input_format = format_sudoers;
} else {
sudo_warnx(U_("unsupported input format %s"), optarg);
usage();
}
break;
case 'L':
list_pw = sudo_getpwnam(optarg);
if (list_pw == NULL) {
sudo_warnx(U_("unknown user %s"), optarg);
usage();
}
FALLTHROUGH;
case 'l':
if (sudo_mode != MODE_RUN) {
sudo_warnx(
"only one of the -l or -v flags may be specified");
usage();
}
sudo_mode = MODE_LIST;
pwflag = I_LISTPW;
orig_cmnd = "list";
break;
case 'p':
pwfile = optarg;
break;
case 'P':
grfile = optarg;
break;
case 'T':
now = parse_gentime(optarg);
if (now == -1)
sudo_fatalx("invalid time: %s", optarg);
break;
case 'R':
user_runchroot = optarg;
break;
case 't':
trace_print = testsudoers_error;
break;
case 'U':
id = sudo_strtoid(optarg, &errstr);
if (errstr != NULL)
sudo_fatalx("user-ID %s: %s", optarg, errstr);
sudoers_conf.sudoers_uid = (uid_t)id;
break;
case 'u':
runas_user = optarg;
SET(sudo_user.flags, RUNAS_USER_SPECIFIED);
break;
case 'v':
if (sudo_mode != MODE_RUN) {
sudo_warnx(
"only one of the -l or -v flags may be specified");
usage();
}
sudo_mode = MODE_VALIDATE;
pwflag = I_VERIFYPW;
orig_cmnd = "validate";
break;
default:
usage();
/* NOTREACHED */
}
}
argc -= optind;
argv += optind;
if (grfile != NULL || pwfile != NULL) {
/* Set group/passwd file and init the cache. */
if (grfile)
testsudoers_setgrfile(grfile);
if (pwfile)
testsudoers_setpwfile(pwfile);
/* Use custom passwd/group backend. */
sudo_pwutil_set_backend(testsudoers_make_pwitem,
testsudoers_make_gritem, testsudoers_make_gidlist_item,
testsudoers_make_grlist_item);
}
if (argc < 2) {
/* No command or user specified. */
if (dflag) {
orig_cmnd = "true";
} else if (pwflag == 0) {
usage();
}
user_name = argc ? *argv++ : (char *)"root";
argc = 0;
} else {
if (argc > 2 && sudo_mode == MODE_LIST)
sudo_mode = MODE_CHECK;
user_name = *argv++;
argc--;
if (orig_cmnd == NULL) {
orig_cmnd = *argv++;
argc--;
}
}
user_cmnd = strdup(orig_cmnd);
if (user_cmnd == NULL)
sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
user_base = sudo_basename(user_cmnd);
if (getcwd(cwdbuf, sizeof(cwdbuf)) == NULL)
strlcpy(cwdbuf, "/", sizeof(cwdbuf));
user_cwd = cwdbuf;
if ((sudo_user.pw = sudo_getpwnam(user_name)) == NULL)
sudo_fatalx(U_("unknown user %s"), user_name);
user_uid = sudo_user.pw->pw_uid;
user_gid = sudo_user.pw->pw_gid;
if (user_host == NULL) {
if ((user_host = sudo_gethostname()) == NULL)
sudo_fatal("gethostname");
}
if ((p = strchr(user_host, '.'))) {
*p = '\0';
if ((user_shost = strdup(user_host)) == NULL)
sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
*p = '.';
} else {
user_shost = user_host;
}
user_runhost = user_host;
user_srunhost = user_shost;
/* Fill in user_args from argv. */
if (argc > 0) {
char *to, **from;
size_t size, n;
for (size = 0, from = argv; *from; from++)
size += strlen(*from) + 1;
if ((user_args = malloc(size)) == NULL)
sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
for (to = user_args, from = argv; *from; from++) {
n = strlcpy(to, *from, size - (size_t)(to - user_args));
if (n >= size - (size_t)(to - user_args))
sudo_fatalx(U_("internal error, %s overflow"), getprogname());
to += n;
*to++ = ' ';
}
*--to = '\0';
}
/* Initialize default values. */
if (!init_defaults())
sudo_fatalx("%s", U_("unable to initialize sudoers default values"));
/* Set group_plugin callback. */
sudo_defs_table[I_GROUP_PLUGIN].callback = cb_group_plugin;
/* Set runas callback. */
sudo_defs_table[I_RUNAS_DEFAULT].callback = cb_runas_default;
/* Set locale callback. */
sudo_defs_table[I_SUDOERS_LOCALE].callback = sudoers_locale_callback;
/* Load ip addr/mask for each interface. */
if (get_net_ifs(&p) > 0) {
if (!set_interfaces(p))
sudo_fatal("%s", U_("unable to parse network address list"));
free(p);
}
/* Initialize the parser and set sudoers filename to "sudoers". */
sudoers_conf.strict = true;
sudoers_conf.verbose = 2;
init_parser("sudoers", &sudoers_conf);
/*
* Set runas passwd/group entries based on command line or sudoers.
* Note that if runas_group was specified without runas_user we
* run the command as the invoking user.
*/
if (runas_group != NULL) {
set_runasgr(runas_group);
set_runaspw(runas_user ? runas_user : user_name);
} else
set_runaspw(runas_user ? runas_user : def_runas_default);
/* Parse the policy file. */
sudoers_setlocale(SUDOERS_LOCALE_SUDOERS, NULL);
switch (input_format) {
case format_ldif:
if (!sudoers_parse_ldif(&parsed_policy, stdin, NULL, true)) {
(void) puts("Parse error in LDIF");
parse_error = true;
}
break;
case format_sudoers:
if (sudoersparse() != 0)
parse_error = true;
break;
default:
sudo_fatalx("error: unhandled input %d", input_format);
}
if (!update_defaults(&parsed_policy, NULL, SETDEF_ALL, false))
parse_error = true;
if (!parse_error)
(void) puts("Parses OK");
if (dflag) {
(void) putchar('\n');
dump_sudoers();
if (argc < 2) {
exitcode = parse_error ? 1 : 0;
goto done;
}
}
/* Fake up a minimal sudo nss list with the parsed policy. */
TAILQ_INSERT_TAIL(&snl, &testsudoers_nss, entries);
testsudoers_nss.query = testsudoers_query;
testsudoers_nss.parse_tree = &parsed_policy;
printf("\nEntries for user %s:\n", user_name);
validated = sudoers_lookup(&snl, sudo_user.pw, now, cb_lookup, NULL,
&status, pwflag);
/* Validate user-specified chroot or cwd (if any) and runas user shell. */
if (ISSET(validated, VALIDATE_SUCCESS)) {
if (!check_user_shell(runas_pw)) {
printf(U_("\nInvalid shell for user %s: %s\n"),
runas_pw->pw_name, runas_pw->pw_shell);
CLR(validated, VALIDATE_SUCCESS);
SET(validated, VALIDATE_FAILURE);
}
if (check_user_runchroot() != true) {
printf("\nUser %s is not allowed to change root directory to %s\n",
user_name, user_runchroot);
CLR(validated, VALIDATE_SUCCESS);
SET(validated, VALIDATE_FAILURE);
}
if (check_user_runcwd() != true) {
printf("\nUser %s is not allowed to change directory to %s\n",
user_name, user_runcwd);
CLR(validated, VALIDATE_SUCCESS);
SET(validated, VALIDATE_FAILURE);
}
}
if (def_authenticate) {
puts(U_("\nPassword required"));
}
/*
* Exit codes:
* 0 - parsed OK and command matched.
* 1 - parse error
* 2 - command not matched
* 3 - command denied
*/
if (parse_error || ISSET(validated, VALIDATE_ERROR)) {
puts(U_("\nParse error"));
exitcode = 1;
} else if (ISSET(validated, VALIDATE_SUCCESS)) {
puts(U_("\nCommand allowed"));
exitcode = 0;
} else if (ISSET(validated, VALIDATE_FAILURE)) {
puts(U_("\nCommand denied"));
exitcode = 3;
} else {
puts(U_("\nCommand unmatched"));
exitcode = 2;
}
done:
sudo_freepwcache();
sudo_freegrcache();
sudo_debug_exit_int(__func__, __FILE__, __LINE__, sudo_debug_subsys, exitcode);
return exitcode;
}
static void
set_runaspw(const char *user)
{
struct passwd *pw = NULL;
debug_decl(set_runaspw, SUDOERS_DEBUG_UTIL);
if (*user == '#') {
const char *errstr;
uid_t uid = sudo_strtoid(user + 1, &errstr);
if (errstr == NULL) {
if ((pw = sudo_getpwuid(uid)) == NULL)
pw = sudo_fakepwnam(user, user_gid);
}
}
if (pw == NULL) {
if ((pw = sudo_getpwnam(user)) == NULL)
sudo_fatalx(U_("unknown user %s"), user);
}
if (runas_pw != NULL)
sudo_pw_delref(runas_pw);
runas_pw = pw;
debug_return;
}
static void
set_runasgr(const char *group)
{
struct group *gr = NULL;
debug_decl(set_runasgr, SUDOERS_DEBUG_UTIL);
if (*group == '#') {
const char *errstr;
gid_t gid = sudo_strtoid(group + 1, &errstr);
if (errstr == NULL) {
if ((gr = sudo_getgrgid(gid)) == NULL)
gr = sudo_fakegrnam(group);
}
}
if (gr == NULL) {
if ((gr = sudo_getgrnam(group)) == NULL)
sudo_fatalx(U_("unknown group %s"), group);
}
if (runas_gr != NULL)
sudo_gr_delref(runas_gr);
runas_gr = gr;
debug_return;
}
bool
cb_log_input(const char *file, int line, int column,
const union sudo_defs_val *sd_un, int op)
{
return true;
}
bool
cb_log_output(const char *file, int line, int column,
const union sudo_defs_val *sd_un, int op)
{
return true;
}
/*
* Callback for runas_default sudoers setting.
*/
static bool
cb_runas_default(const char *file, int line, int column,
const union sudo_defs_val *sd_un, int op)
{
/* Only reset runaspw if user didn't specify one. */
if (!runas_user && !runas_group)
set_runaspw(sd_un->str);
return true;
}
bool
sudo_nss_can_continue(const struct sudo_nss *nss, int match)
{
return true;
}
void
sudo_setspent(void)
{
return;
}
void
sudo_endspent(void)
{
return;
}
FILE *
open_sudoers(const char *file, char **outfile, bool doedit, bool *keepopen)
{
struct stat sb;
FILE *fp = NULL;
const char *base;
int error, fd;
debug_decl(open_sudoers, SUDOERS_DEBUG_UTIL);
/* Report errors using the basename for consistent test output. */
base = sudo_basename(file);
fd = sudo_secure_open_file(file, sudoers_file_uid(), sudoers_file_gid(),
&sb, &error);
if (fd != -1) {
if ((fp = fdopen(fd, "r")) == NULL) {
sudo_warn("unable to open %s", base);
close(fd);
}
} else {
switch (error) {
case SUDO_PATH_MISSING:
sudo_warn("unable to open %s", base);
break;
case SUDO_PATH_BAD_TYPE:
sudo_warnx("%s is not a regular file", base);
break;
case SUDO_PATH_WRONG_OWNER:
sudo_warnx("%s should be owned by uid %u",
base, (unsigned int) sudoers_file_uid());
break;
case SUDO_PATH_WORLD_WRITABLE:
sudo_warnx("%s is world writable", base);
break;
case SUDO_PATH_GROUP_WRITABLE:
sudo_warnx("%s should be owned by gid %u",
base, (unsigned int) sudoers_file_gid());
break;
default:
sudo_warnx("%s: internal error, unexpected error %d",
__func__, error);
break;
}
}
debug_return_ptr(fp);
}
bool
init_envtables(void)
{
return(true);
}
bool
set_perms(int perm)
{
return true;
}
bool
restore_perms(void)
{
return true;
}
void
init_eventlog_config(void)
{
return;
}
bool
pivot_root(const char *new_root, int fds[2])
{
return true;
}
bool
unpivot_root(int fds[2])
{
return true;
}
int
set_cmnd_path(const char *runchroot)
{
/* Reallocate user_cmnd to catch bugs in command_matches(). */
char *new_cmnd = strdup(orig_cmnd);
if (new_cmnd == NULL)
return NOT_FOUND_ERROR;
free(user_cmnd);
user_cmnd = new_cmnd;
return FOUND;
}
static void
cb_lookup(struct userspec *us, int user_match, struct privilege *priv,
int host_match, struct cmndspec *cs, int date_match, int runas_match,
int cmnd_match, void *closure)
{
static struct privilege *prev_priv;
struct sudo_lbuf lbuf;
/* Only output info for the selected user. */
if (user_match != ALLOW) {
prev_priv = NULL;
return;
}
if (priv != prev_priv) {
/* No word wrap on output. */
sudo_lbuf_init(&lbuf, testsudoers_output, 0, NULL, 0);
sudo_lbuf_append(&lbuf, "\n");
sudoers_format_privilege(&lbuf, &parsed_policy, priv, false);
sudo_lbuf_print(&lbuf);
sudo_lbuf_destroy(&lbuf);
printf("\thost %s\n", host_match == ALLOW ? "allowed" :
host_match == DENY ? "denied" : "unmatched");
}
if (host_match == ALLOW) {
if (date_match != UNSPEC)
printf("\tdate %s\n", date_match == ALLOW ? "allowed" : "denied");
if (date_match != DENY) {
printf("\trunas %s\n", runas_match == ALLOW ? "allowed" :
runas_match == DENY ? "denied" : "unmatched");
if (runas_match == ALLOW) {
printf("\tcmnd %s\n", cmnd_match == ALLOW ? "allowed" :
cmnd_match == DENY ? "denied" : "unmatched");
}
}
}
prev_priv = priv;
}
static int
testsudoers_query(const struct sudo_nss *nss, struct passwd *pw)
{
/* Nothing to do. */
return 0;
}
static bool
print_defaults(struct sudo_lbuf *lbuf)
{
struct defaults *def, *next;
debug_decl(print_defaults, SUDOERS_DEBUG_UTIL);
TAILQ_FOREACH_SAFE(def, &parsed_policy.defaults, entries, next)
sudoers_format_default_line(lbuf, &parsed_policy, def, &next, false);
debug_return_bool(!sudo_lbuf_error(lbuf));
}
static int
print_alias(struct sudoers_parse_tree *parse_tree, struct alias *a, void *v)
{
struct sudo_lbuf *lbuf = v;
struct member *m;
debug_decl(print_alias, SUDOERS_DEBUG_UTIL);
sudo_lbuf_append(lbuf, "%s %s = ", alias_type_to_string(a->type),
a->name);
TAILQ_FOREACH(m, &a->members, entries) {
if (m != TAILQ_FIRST(&a->members))
sudo_lbuf_append(lbuf, ", ");
sudoers_format_member(lbuf, parse_tree, m, NULL, UNSPEC);
}
sudo_lbuf_append(lbuf, "\n");
debug_return_int(sudo_lbuf_error(lbuf) ? -1 : 0);
}
static bool
print_aliases(struct sudo_lbuf *lbuf)
{
debug_decl(print_aliases, SUDOERS_DEBUG_UTIL);
alias_apply(&parsed_policy, print_alias, lbuf);
debug_return_bool(!sudo_lbuf_error(lbuf));
}
static void
dump_sudoers(void)
{
struct sudo_lbuf lbuf;
debug_decl(dump_sudoers, SUDOERS_DEBUG_UTIL);
/* No word wrap on output. */
sudo_lbuf_init(&lbuf, testsudoers_output, 0, NULL, 0);
/* Print Defaults */
if (!print_defaults(&lbuf))
goto done;
if (lbuf.len > 0) {
sudo_lbuf_print(&lbuf);
sudo_lbuf_append(&lbuf, "\n");
}
/* Print Aliases */
if (!print_aliases(&lbuf))
goto done;
if (lbuf.len > 1) {
sudo_lbuf_print(&lbuf);
sudo_lbuf_append(&lbuf, "\n");
}
/* Print User_Specs */
if (!sudoers_format_userspecs(&lbuf, &parsed_policy, NULL, false, true))
goto done;
if (lbuf.len > 1) {
sudo_lbuf_print(&lbuf);
}
done:
if (sudo_lbuf_error(&lbuf)) {
if (errno == ENOMEM)
sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
}
sudo_lbuf_destroy(&lbuf);
debug_return;
}
static int
testsudoers_output(const char * restrict buf)
{
return fputs(buf, stdout);
}
static int
testsudoers_error(const char *restrict buf)
{
return fputs(buf, stderr);
}
sudo_noreturn static void
usage(void)
{
(void) fprintf(stderr, "usage: %s [-dltv] [-G sudoers_gid] [-g group] [-h host] [-i input_format] [-L list_user] [-P grfile] [-p pwfile] [-U sudoers_uid] [-u user] <user> <command> [args]\n", getprogname());
exit(EXIT_FAILURE);
}