#!/bin/sh -- need to mention perl here to avoid recursion
'true' || eval 'exec perl -S $0 $argv:q';
eval '(exit $?0)' && eval 'exec perl -S $0 ${1+"$@"}'
& eval 'exec /usr/users/df/bin/perl.sun4 -S $0 $argv:q'
        if 0;
# & eval 'exec /usr/local/bin/perl -S $0 $argv:q'
#
# kuang - rule based analysis of Unix security
#
# Perl version by Steve Romig of the CIS department, The Ohio State
# University, October 1990. 
# 
# Based on the shell script version by Dan Farmer from his COPS
# package, which in turn is based on a shell version by Robert
# Baldwin. 
#
#-----------------------------------------------------------------------------
# Players:
#	romig	Steve Romig, romig@cis.ohio-state.edu
#	tjt	Tim Tessin, tjt@cirrus.com
#
# History:
# 4/25/91  tjt, romig	Various fixes to filewriters (better messages about 
#			permission problems) and don't update the DBM cache 
#			with local file info.
# 11/1/90  romig	Major rewrite - generic lists, nuking get_entry 
#			and put_entry, moved rules to separate file.
#

#
# Options
#
# -l		list uid's that can access the given target, directly
#		or indirectly
# -d		debug
# -V 		verbose
#
# -k file	load the list of known CO's
# -f file	preload file information from the named file.
# -p file	preload passwd info from the named file.
# -Y		preload passwd info from ypcat + /etc/passwd
# -g group	preload group info from the named file.
# -G		preload group info from ypcat + /etc/group
# 
# 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.
#

$options = "ldVk:p:g:f:YG";
$usage = "usage: kuang [-l] [-d] [-v] [-k known] [-f file] [-Y] [-G] [-p passwd] [-g group] [u.username|g.groupname]\n";

$add_files_to_cache = 1;		# Whether to update the %files cache
					# with local file info or not.

#
# Terminology:
#
#   An "op" is an operation, such as uid, gid, write, or replace. 
#   'uid' means to gain access to some uid, 'gid' means to gain access 
#   to some gid.  'write' and 'replace' refer to files - replace means
#   that we can delete a file and replace it with a new one somehow
#   (for example, if we could write the directory it is in).
#
#   An object is a uid, gid or pathname.  
#
#   A Controlling Operation (CO) is a (operation, object) pair
#   represented as "op object": "uid 216" (become uid 216) or "replace
#   /.rhosts" (replace file /.rhosts).  These are represented
#   internally as "c value", where "c" is a character representing an
#   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.
#

do 'yagrip.pl' ||
  die "can't do yagrip.pl";

# do 'pwgrid.pl' ||
#   die "can't do pwgrid.pl";
do 'pass.cache.pl' ||
  die "can't do pass.cache.pl";

do 'rules.pl' ||
  die "can't do rules.pl";

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

sub canonicalize {
    local($string) = @_;
    local($op, $value);

    if ($string =~ /^([ugrw]) ([^ \t\n]+)$/) { # of form "op value"
	$op = $1;
	$value = $2;
    } elsif ($string =~ /^[^ \t\n]+$/) {       # of form "value"
        $value = $string;
	$op = "u";
    } else {
	return();
    }

    if ($op eq "u" && $value =~ /^[^0-9]+$/) { # user name, not ID
        if (defined($uname2uid{$value})) {
	    $value = $uname2uid{$value};
	} else {
	    printf(stderr "There's no user named '%s'.\n", $value);
	    return();
	}
    } elsif ($op eq "g" && $value =~/^[^0-9]+$/) {
	if (defined($gname2gid{$value})) {
	    $value = $gname2gid{$value};
	} else {
	    printf(stderr "There's no group named '%s'.\n", $value);
	    return();
	}
    }

    return($op, $value);
}

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

sub preload_file_info {
    local($count, $f_type, $f_uid, $f_gid, $f_mode, $f_name);

    if (defined($opt_d)) {
	printf("loading file info...\n");
    }

    if (-f "$opt_f.dir") {
	$add_files_to_cache = 0;

	dbmopen(files, $opt_f, 0644) ||
	  die sprintf("can't open DBM file '%s'", $opt_f);
    } else {
	open(FILEDATA, $opt_f) || 
	  die sprintf("kuang: can't open '%s'", $opt_f);

	$count = 0;
	while (<FILEDATA>) {
	    $count++;

	    chop;
	    ($f_type, $f_uid, $f_gid, $f_mode, $f_name) = split;
	    
	    if ($count % 1000 == 0) {
		printf("line $count, reading entry for $f_name\n");
	    }
	    $files{$f_name} = join(' ', $f_uid, $f_gid, $f_mode);
	}

	close(FILEDATA);
    }
}

