Multiple vulnerabilities in Operator Shell

CVE-2005-3533, CVE-2005-3346

Public disclosure: Feb 8, 2005
Vendor patch: DSA-918

Systems affected

  • osh_1.7-12

Overview

The Operator Shell is a setuid root, security enhanced, restricted shell for providing fine-grain distribution of system privileges for a wide range of usages and requirements. Contrary to its stated design goals however, this programs seems to be designed to subvert security and provide unrestricted root access to any unprivileged user. During the course of a few hours, I discovered eleven vulnerabilities, ranging from vanilla strcpy overflows to format string bugs and more esoteric environment variable issues:

For more information about the Operator Shell, read the paper The Operator Shell: A Means of Privilege Distribution Uner Unix by Michael Neuman and Gary Christoph, presented at the third SANS security conference in 1994.

Source code

I audited the latest osh package available in Debian unstable as of February 2005, version osh_1.7-12. Some of the vulnerbilities have been fixed since then, and the old versions of the Debian package are no longer available for download. I have provided a local copy of the source code that I audited:

Vulnerabilities

Hostname buffer overflow

main.c:759

char host[17];

...

uname(&un);
strcpy(host, un.nodename);

condition

#ifdef HAVE_SYS_UTSNAME

If the hostname of the machine is longer than 16 characters, we have a buffer overflow in host[]. This is not exploitable by an unprivileged user, but in an environment where osh is used, a system administrator might be given permissions to change the hostname, but not full root access. In this situation, the vulnerability can be used for a privilege escallation.

The UNAME(2) man page on Linux says:

Note that there is no standard that says that the hostname set by sethostname(2) is the same string as the nodename field of the struct returned by uname (indeed, some systems allow a 256-byte host- name and an 8-byte nodename), but this is true on Linux.

The length of the fields in the struct varies.

There have been three Linux system calls uname(). The first one used length 9, the second one used 65, the third one also uses 65 but adds the domainname field.

Username buffer overflow

main.c:800

char ebuf[80];

sprintf(ebuf, "LOGIN: %s ran osh", pw->pw_name);
syslog_entry(ebuf, 0);

main.c:572

char ebuf[80];

sprintf(ebuf, "logout: %s left osh", pw->pw_name);
syslog_entry(ebuf, 0);

condition

#ifdef LOGGING
#ifdef SYSLOG

The pw->pw_name variable is a pointer to the username of the currently logged in user. If it is longer then 63 characters, the ebuf buffer will overflow. Older UNIX systems only allowed usernames up to 8 or 32 characters, but the limit is much higer on glibc-2.3.4.

I created a user with a 90 character username and ran a test with a small program using the getpwname function. The results demonstrate that long username are possible.

$ cat getpwname.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <pwd.h>

int main()
{
    uid_t uid;
    struct passwd *pw;

    uid = getuid();
    pw = getpwuid(uid);

    printf("uid: %d\n", uid);
    printf("pw_name: %s\n", pw->pw_name);
    printf("pw_name length: %d\n", strlen(pw->pw_name));

    return 0;
}

$ gcc -o getpwname getpwname.c

$ cat /etc/passwd | grep test
123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890:x:666:666:test:/:/bin/bash

$ ./getpwname
uid: 666
pw_name: 123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
pw_name length: 90

Under normal circumstances, an unprivileged user would not be able to add users to the system. However, in an environment where osh is used, a system administrator might be given permissions to create new users, but not full root access. In this situation, the vulnerability can be used for privilege escallation.

Command line arguments syslog buffer overflow

main.c:503

char ebuf[80];

sprintf(ebuf, "(%s)", pw->pw_name);
    while (++kk<argc) {
  strcat(ebuf, " ");
  strcat(ebuf, argv[kk]);
}
syslog_entry(ebuf, 1);

condition

#ifdef LOGGING
#ifdef SYSLOG

The argv array contains the command line arguments passed to the command invoked through osh. If the total length of the command line arguments is more than 80 bytes, there will be an overflow in ebuf.

Filename buffer overflow

main.c:696

FILE *table;
char dummy[255];
char *prog=(char *)malloc(MAXPATHLEN);

fgets(dummy,255,table);
...
strncpy(prog, dummy, strlen(dummy));

If MAXPATHLEN is less than 255, we have a buffer overflow in prog. On most modern systems the value of MAXPATHLEN is large enough and the vulnerability is not present.

Command line arguments buffer overflow

main.c:311

static char inputstring[1024];

strcpy(inputstring, argv[1]);
for (i=3;i<=argc;i++) {
  strcat(inputstring, " ");
  strcat(inputstring, argv[i-1]);
}

