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

From the pod:

This program 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<execve> with an empty environment, or the defined environment given via the --env parameter.
To function properly a working gcc must be installed.
Only root 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.

perl ./generate_runas_exe.pl -e /usr/bin/sleep -u 1001 -g 1001 -d -o m +ytest -- 20
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'.

In real life you would use such a wrapper to start a chrooted server:

/usr/bin/chroot /chroot/jail /bin/mywrapper

#!/usr/bin/perl # # Author : Tom Regner <t.regner@librics.de> # copyright : (c) 2007 webmaster.programm GmbH, Hannover, Germany # (c) 2007 Tom Regner <cpan@tomsdiner.org> # # 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 - exitin +g"); @@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 <sys/types.h> #include <unistd.h> #include <stdlib.h> #include <stdio.h> 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 <sys/types.h> #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <fcntl.h> 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 - exit +ing\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 - pr +oceeding 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, $dontstr +ip, $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 non +theless - 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 accou +nts)\n"; pod2usage( -verbose => 0, -exitval => 1 ); } if ($tdir && ($tdir =~ m!\.\.! || $tdir !~ m!/[a-z].*!)) { print "--dir <path> 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> 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 ou +tfile 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 w +rite 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 w +ith 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/sta +rting process prior to executing the wrapped program. =item B<--pidfile path> If C<--daemonize> is given, the generated wrapper will write the forke +d childs pid to C<path>, after it dropped to the unprivileged user/gr +oup. Attention: The written pidfile is not deleted after execution of the w +rapped program ends - if you use a pidfile, you will have to take car +e 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 ex +ecuting the wrapped program =item B<--nostrip> Don't strip the resulting executable; default is to strip the executab +le. =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_e +xe.pl options seperated with a double-dash '--'. =head1 DESCRIPTION B<This program> will generate and compile a little c-program that must + be run as root, and if run switches to unpriviliged user and group i +ds and subtitutes itself with another program via C<execve> with an e +mpty environment, or the defined environment given via the --env para +meter. To function properly a working C<gcc> must be installed. Only C<root> 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<perl ./generate_runas_exe.pl -e /usr/bin/sleep -u 1001 -g 1001 + -d -o mytest -- 20> to test the generator this command will generate a wrapper that ru +ns '/usr/bin/sleep 20' as user 1000 in the background. Run the wrappe +r 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 <t.regner@libri +cs.de> or <cpan@tomsdiner.org> =head1 AUTHOR Tom Regner <t.regner@librics.de> =head1 COPYRIGHT Copyright (c) 2007 webmaster.programm GmbH, Hannover, Germany Copyright (c) 2007 Tom Regner <cpan@tomsdiner.org> =head1 LICENSE This program is free software; you can redistribute it and/or modify i +t 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 yo +ur option) any later version. This program is distributed in the hope that it will be useful, but WI +THOUT ANY WARRANTY; without even the implied warranty of MERCHANTABIL +ITY 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 t +he Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, + USA. =cut # vim: sw=4 ts=4 et

regards,
tomte


An intellectual is someone whose mind watches itself.
-- Albert Camus