su.c revision 4ba5c7f83ffc37214b22853dd1ef6752d0178a4e
/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License (the "License").
* You may not use this file except in compliance with the License.
*
* You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at usr/src/OPENSOLARIS.LICENSE.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information: Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*/
/*
* Copyright 2012 Milan Jurik. All rights reserved.
*/
/* Copyright (c) 1984, 1986, 1987, 1988, 1989 AT&T */
/* All Rights Reserved */
/* Copyright (c) 1987, 1988 Microsoft Corporation */
/* All Rights Reserved */
/*
* su [-] [name [arg ...]] change userid, `-' changes environment.
* If SULOG is defined, all attempts to su to another user are
* logged there.
* If CONSOLE is defined, all successful attempts to su to uid 0
* are also logged there.
*
* If su cannot create, open, or write entries into SULOG,
* (or on the CONSOLE, if defined), the entry will not
* be logged -- thus losing a record of the su's attempted
* during this period.
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <crypt.h>
#include <pwd.h>
#include <shadow.h>
#include <time.h>
#include <signal.h>
#include <fcntl.h>
#include <string.h>
#include <locale.h>
#include <syslog.h>
#include <grp.h>
#include <deflt.h>
#include <limits.h>
#include <errno.h>
#include <stdarg.h>
#include <user_attr.h>
#include <priv.h>
#include <bsm/adt_event.h>
#include <security/pam_appl.h>
#define ELIM 128
#define ROOT 0
#ifdef DYNAMIC_SU
#define EMBEDDED_NAME "embedded_su"
#endif /* DYNAMIC_SU */
/*
* Intervals to sleep after failed su
*/
#ifndef SLEEPTIME
#define SLEEPTIME 4
#endif
#define DEFAULT_LOGIN "/etc/default/login"
/*
* Locale variables to be propagated to "su -" environment
*/
static char *initvar;
static char *initenv[] = {
"TZ", "LANG", "LC_CTYPE",
"LC_NUMERIC", "LC_TIME", "LC_COLLATE",
"LC_MONETARY", "LC_MESSAGES", "LC_ALL", 0};
static void envalt(void);
static void log(char *, char *, int);
static void to(int);
static void message(enum messagemode, char *, ...);
static char *alloc_vsprintf(const char *, va_list);
static char *tail(char *);
static void audit_success(int, struct passwd *);
static void audit_failure(int, struct passwd *, char *, int);
#ifdef DYNAMIC_SU
static void validate(char *, int *);
static int legalenvvar(char *);
void *);
static void quotemsg(char *, ...);
static void readinitblock(void);
#else /* !DYNAMIC_SU */
#endif /* DYNAMIC_SU */
char *term;
char *hz;
extern char **environ;
char *ttyn;
char *username; /* the invoker */
static int dosyslog = 0; /* use syslog? */
char *myname;
#ifdef DYNAMIC_SU
int pam_flags = 0;
#endif /* DYNAMIC_SU */
int
{
#ifndef DYNAMIC_SU
char *password;
#endif /* !DYNAMIC_SU */
char *nptr;
char *pshell;
int eflag = 0;
int envidx = 0;
char *ptr;
#ifdef DYNAMIC_SU
char **pam_env = 0;
int flags = 0;
int retcode;
int idx = 0;
#endif /* DYNAMIC_SU */
#if !defined(TEXT_DOMAIN) /* Should be defined by cc -D */
#endif
(void) textdomain(TEXT_DOMAIN);
#ifdef DYNAMIC_SU
}
#endif /* DYNAMIC_SU */
/* Explicitly check for just `-' (no trailing chars) */
eflag++; /* set eflag if `-' is specified */
argv++;
argc--;
} else {
gettext("Usage: %s [-] [ username [ arg ... ] ]"),
prog);
exit(1);
}
}
/*
* Determine specified userid, get their password file entry,
* and set variables to values in password file entry fields.
*/
if (argc > 1) {
/*
* Usernames can't start with a `-', so we check for that to
* catch bad usage (like "su - -c ls").
*/
gettext("Usage: %s [-] [ username [ arg ... ] ]"),
prog);
exit(1);
} else
} else
}
ttyn = "/dev/???";
username = "(null)";
/*
* if Sulog defined, create SULOG, if it does not exist, with
*/
}
#ifdef DYNAMIC_SU
exit(1);
exit(1);
#endif /* DYNAMIC_SU */
#ifdef DYNAMIC_SU
/*
* Use the same value of sleeptime and password required that
* login(1) uses.
* using the def*() functions
*/
if (defopen(DEFAULT_LOGIN) == 0) {
}
}
/*
* Ignore SIGQUIT and SIGINT
*/
/* call pam_authenticate() to authenticate the user through PAM */
} else /* root user does not need to authenticate */
if (retcode != PAM_SUCCESS) {
/*
* 1st step: audit and log the error.
* 2nd step: sleep.
* 3rd step: print out message to user.
*/
/* don't let audit_failure distinguish a role here */
switch (retcode) {
case PAM_USER_UNKNOWN:
closelog();
break;
case PAM_AUTH_ERR:
if (dosyslog)
closelog();
break;
case PAM_CONV_ERR:
default:
if (dosyslog)
closelog();
break;
}
exit(1);
}
if (flags)
exit(2);
}
if (dosyslog)
"'su %s' succeeded for %s on %s",
closelog();
#else /* !DYNAMIC_SU */
closelog();
exit(1);
}
/*
* Prompt for password if invoking user is not root or
* if specified(new) user requires a password
*/
goto ok;
/* clear password file entry */
if (dosyslog)
closelog();
exit(2);
}
/* clear password file entry */
ok:
/* update audit session in a non-pam environment */
update_audit(&pwd);
if (dosyslog)
"'su %s' succeeded for %s on %s",
#endif /* DYNAMIC_SU */
/* set user and group ids to specified user */
/* set the real (and effective) GID */
exit(2);
}
/* Initialize the supplementary group access list. */
if (!nptr)
exit(2);
exit(2);
}
/* set the real (and effective) UID */
exit(2);
}
/*
* set:
*
* pshell = their shell
* su = [-]last component of shell's pathname
*
*/
char *p;
else
} else {
}
/*
* set environment variables for new user;
* arg0 for exec of shprog must now contain `-'
* so that environment of new user is given
*/
if (eflag) {
int j;
char *var;
}
exit(1);
}
}
/*
* the inherited environment.
*
* We have a priority here for setting TZ. If TZ is set in
* in the inherited environment, that value remains top
* that has second highest priority.
*/
tznam[0] = '\0';
for (j = 0; initenv[j] != 0; j++) {
/*
* Skip over values beginning with '/' for
* security.
*/
if (initvar[0] == '/') continue;
sizeof (tznam));
} else {
var = (char *)
+ 2);
perror("malloc");
exit(4);
}
}
}
}
/*
* Check if TZ was found. If not then try to read it from
*/
if (tznam[0] == '\0') {
if (defopen(DEFAULT_LOGIN) == 0) {
sizeof (tznam));
}
}
}
if (tznam[0] != '\0')
#ifdef DYNAMIC_SU
/*
* set the PAM environment variables -
* check for legal environment variables
*/
}
idx++;
}
}
#endif /* DYNAMIC_SU */
} else {
if (*p == 'L' && p[1] == 'D' && p[2] == '_') {
;
/* pp is not advanced */
} else {
pp++;
}
}
}
#ifdef DYNAMIC_SU
if (pamh)
#endif /* DYNAMIC_SU */
/*
* if new user is root:
* if CONSOLE defined, log entry there;
* if eflag not set, change environment to that of root.
*/
(void) alarm(30);
(void) alarm(0);
}
if (!eflag)
envalt();
}
/*
* Default for SIGCPU and SIGXFSZ. Shells inherit
* signal disposition from parent. And the
* shells should have default dispositions for these
* signals.
*/
#ifdef DYNAMIC_SU
if (embedded) {
(void) puts("SUCCESS");
/*
* After this point, we're no longer talking the
* embedded_su protocol, so turn it off.
*/
}
#endif /* DYNAMIC_SU */
/*
* if additional arguments, exec shell program with array
* of pointers to arguments:
* -> if shell = default, then su = [-]su
* -> if shell != default, then su = [-]last component of
* shell's pathname
*
* if no additional arguments, exec shell with arg0 of su
* where:
* -> if shell = default, then su = [-]su
* -> if shell != default, then su = [-]last component of
* shell's pathname
*/
if (argc > 2) {
} else
/*
* Try to clean up after an administrator who has made a mistake
*/
gettext("No shell %s. Trying fallback shell %s."),
pshell, safe_shell);
if (eflag) {
} else {
}
if (argc > 2) {
} else {
}
} else {
}
return (3);
}
/*
* Environment altering routine -
* This routine is called when a user is su'ing to root
* without specifying the - flag.
* The user's PATH and PS1 variables are reset
* to the correct value for root.
* All of the user's other environment variables retain
* their current values after the su (if they are exported).
*/
static void
envalt(void)
{
/*
* If user has PATH variable in their environment, change its value
* if user does not have PATH variable, add it to the user's
* environment;
* if either of the above fail, an error message is printed.
*/
gettext("unable to obtain memory to expand environment"));
exit(4);
}
/*
* If user has PROMPT variable in their environment, change its value
* to # ;
* if user does not have PROMPT variable, add it to the user's
* environment;
* if either of the above fail, an error message is printed.
*/
gettext("unable to obtain memory to expand environment"));
exit(4);
}
}
/*
* Logging routine -
* where = SULOG or CONSOLE
* towho = specified user ( user being su'ed to )
* how = 0 if su attempt failed; 1 if su attempt succeeded
*/
static void
{
/*
* open SULOG or CONSOLE - if open fails, return
*/
return;
/*
* write entry into SULOG or onto CONSOLE - if write fails, return
*/
}
/*ARGSUSED*/
static void
{}
/*
* audit_success - audit successful su
*
* Entry process audit context established -- i.e., pam_setcred()
* or equivalent called.
* pw_change = PW_TRUE, if successful password change audit
* required.
* pwd = passwd entry for new user.
*/
static void
{
char *kva_value;
"adt_start_session(ADT_su): %m");
return;
}
USERATTR_TYPE_KW)) != NULL) &&
}
"adt_set_user(ADT_su, ADT_FAILURE): %m");
}
"adt_put_event(ADT_su, ADT_SUCCESS): %m");
}
/* Also audit password change */
"adt_alloc_event(ADT_passwd): %m");
ADT_SUCCESS) != 0) {
"adt_put_event(ADT_passwd, ADT_SUCCESS): %m");
}
}
/*
* The preceeding code is a noop if audit isn't enabled,
* but, let's not make a new process when it's not necessary.
*/
if (adt_audit_state(AUC_AUDITING)) {
}
(void) adt_end_session(ah);
}
/*
* audit_logout - audit successful su logout
*
* Entry ah = Successful su audit handle
* event_id = su event ID: ADT_su, ADT_role_login
*
* Exit Errors are just ignored and we go on.
* su logout event written.
*/
static void
{
int status; /* wait status */
} else {
}
"adt_alloc_event(ADT_su_logout): %m");
return;
}
"su audit_logout: could not alloc basic privs: %m");
return;
}
/*
* The child returns and continues su processing.
* The parent's sole job is to wait for child exit, write the
* logout audit record, and replay the child's exit code.
*/
/* child */
return;
}
if (pid == -1) {
/* failure */
"su audit_logout: could not fork: %m");
return;
}
/* parent process */
/*
* When this routine is called, the current working
* directory is the unknown and there are unknown open
* files. For the waiting process, change the current
* directory to root and close open files so that
* directories can be unmounted if necessary.
*/
if (chdir("/") != 0) {
"su audit_logout: could not chdir /: %m");
}
/*
* Reduce privileges to just those needed.
*/
"su audit_logout: could not reduce privs: %m");
}
closefrom(0);
for (;;) {
/*
* No existing child with the given pid. Lets
* audit the logout.
*/
break;
}
continue;
}
/*
* The child shell exited or was terminated by
* a signal. Lets audit logout.
*/
break;
} else if (WIFSTOPPED(status)) {
int fd;
void (*sg_handler)();
/*
* We need to suspend here as well and pass down
* the control to the parent process.
*/
/*
* We stop here. When resumed, mark the child
* shell group as foreground process group
* which gives the child shell a control over
* the controlling terminal.
*/
/*
* Pass down the control over the controlling
* terminal iff we are in a foreground process
* group. Otherwise, we are in a background
* process group and the kernel will send
* SIGTTOU signal to stop us (by default).
*/
}
}
/* Wake up the child shell */
}
}
(void) adt_end_session(ah);
}
/*
* audit_failure - audit failed su
*
* Entry New audit context not set.
* pw_change == PW_FALSE, if no password change requested.
* PW_FAILED, if failed password change audit
* required.
* pwd = NULL, or password entry to use.
* user = username entered. Add to record if pwd == NULL.
* pamerr = PAM error code; reason for failure.
*/
static void
{
char *kva_value;
"adt_start_session(ADT_su, ADT_FAILURE): %m");
return;
}
/* target user authenticated, merge audit state */
"adt_set_user(ADT_su, ADT_FAILURE): %m");
}
USERATTR_TYPE_KW)) != NULL) &&
}
}
"adt_alloc_event(ADT_su, ADT_FAILURE): %m");
return;
}
/*
* can't tell if user not found is a role, so always use su
* If we do pass in pwd when the JNI is fixed, then can
* distinguish and set name in both su and role_login
*/
/*
* this should be "fail_user" rather than "message"
* see adt_xml. The JNI breaks, so for now we leave
* this alone.
*/
}
ADT_FAIL_PAM + pamerr) != 0) {
"adt_put_event(ADT_su(ADT_FAIL, %s): %m",
}
/* Also audit password change failed */
"su: adt_alloc_event(ADT_passwd): %m");
ADT_FAIL_PAM + pamerr) != 0) {
"su: adt_put_event(ADT_passwd, ADT_FAILURE): %m");
}
}
(void) adt_end_session(ah);
}
#ifdef DYNAMIC_SU
/*
* su_conv():
* This is the conv (conversation) function called from
* a PAM authentication module to print error messages
* or garner information from the user.
*/
/*ARGSUSED*/
static int
void *appdata_ptr)
{
struct pam_message *m;
struct pam_response *r;
char *temp;
int k;
char respbuf[PAM_MAX_RESP_SIZE];
if (num_msg <= 0)
return (PAM_CONV_ERR);
sizeof (struct pam_response));
return (PAM_BUF_ERR);
k = num_msg;
m = *msg;
r = *response;
while (k--) {
switch (m->msg_style) {
case PAM_PROMPT_ECHO_OFF:
errno = 0;
return (PAM_CONV_ERR);
return (PAM_BUF_ERR);
}
}
break;
case PAM_PROMPT_ECHO_ON:
}
*temp = '\0';
return (PAM_BUF_ERR);
}
break;
case PAM_ERROR_MSG:
}
break;
case PAM_TEXT_INFO:
}
break;
default:
break;
}
m++;
r++;
}
return (PAM_SUCCESS);
}
/*
* emb_su_conv():
* This is the conv (conversation) function called from
* a PAM authentication module to print error messages
* or garner information from the user.
* This version is used for embedded_su.
*/
/*ARGSUSED*/
static int
{
struct pam_message *m;
struct pam_response *r;
char *temp;
int k;
char respbuf[PAM_MAX_RESP_SIZE];
if (num_msg <= 0)
return (PAM_CONV_ERR);
sizeof (struct pam_response));
return (PAM_BUF_ERR);
/* First, send the prompts */
k = num_msg;
m = *msg;
while (k--) {
switch (m->msg_style) {
case PAM_PROMPT_ECHO_OFF:
(void) puts("PAM_PROMPT_ECHO_OFF");
goto msg_common;
case PAM_PROMPT_ECHO_ON:
(void) puts("PAM_PROMPT_ECHO_ON");
goto msg_common;
case PAM_ERROR_MSG:
(void) puts("PAM_ERROR_MSG");
goto msg_common;
case PAM_TEXT_INFO:
(void) puts("PAM_TEXT_INFO");
/* fall through to msg_common */
else
break;
default:
break;
}
m++;
}
/* Next, collect the responses */
k = num_msg;
m = *msg;
r = *response;
while (k--) {
switch (m->msg_style) {
case PAM_PROMPT_ECHO_OFF:
case PAM_PROMPT_ECHO_ON:
*temp = '\0';
return (PAM_BUF_ERR);
}
break;
case PAM_ERROR_MSG:
case PAM_TEXT_INFO:
break;
default:
break;
}
m++;
r++;
}
return (PAM_SUCCESS);
}
static void
{
struct pam_response *r;
int i;
/* free responses */
r = *response;
for (i = 0; i < num_msg; i++, r++) {
/* Zap it in case it's a password */
}
}
}
/*
* Print a message, applying quoting for lines starting with '.'.
*
* I18n note: \n is "safe" in all locales, and all locales use
* a high-bit-set character to start multibyte sequences, so
* scanning for a \n followed by a '.' is safe.
*/
static void
{
char *msg;
char *p;
va_list v;
va_end(v);
for (p = msg; *p != '\0'; p++) {
if (bol) {
if (*p == '.')
(void) putchar('.');
}
(void) putchar(*p);
if (*p == '\n')
}
(void) putchar('\n');
}
(void) putchar('.');
(void) putchar('\n');
}
/*
* validate - Check that the account is valid for switching to.
*/
static void
{
int error;
int tries;
if (error == PAM_NEW_AUTHTOK_REQD) {
tries = 0;
if ((error == PAM_AUTHTOK_ERR ||
error == PAM_TRY_AGAIN) &&
(tries++ < DEF_ATTEMPTS)) {
continue;
}
if (dosyslog)
"'su %s' failed for %s on %s",
closelog();
exit(1);
}
return;
} else {
if (dosyslog)
closelog();
exit(3);
}
}
}
static char *illegal[] = {
"SHELL=",
"HOME=",
"LOGNAME=",
#ifndef NO_MAIL
"MAIL=",
#endif
"CDPATH=",
"IFS=",
"PATH=",
"TZ=",
"HZ=",
"TERM=",
0
};
/*
* legalenvvar - can PAM modules insert this environmental variable?
*/
static int
legalenvvar(char *s)
{
register char **p;
for (p = illegal; *p; p++)
return (0);
if (s[0] == 'L' && s[1] == 'D' && s[2] == '_')
return (0);
return (1);
}
/*
* The embedded_su protocol allows the client application to supply
* an initialization block terminated by a line with just a "." on it.
*
* This initialization block is currently unused, reserved for future
* expansion. Ignore it. This is made very slightly more complex by
* the desire to cleanly ignore input lines of any length, while still
* correctly detecting a line with just a "." on it.
*
* I18n note: It appears that none of the Solaris-supported locales
* use 0x0a for any purpose other than newline, so looking for '\n'
* seems safe.
* All locales use high-bit-set leadin characters for their multi-byte
* sequences, so a line consisting solely of ".\n" is what it appears
* to be.
*/
static void
readinitblock(void)
{
char buf[100];
for (;;) {
return;
return;
}
}
#else /* !DYNAMIC_SU */
static void
{
if (dosyslog)
"cannot start audit session %m",
closelog();
exit(2);
}
if (dosyslog)
"cannot update audit session %m",
closelog();
exit(2);
}
}
#endif /* DYNAMIC_SU */
/*
* Report an error, either a fatal one, a warning, or a usage message,
* depending on the mode parameter.
*/
/*ARGSUSED*/
static void
{
char *s;
va_list v;
s = alloc_vsprintf(fmt, v);
va_end(v);
#ifdef DYNAMIC_SU
if (embedded) {
(void) printf("CONV 1\n");
(void) printf("PAM_ERROR_MSG\n");
} else { /* ERR, USAGE */
(void) printf("ERROR\n");
}
quotemsg("%s", s);
} else { /* ERR, WARN */
}
} else {
#endif /* DYNAMIC_SU */
} else { /* ERR, WARN */
}
#ifdef DYNAMIC_SU
}
#endif /* DYNAMIC_SU */
free(s);
}
/*
* Return a pointer to the last path component of a.
*/
static char *
tail(char *a)
{
char *p;
p = strrchr(a, '/');
if (p == NULL)
p = a;
else
p++; /* step over the '/' */
return (p);
}
static char *
{
int n;
char buf[1];
char *s;
/*
* We need to scan the argument list twice. Save off a copy
* of the argument list pointer(s) for the second pass. Note that
* we are responsible for va_end'ing our copy.
*/
/*
* vsnprintf into a dummy to get a length. One might
* think that passing 0 as the length to snprintf would
* do what we want, but it's defined not to.
*
* Perhaps we should sprintf into a 100 character buffer
* or something like that, to avoid two calls to snprintf
* in most cases.
*/
/*
* Allocate an appropriately-sized buffer.
*/
s = malloc(n + 1);
if (s == NULL) {
perror("malloc");
exit(4);
}
return (s);
}