Why Perl do But Python can't?

Thomas Wouters thomas at xs4all.net
Mon Jan 31 14:12:58 EST 2000


On Mon, Jan 31, 2000 at 06:13:11PM -0000, Adrian Eyre wrote:

> > : Scripts (of any sort) are inherently unsafe when run setuid or setgid.
> > : See the perlsec manpage for why.
> > 
> > This is not true, and is a myth people need to get over.  The danger
> > Kelly referred to exists on some Unix-like systems and not on others.
> > On a system where they are safe, there is no reason whatsoever to use
> > a setuid wrapper.
> 
> Just out of interest, what is the reason why this in unsafe. I couldn't
> see the difference between a script and a binary.

When you try to execute a script (using any of the exec*() family C calls)
the UNIX (or perhaps C library on some systems) opens the program in
question and tries to determine what kind of binary format it is. If it's a
pure binary, it gets handled as pure code, executed right away. If it has
shared libraries of any kind the runtime link/loader is called in to do the
setup. But if it's a script, it has to do something different :P

It has to read the first line of the file, strip the #!, check if the
interpreter exists, and exec()'s that, with a modified argv: basically the
shebang line, minus the #!, with the script name and the original arguments
to the script attached. Thus,

spam.py:
-
#!/usr/bin/python -X
-

centurion: ~> ./spam.py -i10 foobar


becomes (pseudocode)

execv("/usr/bin/python -X spam.py -i10 foobar")

If setuid bits on scripts where allowed, it would have to do the exec()
running as the owner of the file already, so that '/usr/bin/python' is
called as the right user. But this means there is a possible race condition,
using symlinks (or hard links, if you are on the same disk.)

Say a system I work on has a setuid-root script somewhere, like
/sbin/fixperms. It has its own security-checking inside the script, so it's
setuid-root and executable by everyone, or perhaps by a limited group.

Mr. M. Alicious wants to hack me, and he can execute the above script, but
is stopped by the internal security checks. What he does is this:

--- hack.py --
#!/usr/bin/python

import os

os.execv("/bin/sh",("/bin/sh",))
--- hack.py --

hack.py is owned by alicious, just executable, not setuid. Now Mr. A.
creates a symlink to /sbin/fixperms, say, 'runme', and then starts to create
a heavy load on the machine (not necessary, but makes the next step a lot
easier.) preferably by generating lots of swap-activity, or lots of I/O on
the same disk the python executable is located on.

Then Mr. Alicious starts /sbin/fixperms _via the symlink_, and at the same
time sets the symlink to hack.py. He has to take care to replace the symlink
at exactly the right moment, though, between the setuid-bit checks in the
kernel/libc and the opening of the script by python. On a heavily loaded
machine this isn't that big a feat. Python gets started as root, opens
'runme', and gets the contents of hack.py. and exec()'s /bin/sh, giving Mr.
M a nice root-owned shell.

There are a couple of solutions possible, unfortunately, most of these are
incompatible in some minor way with how it is currently done. The most
common one is not to pass the script to be run by the interpreter as a
command line, but instead as an open filedescriptor, fd 3. The interpreter
would have to be altered to accept that, however(*), and since UNIX in
general has a lot of different interpreters.. icky. 

Another option would be for the python interpreter itself to check for
setuid bits... I only thought that up just now, so I'm not sure if there's a
problem with that, but now that I look at the perlsec manpage, I believe
this is what suidperl does; Python could check for setuid or setgid bits on
the scripts it is about to execute, and invoke a special setuid-root part of
the interpreter (seperate binary, of course) to re-invoke python with the
proper userid/groupid (in a race-free manner, such as passing an open fd.)
Perhaps for backward compatibility it would be best only to invoke the
setuid part of python if python is started with, say, the -s option. 

Any takers on that idea ? It wouldn't be hard to write, I bet ;) But then,
it's probably already been suggested and rejected before ;)

*) on Linux systems, passing an open fd is actually fairly easy and fairly
compatible; old (or perhaps just all) interpreters could be started with
'/proc/fd/3' as the script. There was talk about this on the linux kernel
list a year or so ago (and probably more often before and after that) but
the problem was, and probably still is, that it would break interpreters
that rely on opening a filedescriptor right after starting and assuming it
will be fd 3.

Paranoid-android-ly y'rs,
-- 
Thomas Wouters <thomas at xs4all.net>

Hi! I'm a .signature virus! copy me into your .signature file to help me spread!




More information about the Python-list mailing list