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