# Usage:
# To fix one or more specific suidperl executables in, say, /usr/bin:
# cd /usr/bin
# fixsperl sperl?.???
# or, to check all $PATH directories for sperl?.??? and fix them, just say:
# fixsperl
# or just pipe the article containing this script to "perl -x" for
# a similar effect.
#
# If this fails, you may need to tweak one or more of the four customization
# variables below. The default values are just good guesses.
# Customization.
# Pick a C compiler. Other possibilities are "gcc", "acc", "lcc", etc.
$CC = "cc";
# Pick an unused directory. This will be created automatically.
$SUIDTMP = "/usr/suidtmp";
# Pick first one of setresuid, setreuid, seteuid, setuid that libc has.
$SETUID = "setreuid";
# Pick first one of setresgid, setregid, setegid, setgid that libc has.
$SETGID = "setregid";
# End of Customization.
# Do some sanity checks first.
if ($>) { die "You must run fixsperl as root\n"; }
# Copy the new suidperl code out to where cc can see it in $SUIDTMP.
sub make_suidtmp {
-d $SUIDTMP || system "mkdir -p $SUIDTMP";
-d $SUIDTMP || die "Could't create $SUIDTMP\n";
-o $SUIDTMP || die "root doesn't own $SUIDTMP\n";
chmod 0755, $SUIDTMP;
open(CODE, ">$SUIDTMP/sperl.c") || die "Can't write $SUIDTMP/sperl.c: $!\n";
while (<DATA>) {
last if /END OF CODE/;
print CODE;
}
close CODE;
$made_suidtmp++;
}
# Look down the current PATH for instances of suidperl.
if (!@ARGV) {
# Round up the likely suspects.
@dirs = grep(!$seen{$_}++, split(/:/, $ENV{'PATH'}),
"/bin",
"/usr/bin",
"/usr/local",
</*/bin /usr/*/bin>);
foreach $dir (@dirs) {
next if $dir eq "." || $dir eq "";
next unless -d $dir;
push(@ARGV, $dir);
}
}
# Translate each directory to its contained suidperl files, if any.
chmod 0600, $sperl; # Disable old suidperl.
rename($sperl, "$sperl.bad");
# Find something to run as taintperl. (Perl 5 uses normal perl for that.)
$tperl = $sperl;
$tperl =~ s#/sperl5#/perl5#;
$tperl =~ s#/sperl4#/tperl4#;
$tperl =~ s#/suidperl#/taintperl#; # ancient
$tperl =~ s#(.*)/.*#$1/perl# unless -f $tperl;
$tperl = "/usr/bin/perl" unless -f $tperl;
$defs = qq($HASUID $HASGID -DTAINTPERL='"$tperl"');
$cmd = "$CC $defs -o $sperl $SUIDTMP/sperl.c";
warn "\t$cmd\n";
system $cmd;
if ($?) {
warn "FAILED--try changing one of the customization constants\n";
rename("$sperl.bad", $sperl);
}
else {
chmod 04711, $sperl;
}
}
die "Nothing to fix" unless $made_suidtmp;
unlink "$SUIDTMP/sperl.c";
# Following is the C program that the script compiles to replace suidperl.
# POSIX.1 capabilities are assumed. (If you don't have POSIX.1, you probably
# don't need this fix anyway.)
__END__
#ifndef TAINTPERL
#include "please define TAINTPERL"
#endif
long
ingroup(testgid,effective)
long testgid;
long effective;
{
if (testgid == (effective ? egid : gid))
return 1;
#ifndef NGROUPS
#define NGROUPS 32
#endif
{
gid_t gary[NGROUPS];
long anum;
anum = getgroups(NGROUPS,gary);
while (--anum >= 0)
if (gary[anum] == testgid)
return 1;
}
return 0;
}
long
cando(bit, effective, statbufp)
long bit;
long effective;
struct stat *statbufp;
{
if ((effective ? euid : uid) == 0) { /* root is special */
if (bit == S_IXUSR) {
if (statbufp->st_mode & 0111 || S_ISDIR(statbufp->st_mode))
return 1;
}
else
return 1; /* root reads and writes anything */
return 0;
}
if (statbufp->st_uid == (effective ? euid : uid) ) {
if (statbufp->st_mode & bit)
return 1; /* ok as "user" */
}
else if (ingroup((long)statbufp->st_gid,effective)) {
if (statbufp->st_mode & bit >> 3)
return 1; /* ok as "group" */
}
else if (statbufp->st_mode & bit >> 6)
return 1; /* ok as "other" */
return 0;
}
int
main(argc,argv)
int argc;
char **argv;
{
char **newargv = (char**)malloc((argc + 5) * sizeof(char*));
int childpid;
int saveumask;
int old;
int new;
FILE *oldfp;
FILE *newfp;
char *oldscript = "script";
char newscript[1024];
struct stat statbuf;
char *perlname;
char realperl[1024];
char buf[8192];
long line = 0;
char *validarg = "";
char *s;
if (fstat(fileno(oldfp),&statbuf) < 0) { /* normal stat is insecure */
fprintf(stderr, "Can't stat script \"%s\"",oldscript);
goto oops;
}
if (statbuf.st_mode & (S_ISUID|S_ISGID)) {
long len;
#if !defined(HAS_SETREUID) && !defined(HAS_SETRESUID)
/* On this access check to make sure the directories are readable,
* there is actually a small window that the user could use to make
* filename point to an accessible directory. So there is a faint
* chance that someone could execute a setuid script down in a
* non-accessible directory. I don't know what to do about that.
* But I don't think it's too important. The manual lies when
* it says access() is useful in setuid programs.
*/
if (access(oldscript,1)) { /*double check*/
fprintf(stderr,"Permission denied");
goto oops;
}
#else
/* If we can swap euid and uid, then we can determine access rights
* with a simple stat of the file, and then compare device and
* inode to make sure we did stat() on the same file we opened.
* Then we just have to make sure he or she can execute it.
*/
{
struct stat tmpstatbuf;
if (
#ifdef HAS_SETREUID
setreuid(euid,uid) < 0
#else
# if HAS_SETRESUID
setresuid(euid,uid,(uid_t)-1) < 0
# endif
#endif
|| getuid() != euid || geteuid() != uid) {
fprintf(stderr,"Can't swap uid and euid"); /* really paranoid */
goto oops;
}
if (stat(oldscript,&tmpstatbuf) < 0) {
fprintf(stderr,"Permission denied"); /* testing full pathname here */
goto oops;
}
if (tmpstatbuf.st_dev != statbuf.st_dev ||
tmpstatbuf.st_ino != statbuf.st_ino) {
(void)fclose(oldfp);
fprintf(stderr,"Permission denied\n");
goto oops;
}
if (
#ifdef HAS_SETREUID
setreuid(uid,euid) < 0
#else
# if defined(HAS_SETRESUID)
setresuid(uid,euid,(uid_t)-1) < 0
# endif
#endif
|| getuid() != uid || geteuid() != euid) {
fprintf(stderr,"Can't reswap uid and euid");
goto oops;
}
if (!cando(S_IXUSR,0,&statbuf)) { /* can real uid exec? */
fprintf(stderr,"Permission denied\n");
goto oops;
}
}
#endif /* HAS_SETREUID */
if (!S_ISREG(statbuf.st_mode)) {
fprintf(stderr,"Permission denied");
goto oops;
}
if (statbuf.st_mode & S_IWOTH) {
fprintf(stderr,"Setuid/gid script is writable by world");
goto oops;
}
if (!fgets(buf,sizeof buf, oldfp) ||
strncmp(buf,"#!",2) != 0 ) { /* required even on Sys V */
fprintf(stderr,"No #! line");
goto oops;
}
s = buf+2;
if (*s == ' ') s++;
while (!isspace(*s)) s++;
if (strncmp(s-4,"perl",4) != 0 && strncmp(s-9,"perl",4) != 0) { /* sanity check */
fprintf(stderr,"Not a perl script");
goto oops;
}
while (*s == ' ' || *s == '\t') s++;
/*
* #! arg must be what we saw above. They can invoke it by
* mentioning suidperl explicitly, but they may not add any strange
* arguments beyond what #! says if they do invoke suidperl that way.
*/
len = strlen(validarg);
if (strncmp(s,validarg,len) != 0 || !isspace(s[len])) {
fprintf(stderr,"Args must match #! line");
goto oops;
}
if (euid) { /* oops, we're not the setuid root perl */
(void)fclose(oldfp);
fprintf(stderr,"Can't do setuid\n");
goto oops;
}
if (!cando(S_IXUSR,1,&statbuf)) {
fprintf(stderr,"Permission denied\n"); /* they can't do this */
goto oops;
}
}
else {
fprintf(stderr,"Script is not setuid/setgid in suidperl\n");
goto oops;
}
/* end of code borrowed from suidperl */
fclose(oldfp);
fclose(newfp);
/* Now copy the rest of the arguments. */
#if !defined(HAS_SETREUID) && !defined(HAS_SETRESUID)
/* We've lost ruid and rgid, so force tainting. */
if (strncmp(perlname, "perl5", 5) == 0) {
newargv[new++] = "-T"; /* no taintperl in Perl 5 */
}
#endif