====================================
FreeBSD crontab information leakage
====================================
For its implementation of the standard UNIX cron daemon, FreeBSD uses a version
based off vixie-cron. This package is installed by default, and includes a
setuid-root crontab binary to allow unprivileged users to list and modify their
own cronjobs.
I recently audited this code [1], and found a few interesting race conditions
and symlink attacks that allow for very minor information leakage. I thought
I'd share my findings because I enjoyed exploiting these issues and they don't
pose any significant risk to live systems - in other words, this advisory is
intended for system administrators and developers of FreeBSD-based systems;
journalists, end users and other non-technical readers do not need to be
concerned. :p
OpenBSD and NetBSD are not affected. Nor is Debian/Ubuntu cron, which is based
on vixie-cron 3.0, or Red Hat/Fedora cronie, which is a fork off ISC Cron (aka
vixie-cron 4.1). It seems the vulnerable code was specially inserted into the
FreeBSD codebase as additional security checks that introduced new issues of
their own. Perhaps it was inserted as a government-sponsored backdoor. Only
kidding. Because of its heavy reliance on FreeBSD source code, Mac OS X is
also affected [2], except for the realpath() case, which is conveniently
#ifdef'd out.
=====================================================
Leakage of file/directory existence via stat() calls
=====================================================
At two points (lines 366 and 436 in crontab.c), crontab makes calls to stat()
on a user-owned temporary file while retaining an euid of 0. Since stat()
follows symbolic links and returns ENOENT when called on a symbolic link
pointing to a non-existent resource, this can be used to determine the existence of
files or directories in ways that violate directory search permissions.
The first of these instances, on line 436, is trivially exploitable. First,
invoke crontab with the -e flag to edit an existing cronjob. This will result
in crontab opening a text editor to edit the cronjob. While this editor is
open, simply remove the temporary file created by crontab (of the form
"/tmp/crontab.XXXXXXXXXX") and replace it with a symlink to a file whose
existence you wish to verify. On exiting the editor, crontab will print a
warning if the call to stat() on this symlink fails, confirming the
non-existence of the target file. Likewise, if the file exists, a different
error will be generated ("temp file must be edited in place").
The second of these instances, on line 366, doesn't have the luxury of an
editor holding everything up, and so requires exploitation of a race condition.
The temporary file is created on line 338. It can't be removed at this time,
since it's created with euid 0 in a presumably sticky-bit /tmp directory, but
shortly after it's fchown()'d to the user's id. At this point, if it's deleted
and replaced with a symlink to the file whose existence is to be confirmed, the
call to stat() on line 366 will perform identically to the first case.
==============================================
Leakage of directory existence via realpath()
==============================================
When crontab is run with a file argument, it makes a call to realpath() with
euid 0 to canonicalize the provided argument:
--snip--
} else if (realpath(Filename, resolved_path) != NULL &&
!strcmp(resolved_path, SYSCRONTAB)) {
err(ERROR_EXIT, SYSCRONTAB " must be edited manually");
}
--snip--
SYSCRONTAB is defined as /etc/crontab. Because realpath() resolves each member
of the requested path individually, in this case with euid 0, it's possible to
reveal the existence of directories regardless of search permissions, again
violating DAC. For example, consider the following request:
crontab /my/secret/directory/../../../../etc/crontab
If /my/secret/directory exists, realpath() will return a non-NULL value and the
resolved path will still be equal to SYSCRONTAB. If not, the above error
message will be displayed, because realpath() will return an error if any
directories in the search path do not exist.
====================================
MD5 comparisons for arbitrary files
====================================
FreeBSD's crontab calculates the MD5 sum of the previous and new cronjob to
determine if any changes have been made before copying the new version in.
This seems entirely superfluous to me, but maybe there's a good explanation.
In particular, it uses the MD5File() function, which takes a pathname as an
argument, and is again called with euid 0. The following relevant steps are
performed by crontab:
1. Create the temporary file (of the form "/tmp/crontab.XXXXXXXXXX")
2. chown() this file to the user's id
3. Open the existing cronjob and copy it into the temp file
4. Call fstat() on the file descriptor to the temp file
5. Call stat() on the temp file's name
6. Compare the inode and device numbers returned by stat() and fstat(),
and abort if not equal
7. Call MD5File() on the temp file's name
8. Perform the edits by launching an editor
9. Call stat() again on the temp file's name
10. Again compare the inode and device numbers from the first fstat()
call and most recent stat() call, aborting on mismatch
11. Call MD5File() on the temp file's name
12. Abort if the two MD5 sums are equal
The race created here is difficult, but entirely possible. Exploitation looks
like this:
1. Once the temp file is created, create a hard link to it (for example
at /tmp/link)
2. After the stat() call but before the MD5File() call, remove the temp
file and replace it with a symlink to the first target file
3. While the editor is open, remove the symlink and replace it with the
original file, re-created by making a hard link to your previously
created hard link
4. After the second stat() call but before the second MD5File() call,
remove the temp file and replace it with a symlink to the second
target file
5. The results of the MD5 sum comparison will tell you if the two
targets you chose have the same MD5 sum
In practice, this is more easily achieved by creating the hard link
immediately, removing the temp file, and rapidly toggling a symlink to
alternately point to the newly created hard link and the target file. Of
course, this is a ridiculous amount of work for such little gain, but hey, it's
still fun.
============
Conclusions
============
I think there are a few lessons to be learned here. First, any library calls
that rely on user-controlled paths (or paths pointing to user-controlled
resources) rather than file descriptors should be performed with reduced
privileges if possible. Second, even heavily audited code can still have
interesting bugs, especially if new functionality is introduced. Finally, it's
about time that UNIX-based operating systems moved towards a more restrictive
symlink and hard link policy that prevents these kinds of attacks. One
solution, originally found in Openwall Linux, followed by grsecurity, and most
recently as the Yama LSM enabled by default in Ubuntu Maverick, prevents all
users from following symlinks created by other users in sticky-bit directories.
This simple restriction successfully prevents exploitation of a majority of
these types of attacks.
Greets to $1$kk1q85Xp$Id.gAcJOg7uelf36VQwJQ/ and #busticati.
Happy hacking,
Dan Rosenberg
@djrbliss on twitter
[1] http://svn.freebsd.org/viewvc/base/head/usr.sbin/cron/crontab/crontab.c?
view=markup
[2] http://opensource.apple.com/source/cron/cron-35/crontab/crontab.c