http://www.perlmonks.org?node_id=685000

5mi11er has asked for the wisdom of the Perl Monks concerning the following question:

Hi all,

I've had a lengthy hiatus from perl monks, but I've been lurking around off and on in the last few months.

Anyway, I've recently been forced to actually learn about sudo; I've known it existed, but didn't want/need to take the time to learn about it. (I've lived as root for most of my linux life)

But now at a client site, they're in dire need of being able to script making administrative tasks across their servers. So, I've created a normal user "admin" account which is able to ssh via pki (no password needed) across all the servers.

During my investigation of sudo, I've come to realize just how easy it is to allow too much access that would in turn allow pretty trivial exploits to gain full root access. Simply allowing any copy, or move functionality pretty much grants root access. But without that functionality, it's impossible to allow the admin user the ability to do many tasks we want to be able to automate.

Now admittedly, we've already give the user relatively easy exploits by granting them the ability to install software via yum and rpm. But those entail at least some additional non-trivial work to accomplish the task, so we're willing to allow that vulnerability. But I think it's possible to create a semi-secure perl script that would allow this user the ability to copy to a restricted list of files.

Here's a run down of what I think the script needs do:

o Allow only simple file copies, no recursive/directory copies allowed.
o No symbolic links allowed, more on this later
o Check a root owned file for a list of valid targets
    To allow or not allow perl regexes to define valid targets?  Not convinced either way yet.
