COMMAND
rfork()
SYSTEMS AFFECTED
OpenBSD 2.1, FreeBSD 3.0
PROBLEM
Following info is based on OpenBSD Security Advisory. A
vulnerability in certain 4.4BSD kernels allows processes to gain
access to restricted resources by manipulating the file
descriptor tables of SUID and SGID executables. Applications of
this vulnerability will allow users to gain root access. It is
believed that all 4.4BSD operating systems implementing the
rfork() system call are currently vulnerable to this problem.
Recent 4.4BSD operating systems added the rfork() system call as
an additional method of creating a new process. Unlike fork(),
rfork() allows the caller tighter control over which resources are
shared between the parent and child processes. These resources
include the per-process descriptor table.
The descriptor table of a process lists all open file descriptors
for that process. Input and output on files, sockets, and pipes is
done through these descriptors. Two processes sharing the same
descriptor table can read from any file either has open in read
mode, and write to any file either has in write mode.
Unfortunately, the 4.4BSD implementation of rfork() allows this to
occur with processes whose credentials have been altered via
SUID/SGID programs. A process can execute any SUID program on the
system and gain access to it's file descriptor table. This can be
exploited to allow unprivileged processes to access
security-critical resources, such as the password file.
The default behavior of rfork() is to share the file descriptor
table between the child and parent processes. A process created
with rfork() can therefore, by default, be manipulated by it's
parent.
An example of this problem occurs in passwd(1), an SUID program
that modifies the password database. A user on the system can
rfork() a process and use it to execute passwd(1). The child
process will gain effective superuser credentials as a result of
executing the SUID program. The parent can then wait for the
temporary copy of the password database to be opened, and inject
a fake entry into it using the file descriptor it now shares with
passwd(1). When the password database is rebuilt, the fake entry
will be commited to it and system security will be compromised.
It should be noted that this is not the only avenue of
exploitation for this problem. The vulnerability allows complete
control over the file descriptor tables of privileged programs;
this can be exploited in a variety of ways with any SUID program.
Another possible attack allows an attacker to, among other things,
steal sockets from network programs; an attacker can execute an
SUID networking program such as "ping", duplicate the descriptor
associated with a raw socket, and close the original descriptor.
The unprivileged attacker now controls a raw socket.
Additionally, an attacker can close a descriptor opened by an SUID
program, and re-open it pointing elsewhere, causing the SUID
program to unwittingly alter any file accessible by the attacker.
Credit goes to Danny Dulai (discovery), Theo de Raadt (OpenBSD
patch) and Tim Newsham (proof-of-concept code).
The following code tests for the presence of the rfork()
vulnerability on 4.4BSD systems. If, after running this program,
a file is created in "/" containing the word "VULNERABLE", the
system is vulnerable to the problem.
To use this test, extract the following two C programs. Compile
the first ("dummy-suid") and make it SUID root, world executable.
Compile and run the second in the same directory.
-- cut here (dummy-suid.c) --
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
int main() {
int fd;
umask(2);
/* open a file in the root directory */
if(fd = open("/VULNERABLE", O_RDWR|O_CREAT) < 0) {
perror("open");
exit(0);
}
/* wait for something to happen */
for(;;);
exit(0);
}
-- cut here (test.c) --
#include <stdio.h>
#include <unistd.h>
int main() {
int p;
/* UNPRIVILEGED */
/* create a new process that shares it's parent's file
* descriptor table
*/
if(!(p = rfork(RFPROC))) {
/* wait for parent to open a file, write
* to it.
*/
sleep(1);
write(3, "VULNERABLE\n", 10);
exit(0);
}
/* PRIVILEGED */
/* execute 'p', an SUID program that opens a file and
* hangs
*/
execl("./dummy-suid", "dummy-suid", NULL);
exit(0);
}
SOLUTION
Provided at the end of this document is a patch from
OpenBSD-current that resolves the problem in OpenBSD systems. The
OpenBSD patch alters execve() to cause it not to honor the SUID or
SGID bit when executing from a process that shares a file
descriptor table with a different process. Also provided is a
modloadable workaround for FreeBSD. The provided module will
disable the rfork() system call from a running system that
supports loadable modules.
OPENBSD PATCH
The following patch resolves the rfork() problem in OpenBSD
systems.
--- kern_exec.c 1997/06/05 08:05:54 1.11
+++ kern_exec.c 1997/08/01 22:54:50 1.12
@@ -1,4 +1,4 @@
-/* $OpenBSD: kern_exec.c,v 1.11 1997/06/05 08:05:54 deraadt Exp $ */
+/* $OpenBSD: kern_exec.c,v 1.12 1997/08/01 22:54:50 deraadt Exp $ */
/* $NetBSD: kern_exec.c,v 1.75 1996/02/09 18:59:28 christos Exp $ */
/*-
@@ -124,7 +124,8 @@
error = EACCES;
goto bad1;
}
- if ((vp->v_mount->mnt_flag & MNT_NOSUID) || (p->p_flag & P_TRACED))
+ if ((vp->v_mount->mnt_flag & MNT_NOSUID) ||
+ (p->p_flag & P_TRACED) || p->p_fd->fd_refcnt > 1)
epp->ep_vap->va_mode &= ~(VSUID | VSGID);
/* check access. for root we have to see if any exec bit on */
FREEBSD WORKAROUND
The following module, when loaded on a FreeBSD system supporting
rfork(), will disable the system call as a temporary resolution
to the problem.
# This is a shell archive. Save it in a file, remove anything
# before this line, and then unpack it by entering "sh file".
# Note, it may create directories; files and directories will be
# owned by you and have default permissions.
#
# This archive contains:
#
# Makefile
# unrfork_mod_load.c
#
echo x - Makefile
sed 's/^X//' >Makefile << 'END-of-Makefile'
XBINDIR= .
XSRCS= unrfork_mod_load.c
XKMOD= disable_rfork
XNOMAN= none
X
XCLEANFILES+= ${KMOD}
X
X.include <bsd.kmod.mk>
END-of-Makefile
echo x - unrfork_mod_load.c
sed 's/^X//' >unrfork_mod_load.c << 'END-of-unrfork_mod_load.c'
X#define RFORK_SYSCALL_NO 251
X
X#include <sys/param.h>
X#include <sys/ioctl.h>
X#include <sys/proc.h>
X#include <sys/systm.h>
X#include <sys/sysproto.h>
X#include <sys/conf.h>
X#include <sys/mount.h>
X#include <sys/exec.h>
X#include <sys/sysent.h>
X#include <sys/lkm.h>
X#include <a.out.h>
X#include <sys/file.h>
X#include <sys/errno.h>
X#include <sys/queue.h>
X#include <sys/mbuf.h>
X#include <sys/socket.h>
X#include <sys/socketvar.h>
X#include <sys/protosw.h>
X#include <sys/kernel.h>
X#include <sys/sockio.h>
X
Xint disable_rfork(struct lkm_table *lkp, int cmd, int ver);
X
XMOD_MISC(disable_rfork);
X
Xstatic int
Xdisable_rfork_load(struct lkm_table *lkp, int cmd) {
X struct sysent *sp = &sysent[RFORK_SYSCALL_NO];
X int err = 0;
X
X switch(cmd) {
X case LKM_E_LOAD:
X sp->sy_call = (sy_call_t *) nosys;
X
X printf("rfork() call disabled\n");
X break;
X
X case LKM_E_UNLOAD:
X sp->sy_call = (sy_call_t *) rfork;
X
X printf("rfork() call enabled\n");
X break;
X
X default:
X err = EINVAL;
X break;
X }
X
X return(err);
X}
X
Xint disable_rfork(struct lkm_table *lkp, int cmd, int ver) {
X DISPATCH(lkp, cmd, ver, disable_rfork_load,
X disable_rfork_load, lkm_nullcmd);
X}
END-of-unrfork_mod_load.c
exit