|  Using 
              Email to Perform UNIX System Monitoring and Control
 Bob Dilworth
              I work in the IS Department at the Medical College of Ohio in 
              Toledo, Ohio and manage a set of SunTM systems running Solaris 2.5.1 
              that host SeeBeyond's e*Gate interface engine to transfer HL7-formatted 
              data among the institution's various clinical systems. These 
              Sun servers are behind our firewall and, as such, are inaccessible 
              from outside the institution. To facilitate after-hours monitoring/control 
              of the interfaces, I've developed a set of Perl scripts that 
              monitor the critical interfaces at frequent intervals and page me 
              upon encountering problems. I've also developed an extensive 
              HTML-based monitoring/control system that runs on each of the Sun 
              boxes. Additionally, I've installed SSH on the Suns and use 
              an SSH client from my home system into the Sun servers to start 
              an X Window session or open an ssh tunnel to the Web-based monitoring/control 
              system. Obviously, the firewall renders monitoring/controlling the 
              interfaces after hours problematic when I'm away from home.
              After searching for a simple solution to away-from-home interface 
              monitoring and control (short of carting around a laptop), I looked 
              into the possibility of performing some minimal interface query 
              and control functions via email.
              A bit of research revealed that Sendmail provides an easy way 
              to deliver email to a program (i.e., script, executable, etc.) rather 
              than to a user mailbox. Accordingly, I modified the Sendmail aliases 
              file on the Sun servers to point a user alias to a Perl script. 
              The script's job is to parse through the email message looking 
              for predefined interface queue inquiry or manipulation commands, 
              execute a secondary script associated with the command, and return 
              the results to the sender of the email (i.e., me). The Perl control 
              script uses Damian Conway's Parse::RecDescent module to verify 
              the command syntax required to run the interface queue inquiry and 
              manipulation scripts. Syntax errors are also reported via email. 
              Since the Sun servers are behind the firewall, I set up a rule in 
              my Novell Groupwise mailbox to forward any email with a particular 
              string in the subject line to the alias on the appropriate Sun server. 
              The system works great -- I can query, stop, and restart my 
              interface queues just by sending an appropriately formatted email 
              to my work address.
              Sendmail Configuration and Issues
              As mentioned previously, we run our clinical interface engines, 
              production and development, on separate Sun (Solaris 2.5.1) boxes. 
              Both Suns use Sendmail as their mail transfer agent. Because I'm 
              not responsible for any of the institution's network infrastructure, 
              I've never had the occasion to wade into the arcane and cryptic 
              Sendmail configuration file, sendmail.cf -- it's 
              always just worked. My interface queue problem paging system uses 
              mailx to move messages to my Novell Groupwise account, which 
              forwards the messages to my pager and it, too, has always worked. 
              Accordingly, I prepared myself for a slog through the swamp to cajole 
              Sendmail into forwarding my email queue control messages to an appropriate 
              Perl script. I was shocked to discover how easy it was to configure.
              It turns out that /etc/mail/aliases is the config file 
              responsible for forwarding mail to account aliases. Critical to 
              my discovery was the base aliases file itself, still untouched from 
              its journey out of the shrink-wrapped Solaris 2.5.1 box, which contained 
              the following commented-out lines:
              
             
# Aliases to handle mail to programs or files, eg news or vacation
# decode: "|/usr/bin/uudecode"
Could it be that all I had to do was create an alias name and follow 
            it with, say, |/home/myscripts/myperl.pl? I set up a little 
            experiment to find out.  I wrote a dumb little Perl script to read STDIN and write whatever 
              it received to a file. I created an alias and pointed it at the 
              Perl script: atest: "|/home/myscripts/dumb.pl". 
              I ran the newaliases program as instructed by another comment 
              at the top of the aliases file, and sent a test email from my Groupwise 
              account to atest@ifdev.mco.edu. Oddly enough, it worked the 
              first time. The message, including all the MIME headers, were written 
              to the file opened in the Perl script. I also discovered that the 
              script was executed under the system's "daemon" account. 
              This was important to note since the daemon account would have to 
              be given the proper permissions to run the interface queue monitoring 
              scripts.
              The next step was to see whether I could simply echo the email 
              to the sender. Accordingly, I wrote another Perl script that parsed 
              though the email received via STDIN, picked out the sender's 
              address in the MIME header information, and emailed the message 
              back. To circumvent the firewall, I set up a rule in my Novell Groupwise 
              account to forward all email with the character string atest 
              at the beginning of the subject line to the Sun server via atest@ifdev.mco.edu. 
              I then tried sending a message from my Hotmail account to my Groupwise 
              mailbox. This time it didn't work.
              A bit of debugging revealed that the base Sendmail configuration 
              on the Sun box was using "Smart Relaying" of email to 
              the institution's SMTP gateway, which was incorrectly stopping 
              the message from leaving the institution. After a brief and painless 
              conversation with one of our networking gurus, I commented out the 
              following line in sendmail.cf:
              
             
