I'm trying to add a feature to a legacy script. The script is suid, and uses perl -T (taint mode: man perlsec), for extra security. The feature I need to add is implemented in Python.
The problem is that I can't convince perlsec to preserve the suid permissions, no matter how much I launder the environment and my command lines.
This is frustrating, since it preserves the suid for other binaries (such as /bin/id). Is there an undocumented special case for /usr/bin/perl? This seems unlikely.
Does anyone know a way to make this work? (As-is: We don't have the resources to re-architect this whole thing.)
Solution: (as per @gbacon)
# use the -p option to bash
system('/bin/bash', '-p', '-c', '/usr/bin/id -un');
# or set real user and group ids
$< = $>;
$( = $);
system('/usr/bin/python', '-c', 'import os; os.system("/usr/bin/id -un")');
This gives the desired results!
Here's a cut-down version of my script, which still shows my problem.
#!/usr/bin/perl -T
## This is an SUID script: man perlsec
%ENV = ( "PATH" => "" );
##### PERLSEC HELPERS #####
sub tainted (@) {
# Prevent errors, stringifying
local(@_, $@, $^W) = @_;
#let eval catch the DIE signal
$SIG{__DIE__} = '';
my $retval = not eval { join("",@_), kill 0; 1 };
$SIG{__DIE__} = 'myexit';
return $retval
}
sub show_taint {
foreach (@_) {
my $arg = $_; #prevent "read-only variable" nonsense
chomp $arg;
if ( tainted($arg) ) {
print "TAINT:'$arg'";
} else {
print "ok:'$arg'";
}
print ", ";
}
print "\n";
}
### END PERLSEC HELPERS ###
# Are we SUID ? man perlsec
my $uid = `/usr/bin/id --user` ;
chomp $uid;
my $reluser = "dt-pdrel";
my $reluid = `/usr/bin/id --user $reluser 2> /dev/null`;
chomp $reluid;
if ( $uid ne $reluid ) {
# what ? we are not anymore SUID ? somebody must do a chmod u+s $current_script
print STDERR "chmod 4555 $myname\n";
exit(14);
}
# comment this line if you don't want to autoflush after every print
$| = 1;
# now, we're safe, single & SUID
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# BEGIN of main code itself
print "\nENVIRON UNDER BASH:\n";
run('/bin/bash', '-c', '/bin/env');
print "\nTAINT DEMO:\n";
print "\@ARGV: ";
show_taint(@ARGV);
print "\%ENV: ";
show_taint(values %ENV);
print "`cat`: ";
show_taint(`/bin/cat /etc/host.conf`);
print "\nworks:\n";
run('/usr/bin/id', '-un');
run('/usr/bin/id -un');
print "\ndoesn't work:\n";
run('/bin/bash', '-c', '/usr/bin/id -un');
run('/bin/bash', '-c', '/bin/date >> /home/dt-pdrel/date');
run('/bin/date >> /home/dt-pdrel/date');
run('/usr/bin/python', '-c', 'import os; os.system("/usr/bin/id -un")');
run('/usr/bin/python', '-c', 'import os; os.system("/usr/bin/id -un")');
sub run {
my @cmd = @_;
print "\tCMD: '@cmd'\n";
print "\tSEC: ";
show_taint(@cmd);
print "\tOUT: ";
system @cmd ;
print "\n";
}
And here's the output:
$ id -un
bukzor
$ ls -l /proj/test/test.pl
-rwsr-xr-x 1 testrel asic 1976 Jul 22 14:34 /proj/test/test.pl*
$ /proj/test/test.pl foo bar
ENVIRON UNDER BASH:
CMD: '/bin/bash -c /bin/env'
SEC: ok:'/bin/bash', ok:'-c', ok:'/bin/env',
OUT: PATH=
PWD=/proj/test2/bukzor/test_dir/
SHLVL=1
_=/bin/env
TAINT DEMO:
@ARGV: TAINT:'foo', TAINT:'bar',
%ENV: ok:'',
`cat`: TAINT:'order hosts,bind',
works:
CMD: '/usr/bin/id -un'
SEC: ok:'/usr/bin/id', ok:'-un',
OUT: testrel
CMD: '/usr/bin/id -un'
SEC: ok:'/usr/bin/id -un',
OUT: testrel
doesn't work:
CMD: '/bin/bash -c /usr/bin/id -un'
SEC: ok:'/bin/bash', ok:'-c', ok:'/usr/bin/id -un',
OUT: bukzor
CMD: '/bin/bash -c /bin/date >> /home/testrel/date'
SEC: ok:'/bin/bash', ok:'-c', ok:'/bin/date >> /home/testrel/date',
OUT: /bin/bash: /home/testrel/date: Permission denied
CMD: '/bin/date >> /home/testrel/date'
SEC: ok:'/bin/date >> /home/testrel/date',
OUT: sh: /home/testrel/date: Permission denied
CMD: '/usr/bin/python -c import os; os.system("/usr/bin/id -un")'
SEC: ok:'/usr/bin/python', ok:'-c', ok:'import os; os.system("/usr/bin/id -un")',
OUT: bukzor
CMD: '/usr/bin/python -c import os; os.system("/usr/bin/id -un")'
SEC: ok:'/usr/bin/python', ok:'-c', ok:'import os; os.system("/usr/bin/id -un")',
OUT: bukzor
You need to set your real userid to the effective (suid-ed) one. You probably want to do the same for your real group id:
#! /usr/bin/perl -T
use warnings;
use strict;
$ENV{PATH} = "/bin:/usr/bin";
system "id -un";
system "/bin/bash", "-c", "id -un";
# set real user and group ids
$< = $>;
$( = $);
system "/bin/bash", "-c", "id -un";
Sample run:
$ ls -l suid.pl -rwsr-sr-x 1 nobody nogroup 177 2010-07-22 20:33 suid.pl $ ./suid.pl nobody gbacon nobody
What you're seeing is documented bash
behavior:
-p
Turn on privileged mode. In this mode, the
$BASH_ENV
and$ENV
files are not processed, shell functions are not inherited from the environment, and theSHELLOPTS
,BASHOPTS
,CDPATH
andGLOBIGNORE
variables, if they appear in the environment, are ignored. If the shell is started with the effective user (group) id not equal to the real user (group) id, and the-p
option is not supplied, these actions are taken and the effective user id is set to the real user id. If the-p
option is supplied at startup, the effective user id is not reset. Turning this option off causes the effective user and group ids to be set to the real user and group ids.
This means you could also
#! /usr/bin/perl -T
use warnings;
use strict;
$ENV{PATH} = "/bin:/usr/bin";
system "/bin/bash", "-p", "-c", "id -un";
to get
nobody
Recall that passing multiple arguments to system
bypasses the shell. A single argument does go to the shell, but probably not bash
—look at the output of perl -MConfig -le 'print $Config{sh}'
.
Maybe consider using use English '-no_match_vars';
for mental sanity:
It will provide $GID
, $EGID
, $UID
, $EUID
, etc.