home *** CD-ROM | disk | FTP | other *** search
/ InfoMagic Internet Tools 1993 July / Internet Tools.iso / RockRidge / security / cops / cops_104 / perl / kuang < prev    next >
Encoding:
Text File  |  1992-03-10  |  15.0 KB  |  645 lines

  1. #!/bin/sh -- need to mention perl here to avoid recursion
  2. 'true' || eval 'exec perl -S $0 $argv:q';
  3. eval '(exit $?0)' && eval 'exec perl -S $0 ${1+"$@"}'
  4. & eval 'exec /usr/users/df/bin/perl.sun4 -S $0 $argv:q'
  5.         if 0;
  6. # & eval 'exec /usr/local/bin/perl -S $0 $argv:q'
  7. #
  8. # kuang - rule based analysis of Unix security
  9. #
  10. # Perl version by Steve Romig of the CIS department, The Ohio State
  11. # University, October 1990. 
  12. # Based on the shell script version by Dan Farmer from his COPS
  13. # package, which in turn is based on a shell version by Robert
  14. # Baldwin. 
  15. #
  16. #-----------------------------------------------------------------------------
  17. # Players:
  18. #    romig    Steve Romig, romig@cis.ohio-state.edu
  19. #    tjt    Tim Tessin, tjt@cirrus.com
  20. #
  21. # History:
  22. # 4/25/91  tjt, romig    Various fixes to filewriters (better messages about 
  23. #            permission problems) and don't update the DBM cache 
  24. #            with local file info.
  25. # 11/1/90  romig    Major rewrite - generic lists, nuking get_entry 
  26. #            and put_entry, moved rules to separate file.
  27. #
  28.  
  29. #
  30. # Options
  31. #
  32. # -l        list uid's that can access the given target, directly
  33. #        or indirectly
  34. # -d        debug
  35. # -V         verbose
  36. #
  37. # -k file    load the list of known CO's
  38. # -f file    preload file information from the named file.
  39. # -p file    preload passwd info from the named file.
  40. # -Y        preload passwd info from ypcat + /etc/passwd
  41. # -g group    preload group info from the named file.
  42. # -G        preload group info from ypcat + /etc/group
  43. # NOTE:
  44. #   If you know where perl is and your system groks #!, put its
  45. # pathname at the top to make this a tad faster.
  46. #
  47. # the following magic is from the perl man page
  48. # and should work to get us to run with perl 
  49. # even if invoked as an sh or csh or foosh script.
  50. # notice we don't use full path cause we don't
  51. # know where the user has perl on their system.
  52. #
  53.  
  54. $options = "ldVk:p:g:f:YG";
  55. $usage = "usage: kuang [-l] [-d] [-v] [-k known] [-f file] [-Y] [-G] [-p passwd] [-g group] [u.username|g.groupname]\n";
  56.  
  57. $add_files_to_cache = 1;        # Whether to update the %files cache
  58.                     # with local file info or not.
  59.  
  60. #
  61. # Terminology:
  62. #
  63. #   An "op" is an operation, such as uid, gid, write, or replace. 
  64. #   'uid' means to gain access to some uid, 'gid' means to gain access 
  65. #   to some gid.  'write' and 'replace' refer to files - replace means
  66. #   that we can delete a file and replace it with a new one somehow
  67. #   (for example, if we could write the directory it is in).
  68. #
  69. #   An object is a uid, gid or pathname.  
  70. #
  71. #   A Controlling Operation (CO) is a (operation, object) pair
  72. #   represented as "op object": "uid 216" (become uid 216) or "replace
  73. #   /.rhosts" (replace file /.rhosts).  These are represented
  74. #   internally as "c value", where "c" is a character representing an
  75. #   operation (u for uid, g for gid, r for replace, w for write) and
  76. #   value is a uid, gid or pathname.
  77. #
  78. #   A plan is a chain of CO's that are connected to each other.  If
  79. #   /.login were writeable by uid 216, we might have a plan such as:
  80. #
  81. #    uid 216 => write /.login => uid 0
  82. #
  83. #   which means (in English) "if we can become uid 216, then write 
  84. #   /.login which gives you access to uid 0 (when root next logs in)."
  85. #   Plans are represented in several ways: as arrays:
  86. #
  87. #    ("u 0", "w /.login", "u 216")
  88. #
  89. #   Note that the order is reversed.  As a string:
  90. #
  91. #    "u 0\034w /.login\034u 216"
  92. #
  93. #   The target is the object that we are trying to gain (a uid, gid or
  94. #   file, typically u.root or some other UID).
  95. #
  96. # Data Structures
  97. #
  98. #   %known        An assocc array, indexed by CO.  This lists
  99. #            the COs that we already have access to.  If we
  100. #                       find a plan that leads from a CO in the known
  101. #                       list to the target, we've succeeded in
  102. #                       finding a major security flaw.  
  103. #
  104. #   @new        An array of plans that are to be evaluated in
  105. #            the next cycle. 
  106. #
  107. #   @old        An array of plans that we are currently
  108. #            evaluating. 
  109. #
  110. #   %beendone        An assoc array that lists the plans that have
  111. #            already been tried.  Used to prevent loops.
  112. #
  113. #   @accessible        An array of the uids that can reach the
  114. #            target. 
  115. #
  116. #   %files        An assoc array, indexed by file name, contains
  117. #            cached file info.  value is of form "uid gid
  118. #            mode". 
  119. #
  120. # From pwgrid:
  121. #
  122. #   %uname2shell    Assoc array, indexed by user name, values are
  123. #            shells. 
  124. #
  125. #   %uname2dir        Assoc array, indexed by user name, values are
  126. #            home directories.
  127. #
  128. #   %uname2uid        Assoc array, indexed by name, values are uids.
  129. #            
  130. #   %uid2names        Assoc array, indexed by uid, value is list of
  131. #            user names with that uid, in form "name name
  132. #            name...". 
  133. #
  134. #   %gid2members    Assoc array, indexed by gid, value is list of
  135. #            group members (user names).
  136. #
  137. #   %gname2gid        Assoc array, indexed by group name, values are
  138. #            matching gids.
  139. #
  140. #   %gid2names        Assoc array, indexed by gid, values are
  141. #            matching group names.
  142. #
  143.  
  144. do 'yagrip.pl' ||
  145.   die "can't do yagrip.pl";
  146.  
  147. # do 'pwgrid.pl' ||
  148. #   die "can't do pwgrid.pl";
  149. do 'pass.cache.pl' ||
  150.   die "can't do pass.cache.pl";
  151.  
  152. do 'rules.pl' ||
  153.   die "can't do rules.pl";
  154.  
  155.  
  156. #
  157. # Turns a string of the form "operation value" or "value" into
  158. # standard "CO" form ("operation value").  Converts user or group
  159. # names into corresponding uid and gid values. 
  160. #
  161. # Returns nothing if it isn't parseable.
  162. #
  163.  
  164. sub canonicalize {
  165.     local($string) = @_;
  166.     local($op, $value);
  167.  
  168.     if ($string =~ /^([ugrw]) ([^ \t\n]+)$/) { # of form "op value"
  169.     $op = $1;
  170.     $value = $2;
  171.     } elsif ($string =~ /^[^ \t\n]+$/) {       # of form "value"
  172.         $value = $string;
  173.     $op = "u";
  174.     } else {
  175.     return();
  176.     }
  177.  
  178.     if ($op eq "u" && $value =~ /^[^0-9]+$/) { # user name, not ID
  179.         if (defined($uname2uid{$value})) {
  180.         $value = $uname2uid{$value};
  181.     } else {
  182.         printf(stderr "There's no user named '%s'.\n", $value);
  183.         return();
  184.     }
  185.     } elsif ($op eq "g" && $value =~/^[^0-9]+$/) {
  186.     if (defined($gname2gid{$value})) {
  187.         $value = $gname2gid{$value};
  188.     } else {
  189.         printf(stderr "There's no group named '%s'.\n", $value);
  190.         return();
  191.     }
  192.     }
  193.  
  194.     return($op, $value);
  195. }
  196.  
  197.  
  198. #
  199. # Preload file information from a text file or DBM database.  
  200. # If $opt_f.dir exists, then we just shadow %files from a DBM
  201. # database.  Otherwise, open the file and read the entries into 
  202. # %files.  
  203. #
  204. # $add_files_to_cache is set to 0 if we get the info from 
  205. # DBM since we wouldn't want to pollute update our DBM cache
  206. # with local file info which wouldn't apply to other hosts.
  207. #
  208.  
  209. sub preload_file_info {
  210.     local($count, $f_type, $f_uid, $f_gid, $f_mode, $f_name);
  211.  
  212.     if (defined($opt_d)) {
  213.     printf("loading file info...\n");
  214.     }
  215.  
  216.     if (-f "$opt_f.dir") {
  217.     $add_files_to_cache = 0;
  218.  
  219.     dbmopen(files, $opt_f, 0644) ||
  220.       die sprintf("can't open DBM file '%s'", $opt_f);
  221.     } else {
  222.     open(FILEDATA, $opt_f) || 
  223.       die sprintf("kuang: can't open '%s'", $opt_f);
  224.  
  225.     $count = 0;
  226.     while (<FILEDATA>) {
  227.         $count++;
  228.  
  229.         chop;
  230.         ($f_type, $f_uid, $f_gid, $f_mode, $f_name) = split;
  231.         
  232.         if ($count % 1000 == 0) {
  233.         printf("line $count, reading entry for $f_name\n");
  234.         }
  235.         $files{$f_name} = join(' ', $f_uid, $f_gid, $f_mode);
  236.     }
  237.  
  238.     close(FILEDATA);
  239.     }
  240. }
  241.  
  242. #
  243. # Preload the known information.  Reads data from a file, 1 entry per line,
  244. # each entry is a CO that we "know" can be used.
  245. #
  246.  
  247. sub preload_known_info {
  248.     local($file_name) = @_;
  249.     local($op, $value, $co);
  250.  
  251.     open(FILE, $file_name) ||
  252.       die sprintf("kuang: can't open '%s'", $file_name);
  253.  
  254.   known_loop:
  255.     while (<FILE>) {
  256.     chop;
  257.     if ((($op, $value) = &canonicalize($_)) == 2) {
  258.         $co = sprintf("%s %s", $op, $value);
  259.         $known{$co} = 1;
  260.     } else {
  261.         printf(stderr "kuang: invalid entry in known list: line %d '%s'.\n",
  262.            $.,
  263.            $_);
  264.     }
  265.     }
  266.  
  267.     close(FILE);
  268. }
  269.     
  270.  
  271. #
  272. # Do various initialization type things.
  273. #
  274.  
  275. sub init_kuang {
  276.     local($which, $name, $uid, $gid);
  277.     local($op, $value, $co);
  278.  
  279.     #
  280.     # Deal with args...
  281.     #
  282.  
  283.     &getopt($options) ||
  284.       die $usage;
  285.  
  286.     if ($#ARGV == -1) {
  287.     push(@ARGV, "u root");
  288.     }
  289.  
  290.     #
  291.     # Preload anything...
  292.     #
  293.     if (defined($opt_f)) {
  294.     &preload_file_info();
  295.     }
  296.  
  297.     if (defined($opt_d)) {
  298.     printf("load passwd info...\n");
  299.     }
  300.  
  301.     if (defined($opt_p)) {
  302.     if (defined($opt_Y)) {
  303.         printf(stderr "You can only specify one of -p or -P, not both.\n");
  304.         exit(1);
  305.     }
  306.  
  307.     &load_passwd_info(0, $opt_p);
  308.     } elsif (defined($opt_Y)) {
  309.     &load_passwd_info(0);
  310.     } else {
  311.     &load_passwd_info(1);
  312.     }
  313.  
  314.     if (defined($opt_d)) {
  315.     printf("load group info...\n");
  316.     }
  317.  
  318.     if (defined($opt_g)) {
  319.     if (defined($opt_G)) {
  320.         printf(stderr "You can only specify one of -g or -G, not both.\n");
  321.         exit(1);
  322.     }
  323.  
  324.     &load_group_info(0, $opt_g);
  325.     } elsif (defined($opt_G)) {
  326.     &load_group_info(0);
  327.     } else {
  328.     &load_group_info(1);
  329.     }
  330.  
  331.     #
  332.     # Need some of the password and group stuff.  Suck in passwd and 
  333.     # group info, store by uid and gid in an associative array of strings
  334.     # which consist of fields corresponding to the passwd and group file 
  335.     # entries (and what the heck, we'll use : as a delimiter also...:-)
  336.     #
  337.     $uname2shell{"OTHER"} = "";
  338.     $uname2dir{"OTHER"} = "";
  339.     $uname2uid{"OTHER"} = -1;
  340.     $uid2names{-1} = "OTHER";
  341.  
  342.     $known{"u -1"} = 1;        # We can access uid OTHER
  343.  
  344.     if (defined($opt_k)) {
  345.     &preload_known_info($opt_k);
  346.     }
  347.  
  348.     #
  349.     # Create the target list from the remaining (non-option) args...
  350.     #
  351.     while ($#ARGV >= 0) {
  352.     $elt = pop(@ARGV);
  353.     if ((($op, $value) = &canonicalize($elt)) == 2) {
  354.         $co = sprintf("%s %s", $op, $value);
  355.         push(@targets, $co);
  356.     } else {
  357.         printf(stderr "target '%s' isn't of correct form\n", $elt);
  358.     }
  359.     }
  360. }
  361.  
  362.  
  363. #
  364. # Call this to set things up for a new target.  Resets old, new, beendone 
  365. # and accessible.  
  366. #
  367. sub set_target {
  368.     local($target) = @_;
  369.  
  370.     @old = ();
  371.     @new = ();
  372.     %beendone = ();
  373.     @accessible = ();
  374. # fixme: reset known?
  375.  
  376.     if ($target =~ /^([ugrw]) ([^ \t]+)$/) {
  377.     &addto($1, $2);
  378.     return(0);
  379.     } else {
  380.     printf(stderr "kuang: bad target '%s'\n", $target);
  381.     return(1);
  382.     }
  383. }
  384.  
  385. #
  386. # Break a CO into an (operation, value) pair and return it.  If it
  387. # isn't in "operation value" form, return ().
  388. #
  389. sub breakup {
  390.     local($co) = @_;
  391.     local($operation, $value);
  392.  
  393.     if ($co =~ /^([ugrw]) ([^ \t]+)$/) {
  394.     $operation = $1;
  395.     $value = $2;
  396.     } else {
  397.     printf(stderr "Yowza, breakup failed on '%s'\n",
  398.         $co);
  399.     exit(1);
  400.     }
  401.  
  402.     return($operation, $value);
  403. }
  404.  
  405. #
  406. # Get the writers of the named file - return as (UID, GID, OTHER)
  407. # triplet.  Owner can always write, since he can chmod the file if he
  408. # wants. 
  409. #
  410. # (fixme) are there any problems in this sort of builtin rule?  should
  411. # we make this knowledge more explicit?
  412. #
  413. sub filewriters {
  414.     local($name) = @_;
  415.     local($tmp, $mode, $uid, $gid, $other);
  416.     
  417.     #
  418.     # Check the file cache - avoid disk lookups for performance and 
  419.     # to avoid shadows...
  420.     #
  421.     if (defined($files{$name})) {
  422.     $cache_hit++;
  423.     
  424.     ($uid, $gid, $mode, $tmp) = split(/ /, $files{$name});
  425.     } else {
  426.     $cache_miss++;
  427.  
  428.     unless (-e $name) {
  429.         if ($add_files_to_cache) {
  430.         $files{$name} = "";
  431.         }
  432.         # ENOTDIR = 20 
  433.         ($! == 20) && print "Warning: Illegal Path: '$name'\n";
  434.         # EACCES = 13
  435.         ($! == 13) && print "Warning: Permission Denied: '$name'\n";
  436.         # all values are returned "" here.
  437.         return;
  438.     }
  439.  
  440.     ($tmp,$tmp,$mode,$tmp,$uid,$gid) = stat(_);
  441.     if ($add_files_to_cache) {
  442.         $files{$name} = join(' ', "$uid", "$gid", "$mode");
  443.     }
  444.     }
  445.  
  446.     if (($mode & 020) != 020) {
  447.     $gid = "";
  448.     }
  449.     
  450.     if (($mode & 02) == 02) {
  451.     $other = 1;
  452.     } else {
  453.     $other = 0;
  454.     }
  455.  
  456.     return($uid, $gid, $other);
  457. }
  458.  
  459.  
  460. sub ascii_plan {
  461.     local(@plan) = @_;
  462.     local($op, $value, $result);
  463.  
  464.     for ($i = $#plan; $i >= 0; $i--) {
  465.     ($op, $value) = &breakup($plan[$i]);
  466.  
  467.       case: 
  468.     {
  469.         if ($op eq "g") {
  470.         $op = "grant gid";
  471.         last case;
  472.         }
  473.  
  474.         if ($op eq "u") {
  475.         $op = "grant uid";
  476.         last case;
  477.         }
  478.  
  479.         if ($op eq "r") {
  480.         $op = "replace";
  481.         last case;
  482.         }
  483.  
  484.         if ($op eq "w") {
  485.         $op = "write";
  486.         last case;
  487.         }
  488.  
  489.         printf(stderr "Bad op '%s' in plan '%s'\n",
  490.            $op,
  491.            join(';', @plan));
  492.         last case;
  493.     }
  494.  
  495.     $result .= "$op $value ";
  496.     }
  497.  
  498.     return($result);
  499. }
  500.  
  501. #
  502. # Add a plan to the list of plans to check out.
  503. #
  504. sub addto {
  505.     local($op, $value, @plan) = @_;
  506.     local($co);
  507.  
  508.     $co = sprintf("%s %s",
  509.           $op,
  510.           $value);
  511.  
  512.     #
  513.     # See if the op and value is "uid root" - if so, and if the @plan 
  514.     # isn't empty, then don't bother checking - if the target isn't root, 
  515.     # its silly to pursue plans that require becoming root since if we can 
  516.     # become root, we can become anything.  If the target is root, then 
  517.     # this would be a loop anyway.
  518.     #
  519.     if ($op eq "u" && $value eq "0" && $#plan >= 0) {
  520.     if (defined($opt_d)) {
  521.         printf("addto: aborted root plan '%s'\n",
  522.            &ascii_plan(@plan, $co));
  523.     }
  524.     return;
  525.     }
  526.  
  527.     #
  528.     # See whether there's an entry for $co in the known list.
  529.     # If so - success, we've found a suitable breakin plan.
  530.     #
  531.     # Yes, we want to check to see whether the whole Controlling Operation 
  532.     # is one that is known to us, rather than just the object.  I
  533.     # might have a hole that allows me to "replace /bin/foo" which is
  534.     # somewhat different than "write /bin/foo"  
  535.     #
  536.     if (! defined($opt_l) && defined($known{$co})) {
  537.     printf("Success! %s\n",
  538.            &ascii_plan(@plan, $co));
  539.     }
  540.  
  541.     #
  542.     # Check for loops -- if the new CO is part of the plan that we're
  543.     # adding it to, this is a loop.
  544.     #
  545.     foreach $entry (@plan) {
  546.     if ($entry eq $co) {
  547.         if (defined($opt_d)) {
  548.         printf("addto: aborted loop in plan '%s'\n",
  549.                &ascii_plan(@plan, $co));
  550.         }
  551.         return;
  552.     }
  553.     }
  554.  
  555.     #
  556.     # Add this CO to the plan array...
  557.     #
  558.     push(@plan, $co);
  559.  
  560.     #
  561.     # Make an ascii version of sorts...
  562.     #
  563.     $text_plan = join($;, @plan);
  564.  
  565.     #
  566.     # Check to see if the new plan has been done.
  567.     #
  568.     if (defined($beendone{$text_plan})) {
  569.     if (defined($opt_d)) {
  570.         printf("addto: plan's been done - '%s'\n",
  571.            &ascii_plan(@plan));
  572.     }
  573.     return;
  574.     }
  575.  
  576.     #
  577.     # If we made it this far, its a new plan and isn't a loop.  
  578.     #
  579.  
  580.     #
  581.     # Add to the beendone list...
  582.     #
  583.     $beendone{$text_plan} = 1;
  584.  
  585.     #
  586.     # Add to new plan list...
  587.     #
  588.     push(@new, $text_plan);
  589.  
  590.     if (defined($opt_V)) {
  591.     printf("addto: %s\n", 
  592.            &ascii_plan(@plan));
  593.     }
  594.  
  595.     #
  596.     # If this is a uid goal, then add the plan to the accessible list.
  597.     #
  598.     if ($op eq "u" && $value ne "0" && defined($opt_l)) {
  599.     push(@accessible, $value);
  600.     }
  601. }
  602.  
  603. #
  604. #----------------------------------------------------------------------
  605. #Main program follows...initialize and loop till we're done.
  606. #
  607.  
  608. &init_kuang();
  609.  
  610. target_loop:
  611. foreach $target (@targets) {
  612.     if (&set_target($target)) {
  613.     next target_loop;
  614.     }
  615.  
  616.     while ($#new >= 0) {
  617.     @old = @new;
  618.     @new = ();
  619.  
  620.     foreach $t_plan (@old) {
  621.         @plan = split(/\034/, $t_plan);
  622.         ($op, $value) = &breakup($plan[$#plan]);
  623.  
  624.         &apply_rules($op, $value, @plan);
  625.     }
  626.     }
  627.  
  628.     if (defined($opt_l)) {
  629.     foreach $elt (@accessible) {
  630.         printf("$elt\n");
  631.     }
  632.     }
  633. }
  634.  
  635. if (defined($opt_d)) {
  636.     printf("File info cache hit/access ratio: %g\n", 
  637.            ($cache_hit + $cache_miss > 0) 
  638.             ? $cache_hit / ($cache_hit + $cache_miss)
  639.             : 0.0);
  640. }
  641.  
  642. 1;
  643.