You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 39 Next »

Many programs and libraries, including the shared library loader on both Unix and Windows systems, depend on environment variable settings. Because environment variables are inherited from the parent process when a program is executed, an attacker can easily sabotage variables, causing a program to behave in an unexpected and insecure manner [[Viega 03]].

Attackers can manipulate environmental variables to trick an executable into running a spoofed version of a shared library or executable. Most modern systems, for example, use dynamic libraries and most executables are dynamically linked (that is, use dynamic libraries). An attacker can run arbitrary code with the privileges of a spoofed process by installing a spoofed version of a shared library and influencing the mechanism for dynamic linking by setting the LD_PRELOAD environmental variable (or another LD_* environmental variable). An interesting example of this vulnerability involving the RFC 1408/RFC 1572 Telnet Environment Option is documented in CERT Advisory CA-1995-14, "Telnetd Environment Vulnerability" [CA-1995-14]. The Telnet Environment Option extension to telnet supports the transfer of environment variables from one system to another, allowing an attacker to transfer environment variables that influence the login program called by the telnet daemon and bypass the normal login and authentication scheme to become root on that system.

Certain variables can cause insecure program behavior if they are missing from the environment or improperly set. As a result, the environment cannot be fully purged. Instead, variables that should exist should be set to safe values or treated as untrusted data and examined closely before being used.

For example, the IFS variable (which stands for "internal field separator") is used by some shells to determine which characters separate command line arguments.  A shell has the option of using the incoming IFS value or resetting it to its default at the time the shell is invoked.  Because the shell is invoked by the C99 system() function and the POSIX popen() function, setting IFS to unusual values can subvert apparently-safe calls.

Environment issues are particularly dangerous with setuid/setgid programs because an attacker can completely control the environment variables.

Non-Compliant Code Example (POSIX) (ls)

This non-compliant code invokes the C99 system() function to execute the /bin/ls program. The C99 system() function passes a string to the command processor in the host environment to be executed.

system("/bin/ls dir.`date +%Y%m%d`");

When the default shell does not ignore the incoming value of the IFS environment value, although IFS does not affect the command portion of this string, /bin/ls, it does determine how the argument is built after calling date.  If an attacker sets IFS to "." the intended directory will not be found.

Compliant Solution (POSIX) (ls)

Sanitize the environment by setting required variables to safe values and removing extraneous environment variables. Set IFS to its default of "{{ \t\n}}" (the first character is a space character). Set the PATH environment variable to the string returned by calling confstr() with the first argument _CS_PATH. Preserve the TZ environment variable (if present) which denotes the time zone (see the Open Group Base Specifications Issue 6 specifies for the format for this variable [[Open Group 04]].

One way to clear the environment is to use the clearenv() function. The function clearenv() has an odd history; it was supposed to be defined in POSIX.1, but never made it into the standard. However, it is defined in POSIX.9 (the Fortran 77 bindings to POSIX), so there is a quasi-official status for it [[Wheeler 03]].

The other technique is to directly manipulate the environment through the environ variable. According to the Open Group Base Specifications Issue 6 [[Open Group 04]]:

The value of an environment variable is a string of characters. For a C-language program, an array of strings called the environment shall be made available when a process begins. The array is pointed to by the external variable environ, which is defined as:

extern char **environ;

These strings have the form name=value; names shall not contain the character '='.

Note that C99 standard states that "The set of environment names and the method for altering the environment list are implementation-defined."

Compliant Solution (Sanitization)

If you explicitly know which environment variables you want to keep, the function below adapted from [[Viega 03]] will remove everything else.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

extern char **environ;

/* These arrays are both null-terminated. */
static char *spc_restricted_environ[  ] = {
  "IFS= \t\n",
  0, /* leave room for PATH=... */
  0
};

static char *spc_preserve_environ[  ] = {
  "TZ",
  0
};

void spc_sanitize_environment(int preservec, const char **preservev) {
  int    i;
  char   **new_environ, *path, *ptr;
  const char *value, *var;
  size_t path_size, arr_size = 1, arr_ptr = 0, len, new_size = 0;

  if ((path_size = confstr(_CS_PATH, NULL, 0)) > 0) {
    path = (char *)malloc(path_size + sizeof("PATH=") - 1);
    if (path == NULL) {
      /* handle error */
    }
    strcpy(path, "PATH=");
    if (confstr(_CS_PATH, path + sizeof("PATH=") - 1, path_size) > 0) {
      spc_restricted_environ[1] = path;
    }
    else {
      free(path);
      path = NULL;
    }
  }
  for (i = 0;  (var = spc_restricted_environ[i]) != 0;  i++) {
    new_size += strlen(var) + 1;
    arr_size++;
  }
  for (i = 0;  (var = spc_preserve_environ[i]) != 0;  i++) {
    if ((value = getenv(var)) == NULL) continue;
    new_size += strlen(var) + strlen(value) + 2; /* include the '=' */
    arr_size++;
  }
  if (preservec && preservev) {
    for (i = 0;  i < preservec && (var = preservev[i]) != 0;  i++) {
      if ((value = getenv(var)) == NULL) continue;
      new_size += strlen(var) + strlen(value) + 2; /* include the '=' */
      arr_size++;
    }
  }

  new_size += (arr_size * sizeof(char *));
  if ((new_environ = (char **)malloc(new_size)) == NULL) abort(  );
  new_environ[arr_size - 1] = 0;

  ptr = (char *)new_environ + (arr_size * sizeof(char *));
  for (i = 0;  (var = spc_restricted_environ[i]) != 0;  i++) {
    new_environ[arr_ptr++] = ptr;
    len = strlen(var);
    memcpy(ptr, var, len + 1);
    ptr += len + 1;
  }
  for (i = 0;  (var = spc_preserve_environ[i]) != 0;  i++) {
    if ((value = getenv(var)) == NULL) continue;
    new_environ[arr_ptr++] = ptr;
    len = strlen(var);
    memcpy(ptr, var, len);
    *(ptr + len + 1) = '=';
    memcpy(ptr + len + 2, value, strlen(value) + 1);
    ptr += len + strlen(value) + 2; /* include the '=' */
  }
  if (preservec && preservev) {
    for (i = 0;  i < preservec && (var = preservev[i]) != 0;  i++) {
      if ((value = getenv(var)) == NULL) continue;
      new_environ[arr_ptr++] = ptr;
      len = strlen(var);
      memcpy(ptr, var, len);
      *(ptr + len + 1) = '=';
      memcpy(ptr + len + 2, value, strlen(value) + 1);
      ptr += len + strlen(value) + 2; /* include the '=' */
    }
  }

  /* new_environ can now be used as the envp argument to execle or execve */
}

POSIX.1-2008 [[Austin Group 08]] defines a new _CS_V7_ENV argument to confstr() to retrieve a list of environment variable settings required for a default conforming environment.  A space-separated list of variable=value pairs is returned, with variable names guaranteed not to contain = signs, and variable=value pairs guaranteed not to contain spaces.  Used together with the _CS_PATH request illustrated above, this completely describes the minimum environment variable settings required to obtain a clean conforming environment.  On systems conforming to the POSIX.1-2008 standard, this should be used to create a sanitized environment.

Non-Compliant Code Example (POSIX) rm

This non-compliant code invokes the C99 system() function to remove the .config file in the users home directory.

system("rm ~/.config");

Given that the vulnerable program has sufficient permissions, an attacker can manipulate the value of HOME so that this program can remove any file named .config anywhere on the system.

Compliant Solution (POSIX) rm

This compliant solution calls the getuid() to determine who the user is, followed by the getpwuid() to get the user's password file record (which contains the user's home directory).

