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
##
##