#
# Preload the known information.  Reads data from a file, 1 entry per line,
# each entry is a CO that we "know" can be used.
#

sub preload_known_info {
    local($file_name) = @_;
    local($op, $value, $co);

    open(FILE, $file_name) ||
      die sprintf("kuang: can't open '%s'", $file_name);

  known_loop:
    while (<FILE>) {
	chop;
	if ((($op, $value) = &canonicalize($_)) == 2) {
	    $co = sprintf("%s %s", $op, $value);
	    $known{$co} = 1;
	} else {
	    printf(stderr "kuang: invalid entry in known list: line %d '%s'.\n",
		   $.,
		   $_);
	}
    }

    close(FILE);
}
    

#
# Do various initialization type things.
#

sub init_kuang {
    local($which, $name, $uid, $gid);
    local($op, $value, $co);

    #
    # Deal with args...
    #

    &getopt($options) ||
      die $usage;

    if ($#ARGV == -1) {
	push(@ARGV, "u root");
    }

    #
    # Preload anything...
    #
    if (defined($opt_f)) {
	&preload_file_info();
    }

    if (defined($opt_d)) {
	printf("load passwd info...\n");
    }

    if (defined($opt_p)) {
	if (defined($opt_Y)) {
	    printf(stderr "You can only specify one of -p or -P, not both.\n");
	    exit(1);
	}

	&load_passwd_info(0, $opt_p);
    } elsif (defined($opt_Y)) {
	&load_passwd_info(0);
    } else {
	&load_passwd_info(1);
    }

    if (defined($opt_d)) {
	printf("load group info...\n");
    }

    if (defined($opt_g)) {
	if (defined($opt_G)) {
	    printf(stderr "You can only specify one of -g or -G, not both.\n");
	    exit(1);
	}

	&load_group_info(0, $opt_g);
    } elsif (defined($opt_G)) {
	&load_group_info(0);
    } else {
	&load_group_info(1);
    }

    #
    # Need some of the password and group stuff.  Suck in passwd and 
    # group info, store by uid and gid in an associative array of strings
    # which consist of fields corresponding to the passwd and group file 
    # entries (and what the heck, we'll use : as a delimiter also...:-)
    #
    $uname2shell{"OTHER"} = "";
    $uname2dir{"OTHER"} = "";
    $uname2uid{"OTHER"} = -1;
    $uid2names{-1} = "OTHER";

    $known{"u -1"} = 1;		# We can access uid OTHER

    if (defined($opt_k)) {
	&preload_known_info($opt_k);
    }

    #
    # Create the target list from the remaining (non-option) args...
    #
    while ($#ARGV >= 0) {
	$elt = pop(@ARGV);
	if ((($op, $value) = &canonicalize($elt)) == 2) {
	    $co = sprintf("%s %s", $op, $value);
	    push(@targets, $co);
	} else {
	    printf(stderr "target '%s' isn't of correct form\n", $elt);
	}
    }
}


#
# Call this to set things up for a new target.  Resets old, new, beendone 
# and accessible.  
#
sub set_target {
    local($target) = @_;

    @old = ();
    @new = ();
    %beendone = ();
    @accessible = ();
# fixme: reset known?

    if ($target =~ /^([ugrw]) ([^ \t]+)$/) {
	&addto($1, $2);
	return(0);
    } else {
	printf(stderr "kuang: bad target '%s'\n", $target);
	return(1);
    }
}

#
# Break a CO into an (operation, value) pair and return it.  If it
# isn't in "operation value" form, return ().
#
sub breakup {
    local($co) = @_;
    local($operation, $value);

    if ($co =~ /^([ugrw]) ([^ \t]+)$/) {
	$operation = $1;
	$value = $2;
    } else {
	printf(stderr "Yowza, breakup failed on '%s'\n",
		$co);
	exit(1);
    }

    return($operation, $value);
}

#
# 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 {
    local($name) = @_;
    local($tmp, $mode, $uid, $gid, $other);
    
    #
    # Check the file cache - avoid disk lookups for performance and 
    # to avoid shadows...
    #
    if (defined($files{$name})) {
	$cache_hit++;
	
	($uid, $gid, $mode, $tmp) = split(/ /, $files{$name});
    } else {
	$cache_miss++;

	unless (-e $name) {
	    if ($add_files_to_cache) {
		$files{$name} = "";
	    }
	    # ENOTDIR = 20 
	    ($! == 20) && print "Warning: Illegal Path: '$name'\n";
	    # EACCES = 13
	    ($! == 13) && print "Warning: Permission Denied: '$name'\n";
	    # all values are returned "" here.
	    return;
	}

	($tmp,$tmp,$mode,$tmp,$uid,$gid) = stat(_);
	if ($add_files_to_cache) {
	    $files{$name} = join(' ', "$uid", "$gid", "$mode");
	}
    }

    if (($mode & 020) != 020) {
	$gid = "";
    }
    
    if (($mode & 02) == 02) {
	$other = 1;
    } else {
	$other = 0;
    }

    return($uid, $gid, $other);
}


