/*
* Run the login shell or command as the given user in a new pty to prevent
* terminal injection attacks.
*
* Copyright (C) 2016-2018 Simon Ruderich
*
* 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 3 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, see .
*/
#define _GNU_SOURCE
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/* Default PATH for new process.*/
#ifndef PTYAS_DEFAULT_PATH
/* Default user PATH from Debian's /etc/profile, change as needed. */
# define PTYAS_DEFAULT_PATH "/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games"
#endif
static void die(const char *s) {
perror(s);
exit(EXIT_FAILURE);
}
static void die_fmt(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
exit(EXIT_FAILURE);
}
static void open_pty_or_die(int *pty_master, int *pty_slave, uid_t uid) {
char *slave_path;
*pty_master = posix_openpt(O_RDWR | O_NOCTTY);
if (*pty_master == -1) {
die("posix_openpt");
}
slave_path = ptsname(*pty_master);
if (!slave_path) {
die("ptsname");
}
if (grantpt(*pty_master) != 0) {
die("grantpt");
}
if (unlockpt(*pty_master) != 0) {
die("unlockpt");
}
*pty_slave = open(slave_path, O_RDWR | O_NOCTTY);
if (*pty_slave == -1) {
die("open slave tty");
}
/* The user must be able to write to the new TTY. Normally grantpt() would
* do this for us, but we don't trust the user and thus don't want to pass
* the pty_master to a process running under that uid. */
if (chown(slave_path, uid, (gid_t)-1) != 0) {
die("chown slave tty");
}
}
static void close_or_die(int fd) {
if (close(fd) != 0) {
die("close");
}
}
static void dup2_or_die(int oldfd, int newfd) {
if (dup2(oldfd, newfd) != newfd) {
die("dup2");
}
}
static int snprintf_or_assert(char *str, size_t size, const char *format, ...) {
int ret;
va_list ap;
va_start(ap, format);
ret = vsnprintf(str, size, format, ap);
assert(size <= (size_t)INT_MAX);
assert(ret < (int)size); /* assert output fit into buffer */
va_end(ap);
return ret;
}
static void drop_privileges_or_die(uid_t uid, gid_t gid) {
/* Drop all supplementary group IDs. */
if (setgroups(0, NULL) != 0) {
die("setgroups");
}
if (getgroups(0, NULL) != 0) {
die_fmt("failed to drop all supplementary groups");
}
/* Dropping groups may require privileges, do that first. */
if (setresgid(gid, gid, gid) != 0) {
die("setresgid");
}
if (setresuid(uid, uid, uid) != 0) {
die("setresuid");
}
/* Ensure we dropped all privileges. */
{
uid_t ruid, euid, suid;
gid_t rgid, egid, sgid;
if (getresuid(&ruid, &euid, &suid) != 0) {
die("getresuid");
}
if (getresgid(&rgid, &egid, &sgid) != 0) {
die("getresgid");
}
if ( uid != ruid || uid != euid || uid != suid
|| gid != rgid || gid != egid || gid != sgid) {
die_fmt("failed to drop privileges");
}
}
/* Just to be safe. */
if (setuid(0) != -1) {
die_fmt("failed to drop privileges (setuid)");
}
}
static void quit_with_matching_code(int status) {
if (WIFEXITED(status)) {
exit(WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
kill(getpid(), WTERMSIG(status));
/* Fall-through, should not happen. */
}
abort(); /* Should never happen, die painfully. */
}
static bool read_from_write_to(int from, int to) {
char buf[4096];
ssize_t r = read(from, buf, sizeof(buf));
if (r < 0) {
return false;
}
size_t left = (size_t)r;
char *data = buf;
while (left > 0) {
ssize_t w = write(to, data, left);
if (w < 0) {
if (errno == EINTR) {
continue;
}
return false;
}
left -= (size_t)w;
data += (size_t)w;
}
return true;
}
static void proxy_input_between_ttys(int pty_master, int ctty, volatile pid_t *pid_to_wait_for) {
struct pollfd fds[] = {
{ /* 0 */
.fd = pty_master,
.events = POLLIN,
},
{ /* 1 */
.fd = ctty,
.events = POLLIN,
},
};
sigset_t sigset, sigset_old;
sigemptyset(&sigset);
sigaddset(&sigset, SIGCHLD);
if (sigprocmask(SIG_BLOCK, &sigset, &sigset_old) != 0) {
die("sigprocmask block sigchld proxy");
}
/* Proxy data until our child has terminated. */
while (*pid_to_wait_for != 0) {
/*
* If a signal happens here _and_ the child hasn't closed pty_slave,
* we would hang in poll(); therefore ppoll() is necessary.
*/
nfds_t nfds = sizeof(fds)/sizeof(*fds);
if (ppoll(fds, nfds, NULL /* no timeout */, &sigset_old) == -1) {
if (errno == EAGAIN || errno == EINTR) {
continue;
}
perror("poll");
break;
}
/*
* Handle errors first. (Data available before the error occurred
* might be dropped, but shouldn't matter here.)
*/
if (fds[0].revents & (POLLERR | POLLNVAL)) {
fprintf(stderr, "poll: error on master: %d\n", fds[0].revents);
break;
}
if (fds[1].revents & (POLLERR | POLLNVAL)) {
fprintf(stderr, "poll: error on ctty: %d\n", fds[1].revents);
break;
}
/* Read data if available. */
if (fds[0].revents & POLLIN) {
if (!read_from_write_to(pty_master, ctty)) {
perror("read from master write to ctty");
break;
}
}
if (fds[1].revents & POLLIN) {
if (!read_from_write_to(ctty, pty_master)) {
perror("read from ctty write to master");
break;
}
}
/* Finally we are done if either side of the pty has disconnected. */
if ((fds[0].revents & POLLHUP) || (fds[1].revents & POLLHUP)) {
break;
}
}
if (sigprocmask(SIG_SETMASK, &sigset_old, NULL) != 0) {
die("sigprocmask setmask proxy");
}
}
/*
* Not sig_atomic_t (as required by POSIX) but I don't know how to do that any
* other way.
*/
static volatile pid_t pid_to_wait_for;
static int pid_to_wait_for_status;
static void sigchld_handler() {
int status;
pid_t pid;
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
if (pid == pid_to_wait_for) {
/* Mark that our child has died and we should exit as well. */
pid_to_wait_for = 0;
/* We must exit like our child, save status. */
pid_to_wait_for_status = status;
}
}
}
int main(int argc, char **argv) {
char *exec_argv_shell[] = { NULL, NULL }; /* filled below */
char **exec_argv = NULL;
if (argc == 2) {
/* exec_argv set below */
} else if (argc > 2) {
exec_argv = argv + 2;
} else {
die_fmt("%s [...]\n", argv[0]);
}
const char *user = argv[1];
struct passwd *passwd = getpwnam(user);
if (!passwd) {
die_fmt("unknown user name '%s'\n", user);
}
uid_t uid = passwd->pw_uid;
gid_t gid = passwd->pw_gid;
if (!exec_argv) {
assert(argc == 2);
exec_argv_shell[0] = passwd->pw_shell;
exec_argv = exec_argv_shell;
}
int pty_master, pty_slave;
open_pty_or_die(&pty_master, &pty_slave, uid);
int ctty = open("/dev/tty", O_RDWR | O_NOCTTY); /* controlling TTY */
if (ctty == -1) {
die("open /dev/tty");
}
sigset_t sigset, sigset_old;
sigemptyset(&sigset);
sigaddset(&sigset, SIGCHLD);
if (sigprocmask(SIG_BLOCK, &sigset, &sigset_old) != 0) {
die("sigprocmask block sigchld");
}
pid_t pid = fork();
if (pid == -1) {
die("fork parent");
} else if (pid == 0) {
/* child, will become a session leader */
if (sigprocmask(SIG_SETMASK, &sigset_old, NULL) != 0) {
die("sigprocmask setmask child");
}
struct winsize size;
if (ioctl(ctty, TIOCGWINSZ, &size) == -1) {
die("ioctl TIOCGWINSZ");
}
close_or_die(pty_master);
close_or_die(ctty);
/* Start a new session and attach controlling TTY. */
if (setsid() == -1) {
die("setsid");
}
if (ioctl(pty_slave, TIOCSCTTY, 0) == -1) {
die("ioctl TIOCSCTTY");
}
if (ioctl(pty_slave, TIOCSWINSZ, &size) == -1) {
die("ioctl TIOCSWINSZ");
}
pid_t pid = fork();
if (pid == -1) {
die("fork child");
} else if (pid == 0) {
/*
* Drop the privileges just now so that the other user doesn't get
* access to the master TTY or the session leader (which might
* have additional privileges).
*/
drop_privileges_or_die(uid, gid);
dup2_or_die(pty_slave, STDIN_FILENO);
dup2_or_die(pty_slave, STDOUT_FILENO);
dup2_or_die(pty_slave, STDERR_FILENO);
close_or_die(pty_slave);
const char *term_orig = getenv("TERM");
const char *term = term_orig;
if (!term) {
term = ""; /* for strlen() below */
}
const char *home = passwd->pw_dir;
/*
* Ignore errors here as we don't want to die on non-existent home
* directories to allow running as any user (think "/nonexistent"
* as home) and an error message will be annoying to ignore when
* running this command in scripts.
*/
chdir(home);
char envp_user[strlen("USER=") + strlen(user) + 1];
char envp_home[strlen("HOME=") + strlen(home) + 1];
char envp_term[strlen("TERM=") + strlen(term) + 1];
snprintf_or_assert(envp_user, sizeof(envp_user), "USER=%s", user);
snprintf_or_assert(envp_home, sizeof(envp_home), "HOME=%s", home);
snprintf_or_assert(envp_term, sizeof(envp_term), "TERM=%s", term);
char *exec_envp[] = {
"PATH=" PTYAS_DEFAULT_PATH,
envp_user,
envp_home,
term_orig ? envp_term : NULL,
NULL,
};
execve(exec_argv[0], exec_argv, exec_envp);
die("execve");
}
close_or_die(pty_slave);
close_or_die(STDIN_FILENO);
close_or_die(STDOUT_FILENO);
close_or_die(STDERR_FILENO);
/* TODO: EINTR? */
int status;
if (waitpid(pid, &status, 0) <= 0) {
die("waitpid child");
}
quit_with_matching_code(status);
}
close_or_die(pty_slave);
pid_to_wait_for = pid;
struct sigaction action = {
.sa_handler = sigchld_handler,
};
if (sigaction(SIGCHLD, &action, NULL) != 0) {
die("sigaction");
}
if (sigprocmask(SIG_SETMASK, &sigset_old, NULL) != 0) {
die("sigprocmask setmask parent");
}
struct termios old_term, term;
/* Change terminal to raw mode. */
if (tcgetattr(ctty, &old_term) != 0) {
die("tcgetattr");
}
term = old_term;
/* From man 3 cfmakeraw; cfmakeraw is non-standard so set it manually. */
term.c_iflag &= ~(tcflag_t)(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
term.c_oflag &= ~(tcflag_t)(OPOST);
term.c_lflag &= ~(tcflag_t)(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
term.c_cflag &= ~(tcflag_t)(CSIZE | PARENB);
term.c_cflag |= CS8;
if (tcsetattr(ctty, TCSADRAIN, &term) != 0) {
die("tcsetattr");
}
proxy_input_between_ttys(pty_master, ctty, &pid_to_wait_for);
/* Restore terminal mode. */
if (tcsetattr(ctty, TCSADRAIN, &old_term) != 0) {
die("tcsetattr restore");
}
/*
* Wait until we got the status code from our child. poll() might already
* exit after POLLHUP while we haven't collected the child yet.
*/
if (sigprocmask(SIG_BLOCK, &sigset, &sigset_old) != 0) {
die("sigprocmask block sigchld loop");
}
while (pid_to_wait_for != 0) {
sigsuspend(&sigset_old);
if (errno != EINTR) {
die("sigsuspend");
}
}
if (sigprocmask(SIG_SETMASK, &sigset, &sigset_old) != 0) {
die("sigprocmask setmask sigchld loop");
}
/* Try to exit the same way as the spawned process. */
if (pid_to_wait_for == 0) {
quit_with_matching_code(pid_to_wait_for_status);
}
return EXIT_FAILURE;
}