#include <sys/types.h>
#include <pwd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

/* ... */

uid_t uid;
struct passwd *pwd;

/* Sanitize environment, from previous compliant example  */
spc_sanitize_environment(0, NULL);

/* Get /etc/passwd entry for current user */
uid = getuid();
if ((pwd = getpwuid(uid)) == NULL) {
  /* handle error */
  endpwent();
  return 1;
}

/* Ensure home dir has no ', so we can quote it */
if (strchr( pwd->pw_dir, '\'') != NULL) {
  /* handle ' in home dir */
}

/* build system cmd using home dir from pw entry */
const char* cmd_format = "rm '%s'/foo";
const size_t len = strlen(pwd->pw_dir) + strlen(cmd_format);
char* cmd = (char*) malloc(len+1);
snprintf( cmd, len, cmd_format, pwd->pw_dir);
if (system(cmd) != 0) {
  /* handle error in rm command */
}

endpwent();
free(cmd);
cmd = NULL;

An easier way to remove a file is to use unlink(2). This compliant solution uses unlink(2) instead of system(3), so it is simpler.

#include <pwd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

/* ... */

uid_t uid;
struct passwd *pwd;

/* Get /etc/passwd entry for current user */
uid = getuid();
if ((pwd = getpwuid(uid)) == NULL) {
  /* handle error */
  endpwent();
  return 1;
}

/* build full pathname home dir from pw entry */
const char* file_format = "%s/foo";
const size_t len = strlen(pwd->pw_dir) + strlen(file_format);
char* file = (char*) malloc(len+1);
snprintf( file, len, file_format, pwd->pw_dir);
if (unlink(file) != 0) {
  /* handle error in unlink */
}

endpwent();
free(cmd);
cmd = NULL;

Risk Assessment

Invoking an external program in an attacker-controlled environment is dangerous.

Recommendation

Severity

Likelihood

Remediation Cost

Priority

Level

ENV03-A

2 (medium)

2 (probable)

2 (medium)

P8

L2

Related Vulnerabilities

Search for vulnerabilities resulting from the violation of this rule on the CERT website.

References

[[Austin Group 08]] vol. 2, System Interfaces, confstr()
[[Dowd 06]] Chapter 10, "UNIX II: Processes"
[[ISO/IEC 9899-1999]] Section 7.20.4, "Communication with the environment"
[[Open Group 04]] Chapter 8, "Environment Variables"
[[Viega 03]] Section 1.1, "Sanitizing the Environment"
[[Wheeler 03]] Section 5.2, "Environment Variables"


ENV02-A. Beware of multiple environment variables with the same name      11. Environment (ENV)       ENV04-A. Do not call system() if you do not need a command processor

  • No labels