Article 7765 of comp.lang.perl:
Xref: feenix.metronet.com alt.security:3701 comp.security.misc:3979 comp.unix.admin:7010 comp.mail.sendmail:3055 comp.lang.perl:7765 alt.sources:2241
Newsgroups: alt.security,comp.security.misc,comp.unix.admin,comp.mail.sendmail,comp.lang.perl,alt.sources
Path: feenix.metronet.com!news.utdallas.edu!wupost!howland.reston.ans.net!paladin.american.edu!darwin.sura.net!sgiblab!a2i!dhesi
From: [email protected] (Rahul Dhesi)
Subject: perl prog mailer for sendmail
Message-ID: <[email protected]>
Followup-To: alt.security,comp.security.misc,comp.unix.admin,comp.mail.sendmail,comp.lang.perl,alt.sources.d
Keywords: CA-93:16.sendmail.vulnerabilty
Sender: [email protected] (Usenet News)
Nntp-Posting-Host: bolero
Organization: a2i network
References: <[email protected]>
Date: Tue, 9 Nov 1993 20:45:46 GMT
Lines: 227

Checksum: 3702811482 (verify with 'brik')
Submitter: Rahul Dhesi <[email protected]>
Archive-name: perl-source/dhesi/a2iprog

#! /local/bin/perl
# (C) Copyright 1993 Rahul Dhesi, All rights reserved.
# Permission for copying and creation of derivative works is granted,
# provided this copyright notice is preserved, to anybody who
# does not discriminate against the copyright owner.

# $Source: /etc/ida/RCS/a2iprog,v $
# $Id: a2iprog,v 1.12 1993/11/09 12:43:02 dhesi Exp $
#
# A sendmail restricted prog mailer in perl.
#
# This perl script allows some degree of security in invoking programs
# from sendmail's prog mailer.  The reason I wrote it was because smrsh
# requires the argument supplied to it to be the name of a program.  This
# is more restrictive than /bin/sh, which simply wants any command line.
#
# The sendmail problem is that the prog mailer can be invoked on commands
# that are neither in a user's .forward file nor in the /etc/aliases
# file.  The attached program will allow any command to be executed if
# any line in the user's .forward file matches it.  This allows any valid
# shell command to be executed.  If the command is not found in the
# .forward file, it may still be executed if it matches an internal list
# of allowed commands.
#
#
# CAVEATS:
# - Before attempting to match command name with lines in .forward file,
#   double quotes, whitespace, and semicolons in both
#   are stripped.  Then the match is considered to be successful if
#   the command being executed is a substring of any line in the
#   .forward file.  A very clever cracker could perhaps
#   find a string that would pass this test and still allow a break-in.
#   The reason for stripping quotes and whitespace is to allow a simple
#   string comparison do be done without actually parsing lines in the
#   .forward file.  The substring match is done so that .forward lines
#   with multiple comma-separated entries will continue to work.
#   Semicolon is stripped because I was seeing a2iprog receive commands
#   in which an embedded semicolon had been lost (perhaps because sendmail
#   strips it out).
# - Although this script appears to work, it has been only briefly tested.
#   Bugs might be lurking!  Use at your own risk.

# Commands allowed even if not in .forward file.  These are perl patterns
# (but note that '/' is a literal here).
@CMDLIST = (
  '/usr/lib/sendmail.*',
  '/local/lib/market/get\.market',
);

$RCSHEADER =
  '$Source: /etc/ida/RCS/a2iprog,v $' .
  "\n" .
  '$Id: a2iprog,v 1.12 1993/11/09 12:43:02 dhesi Exp $';

$myname = 'a2iprog';
$usage = "usage: $myname [-c] [-u user] [-vtx] arg ... (or -h for help)";

# standard error message
$cannot = "Cannot mail directly to programs";

# ignore initial -c without invoking getopts
($ARGV[0] eq '-c') && shift;

if ($ARGV[0] =~ "^-.+" ) {
  require "getopts.pl";
  &Getopts("vtxhcu:");
}

# $opt_c will be ignored
$debug = $opt_x;
$trace = $opt_t;
$verbose = $debug || $trace || $opt_v;