If argv[1] is a valid command, such as "help", it is copied into inputstring followed by argv[2] and argv[3]. If the combined length of the three arguments is longer than 1024, we have a buffer overflow in inputstring. This vulnerability was discovered by Charles Stevenson and prompted me to audit the rest of the source.

Input file race condition

main.c:315

if (access(argv[1], R_OK)) {
  fprintf(stderr,"No access to shell script\n");
  exit(1);
}
inputfp=fopen(argv[1], "r");

There is a race condition between the access() check and the fopen() call. The opened file is used to read shell commands. There seems to be no way of printing the contents of the opened file.

Syslog format string vulnerability

main.c:166

syslog_entry(string, cont)
char *string;
int cont;
{
  ...
  syslog(SYSLOG_PRIORITY, logentry);
}

main.c:503

sprintf(ebuf, "(%s)", pw->pw_name);
while (++kk<argc) {
  strcat(ebuf, " ");
  strcat(ebuf, argv[kk]);
}
syslog_entry(ebuf, 1);

main.c:801

char ebuf[80];

sprintf(ebuf, "LOGIN: %s ran osh", pw->pw_name);
syslog_entry(ebuf, 0);

main.c:572

sprintf(ebuf, "logout: %s left osh", pw->pw_name);
syslog_entry(ebuf, 0);

condition

#ifdef LOGGING
#ifdef SYSLOG

There is a format string bug in the syslog_entry function. It is exploitable from main.c:503 by using the contents of argv. If the attacker can modify her username, the function is also exploitable from main.c:572 and main.c:801

File access race condition

handlers.c:305

if (!access(argv[i],R_OK)) continue;

Before executing a command, osh tests all command line parameters for file readability using the real uid of the user. There is a race condition between this check and the actual execution of the command.

Current working directory buffer overflow

CVE-2005-3533

handlers.c:364

char temp3[255];

if (*file!='/') {
  getcwd(temp3, MAXPATHLEN);
  strcat(temp3,"/");
  strcat(temp3,file);
}

If the length of the current working directory plus the length of the file name is longer than 255 bytes, there will be a buffer overflow in temp3. The size limit of the current direcory is MAXPATHLEN, which is defined as 1024 on modern Linux systems. The limit for the file name is MAXFNAME, defined as 32 in struct.h:116.

This code is in the writable() function, which is called by the handlers for built-in cp, vi, rm and test commands, as well as the redirect function.

Environmental variable overwrite vulnerability

CVE-2005-3346

main.c:439

char env[MAXENV];
char* env2;

...

case TDOLLAR:
  if (gettoken(env, MAXENV)!=TWORD) {
    fprintf(stderr,"Illegal or too long environment variable\n");
    break;
  }
  if ((env2=getenv(env))==NULL) {
    char temp[255];
    char *temp2;

    strcpy(temp,env);
    if ((temp2=(char *)strrchr(temp,'/'))!=NULL) {
      if (temp2!=temp)
        *temp2='\0';
      else
        *(temp2+1)='\0';
      if ((env2=getenv(temp))!=NULL) {
        strcat(env2,"/");
        strcat(env2,temp2+1);
      }
    }
  }

This code is used to handle substitutions of environmental variables on the command line. If the current token starts with a dollar sign, it might be an environmental variable and we call getenv() to get its value. If the first call to getenv() fails, we might have a token that contains a variable followed by a filename, for examle: $VAR/filename. We check if the token contains a '/' character and replace it with '\0'. The temp2 variable points to the filename part of the token. Then we call getenv() on the shortened variable name. If that call succeeds, we use strcat to append append "/filename" to the value of the environmental variable. The problem is that the getenv() returns a pointer to the envp array on the stack, which contains all enviromental variables. By appending to one environmental variable, we are overwriting the value of the variable following it.

This bug allows us to overwrite one of the environmental variables passed to the child process. If we set the environmental variable $VAR to "a" before executing osh, and then pass "$VAR/LD_PRELOAD=evil.so" as a command line parameter, the above code will overwrite the value of some environmental variable located after $VAR with LD_PRELOAD=evil.so. Then osh will execute an external non-suid program and the code in evil.so will be executed.

Output File Race Condition

main.c:889

if (writeable(dstfile)) {
  flags=O_WRONLY|O_CREAT;
  if (!append) flags |= O_EXCL; /* This handles race condition problems */
  if ((dstfd=open(dstfile,flags,0666))==-1) {    

Despite the comment in the source, this code does suffer from race condition problems. If the target of a symlink is changed between the access check in the writable() function and the call to open(), we will open the wrong file for writing.