NetBSD/usr.bin/su/su.c
jrf 285b019fd8 This addresses PR21693. Under certain conditions, su -m will fail because
the pointer to /etc/shells is pointing to the second entry. This change
resets the pointer before looping through the file again. FreeBSD does
this as well. Commit approved by christos and thanks to Geoff Adams for
catching and reporting it.
2003-06-18 21:02:03 +00:00

748 lines
18 KiB
C

/* $NetBSD: su.c,v 1.55 2003/06/18 21:02:03 jrf Exp $ */
/*
* Copyright (c) 1988 The Regents of the University of California.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. All advertising materials mentioning features or use of this software
* must display the following acknowledgement:
* This product includes software developed by the University of
* California, Berkeley and its contributors.
* 4. Neither the name of the University nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
#include <sys/cdefs.h>
#ifndef lint
__COPYRIGHT(
"@(#) Copyright (c) 1988 The Regents of the University of California.\n\
All rights reserved.\n");
#endif /* not lint */
#ifndef lint
#if 0
static char sccsid[] = "@(#)su.c 8.3 (Berkeley) 4/2/94";*/
#else
__RCSID("$NetBSD: su.c,v 1.55 2003/06/18 21:02:03 jrf Exp $");
#endif
#endif /* not lint */
#include <sys/param.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <err.h>
#include <errno.h>
#include <grp.h>
#include <paths.h>
#include <pwd.h>
#include <stdio.h>
#ifdef SKEY
#include <skey.h>
#endif
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <time.h>
#include <tzfile.h>
#include <unistd.h>
#ifdef LOGIN_CAP
#include <login_cap.h>
#endif
#ifdef KERBEROS
#include <des.h>
#include <krb.h>
#include <netdb.h>
static int kerberos __P((char *, char *, int));
static int koktologin __P((char *, char *, char *));
#endif
#ifdef KERBEROS5
#include <krb5.h>
static int kerberos5 __P((char *, char *, int));
#endif
#if defined(KERBEROS) || defined(KERBEROS5)
#define ARGSTRX "-Kdflm"
int use_kerberos = 1;
#else
#define ARGSTRX "-dflm"
#endif
#ifndef SUGROUP
#define SUGROUP "wheel"
#endif
#ifdef LOGIN_CAP
#define ARGSTR ARGSTRX "c:"
#else
#define ARGSTR ARGSTRX
#endif
int main __P((int, char **));
static int chshell __P((const char *));
static char *ontty __P((void));
static int check_ingroup __P((int, const char *, const char *, int));
int
main(argc, argv)
int argc;
char **argv;
{
extern char **environ;
struct passwd *pwd;
char *p;
#ifdef BSD4_4
struct timeval tp;
#endif
uid_t ruid;
int asme, ch, asthem, fastlogin, prio, gohome;
enum { UNSET, YES, NO } iscsh = UNSET;
char *user, *shell, *avshell, *username, **np;
char *userpass, *class;
char shellbuf[MAXPATHLEN], avshellbuf[MAXPATHLEN];
time_t pw_warntime = _PASSWORD_WARNDAYS * SECSPERDAY;
#ifdef LOGIN_CAP
login_cap_t *lc;
#endif
asme = asthem = fastlogin = 0;
gohome = 1;
shell = class = NULL;
while ((ch = getopt(argc, argv, ARGSTR)) != -1)
switch((char)ch) {
#if defined(KERBEROS) || defined(KERBEROS5)
case 'K':
use_kerberos = 0;
break;
#endif
#ifdef LOGIN_CAP
case 'c':
class = optarg;
break;
#endif
case 'd':
asme = 0;
asthem = 1;
gohome = 0;
break;
case 'f':
fastlogin = 1;
break;
case '-':
case 'l':
asme = 0;
asthem = 1;
break;
case 'm':
asme = 1;
asthem = 0;
break;
case '?':
default:
(void)fprintf(stderr,
"Usage: %s [%s] [login [shell arguments]]\n",
getprogname(), ARGSTR);
exit(1);
}
argv += optind;
/* Lower the priority so su runs faster */
errno = 0;
prio = getpriority(PRIO_PROCESS, 0);
if (errno)
prio = 0;
if (prio > -2)
(void)setpriority(PRIO_PROCESS, 0, -2);
openlog("su", 0, LOG_AUTH);
/* get current login name and shell */
ruid = getuid();
username = getlogin();
if (username == NULL || (pwd = getpwnam(username)) == NULL ||
pwd->pw_uid != ruid)
pwd = getpwuid(ruid);
if (pwd == NULL)
errx(1, "who are you?");
username = strdup(pwd->pw_name);
userpass = strdup(pwd->pw_passwd);
if (username == NULL || userpass == NULL)
err(1, "strdup");
if (asme) {
if (pwd->pw_shell && *pwd->pw_shell) {
strlcpy(shellbuf, pwd->pw_shell, sizeof(shellbuf));
shell = shellbuf;
} else {
shell = _PATH_BSHELL;
iscsh = NO;
}
}
/* get target login information, default to root */
user = *argv ? *argv : "root";
np = *argv ? argv : argv-1;
if ((pwd = getpwnam(user)) == NULL)
errx(1, "unknown login %s", user);
#ifdef LOGIN_CAP
/* force the usage of specified class */
if (class) {
if (ruid)
errx(1, "Only root may use -c");
pwd->pw_class = class;
}
lc = login_getclass(pwd->pw_class);
pw_warntime = login_getcaptime(lc, "password-warn",
_PASSWORD_WARNDAYS * SECSPERDAY,
_PASSWORD_WARNDAYS * SECSPERDAY);
#endif
if (ruid
#ifdef KERBEROS5
&& (!use_kerberos || kerberos5(username, user, pwd->pw_uid))
#endif
#ifdef KERBEROS
&& (!use_kerberos || kerberos(username, user, pwd->pw_uid))
#endif
) {
char *pass = pwd->pw_passwd;
int ok = pwd->pw_uid != 0;
#ifdef ROOTAUTH
/*
* Allow those in group rootauth to su to root, by supplying
* their own password.
*/
if (!ok) {
if ((ok = check_ingroup(-1, ROOTAUTH, username, 0))) {
pass = userpass;
user = username;
}
}
#endif
/*
* Only allow those in group SUGROUP to su to root,
* but only if that group has any members.
* If SUGROUP has no members, allow anyone to su root
*/
if (!ok) {
ok = check_ingroup(-1, SUGROUP, username, 1);
}
if (!ok)
errx(1,
"you are not listed in the correct secondary group (%s) to su %s.",
SUGROUP, user);
/* if target requires a password, verify it */
if (*pass) {
p = getpass("Password:");
#ifdef SKEY
if (strcasecmp(p, "s/key") == 0) {
if (skey_haskey(user))
errx(1, "Sorry, you have no s/key.");
else {
if (skey_authenticate(user)) {
goto badlogin;
}
}
} else
#endif
if (strcmp(pass, crypt(p, pass))) {
#ifdef SKEY
badlogin:
#endif
fprintf(stderr, "Sorry\n");
syslog(LOG_WARNING,
"BAD SU %s to %s%s", username,
pwd->pw_name, ontty());
exit(1);
}
}
}
if (asme) {
/* if asme and non-standard target shell, must be root */
if (!chshell(pwd->pw_shell) && ruid)
errx(1,"permission denied (shell).");
} else if (pwd->pw_shell && *pwd->pw_shell) {
shell = pwd->pw_shell;
iscsh = UNSET;
} else {
shell = _PATH_BSHELL;
iscsh = NO;
}
if ((p = strrchr(shell, '/')) != NULL)
avshell = p+1;
else
avshell = shell;
/* if we're forking a csh, we want to slightly muck the args */
if (iscsh == UNSET)
iscsh = strstr(avshell, "csh") ? YES : NO;
/* set permissions */
#ifdef LOGIN_CAP
if (setusercontext(lc, pwd, pwd->pw_uid,
(asthem ? (LOGIN_SETPRIORITY | LOGIN_SETUMASK) : 0) |
LOGIN_SETRESOURCES | LOGIN_SETGROUP | LOGIN_SETUSER))
err(1, "setting user context");
#else
if (setgid(pwd->pw_gid) < 0)
err(1, "setgid");
if (initgroups(user, pwd->pw_gid))
errx(1, "initgroups failed");
if (setuid(pwd->pw_uid) < 0)
err(1, "setuid");
#endif
if (!asme) {
if (asthem) {
p = getenv("TERM");
/* Create an empty environment */
if ((environ = malloc(sizeof(char *))) == NULL)
err(1, NULL);
environ[0] = NULL;
#ifdef LOGIN_CAP
if (setusercontext(lc, pwd, pwd->pw_uid, LOGIN_SETPATH))
err(1, "setting user context");
#else
(void)setenv("PATH", _PATH_DEFPATH, 1);
#endif
if (p)
(void)setenv("TERM", p, 1);
if (gohome && chdir(pwd->pw_dir) < 0)
errx(1, "no directory");
}
if (asthem || pwd->pw_uid)
(void)setenv("USER", pwd->pw_name, 1);
(void)setenv("HOME", pwd->pw_dir, 1);
(void)setenv("SHELL", shell, 1);
}
(void)setenv("SU_FROM", username, 1);
if (iscsh == YES) {
if (fastlogin)
*np-- = "-f";
if (asme)
*np-- = "-m";
} else {
if (fastlogin)
unsetenv("ENV");
}
if (asthem) {
avshellbuf[0] = '-';
(void)strlcpy(avshellbuf+1, avshell, sizeof(avshellbuf) - 1);
avshell = avshellbuf;
} else if (iscsh == YES) {
/* csh strips the first character... */
avshellbuf[0] = '_';
(void)strlcpy(avshellbuf+1, avshell, sizeof(avshellbuf) - 1);
avshell = avshellbuf;
}
*np = avshell;
#ifdef BSD4_4
if (pwd->pw_change || pwd->pw_expire)
(void)gettimeofday(&tp, (struct timezone *)NULL);
if (pwd->pw_change) {
if (tp.tv_sec >= pwd->pw_change) {
(void)printf("%s -- %s's password has expired.\n",
(ruid ? "Sorry" : "Note"), user);
if (ruid != 0)
exit(1);
} else if (pwd->pw_change - tp.tv_sec < pw_warntime)
(void)printf("Warning: %s's password expires on %s",
user, ctime(&pwd->pw_change));
}
if (pwd->pw_expire) {
if (tp.tv_sec >= pwd->pw_expire) {
(void)printf("%s -- %s's account has expired.\n",
(ruid ? "Sorry" : "Note"), user);
if (ruid != 0)
exit(1);
} else if (pwd->pw_expire - tp.tv_sec <
_PASSWORD_WARNDAYS * SECSPERDAY)
(void)printf("Warning: %s's account expires on %s",
user, ctime(&pwd->pw_expire));
}
#endif
if (ruid != 0)
syslog(LOG_NOTICE, "%s to %s%s",
username, pwd->pw_name, ontty());
/* Raise our priority back to what we had before */
(void)setpriority(PRIO_PROCESS, 0, prio);
execv(shell, np);
err(1, "%s", shell);
/* NOTREACHED */
}
static int
chshell(sh)
const char *sh;
{
const char *cp;
setusershell();
while ((cp = getusershell()) != NULL)
if (!strcmp(cp, sh))
return (1);
return (0);
}
static char *
ontty()
{
char *p;
static char buf[MAXPATHLEN + 4];
buf[0] = 0;
if ((p = ttyname(STDERR_FILENO)) != NULL)
(void)snprintf(buf, sizeof buf, " on %s", p);
return (buf);
}
#ifdef KERBEROS5
static int
kerberos5(username, user, uid)
char *username, *user;
int uid;
{
krb5_error_code ret;
krb5_context context;
krb5_principal princ = NULL;
krb5_ccache ccache, ccache2;
char *cc_name;
const char *filename;
ret = krb5_init_context(&context);
if (ret)
return (1);
if (strcmp (user, "root") == 0)
ret = krb5_make_principal(context, &princ,
NULL, username, "root", NULL);
else
ret = krb5_make_principal(context, &princ,
NULL, user, NULL);
if (ret)
goto fail;
if (!krb5_kuserok(context, princ, user) && !uid) {
warnx ("kerberos5: not in %s's ACL.", user);
goto fail;
}
ret = krb5_cc_gen_new(context, &krb5_mcc_ops, &ccache);
if (ret)
goto fail;
ret = krb5_verify_user_lrealm(context, princ, ccache, NULL, TRUE,
NULL);
if (ret) {
krb5_cc_destroy(context, ccache);
switch (ret) {
case KRB5_LIBOS_PWDINTR :
break;
case KRB5KRB_AP_ERR_BAD_INTEGRITY:
case KRB5KRB_AP_ERR_MODIFIED:
krb5_warnx(context, "Password incorrect");
break;
default :
krb5_warn(context, ret, "krb5_verify_user");
break;
}
goto fail;
}
ret = krb5_cc_gen_new(context, &krb5_fcc_ops, &ccache2);
if (ret) {
krb5_cc_destroy(context, ccache);
goto fail;
}
ret = krb5_cc_copy_cache(context, ccache, ccache2);
if (ret) {
krb5_cc_destroy(context, ccache);
krb5_cc_destroy(context, ccache2);
goto fail;
}
filename = krb5_cc_get_name(context, ccache2);
asprintf(&cc_name, "%s:%s", krb5_cc_get_type(context, ccache2),
filename);
if (chown (filename, uid, -1) < 0) {
warn("chown %s", filename);
free(cc_name);
krb5_cc_destroy(context, ccache);
krb5_cc_destroy(context, ccache2);
goto fail;
}
setenv("KRB5CCNAME", cc_name, 1);
free(cc_name);
krb5_cc_close(context, ccache2);
krb5_cc_destroy(context, ccache);
return (0);
fail:
if (princ != NULL)
krb5_free_principal (context, princ);
krb5_free_context (context);
return (1);
}
#endif
#ifdef KERBEROS
static int
kerberos(username, user, uid)
char *username, *user;
int uid;
{
KTEXT_ST ticket;
AUTH_DAT authdata;
struct hostent *hp;
int kerno;
u_long faddr;
char lrealm[REALM_SZ], krbtkfile[MAXPATHLEN];
char hostname[MAXHOSTNAMELEN + 1], savehost[MAXHOSTNAMELEN + 1];
if (krb_get_lrealm(lrealm, 1) != KSUCCESS)
return (1);
if (koktologin(username, lrealm, user) && !uid) {
warnx("kerberos: not in %s's ACL.", user);
return (1);
}
(void)snprintf(krbtkfile, sizeof krbtkfile, "%s_%s_%d", TKT_ROOT,
user, getuid());
(void)setenv("KRBTKFILE", krbtkfile, 1);
(void)krb_set_tkt_string(krbtkfile);
/*
* Set real as well as effective ID to 0 for the moment,
* to make the kerberos library do the right thing.
*/
if (setuid(0) < 0) {
warn("setuid");
return (1);
}
/*
* Little trick here -- if we are su'ing to root,
* we need to get a ticket for "xxx.root", where xxx represents
* the name of the person su'ing. Otherwise (non-root case),
* we need to get a ticket for "yyy.", where yyy represents
* the name of the person being su'd to, and the instance is null
*
* We should have a way to set the ticket lifetime,
* with a system default for root.
*/
{
char prompt[128];
char passw[256];
(void)snprintf (prompt, sizeof(prompt),
"%s's Password: ",
krb_unparse_name_long ((uid == 0 ? username : user),
(uid == 0 ? "root" : ""),
lrealm));
if (des_read_pw_string (passw, sizeof (passw), prompt, 0)) {
memset (passw, 0, sizeof (passw));
return (1);
}
if (strlen(passw) == 0)
return (1); /* Empty passwords are not allowed */
kerno = krb_get_pw_in_tkt((uid == 0 ? username : user),
(uid == 0 ? "root" : ""), lrealm,
KRB_TICKET_GRANTING_TICKET,
lrealm,
DEFAULT_TKT_LIFE,
passw);
memset (passw, 0, strlen (passw));
}
if (kerno != KSUCCESS) {
if (kerno == KDC_PR_UNKNOWN) {
warnx("kerberos: principal unknown: %s.%s@%s",
(uid == 0 ? username : user),
(uid == 0 ? "root" : ""), lrealm);
return (1);
}
warnx("kerberos: unable to su: %s", krb_err_txt[kerno]);
syslog(LOG_WARNING,
"BAD Kerberos SU: %s to %s%s: %s",
username, user, ontty(), krb_err_txt[kerno]);
return (1);
}
if (chown(krbtkfile, uid, -1) < 0) {
warn("chown");
(void)unlink(krbtkfile);
return (1);
}
(void)setpriority(PRIO_PROCESS, 0, -2);
if (gethostname(hostname, sizeof(hostname)) == -1) {
warn("gethostname");
dest_tkt();
return (1);
}
hostname[sizeof(hostname) - 1] = '\0';
(void)strlcpy(savehost, krb_get_phost(hostname), sizeof(savehost));
savehost[sizeof(savehost) - 1] = '\0';
kerno = krb_mk_req(&ticket, "rcmd", savehost, lrealm, 33);
if (kerno == KDC_PR_UNKNOWN) {
warnx("Warning: TGT not verified.");
syslog(LOG_WARNING,
"%s to %s%s, TGT not verified (%s); %s.%s not registered?",
username, user, ontty(), krb_err_txt[kerno],
"rcmd", savehost);
} else if (kerno != KSUCCESS) {
warnx("Unable to use TGT: %s", krb_err_txt[kerno]);
syslog(LOG_WARNING, "failed su: %s to %s%s: %s",
username, user, ontty(), krb_err_txt[kerno]);
dest_tkt();
return (1);
} else {
if (!(hp = gethostbyname(hostname))) {
warnx("can't get addr of %s", hostname);
dest_tkt();
return (1);
}
memmove((char *)&faddr, (char *)hp->h_addr, sizeof(faddr));
if ((kerno = krb_rd_req(&ticket, "rcmd", savehost, faddr,
&authdata, "")) != KSUCCESS) {
warnx("kerberos: unable to verify rcmd ticket: %s",
krb_err_txt[kerno]);
syslog(LOG_WARNING,
"failed su: %s to %s%s: %s", username,
user, ontty(), krb_err_txt[kerno]);
dest_tkt();
return (1);
}
}
return (0);
}
static int
koktologin(name, realm, toname)
char *name, *realm, *toname;
{
return krb_kuserok(name,
strcmp (toname, "root") == 0 ? "root" : "",
realm,
toname);
}
#endif
static int
check_ingroup (gid, gname, user, ifempty)
int gid;
const char *gname;
const char *user;
int ifempty;
{
struct group *gr;
char **g;
#ifdef SU_INDIRECT_GROUP
char **gr_mem;
int n = 0;
int i = 0;
#endif
int ok = 0;
if (gname == NULL)
gr = getgrgid((gid_t) gid);
else
gr = getgrnam(gname);
/*
* XXX we are relying on the fact that we only set ifempty when
* calling to check for SUGROUP and that is the only time a
* missing group is acceptable.
*/
if (gr == NULL)
return ifempty;
if (!*gr->gr_mem) /* empty */
return ifempty;
/*
* Ok, first see if user is in gr_mem
*/
for (g = gr->gr_mem; *g; ++g) {
if (strcmp(*g, user) == 0)
return 1; /* ok */
#ifdef SU_INDIRECT_GROUP
++n; /* count them */
#endif
}
#ifdef SU_INDIRECT_GROUP
/*
* No.
* Now we need to duplicate the gr_mem list, and recurse for
* each member to see if it is a group, and if so whether user is
* in it.
*/
gr_mem = malloc((n + 1) * sizeof (char *));
for (g = gr->gr_mem, i = 0; *g; ++g) {
gr_mem[i] = strdup(*g);
if (!gr_mem[i])
err(1, "strdup");
i++;
}
gr_mem[i++] = NULL;
for (g = gr_mem; ok == 0 && *g; ++g) {
/*
* If we get this far we don't accept empty/missing groups.
*/
ok = check_ingroup(-1, *g, user, 0);
}
for (g = gr_mem; *g; ++g) {
free(*g);
}
free(gr_mem);
#endif
return ok;
}