Here's how to stop shell escapes from vi

Jean-Pierre Radley jpr at jpradley.uucp
Tue Oct 2 10:37:46 AEST 1990


What follows is the oeuvre of the late, lamented Fred Buck:


#! /bin/sh
# This is a shell archive, meaning:
# 1. Remove everything above the #! /bin/sh line.
# 2. Save the resulting text in a file.
# 3. Execute the file with /bin/sh (not csh) to create the files:
#	README
#	rvi.c
#	rvish.c
#	rvi_install
# This archive created: Tue Oct  4 20:35:03 1988
export PATH; PATH=/bin:$PATH
if test -f 'README'
then
	echo shar: will not over-write existing file "'README'"
else
sed 's/^X//' << \SHAR_EOF > 'README'
XBefore running the script 'rvi_install', read the introductory comments
Xin both 'rvi.c' and 'rvi_install'.  These comments will confer a feel
Xfor what this package is intended to do and how it does it.
X
XAs distributed, 'rvi' allows users to reach an interactive RESTRICTED
Xshell.  If you don't want this to happen, then remove the files 'rsh'
Xand 'sh' in the "rvi-directory".  If you need further customization,
Xread on:
X
X-------------------------------------------
X
XThe following material is intended for those who want or need to offer
Xrestricted-'vi' users more flexibility than is provided by the bare editor,
Xas for instance allowing access to specified filters to be used in
Xprocessing text within the editor:
X
XThere's something you should know about the way 'vi' handles the ":!"
Xand "!" shell escapes: the command you specify isn't invoked directly,
Xinstead 'vi' calls *shell* to execute the command, with the syntax
X
X	*shell* -c "<entire-command-line>"
X
XThe reason "*shell*" is enclosed in asterisks is that the shell we're
Xtalking about is 'vi's own internal variable "shell", which is set via the
X":set shell=whatever" command in 'vi'.  This internal variable overrides
Xthe environment variable $SHELL, although by default it starts out with
Xthe value of $SHELL.
X
XIf all you're interested in is letting a restricted user have a fullscreen
Xeditor, then no problem.  However, if you want your restricted user to
Xbe able to use 'vi's filtering capabilities, or if your restricted user
Xmust be able to run shell programs from within 'vi', then be aware that
Xincluded in this package is a counterfeit shell, called "./sh", that
Xinvokes "./rsh" with the same arguments as were passed to "./sh".  The
Xeffect of all this is that a user's "!-escape" commands are executed
Xas if they had been issued to /bin/rsh with the "-c" option-switch,
Xalways.  The user can't avoid the invocation of 'rsh' by his own action.
XHowever, if the user's command is the name of a shell script, 'rsh' will
Xexecute the script as if it were /bin/sh; be aware of this.  If it all
Xseems like doubletalk, read up on rsh(C) [or perhaps rsh(1)] and sh(C)
X[sh(1)].
X
XThe main thing to remember here is that if you absolutely, positively,
Xwant to bar a restricted user from access to any sort of interactive
Xshell, even the restricted shell, then you should remove the supplied
Xprogram "sh" and undo the link or copy of "rsh" into the "rvi-directory".
XIf you do this, then however the user won't be able to use 'vi's ability
Xto filter text through external commands.
SHAR_EOF
chmod +x 'README'
fi # end of overwriting check
if test -f 'rvi.c'
then
	echo shar: will not over-write existing file "'rvi.c'"
