]> ruderich.org/simon Gitweb - ptyas/ptyas.git/blob - ptyas.c
ab99f8ede71cf352d4d7aa2ef9c2af83a5b4ef06
[ptyas/ptyas.git] / ptyas.c
1 /*
2  * Run the login shell or command as the given user in a new pty to prevent
3  * terminal injection attacks.
4  *
5  * Copyright (C) 2016-2017  Simon Ruderich
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation, either version 3 of the License, or
10  * (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
19  */
20
21 #define _GNU_SOURCE
22
23 #include <assert.h>
24 #include <errno.h>
25 #include <fcntl.h>
26 #include <grp.h>
27 #include <limits.h>
28 #include <poll.h>
29 #include <pwd.h>
30 #include <signal.h>
31 #include <stdarg.h>
32 #include <stdbool.h>
33 #include <stdio.h>
34 #include <stdlib.h>
35 #include <string.h>
36 #include <sys/ioctl.h>
37 #include <sys/types.h>
38 #include <sys/wait.h>
39 #include <termios.h>
40 #include <unistd.h>
41
42 /* Default PATH for new process.*/
43 #ifndef PTYAS_DEFAULT_PATH
44 /* Default user PATH from Debian's /etc/profile, change as needed. */
45 # define PTYAS_DEFAULT_PATH "/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games"
46 #endif
47
48
49 static void die(const char *s) {
50     perror(s);
51     exit(EXIT_FAILURE);
52 }
53 static void die_fmt(const char *fmt, ...) {
54     va_list ap;
55
56     va_start(ap, fmt);
57     vfprintf(stderr, fmt, ap);
58     va_end(ap);
59
60     exit(EXIT_FAILURE);
61 }
62
63 static void open_pty_or_die(int *pty_master, int *pty_slave, uid_t uid) {
64     char *slave_path;
65
66     *pty_master = posix_openpt(O_RDWR | O_NOCTTY);
67     if (*pty_master == -1) {
68         die("posix_openpt");
69     }
70     slave_path = ptsname(*pty_master);
71     if (!slave_path) {
72         die("ptsname");
73     }
74     if (grantpt(*pty_master) != 0) {
75         die("grantpt");
76     }
77     if (unlockpt(*pty_master) != 0) {
78         die("unlockpt");
79     }
80
81     *pty_slave = open(slave_path, O_RDWR | O_NOCTTY);
82     if (*pty_slave == -1) {
83         die("open slave tty");
84     }
85     /* The user must be able to write to the new TTY. Normally grantpt() would
86      * do this for us, but we don't trust the user and thus don't want to pass
87      * the pty_master to a process running under that uid. */
88     if (chown(slave_path, uid, (gid_t)-1) != 0) {
89         die("chown slave tty");
90     }
91 }
92
93 static void close_or_die(int fd) {
94     if (close(fd) != 0) {
95         die("close");
96     }
97 }
98 static void dup2_or_die(int oldfd, int newfd) {
99     if (dup2(oldfd, newfd) != newfd) {
100         die("dup2");
101     }
102 }
103 static int snprintf_or_assert(char *str, size_t size, const char *format, ...) {
104     int ret;
105     va_list ap;
106
107     va_start(ap, format);
108     ret = vsnprintf(str, size, format, ap);
109     assert(size <= (size_t)INT_MAX);
110     assert(ret < (int)size); /* assert output fit into buffer */
111     va_end(ap);
112
113     return ret;
114 }
115
116 static void drop_privileges_or_die(uid_t uid, gid_t gid) {
117     /* Drop all supplementary group IDs. */
118     if (setgroups(0, NULL) != 0) {
119         die("setgroups");
120     }
121     if (getgroups(0, NULL) != 0) {
122         die_fmt("failed to drop all supplementary groups");
123     }
124
125     /* Dropping groups may require privileges, do that first. */
126     if (setresgid(gid, gid, gid) != 0) {
127         die("setresgid");
128     }
129     if (setresuid(uid, uid, uid) != 0) {
130         die("setresuid");
131     }
132
133     /* Ensure we dropped all privileges. */
134     {
135         uid_t ruid, euid, suid;
136         gid_t rgid, egid, sgid;
137
138         if (getresuid(&ruid, &euid, &suid) != 0) {
139             die("getresuid");
140         }
141         if (getresgid(&rgid, &egid, &sgid) != 0) {
142             die("getresgid");
143         }
144         if (       uid != ruid || uid != euid || uid != suid
145                 || gid != rgid || gid != egid || gid != sgid) {
146             die_fmt("failed to drop privileges");
147         }
148     }
149     /* Just to be safe. */
150     if (setuid(0) != -1) {
151         die_fmt("failed to drop privileges (setuid)");
152     }
153 }
154
155 static void quit_with_matching_code(int status) {
156     if (WIFEXITED(status)) {
157         exit(WEXITSTATUS(status));
158     } else if (WIFSIGNALED(status)) {
159         kill(getpid(), WTERMSIG(status));
160         /* Fall-through, should not happen. */
161     }
162     abort(); /* Should never happen, die painfully. */
163 }
164
165 static bool read_from_write_to(int from, int to) {
166     char buf[4096];
167
168     ssize_t r = read(from, buf, sizeof(buf));
169     if (r < 0) {
170         return false;
171     }
172
173     size_t left = (size_t)r;
174     char *data = buf;
175
176     while (left > 0) {
177         ssize_t w = write(to, data, left);
178         if (w < 0) {
179             if (errno == EINTR) {
180                 continue;
181             }
182             return false;
183         }
184         left -= (size_t)w;
185         data += (size_t)w;
186     }
187
188     return true;
189 }
190
191 static void proxy_input_between_ttys(int pty_master, int ctty, volatile pid_t *pid_to_wait_for) {
192     struct pollfd fds[] = {
193         { /* 0 */
194             .fd = pty_master,
195             .events = POLLIN,
196         },
197         { /* 1 */
198             .fd = ctty,
199             .events = POLLIN,
200         },
201     };
202
203     sigset_t sigset, sigset_old;
204     sigemptyset(&sigset);
205     sigaddset(&sigset, SIGCHLD);
206     if (sigprocmask(SIG_BLOCK, &sigset, &sigset_old) != 0) {
207         die("sigprocmask block sigchld proxy");
208     }
209
210     /* Proxy data until our child has terminated. */
211     while (*pid_to_wait_for != 0) {
212         /*
213          * If a signal happens here _and_ the child hasn't closed pty_slave,
214          * we would hang in poll(); therefore ppoll() is necessary.
215          */
216         nfds_t nfds = sizeof(fds)/sizeof(*fds);
217         if (ppoll(fds, nfds, NULL /* no timeout */, &sigset_old) == -1) {
218             if (errno == EAGAIN || errno == EINTR) {
219                 continue;
220             } else {
221                 perror("poll");
222             }
223             break;
224         }
225
226         /* Handle errors first. (Data available before the error occurred
227          * might be skipped, but shouldn't matter here.) */
228         if (fds[0].revents & (POLLERR | POLLNVAL)) {
229             fprintf(stderr, "poll: error on master: %d\n", fds[0].revents);
230             break;
231         }
232         if (fds[1].revents & (POLLERR | POLLNVAL)) {
233             fprintf(stderr, "poll: error on ctty: %d\n", fds[1].revents);
234             break;
235         }
236
237         /* Read data if available. */
238         if (fds[0].revents & POLLIN) {
239             if (!read_from_write_to(pty_master, ctty)) {
240                 perror("read from master write to ctty");
241                 break;
242             }
243         }
244         if (fds[1].revents & POLLIN) {
245             if (!read_from_write_to(ctty, pty_master)) {
246                 perror("read from ctty write to master");
247                 break;
248             }
249         }
250
251         /* Finally we are done if either side of the pty has disconnected. */
252         if ((fds[0].revents & POLLHUP) || (fds[1].revents & POLLHUP)) {
253             break;
254         }
255     }
256
257     if (sigprocmask(SIG_SETMASK, &sigset_old, NULL) != 0) {
258         die("sigprocmask setmask proxy");
259     }
260 }
261
262
263 /*
264  * Not sig_atomic_t (as required by POSIX) but I don't know how to do that any
265  * other way.
266  */
267 static volatile pid_t pid_to_wait_for;
268 static int pid_to_wait_for_status;
269
270 static void sigchld_handler() {
271     int status;
272     pid_t pid;
273
274     while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
275         if (pid == pid_to_wait_for) {
276             /* Mark that our child has died and we should exit as well. */
277             pid_to_wait_for = 0;
278             /* We must exit like our child, save status. */
279             pid_to_wait_for_status = status;
280         }
281     }
282 }
283
284
285 int main(int argc, char **argv) {
286     char *exec_argv_shell[] = { NULL, NULL }; /* filled below */
287     char **exec_argv = NULL;
288
289     if (argc == 2) {
290         /* exec_argv set below */
291     } else if (argc > 2) {
292         exec_argv = argv + 2;
293     } else {
294         die_fmt("%s <user> [<cmd>...]\n", argv[0]);
295     }
296
297     const char *user = argv[1];
298
299     struct passwd *passwd = getpwnam(user);
300     if (!passwd) {
301         die_fmt("unknown user name '%s'\n", user);
302     }
303
304     uid_t uid = passwd->pw_uid;
305     gid_t gid = passwd->pw_gid;
306
307     if (!exec_argv) {
308         assert(argc == 2);
309         exec_argv_shell[0] = passwd->pw_shell;
310         exec_argv = exec_argv_shell;
311     }
312
313     int pty_master, pty_slave;
314
315     open_pty_or_die(&pty_master, &pty_slave, uid);
316
317     int ctty = open("/dev/tty", O_RDWR | O_NOCTTY); /* controlling TTY */
318     if (ctty == -1) {
319         die("open /dev/tty");
320     }
321
322     sigset_t sigset, sigset_old;
323     sigemptyset(&sigset);
324     sigaddset(&sigset, SIGCHLD);
325     if (sigprocmask(SIG_BLOCK, &sigset, &sigset_old) != 0) {
326         die("sigprocmask block sigchld");
327     }
328
329     pid_t pid = fork();
330     if (pid == -1) {
331         die("fork parent");
332     } else if (pid == 0) {
333         /* child, will become a session leader */
334
335         if (sigprocmask(SIG_SETMASK, &sigset_old, NULL) != 0) {
336             die("sigprocmask setmask child");
337         }
338
339         struct winsize size;
340         if (ioctl(ctty, TIOCGWINSZ, &size) == -1) {
341             die("ioctl TIOCGWINSZ");
342         }
343
344         close_or_die(pty_master);
345         close_or_die(ctty);
346
347         /* Start a new session and attach controlling TTY. */
348         if (setsid() == -1) {
349             die("setsid");
350         }
351         if (ioctl(pty_slave, TIOCSCTTY, 0) == -1) {
352             die("ioctl TIOCSCTTY");
353         }
354
355         if (ioctl(pty_slave, TIOCSWINSZ, &size) == -1) {
356             die("ioctl TIOCSWINSZ");
357         }
358
359         pid_t pid = fork();
360         if (pid == -1) {
361             die("fork child");
362         } else if (pid == 0) {
363             drop_privileges_or_die(uid, gid);
364
365             dup2_or_die(pty_slave, STDIN_FILENO);
366             dup2_or_die(pty_slave, STDOUT_FILENO);
367             dup2_or_die(pty_slave, STDERR_FILENO);
368             close_or_die(pty_slave);
369
370             const char *term_orig = getenv("TERM");
371             const char *term = term_orig;
372             if (!term) {
373                 term = ""; /* for strlen() below */
374             }
375             const char *home = passwd->pw_dir;
376
377             char envp_user[strlen("USER=") + strlen(user) + 1];
378             char envp_home[strlen("HOME=") + strlen(home) + 1];
379             char envp_term[strlen("TERM=") + strlen(term) + 1];
380             snprintf_or_assert(envp_user, sizeof(envp_user), "USER=%s", user);
381             snprintf_or_assert(envp_home, sizeof(envp_home), "HOME=%s", home);
382             snprintf_or_assert(envp_term, sizeof(envp_term), "TERM=%s", term);
383
384             char *exec_envp[] = {
385                 "PATH=" PTYAS_DEFAULT_PATH,
386                 envp_user,
387                 envp_home,
388                 term_orig ? envp_term : NULL,
389                 NULL,
390             };
391
392             execve(exec_argv[0], exec_argv, exec_envp);
393             die("execve");
394         }
395         close_or_die(pty_slave);
396         close_or_die(STDIN_FILENO);
397         close_or_die(STDOUT_FILENO);
398         close_or_die(STDERR_FILENO);
399
400         /* TODO: EINTR? */
401         int status;
402         if (waitpid(pid, &status, 0) <= 0) {
403             die("waitpid child");
404         }
405         quit_with_matching_code(status);
406     }
407     close_or_die(pty_slave);
408
409     pid_to_wait_for = pid;
410     struct sigaction action = {
411         .sa_handler = sigchld_handler,
412     };
413     if (sigaction(SIGCHLD, &action, NULL) != 0) {
414         die("sigaction");
415     }
416
417     if (sigprocmask(SIG_SETMASK, &sigset_old, NULL) != 0) {
418         die("sigprocmask setmask parent");
419     }
420
421     struct termios old_term, term;
422
423     /* Change terminal to raw mode. */
424     if (tcgetattr(ctty, &old_term) != 0) {
425         die("tcgetattr");
426     }
427     term = old_term;
428     /* From man 3 cfmakeraw; cfmakeraw is non-standard so set it manually. */
429     term.c_iflag &= ~(tcflag_t)(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
430     term.c_oflag &= ~(tcflag_t)(OPOST);
431     term.c_lflag &= ~(tcflag_t)(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
432     term.c_cflag &= ~(tcflag_t)(CSIZE | PARENB);
433     term.c_cflag |= CS8;
434     if (tcsetattr(ctty, TCSADRAIN, &term) != 0) {
435         die("tcsetattr");
436     }
437
438     proxy_input_between_ttys(pty_master, ctty, &pid_to_wait_for);
439
440     /* Restore terminal mode. */
441     if (tcsetattr(ctty, TCSADRAIN, &old_term) != 0) {
442         die("tcsetattr restore");
443     }
444
445     /* Wait until we got the status code from our child. poll() might also
446      * exit after POLLHUP while we haven't collected the child yet. */
447     if (sigprocmask(SIG_BLOCK, &sigset, &sigset_old) != 0) {
448         die("sigprocmask block sigchld loop");
449     }
450     while (pid_to_wait_for != 0) {
451         sigsuspend(&sigset_old);
452         if (errno != EINTR) {
453             die("sigsuspend");
454         }
455     }
456     if (sigprocmask(SIG_SETMASK, &sigset, &sigset_old) != 0) {
457         die("sigprocmask setmask sigchld loop");
458     }
459
460     /* Try to exit the same way as the spawned process. */
461     if (pid_to_wait_for == 0) {
462         quit_with_matching_code(pid_to_wait_for_status);
463     }
464     return EXIT_FAILURE;
465 }