diff --git a/MANIFEST b/MANIFEST index 747c4afeb..b558663b7 100644 --- a/MANIFEST +++ b/MANIFEST @@ -129,6 +129,7 @@ lib/util/getgrouplist.c lib/util/gethostname.c lib/util/getopt_long.c lib/util/gettime.c +lib/util/getusershell.c lib/util/gidlist.c lib/util/glob.c lib/util/host_port.c diff --git a/config.h.in b/config.h.in index 02214844b..a7c1da471 100644 --- a/config.h.in +++ b/config.h.in @@ -343,6 +343,9 @@ /* Define to 1 if you have the `getuserattr' function. */ #undef HAVE_GETUSERATTR +/* Define to 1 if you have the `getusershell' function. */ +#undef HAVE_GETUSERSHELL + /* Define to 1 if you have the `getutid' function. */ #undef HAVE_GETUTID diff --git a/configure b/configure index 66cb86aaa..ff25eb634 100755 --- a/configure +++ b/configure @@ -19397,6 +19397,32 @@ esac fi done +for ac_func in getusershell +do : + ac_fn_c_check_func "$LINENO" "getusershell" "ac_cv_func_getusershell" +if test "x$ac_cv_func_getusershell" = xyes; then : + cat >>confdefs.h <<_ACEOF +#define HAVE_GETUSERSHELL 1 +_ACEOF + +else + + case " $LIBOBJS " in + *" getusershell.$ac_objext "* ) ;; + *) LIBOBJS="$LIBOBJS getusershell.$ac_objext" + ;; +esac + + + for _sym in sudo_getusershell; do + COMPAT_EXP="${COMPAT_EXP}${_sym} +" + done + + +fi +done + for ac_func in reallocarray do : ac_fn_c_check_func "$LINENO" "reallocarray" "ac_cv_func_reallocarray" diff --git a/configure.ac b/configure.ac index 2b1af476d..efef6a7cd 100644 --- a/configure.ac +++ b/configure.ac @@ -3,7 +3,7 @@ dnl Use the top-level autogen.sh script to generate configure and config.h.in dnl dnl SPDX-License-Identifier: ISC dnl -dnl Copyright (c) 1994-1996, 1998-2018 Todd C. Miller +dnl Copyright (c) 1994-1996, 1998-2019 Todd C. Miller dnl dnl Permission to use, copy, modify, and distribute this software for any dnl purpose with or without fee is hereby granted, provided that the above @@ -2565,6 +2565,10 @@ AC_CHECK_FUNCS([getdelim], [], [ SUDO_APPEND_COMPAT_EXP(sudo_getdelim) COMPAT_TEST_PROGS="${COMPAT_TEST_PROGS}${COMPAT_TEST_PROGS+ }getdelim_test" ]) +AC_CHECK_FUNCS([getusershell], [], [ + AC_LIBOBJ(getusershell) + SUDO_APPEND_COMPAT_EXP(sudo_getusershell) +]) AC_CHECK_FUNCS([reallocarray], [], [ AC_LIBOBJ(reallocarray) SUDO_APPEND_COMPAT_EXP(sudo_reallocarray) diff --git a/doc/sudoers.man.in b/doc/sudoers.man.in index c7e7004b0..a1ea24285 100644 --- a/doc/sudoers.man.in +++ b/doc/sudoers.man.in @@ -25,7 +25,7 @@ .nr BA @BAMAN@ .nr LC @LCMAN@ .nr PS @PSMAN@ -.TH "SUDOERS" "@mansectform@" "December 8, 2019" "Sudo @PACKAGE_VERSION@" "File Formats Manual" +.TH "SUDOERS" "@mansectform@" "December 9, 2019" "Sudo @PACKAGE_VERSION@" "File Formats Manual" .nh .if n .ad l .SH "NAME" @@ -2969,6 +2969,28 @@ Older versions of \fBsudo\fR always allowed matching of unknown user and group IDs. .TP 18n +runas_check_shell +.br +If enabled, +\fBsudo\fR +will only run commands as a user whose shell appears in the +\fI/etc/shells\fR +file, even if the invoking user's +\fRRunas_List\fR +would otherwise permit it. +If no +\fI/etc/shells\fR +file is present, a system-dependent list of built-in default shells is used. +On many operating systems, system users such as +\(lqbin\(rq, +do not have a valid shell and this flag can be used to prevent +commands from being run as those users. +This flag is +\fIoff\fR +by default. +.sp +This setting is only supported by version 1.8.30 or higher. +.TP 18n runaspw If set, \fBsudo\fR diff --git a/doc/sudoers.mdoc.in b/doc/sudoers.mdoc.in index 7e2b1f3ff..3b06eda15 100644 --- a/doc/sudoers.mdoc.in +++ b/doc/sudoers.mdoc.in @@ -24,7 +24,7 @@ .nr BA @BAMAN@ .nr LC @LCMAN@ .nr PS @PSMAN@ -.Dd December 8, 2019 +.Dd December 9, 2019 .Dt SUDOERS @mansectform@ .Os Sudo @PACKAGE_VERSION@ .Sh NAME @@ -2794,6 +2794,26 @@ This setting is only supported by version 1.8.30 or higher. Older versions of .Nm sudo always allowed matching of unknown user and group IDs. +.It runas_check_shell +If enabled, +.Nm sudo +will only run commands as a user whose shell appears in the +.Pa /etc/shells +file, even if the invoking user's +.Li Runas_List +would otherwise permit it. +If no +.Pa /etc/shells +file is present, a system-dependent list of built-in default shells is used. +On many operating systems, system users such as +.Dq bin , +do not have a valid shell and this flag can be used to prevent +commands from being run as those users. +This flag is +.Em off +by default. +.Pp +This setting is only supported by version 1.8.30 or higher. .It runaspw If set, .Nm sudo diff --git a/include/sudo_compat.h b/include/sudo_compat.h index e1723657b..a8c932185 100644 --- a/include/sudo_compat.h +++ b/include/sudo_compat.h @@ -413,6 +413,17 @@ __dso_public ssize_t sudo_getdelim(char **bufp, size_t *bufsizep, int delim, FIL # undef getdelim # define getdelim(_a, _b, _c, _d) sudo_getdelim((_a), (_b), (_c), (_d)) #endif /* HAVE_GETDELIM */ +#ifndef HAVE_GETUSERSHELL +__dso_public char *sudo_getusershell(void); +# undef getusershell +# define getusershell() sudo_getusershell() +__dso_public void sudo_setusershell(void); +# undef setusershell +# define setusershell() sudo_setusershell() +__dso_public void sudo_endusershell(void); +# undef endusershell +# define endusershell() sudo_endusershell() +#endif /* HAVE_GETUSERSHELL */ #ifndef HAVE_UTIMENSAT __dso_public int sudo_utimensat(int fd, const char *file, const struct timespec *times, int flag); # undef utimensat diff --git a/lib/util/Makefile.in b/lib/util/Makefile.in index 9bcd002a7..e2c655397 100644 --- a/lib/util/Makefile.in +++ b/lib/util/Makefile.in @@ -706,6 +706,18 @@ gettime.i: $(srcdir)/gettime.c $(incdir)/compat/stdbool.h \ $(CC) -E -o $@ $(CPPFLAGS) $< gettime.plog: gettime.i rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/gettime.c --i-file $< --output-file $@ +getusershell.lo: $(srcdir)/getusershell.c $(incdir)/compat/stdbool.h \ + $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \ + $(incdir)/sudo_gettext.h $(incdir)/sudo_queue.h \ + $(incdir)/sudo_util.h $(top_builddir)/config.h + $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c -o $@ $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/getusershell.c +getusershell.i: $(srcdir)/getusershell.c $(incdir)/compat/stdbool.h \ + $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \ + $(incdir)/sudo_gettext.h $(incdir)/sudo_queue.h \ + $(incdir)/sudo_util.h $(top_builddir)/config.h + $(CC) -E -o $@ $(CPPFLAGS) $< +getusershell.plog: getusershell.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/getusershell.c --i-file $< --output-file $@ gidlist.lo: $(srcdir)/gidlist.c $(incdir)/compat/stdbool.h \ $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \ $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \ diff --git a/lib/util/getusershell.c b/lib/util/getusershell.c new file mode 100644 index 000000000..6aeae3b6b --- /dev/null +++ b/lib/util/getusershell.c @@ -0,0 +1,138 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2019 Todd C. Miller + * + * 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. + */ + +/* + * 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 + +#include + +#include +#include +#include +#include +#include + +#define DEFAULT_TEXT_DOMAIN "sudo" +#include "sudo_gettext.h" /* must be included before sudo_compat.h */ + +#include "sudo_compat.h" +#include "sudo_debug.h" +#include "sudo_util.h" + +static char **allowed_shells, **current_shell; +static char *default_shells[] = { + "/bin/sh", + "/bin/ksh", + "/bin/ksh93", + "/bin/bash", + "/bin/dash", + "/bin/zsh", + "/bin/csh", + "/bin/tcsh", + NULL +}; + +static char ** +read_shells(void) +{ + size_t maxshells = 16, nshells = 0; + size_t linesize = 0; + char *line = NULL; + FILE *fp; + debug_decl(read_shells, SUDO_DEBUG_UTIL) + + if ((fp = fopen("/etc/shells", "r")) == NULL) + goto bad; + + free(allowed_shells); + allowed_shells = reallocarray(NULL, maxshells, sizeof(char *)); + if (allowed_shells == NULL) + goto bad; + + while (sudo_parseln(&line, &linesize, NULL, fp, PARSELN_CONT_IGN) != -1) { + if (nshells + 1 >= maxshells) { + char **new_shells; + + new_shells = reallocarray(NULL, maxshells + 16, sizeof(char *)); + if (new_shells == NULL) + goto bad; + allowed_shells = new_shells; + maxshells += 16; + } + if ((allowed_shells[nshells] = strdup(line)) == NULL) + goto bad; + nshells++; + } + allowed_shells[nshells] = NULL; + + free(line); + fclose(fp); + debug_return_ptr(allowed_shells); +bad: + free(line); + if (fp != NULL) + fclose(fp); + while (nshells != 0) + free(allowed_shells[--nshells]); + free(allowed_shells); + allowed_shells = NULL; + debug_return_ptr(default_shells); +} + +void +sudo_setusershell(void) +{ + debug_decl(setusershell, SUDO_DEBUG_UTIL) + + current_shell = read_shells(); + + debug_return; +} + +void +sudo_endusershell(void) +{ + debug_decl(endusershell, SUDO_DEBUG_UTIL) + + if (allowed_shells != NULL) { + char **shell; + + for (shell = allowed_shells; *shell != NULL; shell++) + free(*shell); + free(allowed_shells); + allowed_shells = NULL; + } + current_shell = NULL; + + debug_return; +} + +char * +sudo_getusershell(void) +{ + debug_decl(getusershell, SUDO_DEBUG_UTIL) + + if (current_shell == NULL) + current_shell = read_shells(); + + debug_return_str(*current_shell++); +} diff --git a/mkdep.pl b/mkdep.pl index 100d1dc14..8b465e86a 100755 --- a/mkdep.pl +++ b/mkdep.pl @@ -116,7 +116,7 @@ sub mkdep { # XXX - fill in AUTH_OBJS from contents of the auth dir instead $makefile =~ s:\@AUTH_OBJS\@:afs.lo aix_auth.lo bsdauth.lo dce.lo fwtk.lo getspwuid.lo kerb5.lo pam.lo passwd.lo rfc1938.lo secureware.lo securid5.lo sia.lo:; $makefile =~ s:\@DIGEST\@:digest.lo digest_openssl.lo digest_gcrypt.lo:; - $makefile =~ s:\@LTLIBOBJS\@:arc4random.lo arc4random_uniform.lo closefrom.lo dup3.lo fchmodat.lo fstatat.lo fnmatch.lo getaddrinfo.lo getcwd.lo getentropy.lo getgrouplist.lo getdelim.lo getopt_long.lo glob.lo inet_ntop_lo inet_pton.lo isblank.lo memrchr.lo memset_s.lo mksiglist.lo mksigname.lo mktemp.lo nanosleep.lo openat.lo pipe2.lo pw_dup.lo reallocarray.lo sha2.lo sig2str.lo siglist.lo signame.lo snprintf.lo str2sig.lo strlcat.lo strlcpy.lo strndup.lo strnlen.lo strsignal.lo unlinkat.lo utimens.lo vsyslog.lo:; + $makefile =~ s:\@LTLIBOBJS\@:arc4random.lo arc4random_uniform.lo closefrom.lo dup3.lo fchmodat.lo fstatat.lo fnmatch.lo getaddrinfo.lo getcwd.lo getentropy.lo getgrouplist.lo getdelim.lo getopt_long.lo getusershell.lo glob.lo inet_ntop_lo inet_pton.lo isblank.lo memrchr.lo memset_s.lo mksiglist.lo mksigname.lo mktemp.lo nanosleep.lo openat.lo pipe2.lo pw_dup.lo reallocarray.lo sha2.lo sig2str.lo siglist.lo signame.lo snprintf.lo str2sig.lo strlcat.lo strlcpy.lo strndup.lo strnlen.lo strsignal.lo unlinkat.lo utimens.lo vsyslog.lo:; # Parse OBJS lines my %objs; diff --git a/plugins/sudoers/check.c b/plugins/sudoers/check.c index ea1d89085..7c8226c03 100644 --- a/plugins/sudoers/check.c +++ b/plugins/sudoers/check.c @@ -328,3 +328,28 @@ get_authpw(int mode) debug_return_ptr(pw); } + +/* + * Returns true if the specified shell is allowed by /etc/shells, else false. + */ +bool +check_user_shell(const struct passwd *pw) +{ + const char *shell; + debug_decl(check_user_shell, SUDOERS_DEBUG_AUTH) + + if (!def_runas_check_shell) + debug_return_bool(true); + + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: checking /etc/shells for %s", __func__, pw->pw_shell); + + setusershell(); + while ((shell = getusershell()) != NULL) { + if (strcmp(shell, pw->pw_shell) == 0) + debug_return_bool(true); + } + endusershell(); + + debug_return_bool(false); +} diff --git a/plugins/sudoers/def_data.c b/plugins/sudoers/def_data.c index 6548eb832..f2a105bc4 100644 --- a/plugins/sudoers/def_data.c +++ b/plugins/sudoers/def_data.c @@ -529,6 +529,10 @@ struct sudo_defs_types sudo_defs_table[] = { "runas_allow_unknown_id", T_FLAG, N_("Allow the use of unknown runas user and/or group ID"), NULL, + }, { + "runas_check_shell", T_FLAG, + N_("Only permit running commands as a user with a valid shell"), + NULL, }, { NULL, 0, NULL } diff --git a/plugins/sudoers/def_data.h b/plugins/sudoers/def_data.h index 063644d09..ad0ebde42 100644 --- a/plugins/sudoers/def_data.h +++ b/plugins/sudoers/def_data.h @@ -244,6 +244,8 @@ #define def_log_server_peer_key (sudo_defs_table[I_LOG_SERVER_PEER_KEY].sd_un.str) #define I_RUNAS_ALLOW_UNKNOWN_ID 122 #define def_runas_allow_unknown_id (sudo_defs_table[I_RUNAS_ALLOW_UNKNOWN_ID].sd_un.flag) +#define I_RUNAS_CHECK_SHELL 123 +#define def_runas_check_shell (sudo_defs_table[I_RUNAS_CHECK_SHELL].sd_un.flag) enum def_tuple { never, diff --git a/plugins/sudoers/def_data.in b/plugins/sudoers/def_data.in index dbb68ec9b..35fd4fb65 100644 --- a/plugins/sudoers/def_data.in +++ b/plugins/sudoers/def_data.in @@ -384,3 +384,7 @@ log_server_peer_key runas_allow_unknown_id T_FLAG "Allow the use of unknown runas user and/or group ID" +runas_check_shell + T_FLAG + "Only permit running commands as a user with a valid shell" + diff --git a/plugins/sudoers/sudoers.c b/plugins/sudoers/sudoers.c index 3fcac02aa..7a2361352 100644 --- a/plugins/sudoers/sudoers.c +++ b/plugins/sudoers/sudoers.c @@ -295,7 +295,7 @@ sudoers_policy_main(int argc, char * const argv[], int pwflag, char *env_add[], /* Not an audit event. */ sudo_warnx(U_("sudoers specifies that root is not allowed to sudo")); goto bad; - } + } if (!set_perms(PERM_INITIAL)) goto bad; @@ -434,6 +434,13 @@ sudoers_policy_main(int argc, char * const argv[], int pwflag, char *env_add[], goto bad; } + /* Check runas user's shell. */ + if (!check_user_shell(runas_pw)) { + log_warningx(SLOG_RAW_MSG, N_("invalid shell for user %s: %s"), + runas_pw->pw_name, runas_pw->pw_shell); + goto bad; + } + /* * We don't reset the environment for sudoedit or if the user * specified the -E command line flag and they have setenv privs. diff --git a/plugins/sudoers/sudoers.h b/plugins/sudoers/sudoers.h index 176effa52..fec36bd51 100644 --- a/plugins/sudoers/sudoers.h +++ b/plugins/sudoers/sudoers.h @@ -256,6 +256,7 @@ int find_path(const char *infile, char **outfile, struct stat *sbp, /* check.c */ int check_user(int validate, int mode); +bool check_user_shell(const struct passwd *pw); bool user_is_exempt(void); /* prompt.c */