else
sed 's/^X//' << \SHAR_EOF > 'rvi.c'
X/* File: rvi.c
X * 	
X * 	Name:
X * 		rvi - restricted fullscreen text editor
X * 	Syntax:
X * 		rvi [filename]
X * 	
X * 	Description:
X * 	'rvi' remedies various security holes in the 'vi' fullscreen
X * 	text editor.  In 'vi', for example, it's impossible to prevent a
X * 	user from invoking an interactive shell if the user chooses to
X * 	do so, even if the user's running 'rsh' and the variable "SHELL"
X * 	is set to "/bin/rsh" and is read-only, because 'vi' maintains
X * 	its own internal "shell" variable and executes it via the ":sh"
X * 	command.  'rvi' encapsulates the edit within a defined
X * 	sub-universe within the filesystem so that the facilities
X * 	accessible to the user are entirely within the control of the
X * 	system administrator.
X * 	
X * 	Mechanics/Implementation:
X * 	'rvi' is invoked from a normal current-working-directory the
X * 	same way that 'vi' would be, except that only one filename per
X * 	invocation is acceptable.  If a filename is specified, and if
X * 	the specified filename exists and is writable by the user, or if
X * 	the specified filename doesn't exist but the current working
X * 	directory is writable by the user, 'rvi' links that filename to
X * 	a temporary file within an "rvi-directory" (see below) and forks
X * 	a child to edit the temporary file.  The child chroot()'s to the
X * 	"rvi-directory", so that nothing on the system outside that
X * 	directory is further accessible and overlays itself with 'vi' to
X * 	perform the edit.  Programs available to the user are only those
X * 	selected by the system administrator and linked (or copied) into
X * 	the "rvi-directory" sub-hierarchy; in particular, interactive
X * 	shells are inaccessible to the user unless /bin/sh (or some
X * 	other shell) is/are linked (or copied) into the sub-hierarchy.
X * 	
X * 	If no filename is specified, 'rvi' creates a file called
X * 	"btxtNNNNN" in the current directory, where "NNNNN" is the
X * 	process ID number of the user's process.  If that file has
X * 	nonzero length on exit, the user is prompted for a new name for
X * 	the file.  The ":f" ex-escape command is available to the user
X * 	from within 'vi', but is ineffectual if the "rvi-directory" is
X * 	secured as the accompanying script 'rvi_install' secures it.
X * 	Essentially the user is restricted to writing his specified edit
X * 	file and no other file.
X * 	
X * 	The "rvi-directory" is an existing directory previously
X * 	established by the system administrator, a directory that will
X * 	mimic the root directory "/" during the edit.  This directory
X * 	must contain certain essential subdirectories, 'bin', 'tmp',
X * 	'etc', within which certain system files must be linked or
X * 	copied.  The accompanying script 'rvi_install' creates the
X * 	"rvi-directory" hierarchy.
X * 	
X * 	'rvi' looks first for an environment variable RVIDIR containing
X * 	the pathname of the "rvi-directory"; if no such variable exists,
X * 	'rvi' looks for a pathname in the file "/etc/default/rvi" for
X * 	the "rvi-directory"; if no such pathname exists in that file, or
X * 	if that file doesn't exist, 'rvi' assumes that the
X * 	"rvi-directory" is "/rvitmp".
X * 	
X * 	Copyright:
X * 	
X * 	This program was written by Fred Buck, who hereby releases it
X * 	into the public domain and expressly relinquishes all copy- or
X * 	other-rights regarding it.  This code is public property.  However,
X * 	the author disclaims liability for any use of this code.
X * 	
X * 	NOTES/BUGS
X * 	
X * 	The filename displayed on the 'vi' status line will always be
X * 	the name of the temporary file within the "rvi-directory",
X * 	regardless of whether 'rvi' is invoked with or without a
X * 	filename.  Users should be warned about this.  If the filename
X * 	is changed with the ":f" ex-escape command, the user will be
X * 	unable to write the results of his or her edit unless the
X * 	filename is changed back to the name of the temporary file;
X * 	users should be alerted to the ":f #" command to recover from
X * 	such a circumstance, or else the system administrator may
X * 	consider mapping an unused command key to the sequence ":f #^M"
X * 	(see vi(1) or vi(C) for details).
X * 	
X * 	The shell variable TERM must be properly set before 'rvi' is
X * 	called, or else the behavior of 'vi' will be erratic.  'rvi' is
X * 	effective as well with 'ex' as with 'vi'; to call 'ex'
X * 	specifically, change "vi" to "ex" in the code below, and link
X * 	"/bin/ex" into the "rvi-directory".
X * 	
X * 	As noted above, the "rvi-directory" MUST reside on the same
X * 	device as the user's current working directory.
X * 	
X * 	On SCO Xenix-286, DO NOT USE THE OPTIMIZING COMPILER SWITCH
X * 	"-O" when compiling 'rvi'.  The optimizer on SCO Xenix-286 is
X * 	less than perfect, and chokes on this program.
X */
X
X#include <stdio.h>
X#include <sys/types.h>
X#include <sys/stat.h>
X#include <signal.h>
X
X#define BOZODIR		"/rvitmp"		/* default rvi-directory */
X#define READ		04
X#define WRITE		02
X#define EXECUTE		01
X#define OWNER		0100
X#define GROUP		010
X#define OTHER		01
X#define HIBYTE(x)	(((x) >> 8) & 0xff)
X#define LOBYTE(x)	((x) & 0xff)
X#define WORD(x)		((x) & 0xffffL)
X
Xstatic struct stat buf;
Xstatic char *progname;
X
X/* void Abort(): terminates execution with an error message.
X * 
X * returns: nothing
X */
XAbort(string1, string2)
Xchar *string1, *string2;
X{
X	fprintf(stderr,"%s: ",progname);
X	fprintf(stderr,string1,string2);
X	exit(1);
X}
X
X/* int perms_ok(): checks read, write or execute permission for real
X *   user or group ID
X *
X * returns: 1 if specified permission exists, 0 otherwise
X */
Xperms_ok(statbuf, mode)
Xstruct stat *statbuf;	/* stat() buffer for file or directory */
Xint mode;		/* READ, WRITE, or EXECUTE */
X{
X	if (mode != READ && mode != WRITE && mode != EXECUTE)
X		return(0);
X	else if (getuid() == 0)
X		return(1);
X	else if ( statbuf->st_uid == getuid()
X	     &&  (statbuf->st_mode & (mode * OWNER)) )
X		return(1);
X	else if ( statbuf->st_gid == getgid()
X	     &&  (statbuf->st_mode & (mode * GROUP)) )
X		return(1);
X	else if ( statbuf->st_mode & (mode * OTHER) )
X		eturn(1);
X	else return(0);
X}
X
X/* main()
X */
Xmain(argc, argv)
Xint argc; char **argv;
X{
X	FILE *defalt;
X	char c, *bozodir;
X	int unnamed, newfile;
X	int kidstatus, kidpid;
X	static char tempname[80], filename[15];
X	static char lastchance[80] = "/rvitmp";
X	extern char *getenv(), *strchr(), *strrchr(), *strncpy();
X	extern FILE *fopen();
X
X	newfile = unnamed = 0;
X	progname = argv[0];
X
X	/* establish which directory to use */
X	if ((bozodir=getenv("RVIDIR"))!=0)
X		;
X	else if ((defalt=fopen("/etc/default/rvi","r")) != (char *)0) {
X		fgets(lastchance,80,defalt);
X		*(strchr(lastchance,'\n')) = '\0';
X		bozodir = lastchance;
X		if (*bozodir == '\0') bozodir = BOZODIR;
X		fclose(defalt);
X	}
X	else bozodir = BOZODIR;
X
X	/* check permissions and initialize stuff */
X	if (argc == 1) {	/* no edit file specified */
X		sprintf(filename,"btxt%d",getpid());
X		newfile = unnamed = 1;
X	}
X	else {			/* file specified */
X		if (argc > 2)
X			Abort("only one filename allowed\n","");
X		if (strchr(argv[1],'/'))
X			Abort("bad filename '%s' (not a basename)\n",argv[1]);
X		if (strlen(argv[1]) > 14)
X			Abort("bad filename '%s' (too long)\n",argv[1]);
X		strncpy(filename,argv[1],14);
X		if (stat(filename,&buf)==0) {	/* file exists */
X			if (!perms_ok(&buf,WRITE))
X				Abort("can't write file '%s'\n",filename);
X		}
X		else newfile = 1;		/* file doesn't exist */
X	}
X
X	if (newfile) {
X		if (stat(".",&buf) < 0)
X			Abort("can't stat working directory\n","");
X		if (!perms_ok(&buf,WRITE))
X			Abort("can't write in working directory\n","");
X		/* let a kid create the file with the user's real uid/gid  */
X		/* (we could create it ourself as 'root', and chown(), but */
X		/* this ensures that the creat() succeeds or fails by the  */
X		/* user's own real permissions).                           */
X		if ((kidpid=fork())==0) {
X			if (setgid(getgid())<0 || setuid(getuid())<0)
X				exit(1);
X			else
X				exit(creat(filename,0644)<0);
X		}
X		wait(&kidstatus);
X		if ( kidpid < 0
X		||   LOBYTE(kidstatus) != 0
X		||   HIBYTE(kidstatus) != 0 )
X			Abort("internal error (filename create)\n","");
X	}
X
X	/* link the file-to-edit to a made-up name in the temp directory */
X	sprintf(tempname,"%s/btxt%d",bozodir,getpid());
X	if (link(filename,tempname)<0) {
X		fprintf(stderr,"%s: ",argv[0]);
X		perror("system error");
X		Abort("internal error (link failure)\n","");
X	}
X
X	/* fork a kid to do the edit */
X	if ((kidpid=fork())==0) {		/* this is the kid */
X		if (chdir(bozodir)<0)
X			Abort("internal error (chdir failure)\n","");
X		if (chroot(".")<0)
X			Abort("internal error (chroot failure)\n","");
X		if (setgid(getgid())<0 || setuid(getuid())<0)
X			Abort("internal error (setgid/setuid)\n","");
X		execl("vi","vi",strrchr(tempname,'/')+1,0);
X		exit(1);				/* exec() failed */
X	}
X	else {				/* this is the parent */
X		signal(SIGINT,SIG_IGN); signal(SIGQUIT,SIG_IGN);
X		wait();
X	}
X
X	/* the edit's over now, take care of supplementary housekeeping */
X	link(tempname,filename);		/* just in case */
X	unlink(tempname);			/* don't need it anymore */
X	if (stat(filename,&buf)<0)		/* weird case: file gone */
X		exit(1);
X	if (buf.st_size==0) {			/* if filesize zero,     */
X		if (newfile)			/*  kill it if new file, */
X			unlink(filename);
X		exit(0);			/*  else just bail out   */
X	}
X		/* if the file's unnamed (i.e., "btxtNNNNN"), name it */
X	if (kidpid > 0 && unnamed) {
X		printf("\nThe current filename is '%s'.\n",filename);
X		printf("Do you want to rename it? ");
X		c = getchar();
X		while (getchar()!='\n');
X		if (c!='y' && c!='Y')
X			exit(0);
X		else {
X			printf("New filename: ");
X			fgets(tempname,15,stdin);
X			*(strchr(tempname,'\n')) = '\0';
X			if (strlen(tempname)==0) exit(0);
X			link(filename,tempname);
X			unlink(filename);
X		}
X	}
X	exit(0);
X}
SHAR_EOF
chmod +x 'rvi.c'
fi # end of overwriting check
if test -f 'rvish.c'
then
	echo shar: will not over-write existing file "'rvish.c'"