Given two user supplied file path names, we need to potentially de-obfuscate the names. I've done this already in other scripts I've written by simply making the file system do the work for me. I use basename to grab the path, and then attempt to CD into that path, then get the CWD. Presto, a de-obfuscated path.
sub get_actual_dir { my ($path) = @_; if(chdir "$path") { # CDing into a directory and then calling /bin/pwd should norm +alize whatever # strange input might be given to us by the user. my $actual_dir = `/bin/pwd`; chomp $actual_dir; return $actual_dir; } else { die "Can't cd into <$path>: $!\n"; } }
This helps serve the "no symbolic links" restriction, as any de-obfuscated path should not have any symbolic links within the path. Which means I only have to check if the actual "file" being referenced is a symbolic link itself, a pretty easy test.

So, are there any big "gotcha's" in this scheme that I've missed? Obviously the files available to modify have to be carefully considered; /etc/passwd /etc/shadow any /etc/cron* areas are all extremely dangerous...

Thanks in advance,

-Scott

Replies are listed 'Best First'.
Re: semi secure sudo script to allow restricted copy ability
by moritz (Cardinal) on May 06, 2008 at 17:06 UTC
    I think that your approach is coming from the wrong direction, and will be very hard to get right. Access control is the job of the operating system, and should stay there.

    A better approach might be a fine graded access control scheme. Maybe ACLs can help you, and may selinux can help you. (In both cases it will still be hard to get right, but not that hard).

      Access control is the job of the operating system, and should stay there.
      - Point taken; you are correct. However sudo's job is specifically to get around those restrictions...

      Thus far, I've been able to ignore the fact that ACL's are available under linux, and the client has a history (policy?) of not installing selinux, so, I have no experience with either of those yet. But, yes, it would be a good idea for me to learn about them.

      Thanks,

      -Scott

        - Point taken; you are correct. However sudo's job is specifically to get around those restrictions...

        ... which is why you have to be extremely careful with sudo. You either allow only trusted users to use sudo (which is a very common case if you have multiple admin accounts) or you only allow a handful of select programs, presumably programs that you know very well.

        Unix has just one line of defense between between malicious local users and system administration: file permissions. These file permissions (and the code that checks them) have been developed and improved for over a decade. To think you can do better in a short perl script is a good example of hubris - in this case a very dangerous case.

        If your script allows an attacker to break that single line of defense, it will be easy to break the rest of your system. That's why you shouldn't try to circumvent file permissions, but adapt them to your needs.

        This was just a small rant to convince you not to do anything foolish with sudo. If you are still convinced that you want to use sudo, the usual hints apply (most of them are usually quoted in the context of web applications; the perl specific can be found in perlsec): use strict input checking. Use taint mode. Use whitelisting istead of blacklisting. Test your restrictions.

Re: semi secure sudo script to allow restricted copy ability
by pc88mxer (Vicar) on May 06, 2008 at 17:28 UTC
    Can you write a script for every admin task? adduser, deluser, ..., web_server [start|stop|clean_logs|...], etc. Then you can use sudo's configuration file to enable only those commands.
      That's sort of the direction I'm headed.

      My real world problem set ATM is this:

       o Nearly 100 servers, and growing fast due to virtual machines
       o I'm still implementing LDAP authentication across them all
          o adduser and the like not needed, auto create home directories on first login.
          o removing user home directories will become a problem eventually
       o Recently added ldap replication, need to add this new server the ldap.conf files
       o Recently added a local yum repository, need to fix the yum repository files
       o want modified standard config files like ntp.conf, /etc/profile.d/colorls.sh for instance
       
      -Scott
Re: semi secure sudo script to allow restricted copy ability
by 5mi11er (Deacon) on May 07, 2008 at 15:20 UTC
    Ok, I've actually investigated ACL's under unix now. Using them will definitely solve the problem I have. But, while investigating that, I've run into boxes where you have to specifically say you want ACL's supported, and thus must remount the partitions. FWIW, a quick check of 3 of our machines shows that CentOS 4.x doesn't natively support them, but CentOS 5.x does.

    For those that were like me and resisting utilizing ACL's, there are two main commands to learn: getfacl and setfacl. A test session: As root do this

    echo "This is a test file" > /tmp/test.file
    chmod 640 /tmp/test.file
    setfacl -m u:admin:rw /tmp/test.file
    setfacl -m g:users:r  /tmp/test.file
    
    Now the admin user has the ability to edit /tmp/test.file and anyone in the users group can read it.

    A normal 'ls -alF' shows that there's an acl attached to the file; notice the plus sign at the end of the permissions list, and following that, we see what getfacl says about the file.

    $ ls -alF /tmp/test.file
    -rw-r-----+ 1 root root 161 May  7 09:35 /tmp/test.file
    
    $ getfacl /tmp/test.file 
    getfacl: Removing leading '/' from absolute path names
    # file: tmp/test.file
    # owner: root
    # group: root
    user::rw-
    user:admin:rw-
    group::r--
    group:users:r--
    mask::rw-
    other::---
    
    On a machine where ACL's aren't natively supported yet, when attempting to set the ACL, you'll get this:
    $ setfacl -m u:admin:rw /tmp/test.file
    setfacl: test.file: Operation not supported
    
    This page states that
    For ACLs to work you have to mount whatever partition you want with the option acl. As an example, notice [the partition] /home [from /etc/fstab]:
    LABEL=/     /     ext3 defaults 1 1
    LABEL=/boot /boot ext3 defaults 1 2
    LABEL=/home /home ext3 rw,acl 1 2
    

    -Scott

Re: semi secure sudo script to allow restricted copy ability
by 5mi11er (Deacon) on May 07, 2008 at 18:16 UTC
    Well, since I won't be able to reboot some of the machines for the foreseeable future, I just went ahead and banged this thing out. Only minor testing has been done thus far, and it doesn't handle all the potential special characters that a filename could possibly have, but it does handle spaces and dollar signs.

    Maybe this will help someone else someday.

    #! /usr/bin/perl # ==================================================================== +===== # sudo-copy - perl script to allow semi-secure file copying ability +for # sudo. Giving pretty much any copy rights to an sudoer makes it triv +ial # for the sudoer to escalate to full root permissions. # # This script attempts to make it difficult to carry out a copy operat +ion # exploit to full root access. # ==================================================================== +===== # Task Description: # De-obfuscate the source and destination files # No recursive or directory copies # No symbolic links allowed in the source or destination # Check /etc/sudo-copy.conf file for valid target list # If all rules pass, allow the copy to happen. # ==================================================================== +===== # Written by Scott L. Miller # # Born on date: 05/08/2008 use strict; use warnings; use File::Basename; use IO::Tee; use Getopt::Std; use vars qw($opt_h $LOG); getopts('h'); sub usage { print <<ENDUSAGE; $0 [-h] <source> <destination> Allow restricted copy operations to suduoers. Why? Unrestricted copy operations make it trivial for any sudoer to escalate to full root permissions. The more restricted the copy operations are, the harder it is to exploit. Output goes both to STDOUT and /var/log/sudo-copy.log file. Options: -h -h or no parameters will print this message. ENDUSAGE exit 1; } my $argc = scalar(@ARGV); usage() if $opt_h || not $argc; die "Need one source and one destination\n" if ($argc != 2); my $logfile = "/var/log/sudo-copy.log"; $|=1; my ($starttime, $startdate) = GetTime(); $LOG = IO::Tee->new(">> $logfile", \*STDOUT) or die "Error during IO::Tee->new\n $!\n"; print $LOG "========================================================== +=======\n"; print $LOG "Started @ $startdate $starttime\n"; sub get_actual_path { my ($path) = @_; if(chdir "$path") { # CDing into a directory and then calling /bin/pwd sho +uld normalize whatever # strange input might be given to us by the user. my $actual_dir = `/bin/pwd`; chomp $actual_dir; return $actual_dir; } else { print $LOG "Can't cd into <$path>: $!\n"; print $LOG "Copy failed.\n"; die; } } my ($src, $dst) = @ARGV; my ($srcFileName, $srcPath, $srcFileExt, $dstFileName, $dstPath, $dstF +ileExt); ($srcFileName, $srcPath, $srcFileExt)=fileparse($src); ($dstFileName, $dstPath, $dstFileExt)=fileparse($dst); $srcPath = get_actual_path($srcPath); $dstPath = get_actual_path($dstPath); $src = $srcPath.'/'.$srcFileName.$srcFileExt; $dst = $dstPath.'/'.$dstFileName.$dstFileExt; if($src =~ /\$/) { $src =~ s/\$/\\\$/g; #escape the dollar signs } if($dst =~ /\$/) { $dst =~ s/\$/\\\$/g; #escape the dollar signs } foreach ($src,$dst) { lstat($_); if ( -l _ ) { print $LOG "Error: symbolic links not allowed\ +n"; print $LOG "Copy failed.\n"; die; } if ( -d _ ) { print $LOG "Error: directory copies not allowe +d\n"; print $LOG "Copy failed.\n"; die; } if ( -e _ && ! -f _ ) { print $LOG "Error: file is not a plain file\n" +; print $LOG "Copy failed.\n"; die; } } if(check_valid_destination($dst)) { my ($dev,$ino,$mode,$nlink,$uid,$gid) = lstat($dst); dosystem("cp -f \"$src\" \"$dst\""); if($uid ne '') { dosystem("/bin/chown $uid.$gid \"$dst\""); chmod $mode, $dst; } print $LOG "Copy complete.\n"; } else { print $LOG "Copy not allowed\n"; } my ($endtime, $enddate) = GetTime(); print $LOG "Finished @ $enddate $endtime\n"; exit; sub check_valid_destination { my $fname = $_[0]; my $cfgfile = '/etc/sudo-copy.conf'; open (CFGFILE, $cfgfile) || die "Couldn't open \"$cfgfile\"\n $!\n"; while(<CFGFILE>) { s/(^\s+)//; #remove indentation if any if(/^$/) { next; } #ignore blank lines if(/^#/) { next; } #ignore comments chomp; if (eval "\'$fname\' =~ $_") { close CFGFILE; return 1; } } close CFGFILE; return 0; } sub dosystem { my $rc = system(@_) & 0xFFFF; if ($rc == 0) { return; } if ($rc == 0xff00 ) { print $LOG "Command [@_] failed: $!\n"; exit 0; } if ($rc > 0x80) { $rc >>= 8; print $LOG "Command [@_] ran with exit code $rc\n"; return; } if ($rc & 0x80) { printf $LOG "Core dumped from signal %d\n", $rc & 0x7 +F; exit 0; } else { printf $LOG "Interrupted by signal %d\n", $rc & 0x7F; exit 0; } print $LOG "Should never be able to get here\n"; } sub numerically { no warnings; $a <=> $b; } sub GetTime { my ($sec,$min,$hr,$mday,$mon,$yr,$wday,$yday,$isdst) = localti +me(time()); my $date = sprintf("%02d/%02d/%04d",$mday,$mon+1,$yr+1900); my $time = sprintf("%02d:%02d:%02d",$hr,$min,$sec); return ($date, $time); }
    Example /etc/sudo-copy.conf
    # A list of perl regexes of files that sudo-copy is allowed # to copy to (the destination) #test entry m#/tmp/test.*# #ntp.conf should be relatively safe m#/etc/ntp\.conf# #ldap.conf could be exploited if an attacker created a new ldap instan +ce m#/etc/ldap\.conf# #resolv.conf could be exploited in many subtle ways (new ldap instance +, yum, etc) m#/etc/resolv\.conf# #yum could be exploited if an attacker created a new yum repository m#/etc/yum\.conf# m#/etc/yum\.repos\.d/.*#
    -Scott