COMMAND

    crontab

SYSTEMS AFFECTED

    RedHat Linux

PROBLEM

    Here are several things you may remove from /etc/crontab on  every
    RedHat Linux system.  They  contain security holes related to  the
    use of  'find' and  'rm' to  expire old  files in  /tmp and  other
    places.

    It  seems  that  awareness  of  this  type  of security problem is
    rather low, so it will be  explained the class of problem and  how
    to fix it.  Credit for this text goes to Zygo Blaxell.

    From Redhat's /etc/crontab file:
    # Remove /var/tmp files not accessed in 10 days
    43 02 * * * root find /var/tmp/* -atime +3 -exec rm -f {} \; 2> /dev/null

    # Remove /tmp files not accessed in 10 days
    # I commented out this line because I tend to "store" stuff in /tmp
    # 41 02 * * * root find /tmp/* -atime +10 -exec rm -f {} \; 2> /dev/null

    # Remove formatted man pages not accessed in 10 days
    39 02 * * * root find /var/catman/cat?/* -atime +10 -exec rm -f {} \; 2> /dev/null

    # Remove and TeX fonts not used in 10 days
    35 02 * * * root find /var/lib/texmf/* -type f -atime +10 -exec rm -f {} \; 2> /dev/null

    The immediate  security problem  is that  'rm' doesn't  check that
    components of  the directory  name are  not symlinks.   This means
    that you can delete any file on the system; indeed, with a  little
    work you can delete *every* file on the system, provided that  you
    can  determine  the  file  names  (though  you might be limited to
    deleting files more than ten days old).

    First, create the directories and file:

        /tmp/hacker-fest/some/arbitrary/set/of/path/names/etc/passwd

    where all  but the  last component  is a  directory.   Be ready to
    replace 'etc' with a symlink to '/etc', so that:

        /tmp/hacker-fest/some/arbitrary/set/of/path/names/etc -> /etc

    i.e. the path  components of the  file name will  point to a  file
    named 'passwd' in a different directory.

    If the replacement operation occurs between when 'find' sets {} to
    "/tmp/hacker...etc/passwd"  and   when  'rm'   calls  unlink    on
    "/tmp/hacker...etc/passwd",   then   rm   will   in   fact  delete
    '/etc/passwd', and not  a file in  /tmp.  Deleting  other files is
    left as an exercise.

    The race condition is really easy to win.  Create a directory
    with 400 path components, like this:

        /tmp/hacker-fest/a/a/a/a/a/a/a.../a/a/a/etc/passwd      (1)

    Then arrange for each of the  'a' components to be a symlink  to a
    directory  somewhere  near  the  bottom  of  a  similar tree.  For
    example,

        /tmp/hacker-fest/a

    could be a symlink to

        /tmp/hacker-fest/b/b/b/b/b/b/b/b/b/.../b/b/b/b/b/b/a

    which could be a symlink to

        /tmp/hacker-fest/c/c/c/c/c/c/.../c/c/c/c/c/c/c

    and so on.  In fact, *each* path component can be a symlink up  to
    about  8  levels  or  so.   Any  operation such as stat(), open(),
    lstat(), etc.  on one of these pathnames will cause the kernel  to
    follow each  and every  symlink.   The difference  between lstat()
    and  stat()  in  this  case  is  that  lstat() will not follow the
    *last* symlink.

    This will make lstat() and friends *extremely* slow, on the  order
    of several *minutes* per  lstat() operation, because each  lstat()
    is now  reading in  several thousand  inodes and  disk blocks.  If
    you fill each directory with several hundred entries, then  create
    the entry you want, then  delete the others, you force  the kernel
    to waste its time reading kilobytes of empty directory  blocks--in
    fact, you can  make one stat()  or unlink() operation  read almost
    the entire disk in an order designed to maximize disk head  motion
    if you  know what  you're doing.   If you  have an  NFS, CDROM, or
    floppy-disk filesystem handy, you can get *weeks* per lstat().

    Of course, 'find'  will normally see  the first symlink  and stop.
    To prevent this, you rename the original directory (at (1)  above)
    and create  another directory  with the  same name  and about 5000
    empty files, some of  which have the same  name as files you  want
    to delete.   Note that  these 5000  empty files  can all  be  hard
    links to the same file, to save precious inodes for more of  those
    symlinks.

    'find' will spend considerable  time iterating through these  5000
    files.  When it does (you'll be able to tell because the atime  of
    the directory changes  as find reads  it), put the  directory with
    the  millions  of  symlinks  at  (1)  back with a couple of rename
    operations.   Some  versions  of  'find'  will  not  be  adversely
    impacted by this, but 'rm' definitely will.

    It is usually sufficient  to simply create the  400-component-long
    directory,  put  5000  files  in  it,  wait  for  the atime of the
    directory to  change, then  do the  rename so  that 'rm' follows a
    symlink.  I used this  technique to remove /etc/crontab as  a test
    case.

    If you have:

        /tmp/hacker-fest/a/a/a/a/a/.../a/etc/passwd (and 5000+ other files)
        /tmp/hacker-fest/a/a/a/a/a/.../a/usr

    where  'usr'  is   a  symlink  to   '/usr',  you  can   get   some
    implementations of find to start recursing through /usr as well.

    There are some other  bugs too.  A  user can set the  atime of any
    file they own to an  arbitrary value, and that programs  like zip,
    tar,  and  cpio  will  do  this  for you automatically; this makes
    'atime' an almost useless indicator  of when a file was  last used
    ('mtime' has the same problem).   Either the file will be  deleted
    too  early,  because  it  was  extracted  from  an archive using a
    program that preserves timestamps, or  users can set the atime  to
    well into the future and  use /tmp space indefinitely.   The later
    of  ctime  (to  detect  writes)  and  atime (to detect reads; must
    check that  atime is  not in  the future)  is a  good indicator of
    when a file was last used.

    Miscellaneous  bugs:   the  use  of  '*'  means  that  files  in a
    directory named '.foo' will never be cleaned (and you can  prevent
    'find' from  working at  all by  putting more  than 1020  files in
    /tmp).   There  are  subdirectories  of  /var/catman  that  aren't
    properly  handled  by  the  'find'  command given (local and X11).
    You can't delete a directory with 'rm -f'.


SOLUTION

    The easiest way  to fix this  is to get  rid of the  find/rm stuff
    completely.  If you need a garbage collector, try our LRU  garbage
    collection daemon at the URL given below.

    Adding a  system call  that sets  a flag  that prevents  a process
    from being able  to ever follow  a symlink would  be non-portable,
    but efficient and effective.

    The  next  easiest  way  to  fix  this  is  to replace 'rm' with a
    program that does  not follow symlinks.   It must check  that each
    filename component in turn by  doing an lstat() of the  directory,
    chdir() into  the directory,  and further  lstat()s to  check that
    the device/inode  number of  '.' is  the same  as the  directory's
    device/inode  number  before  chdir().    The  parameter  of   the
    'unlink' or 'rmdir'  system call must  not contain a  slash; if it
    does, then the directory name before the slash can be replaced  by
    a symlink to  a different directory  between verification of  path
    components and the actual unlink() call.

    Another way  to fix  this is  with a  smarter version  of find.  A
    smart find does the chdir()  and lstat() checks to make  sure that
    it never crosses a symlink, and calls the program in 'exec'  using
    a filename with no  directory components, relative to  the current
    directory.  Thus, to delete:

        /tmp/hacker-fest/a/a/a/a/a/.../etc/passwd

    find  first  carefully  (checking  for  attempts  to  exploit race
    conditions before and *after* each chdir()) chdir()s into

        /tmp/hacker-fest/a/a/a/a/a/.../etc

    and will fail if any of the components is a symlink, plugging  the
    hole  described  above.   After  verifying  that  the '.../etc' is
    really a subdirectory  of /tmp, and  not some random  point on the
    filesystem, find exec's the command:

        rm -f ./passwd

    which is  secure as  long as  '.' isn't  in your  PATH.   Note the
    leading './'  to prevent  rm from  interpreting the  filename as a
    parameter.

    Note: this is in *addition* to the checks that find already  makes
    to determine whether a file is a symlink *before* chdir()ing  into
    it.   It must  make sure  that components  of the  path that  have
    *already* been tested  are not replaced  with symlinks or  renamed
    directories *after* find has started processing subdirectories  of
    them.

    Note that the  'smart' find without  the post-chdir symlink  tests
    won't work.  While smart-find is processing:

        /tmp/hacker-fest/a/a/a/a/*

    you can rename

        /tmp/hacker-fest/a/a/a/a

    to

        /tmp/hacker-fest/a/a/b  (note: one less pathname component)

    and  eventually  smart-find  will  'cd  ..', but since the current
    directory  of  find  has  moved,  '..'  will  move  as  well,  and
    eventually smart-find  will be  one level  too high  and can start
    descending into other subdirectories of  '/'.  To help this  along
    you may need to create:

        /tmp/hacker-fest/usr
        /tmp/hacker-fest/var
        etc.

    SAFE LRU GARBAGE /tmp garbage collector daemon is available at:

        http://www.ultratech.net/~zblaxell/admin_utils/filereaper.txt

    It  is  implemented  in  perl5.   It  depends  on a Linux-specific
    'statfs()'  system  call  to  monitor  available  free  space,  so
    non-Linux people will need to do a port.