# test
## $verbose = 1;

if ($opt_h) {
  &givehelp();
  exit(0);
}

(@ARGV != 1) && &usage_error;
$cmd = $ARGV[0];

# re-initialize environment
$PATH = $ENV{'PATH'};
$HOME = $ENV{'HOME'};
undef %ENV;
$ENV{'PATH'} = $PATH;
$ENV{'HOME'} = $HOME;
$ENV{'IFS'} = ' ';

# If -u given, pretend to have that username or uid, else use our
# effective uid.  THIS IS FOR TESTING PATTERN MATCHING.  IT DOES NOT
# ACTUALLY CHANGE UID.
if ($opt_u) {
  if ($opt_u =~ /^\d+$/) {
     $uid = $opt_u;
  } else {
     # convert name to numeric uid
     $uid = (getpwnam($opt_u))[2];
  }
} else {
  $uid = $>;
}

# get our name and home directory
(
  ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) =
     (getpwuid($uid))
) || &err("Who are you?");

if ($verbose) {
  print "user: $user\n";
  print "dir: $dir\n";
}
print "command: [$cmd]\n";

chdir($dir) || &err("Can't cd to $dir: $!");

# Now check to see if command is allowed

if (&ok_in_forward($cmd) || &ok_in_list($cmd)) {
  $verbose && print "exec: /bin/sh -c $cmd\n";
  $trace && exit(0);
  # disallow real or effective uid 0
  (($< == 0) || ($> == 0)) && &err("What are you?");
  exec '/bin/sh', '-c', $cmd;
  &err("exec failed: $!");
}
&err($cannot);

sub ok_in_forward {
  local($cmd) = @_;
  local($item);
  if (!open(F, ".forward")) {
     $verbose && print "cannot open .forward\n";
     return 0;
  }
  $verbose && print ".forward open\n";

  # squeeze out blanks, double quotes, semicolons
  $cmd =~ s/[ ";]//g;
  $verbose && print "checking command [$cmd]\n";
  while (<F>) {
     chop;
     $fwline = $_;
     $verbose && print ".forward line: $fwline\n";
     # squeeze out blanks, double quotes, semicolons
     $fwline =~ s/[ ";]//g;
     ($cmdpat = $cmd)  =~ s/(\W)/\\$1/g;       # convert command to pattern
     if ($fwline =~ /$cmdpat/) { # if cmd is embedded inside forw line
        $verbose && print "matched item [$cmd]\n";
        return 1;
     } elsif ($verbose) {
        print "mismatch [$fwline] [$cmd]\n";
        print "pattern was: [$cmdpat]\n";
     }
  }
  $verbose && print ".forward check failed\n";
  0;
}

# check against internal list
sub ok_in_list {
  local($cmd) = @_;
  local($item);
  for $item (@CMDLIST) {
     if ($cmd =~ /^$item$/) {
        if ($verbose) {
           print "cmd [$cmd] item [$item]\n";
           "cmdlist check ok\n";
        }
        return 1;
     }
  }
  0;
}

sub usage_error {
  local($msg) = @_;
  if ($msg) {
     die "$msg\n";
  } else {
     die "$usage\n";
  }
}

sub givehelp {
  ## require 'local/page.pl';
  ## &page(<<EOF);
  print <<EOF;
$usage

This is a restricted prog mailer for use from sendmail.  It allows
execution of a shell command only if one of the following two
conditions holds.

1.  The effective user's .forward file contains that command.
2.  The command matches a pattern in a hard-coded list inside $myname.

Refer to source code for more details about string comparisons.

Based on ideas in smrsh.

  -u uid       Pretend to be this username or uid (for testing pattern
               matching -- does not actually change to that uid).
  -c           Ignored, for compatibility with sh and smrsh.
  -v           Be verbose.
  -t           Trace only -- show what would be done but don't do it.
  -x           Enable debugging -- for program maintainers.

$RCSHEADER
EOF
}

# Error exit -- always exit with error code.
# Caller must include trailing newline in message.
sub err {
  @_ && print "$_[0]\n";
  exit(1);
}
# END OF SCRIPT