/* * sulogin * * This program gives Linux machines a reasonable secure way to boot single * user. It forces the user to supply the root password before a shell is * started. If there is a shadow password file and the encrypted root password * is "x" the shadow password will be used. * * Copyright (C) 1998-2003 Miquel van Smoorenburg. * Copyright (C) 2012 Karel Zak * Copyright (C) 2012 Werner Fink * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_CRYPT_H # include #endif #ifdef HAVE_LIBSELINUX # include # include #endif #include "c.h" #include "closestream.h" #include "nls.h" #include "pathnames.h" #include "strutils.h" #include "ttyutils.h" #include "sulogin-consoles.h" #define CONMAX 16 static unsigned int timeout; static int profile; static volatile uint32_t openfd; /* Remember higher file descriptors */ static volatile uint32_t *usemask; struct sigaction saved_sigint; struct sigaction saved_sigtstp; struct sigaction saved_sigquit; struct sigaction saved_sighup; struct sigaction saved_sigchld; static volatile sig_atomic_t alarm_rised; static volatile sig_atomic_t sigchild; #ifndef IUCLC # define IUCLC 0 #endif /* * Fix the tty modes and set reasonable defaults. */ static void tcinit(struct console *con) { int mode = 0, flags = 0; struct termios *tio = &con->tio; int fd = con->fd; errno = 0; if (tcgetattr(fd, tio) < 0) { warn(_("tcgetattr failed")); con->flags |= CON_NOTTY; return; } /* Handle serial lines here */ if (ioctl(fd, TIOCMGET, (char *) &mode) == 0) { speed_t ispeed, ospeed; struct winsize ws; /* this is a modem line */ con->flags |= CON_SERIAL; /* Flush input and output queues on modem lines */ tcflush(fd, TCIOFLUSH); ispeed = cfgetispeed(tio); ospeed = cfgetospeed(tio); if (!ispeed) ispeed = TTYDEF_SPEED; if (!ospeed) ospeed = TTYDEF_SPEED; tio->c_cflag = CREAD | CS8 | HUPCL | (tio->c_cflag & CLOCAL); tio->c_iflag = 0; tio->c_lflag = 0; tio->c_oflag &= OPOST | ONLCR; cfsetispeed(tio, ispeed); cfsetospeed(tio, ospeed); tio->c_line = 0; tio->c_cc[VTIME] = 0; tio->c_cc[VMIN] = 1; if (ioctl(fd, TIOCGWINSZ, &ws) == 0) { int set = 0; if (ws.ws_row == 0) { ws.ws_row = 24; set++; } if (ws.ws_col == 0) { ws.ws_col = 80; set++; } if (set) ignore_result( ioctl(fd, TIOCSWINSZ, &ws) ); } setlocale(LC_CTYPE, "POSIX"); goto setattr; } #if defined(IUTF8) && defined(KDGKBMODE) /* Detect mode of current keyboard setup, e.g. for UTF-8 */ if (ioctl(fd, KDGKBMODE, &mode) < 0) mode = K_RAW; switch(mode) { case K_UNICODE: setlocale(LC_CTYPE, "C.UTF-8"); flags |= UL_TTY_UTF8; break; case K_RAW: case K_MEDIUMRAW: case K_XLATE: default: setlocale(LC_CTYPE, "POSIX"); break; } #else setlocale(LC_CTYPE, "POSIX"); #endif reset_virtual_console(tio, flags); setattr: if (tcsetattr(fd, TCSANOW, tio)) warn(_("tcsetattr failed")); /* Enable blocking mode for read and write */ if ((flags = fcntl(fd, F_GETFL, 0)) != -1) ignore_result( fcntl(fd, F_SETFL, flags & ~O_NONBLOCK) ); } /* * Finalize the tty modes on modem lines. */ static void tcfinal(struct console *con) { struct termios *tio; int fd; if ((con->flags & CON_SERIAL) == 0) { setenv("TERM", "linux", 1); return; } if (con->flags & CON_NOTTY) return; setenv("TERM", "vt102", 1); tio = &con->tio; fd = con->fd; tio->c_iflag |= (IXON | IXOFF); tio->c_lflag |= (ICANON | ISIG | ECHO|ECHOE|ECHOK|ECHOKE); tio->c_oflag |= OPOST; tio->c_cc[VINTR] = CINTR; tio->c_cc[VQUIT] = CQUIT; tio->c_cc[VERASE] = con->cp.erase; tio->c_cc[VKILL] = con->cp.kill; tio->c_cc[VEOF] = CEOF; #ifdef VSWTC tio->c_cc[VSWTC] = _POSIX_VDISABLE; #else tio->c_cc[VSWTCH] = _POSIX_VDISABLE; #endif tio->c_cc[VSTART] = CSTART; tio->c_cc[VSTOP] = CSTOP; tio->c_cc[VSUSP] = CSUSP; tio->c_cc[VEOL] = _POSIX_VDISABLE; if (con->cp.eol == CR) { tio->c_iflag |= ICRNL; tio->c_iflag &= ~(INLCR|IGNCR); tio->c_oflag |= ONLCR; tio->c_oflag &= ~(OCRNL|ONLRET); } switch (con->cp.parity) { default: case 0: tio->c_cflag &= ~(PARODD | PARENB); tio->c_iflag &= ~(INPCK | ISTRIP); break; case 1: /* odd parity */ tio->c_cflag |= PARODD; /* fall through */ case 2: /* even parity */ tio->c_cflag |= PARENB; tio->c_iflag |= (INPCK | ISTRIP); /* fall through */ case (1 | 2): /* no parity bit */ tio->c_cflag &= ~CSIZE; tio->c_cflag |= CS7; break; } /* Set line attributes */ tcsetattr(fd, TCSANOW, tio); } /* * Called at timeout. */ static void alrm_handler(int sig __attribute__((unused))) { /* Timeout expired */ alarm_rised++; } static void chld_handler(int sig __attribute__((unused))) { sigchild++; } static void mask_signal(int signal, void (*handler)(int), struct sigaction *origaction) { struct sigaction newaction; newaction.sa_handler = handler; sigemptyset(&newaction.sa_mask); newaction.sa_flags = 0; sigaction(signal, &newaction, origaction); } static void unmask_signal(int signal, struct sigaction *sa) { sigaction(signal, sa, NULL); } /* * See if an encrypted password is valid. The encrypted password is checked for * traditional-style DES and FreeBSD-style MD5 encryption. */ static int valid(const char *pass) { const char *s; char id[5]; size_t len; off_t off; if (pass[0] == 0) return 1; if (pass[0] != '$') goto check_des; /* * up to 4 bytes for the signature e.g. $1$ */ for (s = pass+1; *s && *s != '$'; s++); if (*s++ != '$') return 0; if ((off = (off_t)(s-pass)) > 4 || off < 3) return 0; memset(id, '\0', sizeof(id)); strncpy(id, pass, off); /* * up to 16 bytes for the salt */ for (; *s && *s != '$'; s++); if (*s++ != '$') return 0; if ((off_t)(s-pass) > 16) return 0; len = strlen(s); /* * the MD5 hash (128 bits or 16 bytes) encoded in base64 = 22 bytes */ if ((strcmp(id, "$1$") == 0) && (len < 22 || len > 24)) return 0; /* * the SHA-256 hash 43 bytes */ if ((strcmp(id, "$5$") == 0) && (len < 42 || len > 44)) return 0; /* * the SHA-512 hash 86 bytes */ if ((strcmp(id, "$6$") == 0) && (len < 85 || len > 87)) return 0; /* * e.g. Blowfish hash */ return 1; check_des: if (strlen(pass) != 13) return 0; for (s = pass; *s; s++) { if ((*s < '0' || *s > '9') && (*s < 'a' || *s > 'z') && (*s < 'A' || *s > 'Z') && *s != '.' && *s != '/') return 0; } return 1; } /* * Set a variable if the value is not NULL. */ static inline void set(char **var, char *val) { if (val) *var = val; } /* * Get the root password entry. */ static struct passwd *getrootpwent(int try_manually) { static struct passwd pwd; struct passwd *pw; struct spwd *spw; FILE *fp; static char line[256]; static char sline[256]; char *p; /* * First, we try to get the password the standard way using normal * library calls. */ if ((pw = getpwnam("root")) && !strcmp(pw->pw_passwd, "x") && (spw = getspnam("root"))) pw->pw_passwd = spw->sp_pwdp; if (pw || !try_manually) return pw; /* * If we come here, we could not retrieve the root password through * library calls and we try to read the password and shadow files * manually. */ pwd.pw_name = "root"; pwd.pw_passwd = ""; pwd.pw_gecos = "Super User"; pwd.pw_dir = "/"; pwd.pw_shell = ""; pwd.pw_uid = 0; pwd.pw_gid = 0; if ((fp = fopen(_PATH_PASSWD, "r")) == NULL) { warn(_("cannot open %s"), _PATH_PASSWD); return &pwd; } /* * Find root in the password file. */ while ((p = fgets(line, 256, fp)) != NULL) { if (strncmp(line, "root:", 5) != 0) continue; p += 5; set(&pwd.pw_passwd, strsep(&p, ":")); strsep(&p, ":"); strsep(&p, ":"); set(&pwd.pw_gecos, strsep(&p, ":")); set(&pwd.pw_dir, strsep(&p, ":")); set(&pwd.pw_shell, strsep(&p, "\n")); p = line; break; } fclose(fp); /* * If the encrypted password is valid or not found, return. */ if (p == NULL) { warnx(_("%s: no entry for root\n"), _PATH_PASSWD); return &pwd; } if (valid(pwd.pw_passwd)) return &pwd; /* * The password is invalid. If there is a shadow password, try it. */ strcpy(pwd.pw_passwd, ""); if ((fp = fopen(_PATH_SHADOW_PASSWD, "r")) == NULL) { warn(_("cannot open %s"), _PATH_PASSWD); return &pwd; } while ((p = fgets(sline, 256, fp)) != NULL) { if (strncmp(sline, "root:", 5) != 0) continue; p += 5; set(&pwd.pw_passwd, strsep(&p, ":")); break; } fclose(fp); /* * If the password is still invalid, NULL it, and return. */ if (p == NULL) { warnx(_("%s: no entry for root"), _PATH_SHADOW_PASSWD); strcpy(pwd.pw_passwd, ""); } if (!valid(pwd.pw_passwd)) { warnx(_("%s: root password garbled"), _PATH_SHADOW_PASSWD); strcpy(pwd.pw_passwd, ""); } return &pwd; } /* * Ask by prompt for the password. */ static void doprompt(const char *crypted, struct console *con) { struct termios tty; if (con->flags & CON_SERIAL) { tty = con->tio; /* * For prompting: map NL in output to CR-NL * otherwise we may see stairs in the output. */ tty.c_oflag |= (ONLCR | OPOST); tcsetattr(con->fd, TCSADRAIN, &tty); } if (con->file == (FILE*)0) { if ((con->file = fdopen(con->fd, "r+")) == (FILE*)0) goto err; } #if defined(USE_ONELINE) if (crypted[0]) fprintf(con->file, _("Give root password for login: ")); else fprintf(con->file, _("Press enter for login: ")); #else if (crypted[0]) fprintf(con->file, _("Give root password for maintenance\n")); else fprintf(con->file, _("Press enter for maintenance")); fprintf(con->file, _("(or type Control-D to continue): ")); #endif fflush(con->file); err: if (con->flags & CON_SERIAL) tcsetattr(con->fd, TCSADRAIN, &con->tio); } /* * Make sure to have an own session and controlling terminal */ static void setup(struct console *con) { pid_t pid, pgrp, ppgrp, ttypgrp; int fd; if (con->flags & CON_NOTTY) return; fd = con->fd; /* * Only go through this trouble if the new * tty doesn't fall in this process group. */ pid = getpid(); pgrp = getpgid(0); ppgrp = getpgid(getppid()); ttypgrp = tcgetpgrp(fd); if (pgrp != ttypgrp && ppgrp != ttypgrp) { if (pid != getsid(0)) { if (pid == getpgid(0)) setpgid(0, getpgid(getppid())); setsid(); } mask_signal(SIGHUP, SIG_IGN, &saved_sighup); if (ttypgrp > 0) ioctl(STDIN_FILENO, TIOCNOTTY, (char *)1); unmask_signal(SIGHUP, &saved_sighup); if (fd > STDIN_FILENO) close(STDIN_FILENO); if (fd > STDOUT_FILENO) close(STDOUT_FILENO); if (fd > STDERR_FILENO) close(STDERR_FILENO); ioctl(fd, TIOCSCTTY, (char *)1); tcsetpgrp(fd, ppgrp); } dup2(fd, STDIN_FILENO); dup2(fd, STDOUT_FILENO); dup2(fd, STDERR_FILENO); con->fd = STDIN_FILENO; for (fd = STDERR_FILENO+1; fd < 32; fd++) { if (openfd & (1<flags & CON_NOTTY) goto out; fd = con->fd; cp = &con->cp; tty = con->tio; tty.c_iflag &= ~(IUCLC|IXON|IXOFF|IXANY); tty.c_lflag &= ~(ECHO|ECHOE|ECHOK|ECHONL|TOSTOP|ISIG); tc = (tcsetattr(fd, TCSAFLUSH, &tty) == 0); sa.sa_handler = alrm_handler; sa.sa_flags = 0; sigaction(SIGALRM, &sa, NULL); if (timeout) alarm(timeout); ptr = &pass[0]; cp->eol = *ptr = '\0'; eightbit = ((con->flags & CON_SERIAL) == 0 || (tty.c_cflag & (PARODD|PARENB)) == 0); while (cp->eol == '\0') { if (read(fd, &c, 1) < 1) { if (errno == EINTR || errno == EAGAIN) { usleep(1000); continue; } ret = (char*)0; switch (errno) { case 0: case EIO: case ESRCH: case EINVAL: case ENOENT: break; default: fprintf(stderr, "sulogin: read(%s): %m\n\r", con->tty); break; } goto quit; } if (eightbit) ascval = c; else if (c != (ascval = (c & 0177))) { uint32_t bits, mask; for (bits = 1, mask = 1; mask & 0177; mask <<= 1) { if (mask & ascval) bits++; } cp->parity |= ((bits & 1) ? 1 : 2); } switch (ascval) { case 0: *ptr = '\0'; goto quit; case CR: case NL: *ptr = '\0'; cp->eol = ascval; break; case BS: case CERASE: cp->erase = ascval; if (ptr > &pass[0]) ptr--; break; case CKILL: cp->kill = ascval; while (ptr > &pass[0]) ptr--; break; case CEOF: goto quit; default: if ((size_t)(ptr - &pass[0]) >= (sizeof(pass) -1 )) { fprintf(stderr, "sulogin: input overrun at %s\n\r", con->tty); ret = (char*)0; goto quit; } *ptr++ = ascval; break; } } quit: alarm(0); if (tc) tcsetattr(fd, TCSAFLUSH, &con->tio); if (ret && *ret != '\0') tcfinal(con); printf("\r\n"); out: return ret; } /* * Password was OK, execute a shell. */ static void sushell(struct passwd *pwd) { char shell[PATH_MAX]; char home[PATH_MAX]; char *p; char *su_shell; /* * Set directory and shell. */ if (chdir(pwd->pw_dir) != 0) { warn(_("%s: change directory failed"), pwd->pw_dir); printf(_("Logging in with home = \"/\".\n")); if (chdir("/") != 0) warn(_("change directory to system root failed")); } if ((p = getenv("SUSHELL")) != NULL) su_shell = p; else if ((p = getenv("sushell")) != NULL) su_shell = p; else { if (pwd->pw_shell[0]) su_shell = pwd->pw_shell; else su_shell = "/bin/sh"; } if ((p = strrchr(su_shell, '/')) == NULL) p = su_shell; else p++; snprintf(shell, sizeof(shell), profile ? "-%s" : "%s", p); /* * Set some important environment variables. */ if (getcwd(home, sizeof(home)) == NULL) strcpy(home, "/"); setenv("HOME", home, 1); setenv("LOGNAME", "root", 1); setenv("USER", "root", 1); if (!profile) setenv("SHLVL","0",1); /* * Try to execute a shell. */ setenv("SHELL", su_shell, 1); unmask_signal(SIGINT, &saved_sigint); unmask_signal(SIGTSTP, &saved_sigtstp); unmask_signal(SIGQUIT, &saved_sigquit); mask_signal(SIGHUP, SIG_DFL, NULL); #ifdef HAVE_LIBSELINUX if (is_selinux_enabled() > 0) { security_context_t scon=NULL; char *seuser=NULL; char *level=NULL; if (getseuserbyname("root", &seuser, &level) == 0) { if (get_default_context_with_level(seuser, level, 0, &scon) == 0) { if (setexeccon(scon) != 0) warnx(_("setexeccon failed")); freecon(scon); } } free(seuser); free(level); } #endif execl(su_shell, shell, NULL); warn(_("%s: exec failed"), su_shell); setenv("SHELL", "/bin/sh", 1); execl("/bin/sh", profile ? "-sh" : "sh", NULL); warn(_("%s: exec failed"), "/bin/sh"); } static void usage(FILE *out) { fputs(USAGE_HEADER, out); fprintf(out, _( " %s [options] [tty device]\n"), program_invocation_short_name); fputs(USAGE_OPTIONS, out); fputs(_(" -p, --login-shell start a login shell\n" " -t, --timeout max time to wait for a password (default: no limit)\n" " -e, --force examine password files directly if getpwnam(3) fails\n"), out); fputs(USAGE_SEPARATOR, out); fputs(USAGE_HELP, out); fputs(USAGE_VERSION, out); fprintf(out, USAGE_MAN_TAIL("sulogin(8)")); } int main(int argc, char **argv) { LIST_HEAD(consoles); struct list_head *ptr; struct console *con; char *tty = NULL; struct passwd *pwd; int c, status = 0; int reconnect = 0; int opt_e = 0; pid_t pid; static const struct option longopts[] = { { "login-shell", 0, 0, 'p' }, { "timeout", 1, 0, 't' }, { "force", 0, 0, 'e' }, { "help", 0, 0, 'h' }, { "version", 0, 0, 'V' }, { NULL, 0, 0, 0 } }; /* * If we are init we need to set up a own session. */ if ((pid = getpid()) == 1) { setsid(); ignore_result( ioctl(STDIN_FILENO, TIOCSCTTY, (char *) 1) ); } setlocale(LC_ALL, ""); bindtextdomain(PACKAGE, LOCALEDIR); textdomain(PACKAGE); atexit(close_stdout); /* XXX */ /* * See if we have a timeout flag. */ while ((c = getopt_long(argc, argv, "ehpt:V", longopts, NULL)) != -1) { switch(c) { case 't': timeout = strtou32_or_err(optarg, _("invalid timeout argument")); break; case 'p': profile = 1; break; case 'e': opt_e = 1; break; case 'V': printf(UTIL_LINUX_VERSION); return EXIT_SUCCESS; case 'h': usage(stdout); return EXIT_SUCCESS; default: usage(stderr); /* Do not exit! */ break; } } if (geteuid() != 0) errx(EXIT_FAILURE, _("only root can run this program.")); mask_signal(SIGQUIT, SIG_IGN, &saved_sigquit); mask_signal(SIGTSTP, SIG_IGN, &saved_sigtstp); mask_signal(SIGINT, SIG_IGN, &saved_sigint); mask_signal(SIGHUP, SIG_IGN, &saved_sighup); emergency_do_mounts(); atexit( emergency_do_umounts ); /* * See if we need to open an other tty device. */ if (optind < argc) tty = argv[optind]; if (!tty || *tty == '\0') tty = getenv("CONSOLE"); /* * Detect possible consoles, use stdin as fallback. * If an optional tty is given, reconnect it to stdin. */ reconnect = detect_consoles(tty, STDIN_FILENO, &consoles); /* * If previous stdin was not the speified tty and therefore reconnected * to the specified tty also reconnect stdout and stderr. */ if (reconnect) { if (isatty(STDOUT_FILENO) == 0) dup2(STDOUT_FILENO, STDIN_FILENO); if (isatty(STDERR_FILENO) == 0) dup2(STDOUT_FILENO, STDERR_FILENO); } /* * Should not happen */ if (list_empty(&consoles)) { if (!errno) errno = ENOENT; errx(EXIT_FAILURE, _("cannot open console: %m\n")); } /* * Get the root password. */ if ((pwd = getrootpwent(opt_e)) == NULL) { warnx(_("cannot open password database.")); sleep(2); } /* * Ask for the password on the consoles. */ list_for_each(ptr, &consoles) { con = list_entry(ptr, struct console, entry); if (con->id >= CONMAX) break; if (con->fd >= 0) { openfd |= (1 << con->fd); tcinit(con); continue; } if ((con->fd = open(con->tty, O_RDWR | O_NOCTTY | O_NONBLOCK)) < 0) continue; openfd |= (1 << con->fd); tcinit(con); } ptr = (&consoles)->next; usemask = (uint32_t*) mmap(NULL, sizeof(uint32_t), PROT_READ|PROT_WRITE, MAP_ANONYMOUS|MAP_SHARED, -1, 0); if (ptr->next == &consoles) { con = list_entry(ptr, struct console, entry); goto nofork; } mask_signal(SIGCHLD, chld_handler, &saved_sigchld); do { con = list_entry(ptr, struct console, entry); if (con->id >= CONMAX) break; switch ((con->pid = fork())) { case 0: mask_signal(SIGCHLD, SIG_DFL, NULL); /* fall through */ nofork: setup(con); while (1) { const char *passwd = pwd->pw_passwd; const char *answer; int failed = 0, doshell = 0; doprompt(passwd, con); if ((answer = getpasswd(con)) == NULL) break; if (passwd[0] == '\0') doshell++; else { const char *cryptbuf; cryptbuf = crypt(answer, passwd); if (cryptbuf == NULL) warnx(_("crypt failed: %m\n")); else if (strcmp(cryptbuf, pwd->pw_passwd) == 0) doshell++; } if (doshell) { *usemask |= (1<id); sushell(pwd); *usemask &= ~(1<id); failed++; } mask_signal(SIGQUIT, SIG_IGN, &saved_sigquit); mask_signal(SIGTSTP, SIG_IGN, &saved_sigtstp); mask_signal(SIGINT, SIG_IGN, &saved_sigint); if (failed) { fprintf(stderr, _("Can not execute su shell\n\n")); break; } fprintf(stderr, _("Login incorrect\n\n")); } if (alarm_rised) { tcfinal(con); warnx(_("Timed out\n\n")); } /* * User pressed Control-D. */ exit(0); case -1: warnx(_("Can not fork: %m\n")); /* fall through */ default: break; } ptr = ptr->next; } while (ptr != &consoles); while ((pid = wait(&status))) { if (errno == ECHILD) break; if (pid < 0) continue; list_for_each(ptr, &consoles) { con = list_entry(ptr, struct console, entry); if (con->pid == pid) { *usemask &= ~(1<id); continue; } if (kill(con->pid, 0) < 0) { *usemask &= ~(1<id); continue; } if (*usemask & (1<id)) continue; kill(con->pid, SIGHUP); usleep(5000); kill(con->pid, SIGKILL); } } mask_signal(SIGCHLD, SIG_DFL, NULL); return EXIT_SUCCESS; }