# "Smart" relay host (may be null)
#DSmailhost.$m
A retest was successful. My Hotmail account received the return email 
            from daemon@ifdev.mco.edu.  The next step was to create a generic command executor Perl script 
              and associate it with an appropriate Sendmail alias. Following the 
              long-held UNIX tradition of cryptic command names, I called the 
              script emctl.pl. The "commands" that emctl.pl 
              would execute were the interface engine queue inquiry/manipulation 
              scripts, some of which require their own command-line options. For 
              example, an email containing Command: dosomething thing1 thing2 
              would run a script or program associated with dosomething, 
              pass it parameters thing1 and thing2, and email the 
              results of the execution back to the sender. To implement this functionality, 
              emctl.pl would need to perform the following tasks:
              
              1. Determine the source email address.
              2. Immediately exit if the source address did not match a list 
              of allowed return addresses. Send email notification of the illegal 
              attempt to my internal Groupwise account.
              3. Parse the body of the email looking for command(s) to execute.
              4. Run the scripts associated with those commands or return an 
              error email to the sender if malformed command(s) are encountered.
              5. Return the results of command execution to the sender of the 
              email.
              Determining the Source Email Address
              As described above, I set up a Sendmail alias that pointed to 
              a Perl script instead of a user mailbox. When mail arrives addressed 
              to this alias the Perl script is executed and receives the mail, 
              headers and all, via STDIN. I used the return email's address 
              rather than the reply-to address to ensure that the return email 
              detailing the script results made it back to the location from which 
              it was sent. As a sanity check, here's what the MIME headers 
              passed to the Perl script look like:
              
             
Received: from bogusaddr1
    (bogusaddr1.mco.edu [123.123.78.9])
    by bogusaddr2.mco.edu; Fri, 24 Aug 2001 09:27:39 -0400
Received: by webshield1; id JAA12868; Fri, 24 Aug 2001 09:31:03 -0400 (EDT)
Received: from f155.pav2.hotmail.com(64.4.37.155) by bogusaddr1 via csmap (V1.5)
id srcAAAlqbLY_; Fri, 24 Aug 01 09:31:02 -0400
Received: from mail pickup service by hotmail.com with Microsoft SMTPSVC;
 Fri, 24 Aug 2001 06:27:42 -0700
Received: from 123.123.78.111 by pv2fd.pav2.hotmail.msn.com with HTTP;   
 Fri, 24 Aug 2001 13:27:42 GMT
