[comp.sources.misc] v06i039: setuid.c version 1.1

allbery@uunet.UU.NET (Brandon S. Allbery - comp.sources.misc) (02/04/89)

Posting-number: Volume 6, Issue 39
Submitted-by: maart@cs.vu.nl (Maarten Litmaath)
Archive-name: setuid-1.1

["Secure" setuid shell scripts.  Again.  ++bsa]

of course Alexis was right: version 1.0 still suffered from the `linking trick
race condition'. Yet there is a way to prevent it, I venture! We could fstat()
the file descriptor we've got, to see if uid + mode of our inputfile are what
we expect them to be. Or? Alexis' other point has been taken care of too: now
it's possible to supply arguments to the setuid script (a trivial change).
So how about version 1.1?
Regards,
				Maarten Litmaath @ VU Amsterdam:
				maart@cs.vu.nl, mcvax!botter!maart

: This is a shar archive.  Extract with sh, not csh.
: This archive ends with exit, so do not worry about trailing junk.
: --------------------------- cut here --------------------------
PATH=/bin:/usr/bin:/usr/ucb
echo Extracting 'setuid.8'
sed 's/^X//' > 'setuid.8' << '+ END-OF-FILE ''setuid.8'
X.\" maart@cs.vu.nl - Maarten Litmaath Thu Dec 29 01:14:49 MET 1988
X.\"
X.TH SETUID 8 "Dec 23, 1988"
X.UC 4
X.SH NAME
X.B setuid
X\- run setuid shell scripts safely
X.SH SYNOPSIS
X.B #! /bin/setuid
X.br
X.SH DESCRIPTION
X.B setuid
Xis never normally executed from a shell. Instead it can be used
Xas the interpreter for shell scripts that need to be run setuid to someone
Xelse: this is done by making the first line of the script
X.PP
X.ti+5n
X#! /bin/setuid
X.PP
Xrather than the usual
X.PP
X.ti+5n
X#! /bin/sh
X.PP
X.B setuid
Xtries to open the script for reading. If successful it discards the first
Xline (containing the \fB#! /bin/setuid\fR) and tries to read a line
Xformatted as follows:
X.br
X.br
X.PP
X.ti+5n
X#? absolute\-path\-of\-interpreter arguments
X.PP
XExample:
X.PP
X.ti+5n
X#? /bin/csh -bf /usr/etc/setuid_script
X.PP
XTo avoid `linking tricks' for obtaining all privileges of the owner of the
Xfile, 2 measures have been taken:
X.br
X.br
X.PP
X1) the script must contain its own safe invocation, that is the `#?' line;
X.br
X2) just before the final
X.B execv(2)
X.B setuid
Xchecks if the owner and mode of the script are still what they are supposed
Xto be; if there is a discrepancy,
X.B setuid
Xwill abort with an error message.
X.PP
X.B setuid
Xwill only
Xexec a pathname beginning with a '/', to improve security.
XOf course
X.B setuid
Xcan be `fooled' by supplying dubious arguments to the interpreter, like
Xrelative pathnames. Furthermore it is a mistake to let the last directory
Xcomponent of the ultimate path be writable by others. In our example /usr/etc
Xshould not be writable for ordinary users.
X.PP
XIn addition, for the sake of security,
X.B setuid
Xsets the PATH environment variable back to a simple default, and deletes
Xthe IFS environment variable.
X.SH AUTHOR
XMaarten Litmaath @ VU Informatika Amsterdam (maart@cs.vu.nl)
X.SH "SEE ALSO"
X.BR sh (1), csh (1)
X.SH BUGS
XThe maintenance of setuid scripts is a bit annoying: if a script is moved,
Xone must not forget to change the path mentioned in the script. Possibly
Xthe editing causes the setuid bit to get turned off.
+ END-OF-FILE setuid.8
chmod 'u=rw,g=r,o=r' 'setuid.8'
set `wc -c 'setuid.8'`
count=$1
case $count in
2044)	:;;
*)	echo 'Bad character count in ''setuid.8' >&2
		echo 'Count should be 2044' >&2
