// SPDX-License-Identifier: GPL-2.0-or-later // // su [-|-l] [-p] [-c COMMAND] [USER] // // Run a shell, or one command, as another user -- root by default. Setuid root, // and a program of its own rather than a toybox applet: toybox is one binary // under every name, and a setuid copy of it can be asked for any of its other // applets by naming them, all as root. // // The rules are the ones su has always had, kept short: // // - root may become anyone, with no password; // - anyone else is asked for the target's password, and gets three tries; // - becoming root takes membership in `wheel` first, whatever the password // is -- an account with no password is otherwise a door for every user. // // -, -l a login shell: a clean environment, the target's home, argv[0] "-sh" // -p keep the environment (HOME, SHELL, USER and LOGNAME are still set // unless the target is root) // -c CMD run CMD with the shell's -c rather than an interactive shell #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include static int in_wheel(uid_t uid, gid_t gid) { struct group *g = getgrnam("wheel"); struct passwd *pw = getpwuid(uid); if (!g) return 0; if (gid == g->gr_gid) return 1; for (char **m = g->gr_mem; pw && m && *m; m++) if (strcmp(*m, pw->pw_name) == 0) return 1; return 0; } // A line from the terminal with echo off. The password is read from the // controlling terminal, not stdin, so a script cannot feed it. static int read_password(const char *prompt, char *buf, size_t n) { int fd = open("/dev/tty", O_RDWR | O_NOCTTY); struct termios t, old; size_t i = 0; char c; if (fd < 0) return -1; if (tcgetattr(fd, &old) == 0) { t = old; t.c_lflag &= ~(ECHO | ECHONL); tcsetattr(fd, TCSAFLUSH, &t); } if (write(fd, prompt, strlen(prompt)) < 0) {} while (read(fd, &c, 1) == 1 && c != '\n' && c != '\r') if (i + 1 < n) buf[i++] = c; buf[i] = 0; tcsetattr(fd, TCSAFLUSH, &old); if (write(fd, "\n", 1) < 0) {} close(fd); return 0; } // Does `pw` accept this password? An empty hash accepts anything, a locked one // ("*", "!...") nothing. static int password_ok(const struct passwd *pw, const char *pass) { const char *hash = pw->pw_passwd; struct spwd *sp; char *enc; if (hash[0] == 'x' && hash[1] == 0) { sp = getspnam(pw->pw_name); if (!sp) return 0; hash = sp->sp_pwdp; } if (hash[0] == 0) return 1; if (hash[0] == '*' || hash[0] == '!') return 0; enc = crypt(pass, hash); return enc && strcmp(enc, hash) == 0; } int main(int argc, char **argv) { const char *target = "root", *cmd = 0; int login = 0, keep = 0, i; struct passwd *pw, *me; uid_t uid = getuid(); char pass[256]; for (i = 1; i < argc; i++) { if (!strcmp(argv[i], "-") || !strcmp(argv[i], "-l")) login = 1; else if (!strcmp(argv[i], "-p")) keep = 1; else if (!strcmp(argv[i], "-c") && i + 1 < argc) cmd = argv[++i]; else if (argv[i][0] == '-' && argv[i][1]) { fprintf(stderr, "usage: su [-|-l] [-p] [-c COMMAND] [USER]\n"); return 2; } else target = argv[i]; } if (geteuid() != 0) { fprintf(stderr, "su: must be setuid root\n"); return 1; } // getpwnam and getpwuid return the same static buffer, so the second call // overwrites the first answer. Keep our own copy of the one that matters. if (!(pw = getpwnam(target))) { fprintf(stderr, "su: unknown user %s\n", target); return 1; } { static struct passwd copy; copy = *pw; copy.pw_name = strdup(pw->pw_name); copy.pw_passwd = strdup(pw->pw_passwd); copy.pw_dir = strdup(pw->pw_dir); copy.pw_shell = strdup(pw->pw_shell); pw = © } me = getpwuid(uid); if (uid != 0) { int tries, ok = 0; if (pw->pw_uid == 0 && !in_wheel(uid, getgid())) { fprintf(stderr, "su: %s is not in the wheel group\n", me ? me->pw_name : "you"); return 1; } for (tries = 0; tries < 3 && !ok; tries++) { if (pw->pw_passwd[0] == 0 || (pw->pw_passwd[0] == 'x' && getspnam(pw->pw_name) && getspnam(pw->pw_name)->sp_pwdp[0] == 0)) { ok = 1; break; } if (read_password("Password: ", pass, sizeof pass) < 0) { fprintf(stderr, "su: no terminal\n"); return 1; } ok = password_ok(pw, pass); memset(pass, 0, sizeof pass); if (!ok) { sleep(2); fprintf(stderr, "su: incorrect password\n"); } } if (!ok) return 1; } if (initgroups(pw->pw_name, pw->pw_gid) < 0 || setgid(pw->pw_gid) < 0 || setuid(pw->pw_uid) < 0) { fprintf(stderr, "su: cannot become %s: %s\n", pw->pw_name, strerror(errno)); return 1; } { const char *shell = pw->pw_shell && *pw->pw_shell ? pw->pw_shell : "/bin/sh"; char *sh_argv[5]; int n = 0; char arg0[256]; if (login) { clearenv(); setenv("PATH", "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", 1); if (getenv("TERM") == 0) setenv("TERM", "linux", 1); } if (login || !keep || pw->pw_uid != 0) { setenv("HOME", pw->pw_dir, 1); setenv("SHELL", shell, 1); setenv("USER", pw->pw_name, 1); setenv("LOGNAME", pw->pw_name, 1); } if (login && chdir(pw->pw_dir) < 0) chdir("/"); if (login) { const char *base = strrchr(shell, '/'); snprintf(arg0, sizeof arg0, "-%s", base ? base + 1 : shell); sh_argv[n++] = arg0; } else { const char *base = strrchr(shell, '/'); sh_argv[n++] = (char *)(base ? base + 1 : shell); } if (cmd) { sh_argv[n++] = "-c"; sh_argv[n++] = (char *)cmd; } sh_argv[n] = 0; execv(shell, sh_argv); fprintf(stderr, "su: %s: %s\n", shell, strerror(errno)); } return 1; }