X-Originating-IP: [123.123.78.111]
Reply-To: myaccount@hotmail.com
From: "Bob Dilworth" <myacctount@hotmail.com>
Note that the "From:" address contains myacctount@hotmail.com 
            bounded by angle brackets. It was simple to code the Perl script to 
            grab the address between the angle brackets, check it against a hash 
            of allowable return-addresses, and stash it away for later use (see 
            lines 32 - 37 in the script).  Command Parser
              I decided to make it easy on myself and prefix each command with 
              the word: "Command:" Whatever followed had to be syntax 
              checked for allowable script names and command-line options and 
              I scratched my head for a while looking for an elegant solution. 
              One night, I was at a local bookstore perusing the Perl books and 
              picked up Data Munging with Perl by David Cross. While flipping 
              through the book, I came across a chapter describing a Perl module 
              called Parse::RecDescent written by Damian Conway. It quickly became 
              apparent that Parse::RecDescent was the answer to all of my current 
              command parsing problems. I went home, grabbed the module from CPAN, 
              and began reading the documentation.
              I will not fully describe Parse::RecDescent in this article. It's 
              a fairly complicated module to use, but once the hang of it is acquired, 
              it's pretty easy to set up a simple grammar to check phrases 
              (e.g., commands and options) for allowable structure. In addition 
              to the POD of the module itself, I also found the following Web-based 
              docs extremely helpful:
              
              
             
               Damian Conway's The man(1) of descent -- http://ssdw11.ssdw.com/perldoc/tutorial/tutorial.html 
               Jeff Goff's Parse::RecDescent Tutorial --http://www.perl.com/pub/a/2001/06/13/recdecent.html 
               Teodor Zlatanov's Cultured Perl: Writing Perl Programs 
                that speak English -- Using Parse::RecDescent to create a 
                simple and efficient command-line user interface -- http://www-106.ibm.com/developerworks/library/perl-speak.html
              Simply stated, Parse::RecDescent's job is to check and verify 
              text against a set of grammar rules. The grammar itself is built 
              by you, the programmer, as a series of top-down rules starting with 
              the highest level of allowable syntax (e.g., a sentence), followed 
              by the items and sub-items (e.g., phrases, nouns, verbs, adverbs, 
              adjectives, letters) that make up the highest level of allowable 
              syntax. Once the grammar is built and submitted to the parser engine, 
              it's a simple matter to pass it text strings for syntax checking.
              Perl code, including regular expressions, can be built directly 
              into the grammar rules. The grammar can also use and return variables 
              to the caller, which makes it fairly straightforward to implement 
              error handling as well as passing data back to the caller. Listing 
              1 shows the structure of the grammar used with emctl.pl (Listing 
              3). Note that the explanation that follows is not intended to replace 
              the module's POD. Parse::RecDescent is rich in features and 
              only an extremely small subset of those features are used in this 
              application.
              The grammar in Listing 1 is made up of a series of rules (the 
              top-most level in the example is labeled "Command", and 
              the bottom-most level is labeled "EndOfString"). Each 
              rule must have one or more "productions" consisting of 
              those "things" that make up the rule. Such "things" 
              can be lower-level rules, strings of text, or regular expressions. 
              A vertical bar ("|") indicates an "or" condition 
              and separates the possible productions for the rule. Each component 
              of the rule (i.e., the rule name itself and the items in the production) 
              is assigned a slot in an array called @item. The rule name 
              itself is slotted into $item[0], the first production component 
              into $item[1], and so on. Perl code included in the production 
              has full access to the @item array. Another special variable, 
              $return, is passed back to the caller at the end of the parse 
              and can be filled with whatever you like by the Perl code included 
              in the production.
              The intent of the grammar is to support the allowable syntax (i.e., 
              name and options) of programs or scripts that emctl.pl will 
              be requested to execute when passed email via the alias in the Sendmail 
              aliases file. Let's examine the grammar (Listing 1) and see 
              what it'll do.
              Starting from the top of the grammar (line 1), the rule labeled 
              "Command" is allowed three forms. The first form (line 
              1) has a production of nooptcmd EndOfString, the second (line 
              3) has a production of oneoptcmd anoption EndOfString, and 
              the third form (line 5) has a production of twooptcmd anoption 
              anoption EndOfString.
              Decending, we find that the rule oneoptcmd consists of 
              the string script1, the rule twooptcmd consists of 
              the string script2 or script1, and the rule nooptcmd 
              consists of the string script3.
              Further down we find that the rule anoption consists of 
              a regular expression allowing any number of upper- or lower-case 
              characters, numbers, dashes, underscores, asterisks, and/or periods. 
              The final rule, EndOfString, consists of a regular expression 
              for end-of-string.
              This grammar will allow syntax checking for very specific things 
              -- precise commands (i.e., scripts) consisting of none, one, 
              or two options followed by an EOS. The command script1 may 
              have one but no more than two options, script2 must have 
              two options, and script3 is allowed no options whatsoever. 
              If the text submitted to the parser containing this grammar passes 
              muster, the Perl code in the "Command" rule will return 
              the text (i.e., the command and any options) back to the caller. 
              A syntax error simply returns a 0. Listing 2 is a simple test script 
              for the grammar and is based on one of the test scripts included 
              with the Parse::RecDescent module.
              
             A Brief Walk Through emctl.pl
              Initialization (lines 7 through 26)
              The emctl.pl script (Listing 3) requires three configuration 
              files to perform its duties. The first file, emctl.dat (Listing 
              4), contains comma-delimited records containing the "command" 
              referred to in the grammar followed by a path to the script or program 
              corresponding to the "command". The file is used to initialize 
              a hash, %validcmd, which is employed later to actually run 
              the script corresponding to the "command" received via 
              email.
              The second file, grammar.dat (Listing 1), is the Parse::RecDescent 
              grammar used to validate the command syntax. Lines 21 through 26 
              of Listing 3 initialize the parser with the grammar.
              The third file, users.dat (Listing 5), contains the "from" 
              email addresses that are allowed to execute the commands. The file 
              is used to initialize a hash, %validusers, which is employed 
              later to validate the requester prior to running the scripts corresponding 
              to valid "commands".
              User Validation (lines 29 through 41)
              Recall that the text of the email, including the MIME headers, 
              is available to emctl.pl via STDIN. Accordingly, the email 
              text is read, line by line, looking for the key words "From:" 
              and "Command:". A bit of white space filtering is done 
              in lines 29 through 31 to make the job easier. The key word "From:" 
              indicates the return address of the originator of the email. The 
              return address is extracted (lines 33 through 37), saved in the 
              $from variable, and checked against the hash of valid users 
              (line 38). If the return address is not in the valid-users hash, 
              an email is sent to my work mailbox informing me of the attempted 
              illegal command execution and the script is halted (lines 39 through 
              41).
              Command Execution (lines 47 through 63)
              Each instance of the word "Command:" found at the beginning 
              of a line in the email triggers the following processing. (More 
              than one command can be sent in the email and each instance of "Command:" 
              will trigger a separate return email with the command results.):
              
              1. A command count is incremented (line 48).
              2. White space and the word "Command:" are eliminated 
              from the line so that the actual command and options can be easily 
              extracted (lines 49 through 50).
              3. The remaining command and options are sent to the parser, and 
              the result of the parsing is returned in the $rval variable 
              (line 51). $rval will contain either the command and options 
              passed to the parser (success) or a 0 (parse failure).
              4. If the parse was successful, the command itself is used as 
              an index into the %validcmd hash to obtain the path to the 
              script or program that is to be executed (lines 53 and 54). The 
              script or program is run (lines 55 and 56), and the results mailed 
              back to the requesting address (line 58).
              5. If the parse was unsuccessful, an appropriate email is sent 
              to the requester (lines 61 through 63).
              
              If there are no commands found in the email (i.e., if there are 
              no instances of "Command:" at the start of a line in the 
              text of the email), an appropriate message is sent to the requester 
              (lines 68 through 71).
              As you've seen, the commands delivered via email are actually 
              scripts or executable programs. As such, they can do anything you 
              want them to do, even shutdown and reboot the system. The only restriction 
              in their design (and it's really no restriction at all) is 
              that they should deliver output to STDOUT so that an appropriate 
              message can be included in the email sent back to the requester.
              Security Issues
              Without careful consideration of how it should be used, this application 
              breaches the firewall and allows outside manipulation of a critical 
              system without traditional methods of authentication or encryption. 
              It was created to allow very limited monitoring and control functionality 
              of one particular application. Moreover, there are some built-in, 
              albeit primitive, safeguards:
              
              1. You're in control of what is allowed by creating scripts 
              or programs with limited functionality.
              2. The scripts can be executed only by email accounts you specify. 
              Spoofed email would work only if the spoofer provided a return address 
              that was included in your users.dat file. Even then, the 
              results generated by the script(s) would go to an authorized email 
              account, not back to the spoofer. Any damage done would be limited 
              by what you allowed by way of the scripts.
              3. Commands that are executed have highly specific syntax requirements, 
              both in how they're passed in the email and in what you choose 
              to call them.
              
              Before using this method of remote system control and monitoring, 
              it is critical to carefully consider how to use it. For example, 
              it would be unsafe and unwise to allow the scripts to perform tasks 
              including but not limited to:
              
              1. System shutdown and reboot 
              2. Process manipulation via kill, nice, etc.
              3. Critical file editing and deletion
              4. Any activity requiring root privileges
              5. Clear text transfer of passwords, patient or customer data, 
              and critical system files such as /etc/passwd, etc.
              
              Finally, make sure that the use of this script follows the policies 
              set up by your department and institution. All of the server names, 
              email addresses, and script names contained in this article have 
              been substantially changed to protect the innocent (i.e., me). But 
              you already knew that, right?
              Bob Dilworth is a Systems Analyst for the Medical College of 
              Ohio in Toledo, Ohio. He's been working as a C, Perl, Cobol, 
              and assembly language programmer and on-again, off-again UNIX, VMS, 
              and Unisys systems administrator since 1986. He can be reached at 
              bdilworth@mco.edu.
           |