esac
echo Extracting 'setuid.c'
sed 's/^X//' > 'setuid.c' << '+ END-OF-FILE ''setuid.c'
Xchar	sccsid[] = "@(#) setuid.c 1.1 88/12/29 Maarten Litmaath";
X
X/*
X * setuid.c
X * execute setuid shell scripts safely
X * see setuid.8
X */
X
X#include	<sys/types.h>
X#include	<sys/stat.h>
X#include	<stdio.h>
X
X
X#define		COMMENT		'#'
X#define		MAGIC		'?'
X#define		MAXARGV		1024
X#define		MAXLEN		256
X#define		SEC_ARGC	1
X#define		SEC_OPEN	2
X#define		SEC_FMT		3
X#define		SEC_READ	4
X#define		SEC_EXEC	5
X#define		SEC_STAT	6
X#define		SEC_ALARM	7
X
X
Xchar	E_argc[] = "%s: argument expected\n",
X	E_open[] = "%s: cannot open ",
X	E_fmt[] = "%s: format error in %s\n",
X	E_read[] = "%s: read error in ",
X	E_exec[] = "%s: cannot execute ",
X	E_stat[] = "%s: cannot fstat ",
X	E_alarm[] = "%s: %s is fake!\n",
X	Defaultpath[] = "/bin:/usr/bin",
X	*interpreter,
X	*newargv[MAXARGV];
X
X
Xmain(argc, argv)
Xint	argc;
Xchar	**argv;
X{
X	FILE	*fp;
X	int	c;
X	char	*prog, *file, *strrchr();
X	struct	stat	st;
X	
X	
X	if (!(prog = strrchr(argv[0], '/')))
X		prog = argv[0];
X	else
X		++prog;
X
X	if (argc < 2) {
X		fprintf(stderr, E_argc, prog);
X		exit(SEC_ARGC);
X	}
X	file = argv[1];
X
X	if (!(fp = fopen(file, "r"))) {
X		fprintf(stderr, E_open, prog);
X		perror(file);
X		exit(SEC_OPEN);
X	}
X
X	while ((c = getc(fp)) != '\n' && c != EOF)	/* skip #! line */
X		;
X
X
X	if (!(c = getparams(fp))) {
X		if (ferror(fp)) {
X			fprintf(stderr, E_read, prog);
X			perror(file);
X			exit(SEC_READ);
X		}
X		fprintf(stderr, E_fmt, prog, file);
X		exit(SEC_FMT);
X	}
X
X	argv += 2;			/* skip prog + file */
X
X	while (*argv && c < MAXARGV - 1)
X		newargv[c++] = *argv++;
X
X	newargv[c] = 0;
X
X#ifdef	DEBUG
X	printf("interpreter='%s'\n", interpreter);
X	for (c = 0; newargv[c]; c++)
X		printf("newargv[%d]='%s'\n", c, newargv[c]);
X#endif	DEBUG
X
X	(void) unsetenv("IFS");
X	(void) setenv("PATH", Defaultpath, 1);
X
X	if (fstat(fileno(fp), &st) != 0) {
X		fprintf(stderr, E_stat, prog);
X		perror(file);
X		exit(SEC_STAT);
X	}
X
X	if (!(st.st_mode & (S_ISUID | S_ISGID)) ||
X		(st.st_mode & S_ISUID) && st.st_uid != geteuid() ||
X		(st.st_mode & S_ISGID) && st.st_gid != getegid()) {
X		fprintf(stderr, E_alarm, prog, file);
X		exit(SEC_ALARM);
X	}
X
X	execv(interpreter, newargv);
X
X	fprintf(stderr, E_exec, prog);
X	perror(interpreter);
X	exit(SEC_EXEC);
X}
X
X
Xstatic	int	getparams(fp)
XFILE	*fp;
X{
X	char	buf[MAXLEN], *skipblanks(), *skiptoblank(), *strrchr();
X	register char	*p;
X	register int	i = 0;
X
X
X	for (;;) {
X		if (!fgets(buf, sizeof buf, fp) ||
X			buf[strlen(buf) - 1] != '\n')
X			return 0;
X
X		p = skipblanks(buf);
X
X		switch (*p++) {
X			case '\n':
X				continue;
X			case COMMENT:
X				break;
X			default:
X				return 0;
X		}
X
X		if (*p++ == MAGIC)
X			break;
X	}
X
X	p = skipblanks(p);
X
X	if (*(interpreter = p) != '/')
X		return 0;
X
X	p = skiptoblank(p);
X
X	if (*p == '\n')
X		return 0;
X
X	*p++ = '\0';
X
X	newargv[0] = strrchr(interpreter, '/') + 1;
X
X	while (i < MAXARGV - 1) {
X		p = skipblanks(p);
X
X		if (*p == '\n')
X			break;
X
X		newargv[++i] = p;
X
X		p = skiptoblank(p);
X
X		if (*p == '\n')
X			break;
X
X		*p++ = '\0';
X	}
X
X	newargv[++i] = 0;
X
X	/*
X	 * we expect 2 args at least: interpreter + file
X	 * furthermore we expect p to point to end of line by now
X	 */
X
X	if (i <= 1 || *p != '\n')
X		return 0;
X
X	*p = '\0';
X	return i;
X}
X
X
Xstatic	char	*skipblanks(p)
Xregister char	*p;
X{
X	while (*p == ' ' || *p == '\t')
X		++p;
X
X	return p;
X}
X
X
Xstatic	char	*skiptoblank(p)
Xregister char	*p;
X{
X	while (*p != ' ' && *p != '\t' && *p != '\n')
X		++p;
X
X	return p;
X}
+ END-OF-FILE setuid.c
chmod 'u=rw,g=r,o=r' 'setuid.c'
set `wc -c 'setuid.c'`
count=$1
case $count in
3320)	:;;
*)	echo 'Bad character count in ''setuid.c' >&2
		echo 'Count should be 3320' >&2
esac
exit 0