Security of setuid wrappers 
Author Message
 Security of setuid wrappers

For a while now, I've been working on a rewrite of the venerable Majordomo
mailing list manager.  We are beginning another security review before we
begin trying to broaden our user base significantly.  (Feel free to contact
me if you're interested.)

Majordomo2 runs with its own UID, and can be called from an MTA, a web
server or the command line.  So obviously we need to make use of setuid
scripts or setuid wrappers to get things running properly.  The command
line application also requires switching back to the saved UID at runtime
to run the user's preferred editor.

So far we've been using the simple wrapper setup detailed in the perlsec
manpage.  This has worked well everywhere we port to, but questions have
arisen about whether or not this is truly secure.  The main issue has been
whether or not we need to sanitize the environment in the wrapper before
Perl gets called.  I assume that perlsec would have mentioned this if it
was an issue, but assumptions can be deadly and other wrappers I've seen do
explicitly nuke environment variables.

Another issue is whether we should be obliterating the saved UID to protect
from a malicious hack switching back to the web or mail server's UID and
doing damage.  It is my understanding that this generally requires root
privileges, which means that the wrapper must have setuid privileges and
this opens another can of worms.  It would also mean having multiple
wrappers for different purposes, and also introduces the issue of
portability of the C code.

So basically I'm looking for guidance through what seems to be a
minefield.  Can anyone point is to sources of information about this kind
of thing?  Is anyone willing to lend a hand and help us figure out a secure
solution?

Thanks,
--

    System Manager:  University of Houston Department of Mathematics



Sun, 16 Feb 2003 03:00:00 GMT  
 Security of setuid wrappers
[cc to poster]

I'm not an expert on this subject, but I notice that no one has responded,
so I have a few comments.


Quote:
>For a while now, I've been working on a rewrite of the venerable Majordomo
>mailing list manager.  We are beginning another security review before we
>begin trying to broaden our user base significantly.  (Feel free to contact
>me if you're interested.)

>Majordomo2 runs with its own UID, and can be called from an MTA, a web
>server or the command line.  So obviously we need to make use of setuid
>scripts or setuid wrappers to get things running properly.  The command
>line application also requires switching back to the saved UID at runtime
>to run the user's preferred editor.

>So far we've been using the simple wrapper setup detailed in the perlsec
>manpage.  This has worked well everywhere we port to, but questions have
>arisen about whether or not this is truly secure.  The main issue has been
>whether or not we need to sanitize the environment in the wrapper before
>Perl gets called.  I assume that perlsec would have mentioned this if it
>was an issue, but assumptions can be deadly and other wrappers I've seen do
>explicitly nuke environment variables.

I assume this approach is so that you can run in environments that have the
race condition referred to in perlsec.  I suppose that even though they are
rare, that something like Majordomo2 will find itself running in one of
everything.

The environment issue is relevant when your script (not the wrapper)
intends to use the shell to exec another program or script.  It is not a
factor to the wrapper because the wrapper is presumably using an absolute
path to its target and not requiring the shell.  Once that script is in
control though, the environment is important.  If the target of the wrapper
will invoke a shell, then the variables mentioned in perlsec can come into
play.  (The script *does* specify `-T' on its shebang line, doesn't it?)

Depending on the other programs that the script will call, other
environment variables may be relevant.  I suppose a paranoid policy could
delete everything except the PATH variable and set it to
"/bin:/usr/bin".  In practice, I have been content to set PATH and delete
IFS, CDPATH, ENV and BASH_ENV as the perlsec page recommends.  Ultimately
this decision rests on some knowledge of the programs the script intends to
exec.

Quote:
>Another issue is whether we should be obliterating the saved UID to protect
>from a malicious hack switching back to the web or mail server's UID and
>doing damage.  It is my understanding that this generally requires root
>privileges, which means that the wrapper must have setuid privileges and
>this opens another can of worms.  It would also mean having multiple
>wrappers for different purposes, and also introduces the issue of
>portability of the C code.

I don't understand this question.  The whole idea behind the "wrapper" is
to provide a safe means of executing a setuid/setgid script in the first
place.  It is meaningless, if it is not setuid/setgid.  (And it's only
required to support setuid/setgid operations in environments that have the
race condition described in the perlsec manual page.)  The perlsec man page
suggests a method for a setuid/setgid script (whether it's called by a
wrapper or not) to do certain operations safely by dropping its setid
privileges:

     Here's a way to do backticks reasonably safely. Notice how the exec is
     not called with a string that the shell could expand. This is by far the
     best way to call something that might be subjected to shell escapes:
     just never call the shell at all.

         use English;
         die "Can't fork: $!" unless defined $pid = open(KID, "-|");
         if ($pid) {           # parent
             while (<KID>) {
                 # do something
             }
             close KID;
         } else {

             $EUID = $UID;
             $EGID = $GID;    #      initgroups() also called!
             # Make sure privs are really gone

             die "Can't drop privileges"
                     unless $UID == $EUID  && $GID eq $EGID;
             $ENV{PATH} = "/bin:/usr/bin";
             exec 'myprog', 'arg1', 'arg2'
                 or die "can't exec myprog: $!";
         }

I find two problems with this suggestion:

1) It won't compile in my environment (5.6.0 on Solaris), producing

         Can't modify defined operator in scalar assignment at /dev/fd/3
line 3, near ");"
         Execution of /dev/fd/3 aborted due to compilation errors.

I would call this a (documentation) bug.

2) Once that is fixed, it doesn't seem to produce the desired results in my
environment:

         $ cat /tmp/x
         #!/usr/local/bin/perl -w
         use English;
         die "Can't fork: $!" unless defined($pid = open(KID, "-|"));
         if ($pid) {           # parent
             while (<KID>) {
                 print;
             }
             close KID;
         } else {

             $EUID = $UID;
             $EGID = $GID;    #      initgroups() also called!
             # Make sure privs are really gone

             die "Can't drop privileges"
                     unless $UID == $EUID  && $GID eq $EGID;
             $ENV{PATH} = "/bin:/usr/bin";
             exec 'cat', '/home/garry/.profile'
                 or die "can't exec cat: $!";
         }
         $ /tmp/x
         Can't drop privileges at /dev/fd/3 line 15.
         $

I modified the code to print the value of $EUID and $UID after modifying
them and also confirmed this by sleeping and then displaying the running
processes with ps.  The child *was* running as my original UID.  But after
this, I can still return to the original effective UID (0).  This is
strange because the manual page for setuid() in Solaris says that the
*saved* UID is overwritten, if the effective UID is zero (root).  So I ran
the setuid script under truss (which I had to do as root) and noticed that
perl does *not* call setuid(); it calls seteuid().  I modified the code to
save the UID and then set it to its saved value and then ran truss
again.  Now I see that perl calls setreuid().  But from the setreuid(2)
manual page:

         USAGE
              If a set-user-ID process sets its effective user ID  to  its
              real user ID, it can still set its effective user ID back to
              the saved set-user ID.

Is this a bug?  The perlsec manual page seems to imply that it is.

My conclusion is that there is no way to obliterate the saved UID in perl
when executing a setuid root script.

I wouldn't call this a show-stopper, though.  If you must access files or
exec other programs, then be sure to "scrub" user-supplied data and revert
to the original UID before doing it.  This is safe as long as you know the
program you're calling.

Hope this helps.

-Garry Williams

--------------------------------------------------------

For your information:

$ uname -a
SunOS ifr 5.8 Generic_108528-01 sun4u sparc SUNW,Ultra-5_10
$ perl -V
Summary of my perl5 (revision 5.0 version 6 subversion 0) configuration:
   Platform:
     osname=solaris, osvers=2.7, archname=sun4-solaris
     uname='sunos repos 5.7 generic_106541-11 sun4u sparc sunw,ultra-60 '
     config_args=''
     hint=recommended, useposix=true, d_sigaction=define
     usethreads=undef use5005threads=undef useithreads=undef
usemultiplicity=undef
     useperlio=undef d_sfio=undef uselargefiles=define
     use64bitint=undef use64bitall=undef uselongdouble=undef usesocks=undef
   Compiler:
     cc='cc', optimize='-O', gccversion=
     cppflags='-I/usr/local/include'
     ccflags ='-I/usr/local/include -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64'
     stdchar='char', d_stdstdio=define, usevfork=false
     intsize=4, longsize=4, ptrsize=4, doublesize=8
     d_longlong=define, longlongsize=8, d_longdbl=define, longdblsize=16
     ivtype='long', ivsize=4, nvtype='double', nvsize=8, Off_t='off_t',
lseeksize=8
     alignbytes=8, usemymalloc=y, prototype=define
   Linker and Libraries:
     ld='cc', ldflags ='-L/usr/local/lib '
     libpth=/usr/local/lib /opt/SUNWspro/SC5.0/lib /lib /usr/lib /usr/ccs/lib
     libs=-lsocket -lnsl -ldb -ldl -lm -lc -lcrypt -lsec
     libc=/lib/libc.so, so=so, useshrplib=false, libperl=libperl.a
   Dynamic Linking:
     dlsrc=dl_dlopen.xs, dlext=so, d_dlsymun=undef, ccdlflags=' '
     cccdlflags='-KPIC', lddlflags='-G -L/usr/local/lib'

Characteristics of this binary (from libperl):
   Compile-time options: USE_LARGE_FILES
   Built under solaris
   Compiled at Aug  1 2000 07:48:21

     /usr/local/lib/perl5/5.6.0/sun4-solaris
     /usr/local/lib/perl5/5.6.0
     /usr/local/lib/perl5/site_perl/5.6.0/sun4-solaris
     /usr/local/lib/perl5/site_perl/5.6.0
     /usr/local/lib/perl5/site_perl
     .
$



Tue, 18 Feb 2003 12:37:46 GMT  
 Security of setuid wrappers
[cc sent to poster; please reply via news if this propagates]

GTW> I assume this approach is so that you can run in environments that
GTW> have the race condition referred to in perlsec.

Well, that's one reason.  Another is that there are people who will not
tolerate making a script setuid because of the history.  Another is that
there are folks who don't think that Perl should ever be trusted with an
unsanitized environment.

GTW> The environment issue is relevant when your script (not the wrapper)
GTW> intends to use the shell to exec another program or script.  It is not
GTW> a factor to the wrapper because the wrapper is presumably using an
GTW> absolute path to its target and not requiring the shell.

The arguments I'm getting are that the environment must be sanitized
because Perl (and its modules) cannot be trusted with it.  And because of
the general argument that sanitizing makes sense.  Sure, we have tainting
and technically can't access anything we don't want to without actually
trying to do it, but Perl also makes use of the environment and we can
never tell what gets done in all of the modules we use (since the users'
installed versions will probably differ from the ones we test with).

The problem is that I'm not a security expert; it makes sense to me that
you should probably sanitize the environment just in case.

GTW> (The script *does* specify `-T' on its shebang line, doesn't it?)

All of our scripts run with taint checks on, even if they don't run behind
wrappers.

GTW> In practice, I have been content to set PATH and delete IFS, CDPATH,
GTW> ENV and BASH_ENV as the perlsec page recommends.

We do this already, but some do not believe that it is enough.

GTW> Ultimately this decision rests on some knowledge of the programs the
GTW> script intends to exec.

Currently only one of our scripts execs anything else, and only after
switching back to the saved UID.  This is the contents of EDITOR, which
means that while we have essentially no control over what gets run, it does
get run as the invoking user.  (I have never felt really safe doing this,
but it is such a common programming method that I have to accept that if
done correctly it's OK.)

Quote:
>> Another issue is whether we should be obliterating the saved UID [...]

GTW> I don't understand this question.  The whole idea behind the "wrapper"
GTW> is to provide a safe means of executing a setuid/setgid script in the
GTW> first place.

A setuid program can switch back to the original UID ("drop privileges") at
any time to do something unsafe like calling an editor.  But what happens
when Sendmail calls you?  You can still switch back to the mail system's
UID (assuming either it's not root or your OS doesn't do saved UID
obliteration when root calls a setuid binary) and do something.  Our code
doesn't do that, but an exploit could theoretically exist that lets someone
inject Perl code that gets executed RAW, switches back to the saved UID and
does damage.  (Obviously we're very careful to use Safe all over the place
when touching generated code, but can we trust Safe.pm?)

I am not yet convinced that the requirements of this (a setuid wrapper,
hence real root exposure and people questioning why we need root,
portability issues, etc.) are worth the benefits.  That's mainly what I'm
here to figure out.

GTW> The perlsec man page suggests a method for a setuid/setgid script
GTW> (whether it's called by a wrapper or not) to do certain operations
GTW> safely by dropping its setid privileges:

That wasn't in perlsec when I started on this project (perl 5.004 was
current then) but here's what we use:

      # Switch UIDs, run the editor, restore
      $tmpu = $>; $> = $<; $tmpg = $); $) = $(;
      $ok = system("$editor $::WTMPDIR/mjs.$$.AAA");
      $> = $tmpu; $) = $tmpg;

$editor is the only user-supplied data, generated by:

    $editor = $ENV{EDITOR} || $ENV{VISUAL} || '/bin/vi';
    $editor =~ /(.*)/;
    $editor = $1;

GTW> My conclusion is that there is no way to obliterate the saved UID in
GTW> perl when executing a setuid root script.

Interesting.  But the request we're getting is to do it in the wrapper.

 - J<



Wed, 19 Feb 2003 00:46:23 GMT  
 Security of setuid wrappers

Quote:
> GTW> In practice, I have been content to set PATH and delete IFS, CDPATH,
> GTW> ENV and BASH_ENV as the perlsec page recommends.

> We do this already, but some do not believe that it is enough.

I believe LC* is another route to defend against.

Quote:
> GTW> Ultimately this decision rests on some knowledge of the programs the
> GTW> script intends to exec.

> Currently only one of our scripts execs anything else, and only after
> switching back to the saved UID.  This is the contents of EDITOR, which
> means that while we have essentially no control over what gets run, it does
> get run as the invoking user.  (I have never felt really safe doing this,
> but it is such a common programming method that I have to accept that if
> done correctly it's OK.)

You might consider a 'shells'-like approach - use EDITOR only if it is
on the approved list of editors, else complain.

Hugo



Wed, 19 Feb 2003 01:32:34 GMT  
 Security of setuid wrappers
[cc to poster]


...

So deleting the environment in the wrapper before exec'ing the perl script
is what you intend to do.  This also covers the LC* variables that Hugo van
der Sanden mentioned in another post.  I note also that perl ignores the
PERL5LIB family of variables when it detects that it is running setuid, but
a wrapper may mask that, so I suppose the "paranoid" approach of deleting
everything from the environment is appropriate.

Quote:
>GTW> Ultimately this decision rests on some knowledge of the programs the
>GTW> script intends to exec.

>Currently only one of our scripts execs anything else, and only after
>switching back to the saved UID.  This is the contents of EDITOR, which
>means that while we have essentially no control over what gets run, it does
>get run as the invoking user.  (I have never felt really safe doing this,
>but it is such a common programming method that I have to accept that if
>done correctly it's OK.)

I conclude from the test results that I mentioned that this is unsafe.  I
don't see how perl can be coerced into calling setuid() therefore the saved
UID is available to the program referenced in the EDITOR variable.  The
"bad guy" can issue seteuid() and friends from that program and recover the
saved UID.  I don't think you should do this from perl because I cannot see
that it is "done correctly".

Quote:
> >> Another issue is whether we should be obliterating the saved UID [...]

>GTW> I don't understand this question.  The whole idea behind the "wrapper"
>GTW> is to provide a safe means of executing a setuid/setgid script in the
>GTW> first place.

>A setuid program can switch back to the original UID ("drop privileges") at
>any time to do something unsafe like calling an editor.

Perl is a problem here.  See above.

Quote:
>But what happens
>when Sendmail calls you?  You can still switch back to the mail system's
>UID (assuming either it's not root or your OS doesn't do saved UID
>obliteration when root calls a setuid binary) and do something.  Our code
>doesn't do that, but an exploit could theoretically exist that lets someone
>inject Perl code that gets executed RAW, switches back to the saved UID and
>does damage.

I understand sendmail to have already prevented this.  I.e., sendmail will
have already dropped privileges correctly whenever it exec's a program for
the user.

Quote:
>...
>I am not yet convinced that the requirements of this (a setuid wrapper,
>hence real root exposure and people questioning why we need root,
>portability issues, etc.) are worth the benefits.  That's mainly what I'm
>here to figure out.

It's been too long since I've looked at Majordomo.  I don't recall why root
is needed.  I suppose it's to be able to access the Majordomo "repository"
from a mail alias that specifies a pipe.  This would require at least
setuid majordom privileges.

Quote:
>GTW> The perlsec man page suggests a method for a setuid/setgid script
>GTW> (whether it's called by a wrapper or not) to do certain operations
>GTW> safely by dropping its setid privileges:

>That wasn't in perlsec when I started on this project (perl 5.004 was
>current then) but here's what we use:

>       # Switch UIDs, run the editor, restore
>       $tmpu = $>; $> = $<; $tmpg = $); $) = $(;
>       $ok = system("$editor $::WTMPDIR/mjs.$$.AAA");
>       $> = $tmpu; $) = $tmpg;

This is *not* safe.  Run this code under truss and see for yourself.  It
doesn't call setuid(), so it doesn't obliterate the saved UID so it allows
the exec'ed program to restore privileges.  The code above seems to "know"
this because it expects that it can restore its effective UID (the last
line).  I don't get it.  Even if perl handled the dropping of privileges
correctly, you would need an explicit fork/exec to do that in the child
process, if you need to retain privileges after the call to the editor.

This code also allows the shell to get into the picture because you do not
use a list in your call to system().  That means the string `$editor
$::WTMPDIR/mjs.$$.AAA' needs to have been scrubbed carefully.  The perlsec
manual page recommends against this.  Just use the list form of system() or
exec() and the shell is out of the picture.

Quote:
>$editor is the only user-supplied data, generated by:

>     $editor = $ENV{EDITOR} || $ENV{VISUAL} || '/bin/vi';
>     $editor =~ /(.*)/;
>     $editor = $1;

This is *not* safe.  You do not scrutinize the $editor value, so the "bad
guy" just got you to execute anything he wanted by the shell.  (It becomes
moot, if you use the list form of system().)

Quote:
>GTW> My conclusion is that there is no way to obliterate the saved UID in
>GTW> perl when executing a setuid root script.

>Interesting.  But the request we're getting is to do it in the wrapper.

Then there is no way to change the UID from the Perl script, if the wrapper
has dropped privileges correctly.  I don't understand.

-Garry Williams



Wed, 19 Feb 2003 22:38:57 GMT  
 Security of setuid wrappers

Quote:
> In solaris, ways to change the saved uid are:
>    setuid(uid)             (when euid == 0)

Oh, interesting... setuid doesn't even do it all the time.  Quoting some
more from the manual page:

     If the effective user ID of the process calling setuid() is the
     super- user, the real, effective, and saved user IDs are set to the
     uid parameter.

     If the effective user ID of the calling process is not the
     super-user, but uid is either the real user ID or the saved user ID
     of the calling process, the effective user ID is set to uid.

so if you're in the situation where you've temporarily dropped privileges
and your real and effective UIDs are equal and some non-privileged UID but
your saved UID is root, calling setuid to your current effective UID does
absolutely nothing.  You have to regain your privileged effective UID
first and *then* call setuid.

Bizarre.

--
#!/usr/bin/perl -- Russ Allbery, Just Another Perl Hacker





Sat, 22 Feb 2003 08:59:31 GMT  
 
 [ 6 post ] 

 Relevant Pages 

1. Using TTables, GPF's

2. TFDD examples

3. Can't compile winprocs.pas

4. WAV and/or MIDI's

5. Format of dBase III memo files?

6. setuid and C wrapper problem

7. setuid and C-wrappers

8. can't get setuid C wrapper working

9. Setuid C wrapper

10. IOT/Abort trap running using setuid wrapper AIX

11. setuid wrapper program for perl4 script...

12. setuid, C wrapper

 

 
Powered by phpBB® Forum Software