[alt.sources] alpha perl-cops, 2 of 3

df@sei.cmu.edu (Dan Farmer) (05/17/91)

Submitted-by: df@death.cert.sei.cmu.edu
Archive-name: alpha p-cops/part02

#!/bin/sh
# this is apcops.02 (part 2 of alpha p-cops)
# do not concatenate these parts, unpack them in order with /bin/sh
# file p-cops.alpha/kuang continued
#
if test ! -r _shar_seq_.tmp; then
	echo 'Please unpack part 1 first!'
	exit 1
fi
(read Scheck
 if test "$Scheck" != 2; then
	echo Please unpack part "$Scheck" next!
	exit 1
 else
	exit 0
 fi
) < _shar_seq_.tmp || exit 1
if test ! -f _shar_wnt_.tmp; then
	echo 'x - still skipping p-cops.alpha/kuang'
else
echo 'x - continuing file p-cops.alpha/kuang'
sed 's/^X//' << 'SHAR_EOF' >> 'p-cops.alpha/kuang' &&
#   operation (u for uid, g for gid, r for replace, w for write) and
#   value is a uid, gid or pathname.
#
#   A plan is a chain of CO's that are connected to each other.  If
#   /.login were writeable by uid 216, we might have a plan such as:
#
#	uid 216 => write /.login => uid 0
#
#   which means (in English) "if we can become uid 216, then write 
#   /.login which gives you access to uid 0 (when root next logs in)."
#   Plans are represented in several ways: as arrays:
#
#	("u 0", "w /.login", "u 216")
#
#   Note that the order is reversed.  As a string:
#
#	"u 0\034w /.login\034u 216"
#
#   The target is the object that we are trying to gain (a uid, gid or
#   file, typically u.root or some other UID).
#
# Data Structures
#
#   %known		An assocc array, indexed by CO.  This lists
#			the COs that we already have access to.  If we
#                       find a plan that leads from a CO in the known
#                       list to the target, we've succeeded in
#                       finding a major security flaw.  
#
#   @new		An array of plans that are to be evaluated in
#			the next cycle. 
#
#   @old		An array of plans that we are currently
#			evaluating. 
#
#   %beendone		An assoc array that lists the plans that have
#			already been tried.  Used to prevent loops.
#
#   @accessible		An array of the uids that can reach the
#			target. 
#
#   %files		An assoc array, indexed by file name, contains
#			cached file info.  value is of form "uid gid
#			mode". 
#
# From pwgrid:
#
#   %uname2shell	Assoc array, indexed by user name, values are
#			shells. 
#
#   %uname2dir		Assoc array, indexed by user name, values are
#			home directories.
#
#   %uname2uid		Assoc array, indexed by name, values are uids.
#			
#   %uid2names		Assoc array, indexed by uid, value is list of
#			user names with that uid, in form "name name
#			name...". 
#
#   %gid2members	Assoc array, indexed by gid, value is list of
#			group members (user names).
#
#   %gname2gid		Assoc array, indexed by group name, values are
#			matching gids.
#
#   %gid2names		Assoc array, indexed by gid, values are
#			matching group names.
#
X
do 'yagrip.pl' ||
X  die "can't do yagrip.pl";
X
do 'pwgrid.pl' ||
X  die "can't do pwgrid.pl";
X
do 'rules.pl' ||
X  die "can't do rules.pl";
X
X
#
# Turns a string of the form "operation value" or "value" into
# standard "CO" form ("operation value").  Converts user or group
# names into corresponding uid and gid values. 
#
# Returns nothing if it isn't parseable.
#
X
sub canonicalize {
X    local($string) = @_;
X    local($op, $value);
X
X    if ($string =~ /^([ugrw]) ([^ \t\n]+)$/) { # of form "op value"
X	$op = $1;
X	$value = $2;
X    } elsif ($string =~ /^[^ \t\n]+$/) {       # of form "value"
X        $value = $string;
X	$op = "u";
X    } else {
X	return();
X    }
X
X    if ($op eq "u" && $value =~ /^[^0-9]+$/) { # user name, not ID
X        if (defined($uname2uid{$value})) {
X	    $value = $uname2uid{$value};
X	} else {
X	    printf(stderr "There's no user named '%s'.\n", $value);
X	    return();
X	}
X    } elsif ($op eq "g" && $value =~/^[^0-9]+$/) {
X	if (defined($gname2gid{$value})) {
X	    $value = $gname2gid{$value};
X	} else {
X	    printf(stderr "There's no group named '%s'.\n", $value);
X	    return();
X	}
X    }
X
X    return($op, $value);
}
X
X
#
# Preload file information from a text file or DBM database.  
# If $opt_f.dir exists, then we just shadow %files from a DBM
# database.  Otherwise, open the file and read the entries into 
# %files.  
#
# $add_files_to_cache is set to 0 if we get the info from 
# DBM since we wouldn't want to pollute update our DBM cache
# with local file info which wouldn't apply to other hosts.
#
X
sub preload_file_info {
X    local($count, $f_type, $f_uid, $f_gid, $f_mode, $f_name);
X
X    if (defined($opt_d)) {
X	printf("loading file info...\n");
X    }
X
X    if (-f "$opt_f.dir") {
X	$add_files_to_cache = 0;
X
X	dbmopen(files, $opt_f, 0644) ||
X	  die sprintf("can't open DBM file '%s'", $opt_f);
X    } else {
X	open(FILEDATA, $opt_f) || 
X	  die sprintf("kuang: can't open '%s'", $opt_f);
X
X	$count = 0;
X	while (<FILEDATA>) {
X	    $count++;
X
X	    chop;
X	    ($f_type, $f_uid, $f_gid, $f_mode, $f_name) = split;
X	    
X	    if ($count % 1000 == 0) {
X		printf("line $count, reading entry for $f_name\n");
X	    }
X	    $files{$f_name} = join(' ', $f_uid, $f_gid, $f_mode);
X	}
X
X	close(FILEDATA);
X    }
}
X
#
# Preload the known information.  Reads data from a file, 1 entry per line,
# each entry is a CO that we "know" can be used.
#
X
sub preload_known_info {
X    local($file_name) = @_;
X    local($op, $value, $co);
X
X    open(FILE, $file_name) ||
X      die sprintf("kuang: can't open '%s'", $file_name);
X
X  known_loop:
X    while (<FILE>) {
X	chop;
X	if ((($op, $value) = &canonicalize($_)) == 2) {
X	    $co = sprintf("%s %s", $op, $value);
X	    $known{$co} = 1;
X	} else {
X	    printf(stderr "kuang: invalid entry in known list: line %d '%s'.\n",
X		   $.,
X		   $_);
X	}
X    }
X
X    close(FILE);
}
X    
X
#
# Do various initialization type things.
#
X
sub init_kuang {
X    local($which, $name, $uid, $gid);
X    local($op, $value, $co);
X
X    #
X    # Deal with args...
X    #
X
X    &getopt($options) ||
X      die $usage;
X
X    if ($#ARGV == -1) {
X	push(@ARGV, "u root");
X    }
X
X    #
X    # Preload anything...
X    #
X    if (defined($opt_f)) {
X	&preload_file_info();
X    }
X
X    if (defined($opt_d)) {
X	printf("load passwd info...\n");
X    }
X
X    if (defined($opt_p)) {
X	if (defined($opt_P)) {
X	    printf(stderr "You can only specify one of -p or -P, not both.\n");
X	    exit(1);
X	}
X
X	&load_passwd_info(0, $opt_p);
X    } elsif (defined($opt_P)) {
X	&load_passwd_info(0);
X    } else {
X	&load_passwd_info(1);
X    }
X
X    if (defined($opt_d)) {
X	printf("load group info...\n");
X    }
X
X    if (defined($opt_g)) {
X	if (defined($opt_G)) {
X	    printf(stderr "You can only specify one of -g or -G, not both.\n");
X	    exit(1);
X	}
X
X	&load_group_info(0, $opt_g);
X    } elsif (defined($opt_G)) {
X	&load_group_info(0);
X    } else {
X	&load_group_info(1);
X    }
X
X    #
X    # Need some of the password and group stuff.  Suck in passwd and 
X    # group info, store by uid and gid in an associative array of strings
X    # which consist of fields corresponding to the passwd and group file 
X    # entries (and what the heck, we'll use : as a delimiter also...:-)
X    #
X    $uname2shell{"OTHER"} = "";
X    $uname2dir{"OTHER"} = "";
X    $uname2uid{"OTHER"} = -1;
X    $uid2names{-1} = "OTHER";
X
X    $known{"u -1"} = 1;		# We can access uid OTHER
X
X    if (defined($opt_k)) {
X	&preload_known_info($opt_k);
X    }
X
X    #
X    # Create the target list from the remaining (non-option) args...
X    #
X    while ($#ARGV >= 0) {
X	$elt = pop(@ARGV);
X	if ((($op, $value) = &canonicalize($elt)) == 2) {
X	    $co = sprintf("%s %s", $op, $value);
X	    push(@targets, $co);
X	} else {
X	    printf(stderr "target '%s' isn't of correct form\n", $elt);
X	}
X    }
}
X
X
#
# Call this to set things up for a new target.  Resets old, new, beendone 
# and accessible.  
#
sub set_target {
X    local($target) = @_;
X
X    @old = ();
X    @new = ();
X    %beendone = ();
X    @accessible = ();
# fixme: reset known?
X
X    if ($target =~ /^([ugrw]) ([^ \t]+)$/) {
X	&addto($1, $2);
X	return(0);
X    } else {
X	printf(stderr "kuang: bad target '%s'\n", $target);
X	return(1);
X    }
}
X
#
# Break a CO into an (operation, value) pair and return it.  If it
# isn't in "operation value" form, return ().
#
sub breakup {
X    local($co) = @_;
X    local($operation, $value);
X
X    if ($co =~ /^([ugrw]) ([^ \t]+)$/) {
X	$operation = $1;
X	$value = $2;
X    } else {
X	printf(stderr "Yowza, breakup failed on '%s'\n",
X		$co);
X	exit(1);
X    }
X
X    return($operation, $value);
}
X
#
# Get the writers of the named file - return as (UID, GID, OTHER)
# triplet.  Owner can always write, since he can chmod the file if he
# wants. 
#
# (fixme) are there any problems in this sort of builtin rule?  should
# we make this knowledge more explicit?
#
sub filewriters {
X    local($name) = @_;
X    local($tmp, $mode, $uid, $gid, $other);
X    
X    #
X    # Check the file cache - avoid disk lookups for performance and 
X    # to avoid shadows...
X    #
X    if (defined($files{$name})) {
X	$cache_hit++;
X	
X	($uid, $gid, $mode, $tmp) = split(/ /, $files{$name});
X    } else {
X	$cache_miss++;
X
X	unless (-e $name) {
X	    if ($add_files_to_cache) {
X		$files{$name} = "";
X	    }
X	    # ENOTDIR = 20 
X	    ($! == 20) && print "Warning: Illegal Path: '$name'\n";
X	    # EACCES = 13
X	    ($! == 13) && print "Warning: Permission Denied: '$name'\n";
X	    # all values are returned "" here.
X	    return;
X	}
X
X	($tmp,$tmp,$mode,$tmp,$uid,$gid) = stat(_);
X	if ($add_files_to_cache) {
X	    $files{$name} = join(' ', "$uid", "$gid", "$mode");
X	}
X    }
X
X    if (($mode & 020) != 020) {
X	$gid = "";
X    }
X    
X    if (($mode & 02) == 02) {
X	$other = 1;
X    } else {
X	$other = 0;
X    }
X
X    return($uid, $gid, $other);
}
X
X
sub ascii_plan {
X    local(@plan) = @_;
X    local($op, $value, $result);
X
X    for ($i = $#plan; $i >= 0; $i--) {
X	($op, $value) = &breakup($plan[$i]);
X
X      case: 
X	{
X	    if ($op eq "g") {
X		$op = "grant gid";
X		last case;
X	    }
X
X	    if ($op eq "u") {
X		$op = "grant uid";
X		last case;
X	    }
X
X	    if ($op eq "r") {
X		$op = "replace";
X		last case;
X	    }
X
X	    if ($op eq "w") {
X		$op = "write";
X		last case;
X	    }
X
X	    printf(stderr "Bad op '%s' in plan '%s'\n",
X		   $op,
X		   join(';', @plan));
X	    last case;
X	}
X
X	$result .= "$op $value ";
X    }
X
X    return($result);
}
X
#
# Add a plan to the list of plans to check out.
#
sub addto {
X    local($op, $value, @plan) = @_;
X    local($co);
X
X    $co = sprintf("%s %s",
X		  $op,
X		  $value);
X
X    #
X    # See if the op and value is "uid root" - if so, and if the @plan 
X    # isn't empty, then don't bother checking - if the target isn't root, 
X    # its silly to pursue plans that require becoming root since if we can 
X    # become root, we can become anything.  If the target is root, then 
X    # this would be a loop anyway.
X    #
X    if ($op eq "u" && $value eq "0" && $#plan >= 0) {
X	if (defined($opt_d)) {
X	    printf("addto: aborted root plan '%s'\n",
X		   &ascii_plan(@plan, $co));
X	}
X	return;
X    }
X
X    #
X    # See whether there's an entry for $co in the known list.
X    # If so - success, we've found a suitable breakin plan.
X    #
X    # Yes, we want to check to see whether the whole Controlling Operation 
X    # is one that is known to us, rather than just the object.  I
X    # might have a hole that allows me to "replace /bin/foo" which is
X    # somewhat different than "write /bin/foo"  
X    #
X    if (! defined($opt_l) && defined($known{$co})) {
X	printf("Success! %s\n",
X	       &ascii_plan(@plan, $co));
X    }
X
X    #
X    # Check for loops -- if the new CO is part of the plan that we're
X    # adding it to, this is a loop.
X    #
X    foreach $entry (@plan) {
X	if ($entry eq $co) {
X	    if (defined($opt_d)) {
X		printf("addto: aborted loop in plan '%s'\n",
X		       &ascii_plan(@plan, $co));
X	    }
X	    return;
X	}
X    }
X
X    #
X    # Add this CO to the plan array...
X    #
X    push(@plan, $co);
X
X    #
X    # Make an ascii version of sorts...
X    #
X    $text_plan = join($;, @plan);
X
X    #
X    # Check to see if the new plan has been done.
X    #
X    if (defined($beendone{$text_plan})) {
X	if (defined($opt_d)) {
X	    printf("addto: plan's been done - '%s'\n",
X		   &ascii_plan(@plan));
X	}
X	return;
X    }
X
X    #
X    # If we made it this far, its a new plan and isn't a loop.  
X    #
X
X    #
X    # Add to the beendone list...
X    #
X    $beendone{$text_plan} = 1;
X
X    #
X    # Add to new plan list...
X    #
X    push(@new, $text_plan);
X
X    if (defined($opt_v)) {
X	printf("addto: %s\n", 
X	       &ascii_plan(@plan));
X    }
X
X    #
X    # If this is a uid goal, then add the plan to the accessible list.
X    #
X    if ($op eq "u" && $value ne "0" && defined($opt_l)) {
X	push(@accessible, $value);
X    }
}
X
#
#----------------------------------------------------------------------
#Main program follows...initialize and loop till we're done.
#
X
&init_kuang();
X
target_loop:
foreach $target (@targets) {
X    if (&set_target($target)) {
X	next target_loop;
X    }
X
X    while ($#new >= 0) {
X	@old = @new;
X	@new = ();
X
X	foreach $t_plan (@old) {
X	    @plan = split(/\034/, $t_plan);
X	    ($op, $value) = &breakup($plan[$#plan]);
X
X	    &apply_rules($op, $value, @plan);
X	}
X    }
X
X    if (defined($opt_l)) {
X	foreach $elt (@accessible) {
X	    printf("$elt\n");
X	}
X    }
}
X
if (defined($opt_d)) {
X    printf("File info cache hit/access ratio: %g\n", 
X   	    ($cache_hit + $cache_miss > 0) 
X	        ? $cache_hit / ($cache_hit + $cache_miss)
X	        : 0.0);
}
X
X
SHAR_EOF
echo 'File p-cops.alpha/kuang is complete' &&
chmod 0700 p-cops.alpha/kuang ||
echo 'restore of p-cops.alpha/kuang failed'
Wc_c="`wc -c < 'p-cops.alpha/kuang'`"
test 14688 -eq "$Wc_c" ||
	echo 'p-cops.alpha/kuang: original size 14688, current size' "$Wc_c"
rm -f _shar_wnt_.tmp
fi
# ============= p-cops.alpha/misc.chk ==============
if test -f 'p-cops.alpha/misc.chk' -a X"$1" != X"-c"; then
	echo 'x - skipping p-cops.alpha/misc.chk (File already exists)'
	rm -f _shar_wnt_.tmp
else
> _shar_wnt_.tmp
echo 'x - extracting p-cops.alpha/misc.chk (Text)'
sed 's/^X//' << 'SHAR_EOF' > 'p-cops.alpha/misc.chk' &&
#!/bin/sh  # need to mention perl here to avoid recursion
#
#  Usage: misc.chk.pl [-d]
#
#  This shell script checks a variety of miscellaneous potential
# security problems that really don't belong anywhere else.
#
#  Right now this looks for to see if tftp & rexd are enabled,
# to check if the uudecode alias is in the mail alias file and
# not commented out, and if uudecode can create a SUID file.
#
#  Mechanism:  tftp.chk will try to get /etc/motd from the localhost.
# Not much too it; just connect and try to get it.  For rexd, just
# look in the /etc/inetd.conf file to see if it's enabled (e.g., not
# commented out).
#
#  Warning:  it may take a minute or so to complete the test, since tftp
# might take a while to get the test file, or it may take a while to time
# out the connection (which is what usually happens if the test fails.)
# NOTE:
#   If you know where perl is and your system groks #!, put its
# pathname at the top to make this a tad faster.
#
# the following magic is from the perl man page
# and should work to get us to run with perl 
# even if invoked as an sh or csh or foosh script.
# notice we don't use full path cause we don't
# know where the user has perl on their system.
#
eval '(exit $?0)' && eval 'exec perl -S $0 ${1+"$@"}' 
& eval 'exec perl -S $0 $argv:q'
X    if $running_under_some_stupid_shell_instead_of_perl;
X
package main;
require 'chk_strings.pl';
require 'fgrep.pl';
X
if ($ARGV[0] eq '-d') {
X    #$chk_strings'debug = 1;  # verbose debugging
X    $misc_chk'debug = 1;
X    shift;
}
X
die "Usage: $0 [-d]\n" if @ARGV > 0;
X
X
$TFTP="/usr/ucb/tftp" unless defined $TFTP;
$UUDECODE="/usr/bin/uudecode" unless defined $UUDECODE; 
X
package misc_chk;
X
# look for uudecode alias in $aliases
$aliases="/usr/lib/aliases" if -f "/usr/lib/aliases";
$aliases = ( -f '/usr/lib/aliases' && '/usr/lib/aliases' )
X	|| ( -f '/etc/aliases'	   && '/etc/aliases' )
X	|| 'BOGUS';
$uu="decode";
X
# look for rexd in $inetd; this file could be "/etc/servers", too!
if (!defined($inetd)) {
X	$inetd = ( -f '/etc/inetd.conf' && '/etc/inetd.conf') ||
X		 ( -f '/etc/servers' && '/etc/servers') ||
X		 'BOGUS';
X	}
$rexd="rexecd";
X
# tmp and target file (for tftp test)
$target="/etc/motd";
$tmp="./tmp.$$";
X
# should probably generalize routine for chking for pats in file at some point
X
#  Read from $inetd to see if daemons are running.
# Comments are lines starting with a "#", so ignore.
# Checking for rexd:
#
print "Checking for $rexd in $inetd\n" if $debug;
if (@matches = grep(!/\s*#/, &'fgrep($inetd, $rexd))) {
X    print "Warning!  $rexd is enabled in $inetd!\n";
}
X
# Check to see if anything started inetd.conf is writable;
print "Checking for writable dirs in $inetd\n" if $debug;
&'chk_strings($inetd);
X
# Checking for uudecode alias:
print "Checking for $uu alias in $aliases\n" if $debug;
print "Warning!  $uu is enabled in $aliases!\n"
X    if &'fgrep($aliases, "^\s*$uu:");
X
# uucode stuff -- thanks to pete shipley...
print "Checking uudecode out\n" if $debug;
if (-x $'UUDECODE) {
X    open(UU, "| $'UUDECODE");
X    print UU <<EOD_;
begin 4755 ./foobar.$$
X 
end
EOD_
X    close(UU);
}
X
&'is_able($'UUDECODE,'s','s');	# check if uudecode is SUID
$is_able'silent = 1;
print "Warning!  $'UUDECODE creates setuid files!\n"
X   if &'is_able("./foobar.$$",'s','s');
$is_able'silent = 0;
unlink("./foobar.$$");
X
#  The rest is all for tftp stuff:
#
#   Get the local hostname...
$hostname =  ( -x '/bin/hostname'  && `/bin/hostname` ) 
X	  || ( -x '/bin/uname'      && `/bin/uname -n` )
X	  || ( -x '/usr/bin/uuname' && `/usr/bin/uuname -l`)
X	  || 'Amnesiac!';
chop $hostname;
X
#   Do the dirty work -- check tftp for the localhost, if it was found;
# this might take a bit, since tftp might have to time out.
X
print "Checking out tftp on $hostname\n" if $debug;
if (-x $'TFTP) {
X    open(SAVOUT, ">&STDOUT");	# suppress file not found
X    open(SAVERR, ">&STDERR");	# it's not as bad as it looks..
X    open(STDOUT, ">/dev/null") || die "Can't redirect stdout: $!\n";
X    open(STDERR, ">&STDOUT") || die "Can't dup stdout: $!\n";
X    close(STDOUT); close(STDERR);
X    open(TFTP, "| $'TFTP");
print TFTP <<_XXX_;
connect $hostname
get $target $tmp
quit
_XXX_
X    close(TFTP);
X    open(STDERR, ">&SAVERR"); close(SAVERR);
X    open(STDOUT, ">&SAVOUT"); close(SAVOUT);
} # > /dev/null 2> /dev/null
X
print "Warning!  tftp is enabled on $hostname!\n" if -s $tmp;
unlink $tmp;
X
# end of script
X
1;
SHAR_EOF
chmod 0700 p-cops.alpha/misc.chk ||
echo 'restore of p-cops.alpha/misc.chk failed'
Wc_c="`wc -c < 'p-cops.alpha/misc.chk'`"
test 4413 -eq "$Wc_c" ||
	echo 'p-cops.alpha/misc.chk: original size 4413, current size' "$Wc_c"
rm -f _shar_wnt_.tmp
fi
# ============= p-cops.alpha/kuang.1 ==============
if test -f 'p-cops.alpha/kuang.1' -a X"$1" != X"-c"; then
	echo 'x - skipping p-cops.alpha/kuang.1 (File already exists)'
	rm -f _shar_wnt_.tmp
else
> _shar_wnt_.tmp
echo 'x - extracting p-cops.alpha/kuang.1 (Text)'
sed 's/^X//' << 'SHAR_EOF' > 'p-cops.alpha/kuang.1' &&
.TH KUANG 1 "4 October 1990"
.SH NAME
kuang \- find security problems through rule based analysis
.SH SYNOPSIS
.B kuang
.RB "[\|" \-v  "\|]"
.RB "[\|" \-d "\|]"
.RB "[\|" \-l "\|]"
.RB "[\|" \-f filedata "\|]"
.RB "[\|" \-P "\|]"
.RB "[\|" \-G "\|]"
.RB "[\|" \-p passwd "\|]"
.RB "[\|" \-g group "\|]"
.RB "[\|" 
.IR u.username | g.groupname "\|]"
.br
.SH DESCRIPTION
.LP
.B kuang
uses rule based analysis to examine the current security configuration
of a site and determine whether certain security problems exist.
X
.B kuang 
contains embedded rules that describe the projection model and
some of the attacker tricks used on Unix systems.  It uses these rules
to reason backward from a desired goal (such as "grant u.root"),
generating potential "attack" plans from the rules and file system
state and then evaluating them to see whether they are reachable
according to the state recorded in the password and group files and in
the ownership and modes of the file systems.
X
By default, 
.B kuang 
uses "grant u.root" as its initial goal.  You can change that by
specifying a username (u.username) or groupname (g.groupname) on the
command line.  Normally 
.B kuang
determines a plan to be successful if it determines that anyone
(u.other) can become the initial goal.  
X
The 
.B \-v
option causes 
.B kuang
to print a message about every plan added to the evaluation list.
This can help one to understand how 
.B kuang 
works.  The 
.B \-d 
option causes 
.B kuang
to print a message when it evaluates a plan to determine whether to
retain it and add onto it or ignore it.  Beware - these options will often
produce lots of output.
X
Normally 
.B kuang
only registers success when it finds that everyone on the system can
become the target uid or gid.  With the 
.B \-l
option, 
.B kuang
will list every uid that can access the goal.  This provides a more
complete picture of the state of security - you might deem it a
problem if several users can become root, even if u.other cannot.  
X
One might adopt the view that each uid should only be accessible by
itself and root, and that each gid should be accessible only by the
members of that group and root.  One can then compare the expected
access list for a given uid or gid against the 
.B kuang
generated list to find security problems that 
.B kuang
wouldn't ordinarily tell you about.
X
The goals that 
.B kuang
use seem cryptic, but are really pretty straightforward.  Each goal
consists of a list of <action> <object> pairs.  Typical actions are
user, group, write and replace.  Typical objects are user names,
group names and file names.  The goal
"user root" (or u.root) means to have access to the root UID (0), or
in other words, to be able to run any program using that uid.  
Similarly,
"group staff" (or g.staff) means to have access to group staff.
The long goal
"user bill  group graphics replace /n/shoe/0/fred replace
/n/shoe/0/fred/.profile user fred group staff" means become
user bill, get access to the graphics group, replace the file
/n/shoe/0/fred, replace /n/shoe/0/fred/.profile, become fred,
grant access to the staff group.  The problem that allows this to
happen is that the /n/shoe/0 directory is writeable by the graphics
group, meaning that anyone in that group can replace the .profile file
for the fred user and gain access to that account and the groups it
belongs to when fred next logs in.  Ooops.
X
To do a thorough job, 
.B kuang 
really needs to be able to access all of
the controlling files of all users.  In some environments, home
directories are located in NFS mounted file systems where the client
doesn't have root access.  
X
The problem is that some home directories may be
protected so that group foo can read/write them, but OTHER can't.
.B kuang 
running as some user not in group foo won't be able to read or
search the directory, creating a blind spot that may hide security
problems (for example, if group foo can write that user's .login and
gain access to some other important priv...)  Running 
.B kuang
as root
won't help unless we are running on the server that exports that
file system, since root==nobody through NFS here.  Of course, then
you'll find other blind spots on other servers, meaning that you'll
never be able to see a complete picture of how things are from any
spot on the net.  Running 
.B kuang
on every machine might not even
help, since the blind spots might prevent them from seeing viable
paths to Success on any of the machines.  Sigh.
X
Soooo we've added a 
.B -f 
option that causes 
.B kuang 
to preload owner, group and mode information for a list of files.
Each line of the file should be of the form "type uid gid mode name".
.B type
is ignored by 
.B kuang.
.B uid 
and 
.B gid
are the user and group ID numbers, in decimal.
.B mode
is the permissions for the file, in octal.  And 
.B name
is the name of the file.  We've also added a program called
.B get-cf
that can be run as root on a server to create a file of the above form
for the control files for the user's with home directories on that
server.  Then you can run 
.B get-cf 
on every server as root, concatenate all the data together, and
preload it into Perl.  This will fix the shadow problems mentioned
above and should also speed things up since you won't need to do all
the file system references.
.B kuang -f file
will use a DBM database in place of a text file if file.dir exists.
X
.B Kuang
needs to read the entire password and group databases before it
starts, so that it has a complete idea of what users are in what groups
and so on.  This can be somewhat slow on systems using YP, since by
default 
.B kuang
uses the getpwent and getgrent routines to get the information (which
is tedious on a YP client).  
The 
.B -P
and 
.B -G
options cause 
.B kuang
to read /etc/passwd (/etc/group) and to use ypcat to read the rest of
the passwd (group) YP maps, which can be much faster.  In addition, 
the 
.B -p 
and 
.B -g 
options cause 
.B kuang 
to read the named files instead of /etc/passwd.
X
.SH "SEE ALSO"
"Rule Based Analysis of Computer Security", Robert W. Baldwin, MIT,
June 1987.
X
The README file that comes with 
.B kuang
describes many of the design considerations, problems and future
plans.
X
.SH NOTES
.LP
This version of 
.B kuang
is based on the shell script versions that Dan Farmer included with
the 
.B COPS 
security package, which in turn were based on code written by  Robert
Baldwin himself.
X
You should read the other documentation that should come with this
version and modify the rules in 
.B kuang
to suite your site.
X
.SH BUGS
.LP
Probably many.
X
The 
.B -P
and 
.B -G 
options don't work right if you use +@ constructions with YP.  They do
work right if you use a simple "+:" entry, however.
X
SHAR_EOF
chmod 0600 p-cops.alpha/kuang.1 ||
echo 'restore of p-cops.alpha/kuang.1 failed'
Wc_c="`wc -c < 'p-cops.alpha/kuang.1'`"
test 6730 -eq "$Wc_c" ||
	echo 'p-cops.alpha/kuang.1: original size 6730, current size' "$Wc_c"
rm -f _shar_wnt_.tmp
fi
# ============= p-cops.alpha/pass.cache.pl ==============
if test -f 'p-cops.alpha/pass.cache.pl' -a X"$1" != X"-c"; then
	echo 'x - skipping p-cops.alpha/pass.cache.pl (File already exists)'
	rm -f _shar_wnt_.tmp
else
> _shar_wnt_.tmp
echo 'x - extracting p-cops.alpha/pass.cache.pl (Text)'
sed 's/^X//' << 'SHAR_EOF' > 'p-cops.alpha/pass.cache.pl' &&
#
#   Routines for reading and caching user and group information.  These
# are used in multiple programs... it caches the info once, then hopefully
# won't be used again.
#
#  Steve Romig, May 1991.
#
# Provides a bunch of routines and a bunch of arrays.  Routines 
# (and their usage):
#
#    load_passwd_info($use_getent, $file_name)
#
#	loads user information into the %uname* and %uid* arrays 
#	(see below).  
#
#	If $use_getent is non-zero:
#	    get the info via repeated 'getpwent' calls.  This can be
#	    *slow* on some hosts, especially if they are running as a
#	    YP (NIS) client.
#	If $use_getent is 0:
#	    if $file_name is "", then get the info from reading the 
#	    results of "ypcat passwd" and from /etc/passwd.  Otherwise, 
#	    read the named file.  The file should be in passwd(5) 
#	    format.
#
#    load_group_info($use_gentent, $file_name)
#
#	is similar to load_passwd_info.
#
# Information is stored in several convenient associative arrays:
#
#   %uname2shell	Assoc array, indexed by user name, value is 
#			shell for that user name.
#
#   %uname2dir		Assoc array, indexed by user name, value is
#			home directory for that user name.
#
#   %uname2uid		Assoc array, indexed by name, value is uid for 
#			that uid.
#			
#   %uname2passwd	Assoc array, indexed by name, value is password
#			for that user name.
#
#   %uid2names		Assoc array, indexed by uid, value is list of
#			user names with that uid, in form "name name
#			name...". 
#
#   %gid2members	Assoc array, indexed by gid, value is list of
#			group members in form "name name name..."
#
#   %gname2gid		Assoc array, indexed by group name, value is
#			matching gid.
#
#   %gid2names		Assoc array, indexed by gid, value is the
#			list of group names with that gid in form 
#			"name name name...".
#
# You can also use routines named the same as the arrays - pass the index 
# as the arg, get back the value.  If you use this, get{gr|pw}{uid|gid|nam} 
# will be used to lookup entries that aren't found in the cache.
#
# To be done:
#    probably ought to add routines to deal with full names.
#    maybe there ought to be some anal-retentive checking of password 
#	and group entries.
#    probably ought to cache get{pw|gr}{nam|uid|gid} lookups also.
#    probably ought to avoid overwriting existing entries (eg, duplicate 
#       names in password file would collide in the tables that are 
#	indexed by name).
#
# Disclaimer:
#    If you use YP and you use netgroup entries such as 
#	+@servers::::::
#	+:*:::::/usr/local/utils/messages
#    then loading the password file in with &load_passwd_info(0) will get 
#    you mostly correct YP stuff *except* that it won't do the password and 
#    shell substitutions as you'd expect.  You might want to use 
#    &load_passwd_info(1) instead to use getpwent calls to do the lookups, 
#    which would be more correct.
#
X
package main;
X
$PASSWD = '/etc/passwd' unless defined $PASSWD;
X
require 'pathconf.pl';
X
%uname2shell = ();
%uname2dir = ();
%uname2uid = ();
%uname2passwd = ();
%uid2names = ();
%gid2members = ();
%gname2gid = ();
%gid2names = ();
X
$DOMAINNAME = "/bin/domainname" unless defined $DOMAINNAME;
$YPCAT = "/bin/ypcat" unless defined $YPCAT;
X
$yptmp = "./yptmp.$$";
X
$passwd_loaded = 0;		# flags to use to avoid reloading everything
$group_loaded = 0;		# unnecessarily...
X
#
# We provide routines for getting values from the data structures as well.
# These are named after the data structures they cache their data in.  Note 
# that they will all generate password and group file lookups via getpw* 
# and getgr* if they can't find info in the cache, so they will work
# "right" even if load_passwd_info and load_group_info aren't called to 
# preload the caches.
#
# I should point out, however, that if you don't call load_*_info to preload
# the cache, uid2names, gid2names and gid2members *will not* be complete, since 
# you must read the entire password and group files to get a complete picture.
# This might be acceptable in some cases, so you can skip the load_*_info
# calls if you know what you are doing...
#
sub uname2shell {
X    local($key) = @_;
X
X    if (! defined($uname2shell{$key})) {
X	&add_pw_info(getpwnam($key));
X    }
X
X    return($uname2shell{$key});
}
X
sub uname2dir {
X    local($key) = @_;
X    local(@pw_info);
X
X    if (! defined($uname2dir{$key})) {
X	&add_pw_info(getpwnam($key));
X    }
X
X    return($uname2dir{$key});
}
X
sub uname2uid {
X    local($key) = @_;
X    local(@pw_info);
X
X    if (! defined($uname2uid{$key})) {
X	&add_pw_info(getpwnam($key));
X    }
X
X    return($uname2uid{$key});
}
X
sub uname2passwd {
X    local($key) = @_;
X    local(@pw_info);
X
X    if (! defined($uname2passwd{$key})) {
X	&add_pw_info(getpwnam($key));
X    }
X
X    return($uname2passwd{$key});
}
X
sub uid2names {
X    local($key) = @_;
X    local(@pw_info);
X
X    if (! defined($uid2names{$key})) {
X	&add_pw_info(getpwuid($key));
X    }
X
X    return($uid2names{$key});
}
X
sub gid2members {
X    local($key) = @_;
X    local(@gr_info);
X
X    if (! defined($gid2members{$key})) {
X	&add_gr_info(getgrgid($key));
X    }
X
X    return($gid2members{$key});
}
X
sub gname2gid {
X    local($key) = @_;
X    local(@gr_info);
X
X    if (! defined($gname2gid{$key})) {
X	&add_gr_info(getgrnam($key));
X    }
X
X    return($gname2gid{$key});
}
X
sub gid2names {
X    local($key) = @_;
X    local(@gr_info);
X
X    if (! defined($gid2names{$key})) {
X	&add_gr_info(getgrgid($key));
X    }
X
X    return($gid2names{$key});
}
X
#
# Update user information for the user named $name.  We cache the password, 
# uid, login group, home directory and shell.
#
X
sub add_pw_info {
X    local($name, $passwd, $uid, $gid) = @_;
X    local($dir, $shell);
X
#
# Ugh!  argh...yech...sigh.  If we use getpwent, we get back 9 elts, 
# if we parse /etc/passwd directly we get 7.  Pick off the last 2 and 
# assume that they are the $directory and $shell.  
#
X    $dir = $_[$#_ - 1];
X    $shell = $_[$#_];
X
X    if ($name ne "") {
X	$uname2shell{$name} = $shell;
X	$uname2dir{$name} = $dir;
X	$uname2uid{$name} = $uid;
X	$uname2passwd{$name} = $passwd;
X
X	if ($gid ne "") {
X	    # fixme: should probably check for duplicates...sigh
X
X	    if (defined($gid2members{$gid})) {
X		$gid2members{$gid} .= " $name";
X	    } else {
X		$gid2members{$gid} = $name;
X	    }
X	}
X
X	if ($uid ne "") {
X	    if (defined($uid2names{$uid})) {
X		$uid2names{$uid} .= " $name";
X	    } else {
X		$uid2names{$uid} = $name;
X	    }
X	}
X    }
}
X
#
# Update group information for the group named $name.  We cache the gid 
# and the list of group members.
#
X
sub add_gr_info {
X    local($name, $passwd, $gid, $members) = @_;
X
X    if ($name ne "") {
X	$gname2gid{$name} = $gid;
X
X	if ($gid ne "") {
X	    if (defined($gid2names)) {
X		$gid2names{$gid} .= " $name";
X	    } else {
X		$gid2names{$gid} = $name;
X	    }
X
X	    # fixme: should probably check for duplicates
X
X	    $members = join(' ', split(/[, \t]+/, $members));
X
X	    if (defined($gid2members{$gid})) {
X		$gid2members{$gid} .= " " . $members;
X	    } else {
X		$gid2members{$gid} = $members;
X	    }
X	}
X    }
}
X
#
# We need to suck in the entire group and password files so that we can 
# make the %uid2names, %gid2members and %gid2names lists complete.  Otherwise,
# we would just read the entries as needed with getpw* and cache the results.
# Sigh.
#
# There are several ways that we might find the info.  If $use_getent is 1, 
# then we just use getpwent and getgrent calls to read the info in.
#
# That isn't real efficient if you are using YP (especially on a YP client), so
# if $use_getent is 0, we can use ypcat to get a copy of the passwd and
# group maps in a fairly efficient manner.  If we do this we have to also read
# the local /etc/{passwd,group} files to complete our information.  If we aren't 
# using YP, we just read the local pasword and group files.
#
sub load_passwd_info {
X    local($use_getent, $file_name) = @_;
X    local(@pw_info);
X
X    if ($passwd_loaded) {
X	return;
X    }
X
X    $passwd_loaded = 1;
X
X    if ($use_getent) {
X	#
X	# Use getpwent to get the info from the system, and add_pw_info to 
X	# cache it.
X	#
X	while ((@pw_info = getpwent()) != 0) {
X	    &add_pw_info(@pw_info);
X	}
X
X	endpwent();
X
X	return();
X    } elsif ($file_name eq "") {
X	chop($has_yp = `$DOMAINNAME`);
X	if ($has_yp) {
X	    #
X	    # If we have YP (NIS), then use ypcat to get the stuff from the 
X	    # map.@
X	    #
X	    system("$YPCAT passwd > $yptmp 2> /dev/null");
X	    if (-s $yptmp) {
X	    	open(FILE, "$YPCAT passwd|") ||
X	      	die "can't 'ypcat passwd'";
X	    	while (<FILE>) {
X			chop;
X			&add_pw_info(split(/:/));
X	    		}
X	    	}
X	    close(FILE);
X	}
X
X	#
X	# We have to read /etc/passwd no matter what...
X	#
X	$file_name = "/etc/passwd";
X    }
X
X    open(FILE, $file_name) ||
X      die "can't open $file_name";
X
X    while (<FILE>) {
X	chop;
X	    
X	if ($_ !~ /^\+/) {
X	    &add_pw_info(split(/:/));
X	}
X
X	# fixme: if the name matches +@name, then this is a wierd 
X	# netgroup thing, and we aren't dealing with it right.  might want
X	# to warn the poor user...suggest that he use the use_getent 
X	# method instead.
X    }
X
X    close(FILE);
}
X
sub load_group_info {
X    local($use_getent, $file_name) = @_;
X    local(@gr_info);
X
X    if ($group_loaded) {
X	return;
X    }
X
X    $group_loaded = 1;
X
X    if ($use_getent) {
X	#
X	# Use getgrent to get the info from the system, and add_gr_info to 
X	# cache it.
X	#
X	while ((@gr_info = getgrent()) != 0) {
X	    &add_gr_info(@gr_info);
X	}
X
X	endgrent();
X
X	return();
X    } elsif ($file_name eq "") {
X	chop($has_yp = `$DOMAINNAME`);
X	if ($has_yp) {
X	    #
X	    # If we have YP (NIS), then use ypcat to get the stuff from the 
X	    # map.
X	    #
X	    system("$YPCAT passwd > $yptmp 2> /dev/null");
X	    if (-s $yptmp) {
X	    	open(FILE, "$YPCAT group|") ||
X	      	die "can't 'ypcat group'";
X	    	while (<FILE>) {
X			chop;
X			&add_gr_info(split(/:/));
X	    		}
X	    	close(FILE);
X		}
X	}
X
X	#
X	# We have to read /etc/group no matter what...
X	#
X	$file_name = "/etc/group";
X    }
X
X    open(FILE, $file_name) ||
X      die "can't open $file_name";
X
X    while (<FILE>) {
X	chop;
X	if ($_ !~ /^\+/) {
X	    &add_gr_info(split(/:/));
X	}
X
X	# fixme: if the name matches +@name, then this is a wierd 
X	# netgroup thing, and we aren't dealing with it right.  might want
X	# to warn the poor user...suggest that he use the use_getent 
X	# method instead.
X    }
X
X    close(FILE);
}
X
# Load the password stuff -- Do NOT take this out!
&'load_passwd_info(0,$PASSWD);
X
unlink $yptmp;
X
1;
SHAR_EOF
chmod 0700 p-cops.alpha/pass.cache.pl ||
echo 'restore of p-cops.alpha/pass.cache.pl failed'
Wc_c="`wc -c < 'p-cops.alpha/pass.cache.pl'`"
test 10420 -eq "$Wc_c" ||
	echo 'p-cops.alpha/pass.cache.pl: original size 10420, current size' "$Wc_c"
rm -f _shar_wnt_.tmp
fi
# ============= p-cops.alpha/pass.chk ==============
if test -f 'p-cops.alpha/pass.chk' -a X"$1" != X"-c"; then
	echo 'x - skipping p-cops.alpha/pass.chk (File already exists)'
	rm -f _shar_wnt_.tmp
else
> _shar_wnt_.tmp
echo 'x - extracting p-cops.alpha/pass.chk (Text)'
sed 's/^X//' << 'SHAR_EOF' > 'p-cops.alpha/pass.chk' &&
#
#  Pass.chk -- a password guesser.  Functionally equivalent to the original
# cops password guesser.  The -P option doesn't work right now (alpha release,
# don't you know :-), since we're doing funky things with password caching,
# but this will change soon.
#
#  Usage: $0 [options] dictionary
#
#	-P pfile	password file
#	-p		print found passwords (incompatible with -M)
#       -d		check prefix/suffix of digits [0-9]
#       -g		check all words in gcos, plan, project, signature files
#	-r 		reverse the word tests
#	-s 		check all single chars as passwords
#	-u 		output the current user being checked
#	-U 		try uppercase word tests
#	-v		verbose, print advisory information
#
# Originally written by Tim Tessin with lots more features, etc., etc., etc.
# I ripped out all of his extra functionality that duplicated some of the
# other cops stuff, and some things that just didn't fit, and added some
# code to finish the simulation of the old checker. -- dan
#
X
require "pass.cache.pl";
X
$Passwd = "/etc/passwd";
$Adjunct = "/etc/security/passwd.adjunct";
$Log = "passwd.log";
$Tmp = "tmpi.$$";
$Secure_Tmp = "/etc/security/tmpi.$$";
$Secure_Tmp = "./tmpi.$$";
$Secure_Log = "./passwd.log";
$Move = "/bin/mv";
$Create = "/bin/touch";
X
&Getopts("dgiprsuUvwyo:P:l:b:e:") || do {
X	print STDERR "Illegal arguments\n";
X	&usage();
X	exit(1);
X	};
X	
sub usage {
X	print STDERR "usage: pi -firuwyv -p <pwdfile> -l <logfile>\n          -o <nday> -b <bname> -e <ename>\n";
X	}
X
# sanity checks
$opt_P = $Passwd unless $opt_P;
$opt_l = $Log unless $opt_l;
unless (-r $opt_P) {
X	print STDERR "Can't read passwd file $opt_P\n";
X	exit(1);
X	}
X
# unbuffer output
select (STDOUT); $| = 1;
X
$dups = 0;		# duplicate name entries
$new = 0;		# new entries
$changed = 0;		# password (and other data) changed
$deleted = 0;		# deleted entries
$updated = 0;		# data other than password changed
$nlog = 0;		# number of log entries, used for print decisions
$ntest = 0;
$ndone = 0;
X
for $uid (keys %uname2passwd) {
X	$pass = $uname2passwd{$uid};
X	if ($try = &dopwd()) {
X		$pwd = ($opt_p) ? $try : "";
X		# printf "Username: %-8s  <password guessed>  $pwd\n",$P[0];
X		printf "Warning!  Password Problem: Guessed: %s\t\t$pwd\n",$P[0];
X		}
X	$ndone++;
X	$time = time();
X	print LOG "$time$ok:$value\n";
X	}
X
&finish;
1;
# end of program
X
X
######################## Support Subroutines ###########################
# dopwd tests each password entry against several simple checks and all
# the words in the dictionaries specified.  The simple checks consists
# of trying the username as the password ('joe' accounts), words derived
# from the gecos fields (usually first and last names).
X
sub dopwd {
$tries = 0;
X
if ($opt_u) { print "$uid\n"; }
X
# try user name
($try = &testpwd($uid,$pass)) && return $try;
X
# do gcos field?
if ($opt_g) {
X	@gcos = split(/[.,& -]/,$uname2gcos{$uid});
X	foreach $i (@gcos) {
X		next unless $i;		# skip null split values
X		($try = &testpwd($i,$pass)) && return $try;
X		}
X
X	# Try names from misc files
X	#
X	undef %words;
X	# files to check
X	@files2chk = ("/.project", "/.plan", "/.signature");
X	$home = $uname2dir{$uid};
X	for $i (@files2chk) {
X		open (FOOFILE, $home . $i);
X		while (<FOOFILE>) {
X			chop;
X			@line = split(/([.,;\s])/);
X			for $j (@line) {
X				$words{$j}=$j unless $j=~/[\s]/;
X				}
X			}
X		close FOOFILE;
X		}
X	for $k (values %words) {
X		# print "word $k\n";
X		($try = &testpwd($k,$pass)) && return $try;
X		}
X	}
X
# do dictionaries
# save state of upper/reverse so individual dicts can temporarily
# override.
foreach $i (@ARGV) {
X	if (open (DICT,$i)) {
X		while (<DICT>) {
X			chop;
X			if ($try = &testpwd($_,$pass)) {
X				close DICT;
X				return $try;
X				}
X			}
X		close DICT;
X		}
X	}
return 0;
}
X
X
# small subroutines to help the main password cracker.  All are labeled
# p_xxx, where xxx is the identifying name.
#
X
# if leading character is upper-case, also try lower case version
sub p_lc {
local($try) = @_;
local($ntry);
if ( $try =~ /^[A-Z]/ ) {
X	($ntry = $try) =~ y/A-Z/a-z/;
X	push(@total_guesses, $ntry);
X	}
}
X
# reverse check
sub p_rev {
local($try) = @_;
local($ntry);
$ntry = reverse $try;
if ($ntry ne $try) {
X	push(@total_guesses, $ntry);
X	}
}
X
# uppercase check
sub p_up {
local($try) = @_;
local($ntry);
($ntry = $try) =~ y/a-z/A-Z/;
if ($ntry ne $try) { push(@total_guesses, $ntry); }
}
X
# testpwd checks a word to see if it matches the encrpted password
# if the word is capitalized, the lowercase version is tried as well
X
sub testpwd {
local ($try,$pass) = @_;
local (@total_guesses);
X
push(@total_guesses, $try);
X
# free (lower case) check if first letter is uppercase
&p_lc($try);
# reverse?
if ($opt_r) { &p_rev($try); }
# uppercase?
if ($opt_U) { &p_up($try); }
X
if ($opt_d) {
X	if (length ($try) < 8) {
X		foreach $i ('0'..'9') {
X			$ntry = $i.$try;
X			push(@total_guesses, $ntry);
X			if ($opt_r) { &p_rev($ntry); }
X			if ($opt_U) { &p_up($ntry); }
X			}
X		foreach $i ('0'..'9') {
X			$ntry = $try.$i;
X			push(@total_guesses, $ntry);
X			if ($opt_r) { &p_rev($ntry); }
X			if ($opt_U) { &p_up($ntry); }
X			}
X		}
X	}
X
# do single letters, #'s, if needed
if ($opt_s && $user ne $last_user) {
X	$last_user = $user;
X	foreach $i (0..9) { push(@total_guesses, $i); }
X	foreach $i (A..Z) { push(@total_guesses, $i); }
X	foreach $i (a..z) { push(@total_guesses, $i); }
X	}
X
foreach $i (@total_guesses) {
X	# print "trying $i\n";
X	$epw = &pcrypt($i, $pass);
X	($epw eq $pass) && return $i;
X	}
undef @total_guesses;
X
return 0;
}
X
sub finish {
unlink "/tmp/pi.log.$$";
unlink "/tmp/xpi.$$";
unlink $Tmp;
}
X
# These routines taken from the perl library to provide the functions here
# so we don't have to depend on perl libraries being properly installed.
X
# getopts.pl - a better getopt.pl
# Usage:
#      do Getopts('a:bc');  # -a takes arg. -b & -c not. Sets opt_* as a
#                           #  side effect.
sub Getopts {
X    local($argumentative) = @_;
X    local(@args,$_,$first,$rest,$errs);
X    local($[) = 0;
X
X    @args = split( / */, $argumentative );
X    while(($_ = $ARGV[0]) =~ /^-(.)(.*)/) {
X	($first,$rest) = ($1,$2);
X	$pos = index($argumentative,$first);
X	if($pos >= $[) {
X	    if($args[$pos+1] eq ':') {
X		shift(@ARGV);
X		if($rest eq '') {
X		    $rest = shift(@ARGV);
X		}
X		eval "\$opt_$first = \$rest;";
X	    }
X	    else {
X		eval "\$opt_$first = 1";
X		if($rest eq '') {
X		    shift(@ARGV);
X		}
X		else {
X		    $ARGV[0] = "-$rest";
X		}
X	    }
X	}
X	else {
X	    print STDERR "Unknown option: $first\n";
X	    ++$errs;
X	    if($rest ne '') {
X		$ARGV[0] = "-$rest";
X	    }
X	    else {
X		shift(@ARGV);
X	    }
X	}
X    }
X    $errs == 0;
}
X
sub pcrypt {
local($try, $pass) = @_;
X
print "Trying \"$try\" on $uid\n" if $opt_v;
X
$epw = crypt($try,$pass);
X
return $epw;
}
X
SHAR_EOF
chmod 0700 p-cops.alpha/pass.chk ||
echo 'restore of p-cops.alpha/pass.chk failed'
Wc_c="`wc -c < 'p-cops.alpha/pass.chk'`"
test 6684 -eq "$Wc_c" ||
	echo 'p-cops.alpha/pass.chk: original size 6684, current size' "$Wc_c"
rm -f _shar_wnt_.tmp
fi
# ============= p-cops.alpha/passwd.chk ==============
if test -f 'p-cops.alpha/passwd.chk' -a X"$1" != X"-c"; then
	echo 'x - skipping p-cops.alpha/passwd.chk (File already exists)'
	rm -f _shar_wnt_.tmp
else
> _shar_wnt_.tmp
echo 'x - extracting p-cops.alpha/passwd.chk (Text)'
sed 's/^X//' << 'SHAR_EOF' > 'p-cops.alpha/passwd.chk' &&
#
#   passwd.chk
#
# Check password file -- /etc/passwd -- for incorrect number of fields,
# duplicate uid's, non-alphanumeric uids, and non-numeric group id's.
# 
# Mechanism:  This script ensures that each line of the passwd file (in
# $etc, line 47) has 7 fields and is non-blank, as well as examining the
# file for any duplicate users.  It then checks to ensure that the first
# character of the login name is alphanumeric, and that all uid and gid
# numbers are indeed numeric and non-negative.  It also checks the
# validity of the home directory.
# 
# For yellow pages passwords, it does the same checking, but in order to
# get a listing of all members of the password file, it does a "ypcat
# passwd" and uses the output from that as a passwd file.
# 
# The /etc/passwd file has a very specific format, making the task fairly
# simple.  Normally it has lines with 7 fields, each field separated by a
# colon (:).  The first field is the user id, the second field is the
# encrypted password (an asterix (*) means the group has no password,
# otherwise the first two characters are the salt), the third field is the
# user id number, the fourth field is the group id number, the fifth field
# is the GECOS field (basically holds miscellaneous information, varying
# from site to site), the sixth field is the home directory of the user,
# and lastly the seventh field is the login shell of the user.  No blank
# lines should be present.  Uid's will be flagged if over 8 chars, unless
# the $OVER_8 variable (line 45) is set to "YES".
# 
# If a line begins with a plus sign (+), it is a yellow pages entry.  See
# passwd(5) for more information, if this applies to your site.
# 
X
# If your kernel doesn't understand the #! notation, this script will
# feed itself to perl
eval 'exec /usr/local/bin/perl -S $0 $*'
X	if $running_under_some_shell;
X
#   Used for Sun C2 security group file. 'FALSE' (default) will flag
# valid C2 passwd syntax as an error, 'TRUE' attempts to validate it.
# Thanks to Pete Troxell for pointing this out.
$C2='FALSE';
X
#  Some systems allow long uids; set this to 'TRUE', if so (thanks
# to Pete Shipley (lot of petes around here, eh?)):
$OVER_8='NO';
X
#
# Important files:
$etc="/etc/passwd";
$yptmp = "./yptmp.$$";
X
# If YPCAT is in the environment, use that, otherwise use the default
if (defined $ENV{'YPCAT'}) { $YPCAT = $ENV{'YPCAT'}; }
else { $YPCAT = "/usr/bin/ypcat"; }
X
# read in $etc passwd file. This creates an associative array to check
# duplicate login names.
open(PASSWD, $etc) || exit(1);
while(<PASSWD>) {
X	push(@PW_FILE, $_);
X	($log, $pass) = split(/:/);
X	if (!defined $foo{$log}) {
X		$foo{$log} = $pass;
X		}
X	else {
X		print "Warning!  Duplicate uid found in $etc: $log\n";
X		}
X	}
close(PASSWD);
X
# same for yellow pages.
if (-s $YPCAT) {
X	system("$YPCAT passwd > $yptmp 2> /dev/null");
X        if (-s $yptmp && open(YP, "$YPCAT passwd|") != 0) {
X		while(<YP>) {
X			push(@YP_FILE, $_);
X			($log, $pass) = split(/:/);
X			if (!defined $foo{$log}) {
X				$foo{$log} = $pass;
X				}
X			 else {
X				print "Warning!  Duplicate uid found in yellow pages: $log\n";
X				}
X			close(YP);
X			}
X		}
X	}
X
#   First line is for a yellow pages entry in the password file.
# It really should check for correct yellow pages syntax....
$NR = 0;
foreach $_ (@PW_FILE) {
X	$NR++;
X	@ARR = split(/:/);
X	next if ($ARR[0] =~ /^\+/o);
X
X	if (/^\s*$/o) {
X		print "Warning!  Password file, line $NR, is blank\n";
X		next;
X	}
X	
X	if ($#ARR != 6) {
X		print "Warning!  Password file, line $NR, does not have 7 fields: \n\t$_";
X	}
X
X	if ($ARR[0] !~ /^[A-Za-z0-9]/o) {
X		print "Warning!  Password file, line $NR, nonalphanumeric login: \n\t$_";
X	}
X
X	if ($OVER_8 ne 'NO' && length($ARR[0]) > 8) {
X		print "Warning!  Password file, line $NR, uid > 8 chars\n\t$_";
X	}
X
X	if ($ARR[1] eq "") {
X		print "Warning!  Password file, line $NR, no password:\n\t$_";
X	}
X
X 	if ($C2 eq "TRUE" && $ARR[1] =~ /^##/o && "##".$ARR[0] ne $ARR[1]) {
X		print "Warning!  Password file, line $NR, invalid password field for C2: \n\t$_";
X	}
X
X	if ($ARR[2] !~ /^[0-9]/o) {
X		if ($ARR[2] < 0) {
X			print "Warning!  Password file, line $NR, negative user id: \n\t$_";
X		} else {
X			print "Warning!  Password file, line $NR, nonnumeric user id: \n\t$_";
X		}
X	}
X
X	if ($ARR[2] == 0 && $ARR[0] ne "root") {
X		print "Warning!  Password file, line $NR, user $ARR[0] has uid = 0 and is not root\n\t$_";
X	}
X
X	if ($ARR[3] !~ /^[0-9]/o) {
X		print "Warning!  Password file, line $NR, nonnumeric group id: \n\t$_";
X	}
X
X	if ($ARR[5] !~ /^\//o) {
X		print "Warning!  Password file, line $NR, invalid login directory: \n\t$_";
X	}
X
}
X
#
# Test yellow pages passwords as well
# before you complain about me putting the test for YP_FILE first, you get less
# indentation if you do it this way. Change it if you want.
if (defined @YP_FILE) {
X
$NR = 0;
foreach $_ (@YP_FILE) {
X	$NR++;
X	@ARR = split(/:/);
X
X	if (/^\s*$/o) {
X		print "Warning!  YPassword file, line $NR, is blank\n";
X		next;
X	}
X	
X	if ($#ARR != 6) {
X		print "Warning!  YPassword file, line $NR, does not have 7 fields: \n\t$_";
X	}
X
X	if ($ARR[0] !~ /^[A-Za-z0-9]/o) {
X		print "Warning!  YPassword file, line $NR, nonalphanumeric login: \n\t$_";
X	}
X
X	if ($OVER_8 ne 'NO' && length($ARR[0]) > 8) {
X		print "Warning!  YPassword file, line $NR, uid > 8 chars\n\t$_";
X	}
X
X	if ($ARR[1] eq "") {
X		print "Warning!  YPassword file, line $NR, no password:\n\t$_";
X	}
X
X	if ($ARR[2] !~ /^[0-9]/o) {
X		if ($ARR[2] < 0) {
X			print "Warning!  YPassword file, line $NR, negative user id: \n\t$_";
X		} else {
X			print "Warning!  YPassword file, line $NR, nonnumeric user id: \n\t$_";
X		}
X	}
X
X	if ($ARR[2] == 0 && $ARR[0] ne "root") {
X		print "Warning!  YPassword file, line $NR, user $ARR[0] has uid = 0 and is not root\n\t$_";
X	}
X
X	if ($ARR[3] !~ /^[0-9]/o) {
X		print "Warning!  YPassword file, line $NR, nonnumeric group id: \n\t$_";
X	}
X
X	if ($ARR[5] !~ /^\//o) {
X		print "Warning!  YPassword file, line $NR, invalid login directory: \n\t$_";
X	}
X
X	}
}
X
unlink $yptmp;
X
1;
# end
SHAR_EOF
chmod 0700 p-cops.alpha/passwd.chk ||
echo 'restore of p-cops.alpha/passwd.chk failed'
Wc_c="`wc -c < 'p-cops.alpha/passwd.chk'`"
test 5989 -eq "$Wc_c" ||
	echo 'p-cops.alpha/passwd.chk: original size 5989, current size' "$Wc_c"
rm -f _shar_wnt_.tmp
fi
# ============= p-cops.alpha/pathconf.pl ==============
if test -f 'p-cops.alpha/pathconf.pl' -a X"$1" != X"-c"; then
	echo 'x - skipping p-cops.alpha/pathconf.pl (File already exists)'
	rm -f _shar_wnt_.tmp
else
> _shar_wnt_.tmp
echo 'x - extracting p-cops.alpha/pathconf.pl (Text)'
sed 's/^X//' << 'SHAR_EOF' > 'p-cops.alpha/pathconf.pl' &&
$YPCAT = '/usr/bin/ypcat';
$STRINGS = '/usr/ucb/strings';
$TFTP = '/usr/ucb/tftp';
$UUDECODE = '/usr/bin/uudecode';
$CMP = '/bin/cmp';
$LS = '/bin/ls';
X
# end of perl needed programs
X
$AWK = '/bin/awk';
$CAT = '/bin/cat';
$CC = '/bin/cc';
$CHMOD = '/bin/chmod';
$COMM = '/usr/bin/comm';
$CP = '/bin/cp';
$DATE = '/bin/date';
$DIFF = '/bin/diff';
$ECHO = '/bin/echo';
$EGREP = '/usr/bin/egrep';
$EXPR = '/bin/expr';
$FIND = '/usr/bin/find';
$GREP = '/bin/grep';
$MAIL = '/bin/mail';
$MKDIR = '/bin/mkdir';
$MV = '/bin/mv';
$RM = '/bin/rm';
$SED = '/bin/sed';
$SH = '/bin/sh';
$SORT = '/usr/bin/sort';
$TEST = '/bin/test';
$TOUCH = '/usr/bin/touch';
$UNIQ = '/usr/bin/uniq';
X
1;
SHAR_EOF
chmod 0700 p-cops.alpha/pathconf.pl ||
echo 'restore of p-cops.alpha/pathconf.pl failed'
Wc_c="`wc -c < 'p-cops.alpha/pathconf.pl'`"
test 677 -eq "$Wc_c" ||
	echo 'p-cops.alpha/pathconf.pl: original size 677, current size' "$Wc_c"
rm -f _shar_wnt_.tmp
fi
# ============= p-cops.alpha/pathconf.sh ==============
if test -f 'p-cops.alpha/pathconf.sh' -a X"$1" != X"-c"; then
	echo 'x - skipping p-cops.alpha/pathconf.sh (File already exists)'
	rm -f _shar_wnt_.tmp
else
> _shar_wnt_.tmp
echo 'x - extracting p-cops.alpha/pathconf.sh (Text)'
sed 's/^X//' << 'SHAR_EOF' > 'p-cops.alpha/pathconf.sh' &&
YPCAT = '/usr/bin/ypcat';
STRINGS = '/usr/ucb/strings';
TFTP = '/usr/ucb/tftp';
UUDECODE = '/usr/bin/uudecode';
X
# end of perl needed programs
X
AWK = '/bin/awk';
CAT = '/bin/cat';
CC = '/bin/cc';
CHMOD = '/bin/chmod';
CMP = '/bin/cmp';
COMM = '/usr/bin/comm';
CP = '/bin/cp';
DATE = '/bin/date';
DIFF = '/bin/diff';
ECHO = '/bin/echo';
EGREP = '/usr/bin/egrep';
EXPR = '/bin/expr';
FIND = '/usr/bin/find';
GREP = '/bin/grep';
LS = '/bin/ls';
MAIL = '/bin/mail';
MKDIR = '/bin/mkdir';
MV = '/bin/mv';
RM = '/bin/rm';
SED = '/bin/sed';
SH = '/bin/sh';
SORT = '/usr/bin/sort';
TEST = '/bin/test';
TOUCH = '/usr/bin/touch';
UNIQ = '/usr/bin/uniq';
SHAR_EOF
chmod 0700 p-cops.alpha/pathconf.sh ||
echo 'restore of p-cops.alpha/pathconf.sh failed'
Wc_c="`wc -c < 'p-cops.alpha/pathconf.sh'`"
test 644 -eq "$Wc_c" ||
	echo 'p-cops.alpha/pathconf.sh: original size 644, current size' "$Wc_c"
rm -f _shar_wnt_.tmp
fi
# ============= p-cops.alpha/rc.chk ==============
if test -f 'p-cops.alpha/rc.chk' -a X"$1" != X"-c"; then
	echo 'x - skipping p-cops.alpha/rc.chk (File already exists)'
	rm -f _shar_wnt_.tmp
else
> _shar_wnt_.tmp
echo 'x - extracting p-cops.alpha/rc.chk (Text)'
sed 's/^X//' << 'SHAR_EOF' > 'p-cops.alpha/rc.chk' &&
#!/bin/sh  # need to mention perl here to avoid recursion
#
#  Usage: rc.chk
#
#  This checks pathnames and files inside the shell script files /etc/rc*
# for writability.  The commands inside the files /etc/rc* are executed when
# the machine is booted, so are of special interest.
#
# Made easy by chk_strings :-)
#
# Name: Martin Foord	Username: maf  Date: Thu Jan 17 15:11:09 EST 1991 
# Email: maf%dbsm.oz.au@munnari.oz.au
#
# NOTE:
#   If you know where perl is and your system groks #!, put its
# pathname at the top to make this a tad faster.
#
# the following magic is from the perl man page
# and should work to get us to run with perl 
# even if invoked as an sh or csh or foosh script.
# notice we don't use full path cause we don't
# know where the user has perl on their system.
#
eval '(exit $?0)' && eval 'exec perl -S $0 ${1+"$@"}' 
& eval 'exec perl -S $0 $argv:q'
X    if $running_under_some_stupid_shell_instead_of_perl;
X
require 'chk_strings.pl';
X
while (</etc/rc*>) {
X	&chk_strings($_);
}
X
1;
SHAR_EOF
chmod 0700 p-cops.alpha/rc.chk ||
echo 'restore of p-cops.alpha/rc.chk failed'
Wc_c="`wc -c < 'p-cops.alpha/rc.chk'`"
test 1014 -eq "$Wc_c" ||
	echo 'p-cops.alpha/rc.chk: original size 1014, current size' "$Wc_c"
rm -f _shar_wnt_.tmp
fi
# ============= p-cops.alpha/reconfig.pl ==============
if test -f 'p-cops.alpha/reconfig.pl' -a X"$1" != X"-c"; then
	echo 'x - skipping p-cops.alpha/reconfig.pl (File already exists)'
	rm -f _shar_wnt_.tmp
else
> _shar_wnt_.tmp
echo 'x - extracting p-cops.alpha/reconfig.pl (Text)'
sed 's/^X//' << 'SHAR_EOF' > 'p-cops.alpha/reconfig.pl' &&
#!/bin/sh  # need to mention perl here to avoid recursion
# NOTE:
#   If you know where perl is and your system groks #!, put its
# pathname at the top to make this a tad faster.
#
# the following magic is from the perl man page
# and should work to get us to run with perl 
# even if invoked as an sh or csh or foosh script.
# notice we don't use full path cause we don't
# know where the user has perl on their system.
#
eval '(exit $?0)' && eval 'exec perl -S $0 ${1+"$@"}' 
& eval 'exec perl -S $0 $argv:q'
X    if $running_under_some_stupid_shell_instead_of_perl;
X
#  Target shell scripts in question:
$COPS_CONFIG="pathconf.pl";
X
#  Potential directories to find commands:
@all_dirs=("/bin",
X	   "/usr/bin",
X	   "/usr/ucb",
X	   "/usr/local/bin",  # scary
X	   "/usr/bsd");
X
# comment out next line if you want your own current path used instead
#
# @all_dirs = split(/:/, $ENV{'PATH'});
X
#  Target commands in question, sans those checked above:
@all_commands= ("cc", "awk", "cat",
X		"chmod", "cmp", "comm", "cp",
X		"date", "diff", "echo", "egrep", "expr",
X		"find", "grep", "ls", "mail",
X		"mkdir", "mv", "rm", "sed",
X		"sh", "sort", "test", "tftp", "touch",
X		"uudecode", "uniq", "ypcat");
X
@want{@all_commands} = ();
X
%exceptions=   ('strings', 'chk_strings',
X                'tftp', 'misc.chk',
X		'cmp', 'ftp.chk',
X                'uudecode', 'misc.chk');
X
# grab the current values:
open COPS_CONFIG || die "Can't open $COPS_CONFIG: $!";
X
$new = "$COPS_CONFIG.$$";
open(NEW_CONFIG, ">$new") || die "Can't open $new: $!";
X
while (<COPS_CONFIG>) {
X    unless (/\$(\w+)\s*=\s*(['"])(\S*)\2/) {
X	print NEW_CONFIG;
X	next;
X    } 
X    ($cap_command, $path) = ($1, $3);
X    ($command = $cap_command) =~ tr/A-Z/a-z/;
X    unless (($newpath = &getpath($command)) || $command =~ /^yp/) {
X	warn "Warning!  no path for $command!\n";
X	warn "          $exceptions{$command} will not work as planned!\n"
X		     if $exceptions{$command};
X	$errors++;
X    } else {
X	delete $want{$command};
X    } 
X    print "old $path now in $newpath\n" if $newpath ne $path;
X    print NEW_CONFIG "\$$cap_command = '$newpath';\n";
X
}
X
for (sort keys %want) {
X    delete $want{$_} if $path = &getpath($_);
X    tr/a-z/A-Z/;
X    print NEW_CONFIG '$', $_, " = '", $path, "';\n";
} 
X
close(COPS_CONFIG) || die "can't close $COPS_CONFIG: $!";
close(NEW_CONFIG) || die "can't close $new: $!";
X
if (@missing = keys %want) {
X     warn "Warning!   missing paths for @missing!\n";
X     warn "The shell version may not work right!\n";
} 
X
X
if ($errors) {
X    print STDERR "Not all paths were found: write anyway? ";
X    exit 1 if <STDIN> !~ /^\s*y/i;
X    print STDERR "Ok, but this might not be right...\n";
} 
X
$old = "$COPS_CONFIG.old";
X
rename($COPS_CONFIG, $old)
X    || die "can't rename $COPS_CONFIG to $old: $!";
X
rename($new, $COPS_CONFIG)
X    || die "can't rename $new to $COPS_CONFIG: $!";
X
X
open COPS_CONFIG || die "can't re-open $COPS_CONFIG: $!";
($SH_CONF = $COPS_CONFIG) =~ s/\.pl$/.sh/;
open (SH_CONF, ">$SH_CONF") || die "can't create $SH_CONF: $!";
X
while (<COPS_CONFIG>) {
X    s/^\$//;
X    print SH_CONF;
} 
close SH_CONF || die "can't close $SH_CONF: $!";
X
Xexit 0;
X
#############
X
sub getpath {
X    local($cmd) = @_;
X    local($path);
X
X    for (@all_dirs) {
X	return $path if -x ($path = "$_/$cmd");
X    } 
X    '';
} 
SHAR_EOF
chmod 0700 p-cops.alpha/reconfig.pl ||
echo 'restore of p-cops.alpha/reconfig.pl failed'
Wc_c="`wc -c < 'p-cops.alpha/reconfig.pl'`"
test 3296 -eq "$Wc_c" ||
	echo 'p-cops.alpha/reconfig.pl: original size 3296, current size' "$Wc_c"
rm -f _shar_wnt_.tmp
fi
# ============= p-cops.alpha/user.chk ==============
if test -f 'p-cops.alpha/user.chk' -a X"$1" != X"-c"; then
	echo 'x - skipping p-cops.alpha/user.chk (File already exists)'
	rm -f _shar_wnt_.tmp
else
> _shar_wnt_.tmp
echo 'x - extracting p-cops.alpha/user.chk (Text)'
sed 's/^X//' << 'SHAR_EOF' > 'p-cops.alpha/user.chk' &&
#!/bin/sh  # need to mention perl here to avoid recursion
#
#  This combines user.chk and home.chk.  It searches for home directories
# and various user startup files for world writability, as well as flagging
# any .rhosts and .netrc files that are readable.  You can change the
# files checked by changing @ftable and @readables, respectively.
# 
#  NOTE:
# if you know where perl is and your system groks #!, put its
# pathname at the top to make this a tad faster.
#
# the following magic is from the perl man page
# and should work to get us to run with perl 
SHAR_EOF
true || echo 'restore of p-cops.alpha/user.chk failed'
fi
echo 'End of alpha p-cops part 2'
echo 'File p-cops.alpha/user.chk is continued in part 3'
echo 3 > _shar_seq_.tmp
exit 0