Cover V11, I05

Article
Listing 1
Listing 2
Listing 3
Listing 4
Listing 5

may2002.tar

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.