Security Research
by Alexander Sotirov
Multiple vulnerabilities in Operator Shell
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:
- Hostname buffer overflow
- Username buffer overflow
- Command line arguments syslog buffer overflow
- Filename buffer overflow
- Command line arguments buffer overflow
- Input file race condition
- Syslog format string vulnerability
- File access race condition
- Current working directory buffer overflow
- Environmental variable overwrite vulnerability
- Output file race condition
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
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
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.