else
sed 's/^X//' << \SHAR_EOF > 'rvish.c'
X/* File: rvish.c
X * 	
X * 	syntax: sh [arguments]
X *
X * Description:
X * This program is intended solely for use as a dummy shell in
X * connection with the 'rvi' restricted fullscreen editor.  All it does
X * is convert its arguments into arguments for 'rsh', the restricted
X * shell.
X *
X * It should reside within the top level of the "rvi-directory".
X */
X
Xmain( argc, argv )
Xint argc; char **argv;
X{
X	*argv = "rsh";
X	execv(*argv,argv);
X}
SHAR_EOF
chmod +x 'rvish.c'
fi # end of overwriting check
if test -f 'rvi_install'
then
	echo shar: will not over-write existing file "'rvi_install'"
else
sed 's/^X//' << \SHAR_EOF > 'rvi_install'
X:
X# rvi_install: installation script for 'rvi' (must be run as 'root').
X#
X# READ THE FOLLOWING CAREFULLY:
X# This script is invoked as "sh rvi_install <rvi-parent-directory>" where
X# "<rvi-parent-directory>" is an existing directory underneath which you
X# want the "rvi-directory" to reside.  If you want the "rvi-directory" to
X# be called something other than "rvitmp", change the definition of
X# "rvidir" below.  To install the "rvi-directory" as a sub-directory
X# of your current directory, use "sh rvi_install ."; to install it as
X# a sub-directory of some other directory, use "sh rvi_install <otherdir>"
X# where "<otherdir>" is the name of that other directory.  Note that
X# for a user to run 'rvi', there must be an "rvi-directory" on the device
X# or partition where the user's current directory is located, and if
X# this "rvi-directory" isn't the one named in /etc/default/rvi, then
X# the user's environment variable $RVIDIR must be set to the name of
X# the "rvi-directory" on the user's local device or partition.
X#
X# Additional programs that you want to make accessible from within 'rvi'
X# should be linked or copied into either the rvi-directory, or the
X# 'bin' subdirectory of the rvi-directory.  You must not have removed
X# the distributed files 'rvi' or 'sh' in the "rvi-directory" if you want
X# users to be able to reach external programs.
X#
X
Xif [ $# -ne 1 ]; then
X	echo "$0: usage: $0 <rvi-parent-directory>" 1>&2
X	exit 1
Xfi
XRVIPRT=$1
Xrvidir="$RVIPRT/rvitmp"
X
Xif [ ! -f "rvi.c" ]; then
X	echo "$0: can't find file 'rvi.c'" 1>&2
X	echo "$0: aborting..." 1>&2
X	exit 1
Xelse
X	echo "$0: compiling 'rvi.c'....."
X	if cc -o rvi rvi.c; then
X		chown root rvi; chmod u+s rvi; chmod a+x rvi;
X		if [ $? -ne 0 ]; then
X			echo "$0: can't resecure 'rvi'; aborting..." 1>&2
X			exit 1
X		fi
X	else
X		echo "$0: got a problem compiling 'rvi.c'" 1>&2
X		exit 1
X	fi
Xfi
Xif [ -f "rvish.c" ]; then
X	echo "$0: compiling 'rvish.c'; this will eventually become"
X	echo "       the program 'sh' in the 'rvi-directory'....."
X	cc -o rvish rvish.c
X	chown bin rvish; chmod 0755 rvish;
Xfi
Xecho "Now creating working directories, linking files, and so forth:"
Xecho "     $rvidir....."
Xmkdir $rvidir; chown bin $rvidir; chmod 0755 $rvidir
Xecho "     $rvidir/tmp....."
Xmkdir $rvidir/tmp; chown bin $rvidir/tmp; chmod 0777 $rvidir/tmp
Xecho "     $rvidir/etc....."
Xmkdir $rvidir/etc; chown bin $rvidir/etc; chmod 0755 $rvidir/etc
Xecho "     $rvidir/bin....."
Xmkdir $rvidir/bin; chown bin $rvidir/bin; chmod 0755 $rvidir/bin
Xecho "     $rvidir/usr....."
Xmkdir $rvidir/usr; chown bin $rvidir/usr; chmod 0755 $rvidir/usr
Xecho "     $rvidir/usr/lib....."
Xmkdir $rvidir/usr/lib; chown bin $rvidir/usr/lib; chmod 0755 $rvidir/usr/lib
Xif [ $? -ne 0 ]; then echo "$0: aborting"; exit 1; fi
Xecho "$0: linking stuff:"
Xecho "     /etc/termcap....."
Xln /etc/termcap $rvidir/etc/termcap; if [ $? -ne 0 ]
X	then
X		echo "$0: can't 'ln', doing 'cp' instead..."
X		cp /etc/termcap $rvidir/etc/termcap;
X	fi
Xecho "     /usr/lib/ex3.7strings....."
Xln /usr/lib/ex3.7strings $rvidir/usr/lib/ex3.7strings; if [ $? -ne 0 ]
X	then
X		echo "$0: can't 'ln', doing 'cp' instead..."
X		cp /usr/lib/ex3.7strings $rvidir/usr/lib/ex3.7strings
X	fi
Xecho "     /bin/vi....."
Xln /bin/vi $rvidir/vi; if [ $? -ne 0 ]
X	then
X		echo "$0: can't 'ln', doing 'cp' instead..."
X		cp /bin/vi $rvidir/vi
X	fi
Xecho "     /bin/sh (don't worry, it's the restricted one)"
Xln /bin/sh $rvidir/rsh; if [ $? -ne 0 ]
X	then
X		echo "$0: can't 'ln', doing 'cp' instead..."
X		cp /bin/sh $rvidir/rsh
X	fi
Xln $rvidir/rsh $rvidir/bin/rsh
Xln ./rvish $rvidir/sh; if [ $? -ne 0 ]
X	then
X		echo "$0: can't 'ln', doing 'cp' instead..."
X	cp ./rvish $rvidir/sh
X	fi
Xln $rvidir/sh $rvidir/bin/sh
Xecho "$0: writing /etc/default/rvi....."
Xcd $rvidir; echo `pwd` >/etc/default/rvi; chmod 0644 /etc/default/rvi
Xecho "$0: done"
SHAR_EOF
chmod +x 'rvi_install'
fi # end of overwriting check
#	End of shell archive
exit 0



More information about the Comp.unix.sysv386 mailing list