sub ascii_plan {
    local(@plan) = @_;
    local($op, $value, $result);

    for ($i = $#plan; $i >= 0; $i--) {
	($op, $value) = &breakup($plan[$i]);

      case: 
	{
	    if ($op eq "g") {
		$op = "grant gid";
		last case;
	    }

	    if ($op eq "u") {
		$op = "grant uid";
		last case;
	    }

	    if ($op eq "r") {
		$op = "replace";
		last case;
	    }

	    if ($op eq "w") {
		$op = "write";
		last case;
	    }

	    printf(stderr "Bad op '%s' in plan '%s'\n",
		   $op,
		   join(';', @plan));
	    last case;
	}

	$result .= "$op $value ";
    }

    return($result);
}

#
# Add a plan to the list of plans to check out.
#
sub addto {
    local($op, $value, @plan) = @_;
    local($co);

    $co = sprintf("%s %s",
		  $op,
		  $value);

    #
    # See if the op and value is "uid root" - if so, and if the @plan 
    # isn't empty, then don't bother checking - if the target isn't root, 
    # its silly to pursue plans that require becoming root since if we can 
    # become root, we can become anything.  If the target is root, then 
    # this would be a loop anyway.
    #
    if ($op eq "u" && $value eq "0" && $#plan >= 0) {
	if (defined($opt_d)) {
	    printf("addto: aborted root plan '%s'\n",
		   &ascii_plan(@plan, $co));
	}
	return;
    }

    #
    # See whether there's an entry for $co in the known list.
    # If so - success, we've found a suitable breakin plan.
    #
    # Yes, we want to check to see whether the whole Controlling Operation 
    # is one that is known to us, rather than just the object.  I
    # might have a hole that allows me to "replace /bin/foo" which is
    # somewhat different than "write /bin/foo"  
    #
    if (! defined($opt_l) && defined($known{$co})) {
	printf("Success! %s\n",
	       &ascii_plan(@plan, $co));
    }

    #
    # Check for loops -- if the new CO is part of the plan that we're
    # adding it to, this is a loop.
    #
    foreach $entry (@plan) {
	if ($entry eq $co) {
	    if (defined($opt_d)) {
		printf("addto: aborted loop in plan '%s'\n",
		       &ascii_plan(@plan, $co));
	    }
	    return;
	}
    }

    #
    # Add this CO to the plan array...
    #
    push(@plan, $co);

    #
    # Make an ascii version of sorts...
    #
    $text_plan = join($;, @plan);

    #
    # Check to see if the new plan has been done.
    #
    if (defined($beendone{$text_plan})) {
	if (defined($opt_d)) {
	    printf("addto: plan's been done - '%s'\n",
		   &ascii_plan(@plan));
	}
	return;
    }

    #
    # If we made it this far, its a new plan and isn't a loop.  
    #

    #
    # Add to the beendone list...
    #
    $beendone{$text_plan} = 1;

    #
    # Add to new plan list...
    #
    push(@new, $text_plan);

    if (defined($opt_V)) {
	printf("addto: %s\n", 
	       &ascii_plan(@plan));
    }

    #
    # If this is a uid goal, then add the plan to the accessible list.
    #
    if ($op eq "u" && $value ne "0" && defined($opt_l)) {
	push(@accessible, $value);
    }
}

#
#----------------------------------------------------------------------
#Main program follows...initialize and loop till we're done.
#

&init_kuang();

target_loop:
foreach $target (@targets) {
    if (&set_target($target)) {
	next target_loop;
    }

    while ($#new >= 0) {
	@old = @new;
	@new = ();

	foreach $t_plan (@old) {
	    @plan = split(/\034/, $t_plan);
	    ($op, $value) = &breakup($plan[$#plan]);

	    &apply_rules($op, $value, @plan);
	}
    }

    if (defined($opt_l)) {
	foreach $elt (@accessible) {
	    printf("$elt\n");
	}
    }
}

if (defined($opt_d)) {
    printf("File info cache hit/access ratio: %g\n", 
   	    ($cache_hit + $cache_miss > 0) 
	        ? $cache_hit / ($cache_hit + $cache_miss)
	        : 0.0);
}

1;
