perl ./generate_runas_exe.pl -e /usr/bin/sleep -u 1001 -g 1001 -d -o mytest -- 20 #### /usr/bin/chroot /chroot/jail /bin/mywrapper #### #!/usr/bin/perl # # Author : Tom Regner # copyright : (c) 2007 webmaster.programm GmbH, Hannover, Germany # (c) 2007 Tom Regner # # Version: XXX # use strict; use warnings; use Scalar::Util qw(looks_like_number); use File::Temp qw(:mktemp); use Getopt::Long; use Pod::Usage; ## # constants use constant CODE_TEMPLATE_CD => ' @@WS@@if (chdir("@@DIR@@\0") != 0) { @@WS@@ fprintf(stderr, "couldn\'t change working directory - exiting"); @@WS@@ exit(5); @@WS@@} '; use constant CODE_TEMPLATE_SAVEPID => ' @@WS@@save_pid(pid, "@@PIDFILE@@\0"); '; # simply execve the wrapped program use constant CODE_TEMPLATE_EXEC => ' #include #include #include #include void print_help(void); int main(int argc, char** argv) { if (getuid() != 0) { fprintf(stderr, "only root can execute %s - exiting\n", argv[0]); exit(1); } if (argc > 1) { print_help(); exit(0); } else { if (setgid(@@GID@@) == -1) { fprintf(stderr, "couldn\'t switch groupid - exiting\n"); exit(2); } if (setuid(@@UID@@) == -1) { fprintf(stderr, "couldn\'t switch userid - exiting\n"); exit(2); } @@CHDIR@@ char *prog = "@@EXE@@\0"; char *args[@@ARGC@@ + 2]; args[0] = "@@EXE@@\0"; @@ARGS@@ args[@@ARGC@@ + 1] = NULL; char* eenv[@@ENVC@@ + 1]; @@ENV@@ eenv[@@ENVC@@] = NULL; return execve(prog, args, eenv); } } void print_help(void) { fprintf(stdout, "executes \'@@EXE@@ @@ARG_LIST@@\' with user id \'@@UID@@\'\nenvironment \'@@ENV_LIST@@\'\n"); } '; # code to use for a forking daemon use constant CODE_TEMPLATE_DAEMON => ' #include #include #include #include #include void print_help(void); void daemonize(void); void save_pid(pid_t pid,char *pid_file); int main(int argc, char** argv) { pid_t pid; if (getuid() != 0) { fprintf(stderr, "only root can execute %s - exiting\n", argv[0]); exit(1); } if (argc > 1) { print_help(); exit(0); } else { if (setgid(@@GID@@) == -1) { fprintf(stderr, "couldn\'t switch groupid - exiting\n"); exit(2); } if (setuid(@@UID@@) == -1) { fprintf(stderr, "couldn\'t switch userid - exiting\n"); exit(2); } pid = fork(); if (pid == 0) { @@CHDIR@@ daemonize(); char *prog = "@@EXE@@\0"; char *args[@@ARGC@@ + 2]; args[0] = "@@EXE@@\0"; @@ARGS@@ args[@@ARGC@@ + 1] = NULL; char* eenv[@@ENVC@@ + 1]; @@ENV@@ eenv[@@ENVC@@] = NULL; return execve(prog, args, eenv); } else if (pid == -1) { fprintf(stderr, "couldn\'t fork the wrapped process - exiting\n"); exit(3); } /* parent */ @@SAVE_PID@@ exit(0); } } void print_help(void) { fprintf(stdout, "executes \'@@EXE@@ @@ARG_LIST@@\' with user id \'@@UID@@\' in the background\nenvironment \'@@ENV_LIST@@\'\n"); } void daemonize(void) { int fd; if (setsid() == -1) { fprintf(stderr, "couldn\'t create new processgroup - exiting"); exit(4); } if ((fd = open("/dev/null", O_RDWR, 0)) != -1) { (void)dup2(fd, STDIN_FILENO); (void)dup2(fd, STDOUT_FILENO); (void)dup2(fd, STDERR_FILENO); if (fd > STDERR_FILENO) (void)close(fd); } else { fprintf(stderr, "couldn\'t redirect stdout, stderr, stdin - proceeding nontheless\n"); } } void save_pid(pid_t pid,char *pid_file) { FILE *fp; if (!pid_file) return; if (!(fp = fopen(pid_file,"w"))) { fprintf(stderr,"Could not open the pid file %s for writing\n",pid_file); return; } fprintf(fp,"%ld\n",(long) pid); if (fclose(fp) == -1) { fprintf(stderr,"Could not close the pid file %s.\n",pid_file); return; } } '; use constant REVISION => '6454'; #### # configuration and default values my ( $exe, $uid, $gid, $outfile, $tdir, $pidfile, $daemonize, $dontstrip, $verbose, $help, $man, @env ) = ( undef, undef, undef, undef, undef, undef, 0, 0, 0, 0, 0, ()); Getopt::Long::Configure("bundling"); GetOptions( "exe|e=s" => \$exe, "uid|u=i" => \$uid, "gid|g=i" => \$gid, "env=s" => \@env, "dir=s" => \$tdir, "outfile|o=s" => \$outfile, "daemonize|d" => \$daemonize, "nostrip|n" => \$dontstrip, "pidfile|p=s" => \$pidfile, "verbose|v+" => \$verbose, "help|h|?" => \$help, "man|m" => \$man, ) or pod2usage( -verbose => 0, -exitval => 1 ); pod2usage( -verbose => 1, -exitval => 0 ) if ($help); pod2usage( -verbose => 2, -exitval => 0 ) if ($man); my $compiler = `/usr/bin/which gcc`; chomp($compiler); my $strip = `/usr/bin/which strip`; chomp($strip); unless ($compiler =~ m!/[a-z0-9/]+/gcc!i) { print "no valid gcc found - exiting\n"; pod2usage( -verbose => 0, -exitval => 1 ); } unless($dontstrip || $strip =~ m!/[a-z0-9/]+/strip!i) { print "no valid strip found - use --nostrip if I shall proceed nontheless - exiting\n"; pod2usage( -verbose => 0, -exitval => 1 ); } unless ($exe && $exe =~ m!^/[a-z].*$! && $exe !~ m!\.\.!) { print "no exe to wrap given or invalid path (no ..!) - exiting\n"; pod2usage( -verbose => 0, -exitval => 1 ); } unless ($outfile && $outfile =~ m!^[a-z][a-z0-9_]+$!) { print "no filename to write to given - exiting\n"; pod2usage( -verbose => 0, -exitval => 1 ); } unless (looks_like_number($uid) && looks_like_number($gid) && $uid > 0 && $gid > 0) { print "uid and gid have to specified as numbers >0 (non root accounts)\n"; pod2usage( -verbose => 0, -exitval => 1 ); } if ($tdir && ($tdir =~ m!\.\.! || $tdir !~ m!/[a-z].*!)) { print "--dir path must be absolute - no .. allowed\n"; pod2usage( -verbose => 0, -exitval => 1 ); } elsif (!$tdir) { $tdir = "/"; # we always change the working directory # (just in case the wrapper runs in a chroot without --dir specified)! } if ($pidfile && ($pidfile =~ m!\.\.! || $pidfile !~ m!/[a-z].*!)) { print "--pidfile path must be absolute - no .. allowed\n"; pod2usage( -verbose => 0, -exitval => 1 ); } my @args = (); my $count = 1; foreach my $arg (@ARGV) { $arg =~ s/"/\\"/g; $arg =~ s/\0//g; push @args, "args[$count] = \"$arg\\0\";"; ++$count; } $count = 0; my @envc = (); foreach my $var (@env) { $var =~ s/"/\\"/g; $var =~ s/\0//g; push @envc, "eenv[$count] = \"$var\\0\";"; ++$count; } my $cd_source = CODE_TEMPLATE_CD; $cd_source =~ s/\@\@DIR\@\@/$tdir/; if ($daemonize) { $cd_source =~ s/\@\@WS\@\@/ /g; } else { $cd_source =~ s/\@\@WS\@\@/ /g; } my $pid_source = ''; if ($daemonize && $pidfile) { $pid_source = CODE_TEMPLATE_SAVEPID; $pid_source =~ s/\@\@WS\@\@/ /g; $pid_source =~ s/\@\@PIDFILE\@\@/$pidfile/g; } my $parameters = { '@@GID@@' => $gid, '@@UID@@' => $uid, '@@EXE@@' => $exe, '@@ARGC@@' => scalar @ARGV, '@@ENVC@@' => scalar @envc, '@@ENV@@' => join ("\n ", @envc), '@@ENV_LIST@@' => join ("; ", @env), '@@ARGS@@' => join ("\n ", @args), '@@ARG_LIST@@' => join (" ", @ARGV), '@@CHDIR@@' => $cd_source, '@@SAVE_PID@@' => $pid_source, }; if ($verbose) { print STDOUT "$0 - Revision " . REVISION . "\n"; print STDOUT "generating wrapper for '${exe}' \n"; print STDOUT "\tuser_id: '${uid}'\n"; print STDOUT "\tgroup_id: '${gid}'\n"; print STDOUT "\targuments: '" . $parameters->{'@@ARG_LIST@@'} . "'\n"; print STDOUT "\tenvironment: '" . $parameters->{'@@ENV_LIST@@'} . "'\n"; print STDOUT "\tworking dir: '" . $tdir . "'\n"; print STDOUT "\trun as daemon: '" . ($daemonize ? "true" : "false") . "'\n"; print STDOUT "\tpidfile: '" . $pidfile . "'\n" if ($daemonize && $pidfile); } my $source = ''; if ($daemonize) { $source = CODE_TEMPLATE_DAEMON; } else { $source = CODE_TEMPLATE_EXEC; } foreach my $par (keys %$parameters) { $source =~ s/$par/$parameters->{$par}/g; } my ($tmpfile, $tmpfilename) = mkstemps("greXXXXXX", '.c'); print $tmpfile $source; close($tmpfile); print STDOUT "source generated\n" if $verbose; my $status = system($compiler, $tmpfilename, '-o', $outfile); print STDOUT "$outfile compiled\n" if $verbose; if ($status == 0) { unlink($tmpfilename); $status = system($strip, $outfile) unless ($dontstrip); if ($status == 0) { print STDOUT "$outfile stripped\n" if $verbose; } else { print STDERR "couldn't strip $outfile\n"; } print STDOUT "$outfile generated\n"; } else { print STDOUT "couldn't generate $outfile - check $tmpfilename for c syntax errors\n"; } __END__ =head1 NAME generate_runas_exe.pl - generate wrapper programs, that start a fixed program with a fixed set of parameters as a fixed user =head1 SYNOPSIS generate_runas_exe.pl -e /path/to/command -u user_id -g group_id -o outfile ARG1 ARG2 ... Options: --exe|-e /path/to/command the command to execute --gid|-g id the group-id to switch to --uid|-u id the user-id to switch to --env NAME=value define environment variables to define for the wrapped program --outfile|-o filename the name of the file the executable is written to --nostrip|-n dont't strip the resulting executable --daemonize|-d create a daemonizing wrapper --pidfile|-p path if --daemonize is given, wrapper will write child pid to pidfile --verbose|-v be a bit more verbose --help|-h brief help message --man|-m full documentation =head1 OPTIONS =over 8 =item B<--exe program> The program to execute - must be an absolute path. The program is not required to exist on the machine you generate the wrapper at. =item B<--gid id> The generated wrapper switches to this group-id prior to executing the given program. If the wrapper can't switch to this id, it will exit with status 2. =item B<--uid id> The generated wrapper switches to this user-id prior to executing the given program. If the wrapper can't switch to this id, it will exit with status 2. =item B<--outfile filename> The name of the file the generated wrapper is written to =item B<--daemonize> If given the resulting wrapper will detach itself from the console/starting process prior to executing the wrapped program. =item B<--pidfile path> If C<--daemonize> is given, the generated wrapper will write the forked childs pid to C, after it dropped to the unprivileged user/group. Attention: The written pidfile is not deleted after execution of the wrapped program ends - if you use a pidfile, you will have to take care that it's removed properly when needed. =item B<--env NAME=value> With this option one can define an environment for the wrapped program. This option may be given multiple times. =item B<--dir path> Absolute path the wrapper changes the working directory to prior to executing the wrapped program =item B<--nostrip> Don't strip the resulting executable; default is to strip the executable. =item B<--verbose> Puts generate_runas_exe.pl in a kind of chatty mode. =item B<--help> Print a brief help message and exits. =item B<--man> Prints the manual page and exits. =back Supply the options for the wrapped programm after all generate_runas_exe.pl options seperated with a double-dash '--'. =head1 DESCRIPTION B will generate and compile a little c-program that must be run as root, and if run switches to unpriviliged user and group ids and subtitutes itself with another program via C with an empty environment, or the defined environment given via the --env parameter. To function properly a working C must be installed. Only C can excute the generated wrapper. Such a wrapper is especially useful to run servers that aren't capable to switch user id by themselves in a chroot environment. This generator shouldn't be present on a production server - use it on a staging machine to generate wrappers for the production systems. =head2 WRAPPER EXITCODES =over 8 =item C<0> ok =item C<1> only root can execute generated wrappers. =item C<2> the wrapper couldn't switch the user or the group id =item C<3> a daemonizing wrapper couldn't fork =item C<4> the forked process couldn't create a new session. =item C<5> couldn't change the working directory =back =head1 EXAMPLES =over 8 =item C to test the generator this command will generate a wrapper that runs '/usr/bin/sleep 20' as user 1000 in the background. Run the wrapper as root, and check your process table for 'sleep' =back =head1 TODO test and think. So far only tested on different Linux installations. =head1 BUGS So far only tested on different Linux installations. No real bugs known so far - comments and bugreports to or =head1 AUTHOR Tom Regner =head1 COPYRIGHT Copyright (c) 2007 webmaster.programm GmbH, Hannover, Germany Copyright (c) 2007 Tom Regner =head1 LICENSE This program is free software; you can redistribute it and/or modify it under the terms of the Perl Artistic License or the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. If you do not have a copy of the GNU General Public License write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. =cut # vim: sw=4 ts=4 et ####