Roll Your Own IIS Intrusion Detection System

by The Rev. Dr. Jackal-Headed-God

If you're in the web development profession and get as many free professional subscriptions as I do, you will notice that at least once per year, each magazine will run a special edition on hacking.

Usually there's some sort of catchy cover image showing a shady character engaged in symbolically nefarious behavior.  Inside, you'll read about the latest worms, viruses, and "hacks" that your mission-critical web site might be susceptible to.

Then you'll read reviews of the latest web site security software, gasp at the cost, and then either try to convince your boss to open her wallet or just move on.  It's the standard marketing tactic of scaring you into busting your budget.

So it's a given that there are plenty of top-shelf web security solutions out there.  It's also a given that none of them is perfect.  And, of course, almost all of them come with a hefty price tag.

This article will show you how to roll your own intrusion detection system for Microsoft's Internet Information Server (IIS) - one that's absolutely free, 200 lines of code, and about 90 percent effective.

It assumes that you are running IIS 5.x on Windows 2000, with ActiveState Perl installed (free from www.activestate.com) and configured to run CGI scripts.  See ActiveState's documentation on how to set this up.  (Hint:  Don't forget to map .cgi to the Perl interpreter; by default, only .pl is mapped.  Sloppy.)

Attack of the Script Kiddies

So what happens when someone decides to target your web site for an attack?

Typically, the would-be intruder will use the script kiddie tool de jour, which will scan the target site for a laundry list of well-known vulnerabilities.

After the initial scan, the tool will come back with specific vulnerabilities and wait for the order to exploit them.  This is analogous to walking around a house and loudly knocking on all the doors and windows, looking for one that's unlocked.

What we're going to focus on is not how to avoid vulnerabilities (read cert.org daily, keep up with vendor patches, be alert, etc.), but rather on how to take advantage of your server's invulnerabilities.  We'll listen for that knock and answer it.

How IIS Handles Server Errors

What happens to all of the exploit tests that fail?

Usually, they generate server errors (403 Access Forbidden, 400 Bad Request, or 500 Server Error).

These server errors are duly noted in your web server's error log and are never, ever noticed.  Why?  Because no one looks at error logs, of course.  And if they do, it's usually too late.

In addition to writing an entry in the error log, IIS will also display a page to the user informing them that an error has occurred.  These canned error responses (there are about 60 of them) can be found by default in C:\WINNT\Help\iisHelp\common.  Take a look; you'll find one .htm file for each kind of error that IIS understands.

Overriding Default Error Handling

Fire up Internet Services Manager on your web server (usually under Administrative Tools in the Start menu).  Right-click on your web site, click on Properties, and select the Custom Errors tab.

You should see something like this:

You can see that each HTTP error is mapped to an .htm file, the same files that you found in the iisHelp directory.  These pages do a fine job of informing the end user that "Something Bad Has Happened," but they don't do a thing to alert the system administrator.  Let's fix that.

Introducing Watcher

Watcher is a very simple, 200-line Perl script that watches for suspicious server errors and lets you know about them.  The source should be dropped in an appropriate folder on your web server inside the web root.

Let's see what it does...

The program opens with the standard #!/usr/bin/perl header - not necessary in the Windows world, but UNIX habits die hard.

Configuration lines go first.  We start with the address for the main recipient for e-mail alerts.  Note that the @ is escaped with a backslash.  Forget this, and you'll blow the script up.  Next is a list of additional addresses for CC: notification.

Simple Mail Transfer Protocol (SMTP) server information is next.

The $smtp_server_name variable is set to the IP address of an available SMTP server on your network: This server needs to be able to send mail to the outside world.

The $smtp_pickup_path variable is the path to a specific folder on the box that the SMTP server watches for new outgoing mail.  By default, it's: c:\\inetpub\\mailroot\\pickup\\  (Note the double backslashes.)

Finally, we have a list of HTTP errors that we want to watch out for.  The default list should cover all the more interesting situations, but feel free to customize it if you want.

If you're observant, you'll notice an error code that doesn't belong: 1013.  This will be our catch-all for those server errors that IIS doesn't know how to handle.

There are four subroutines:

We're going to make IIS pass us the specific HTTP error code through the path, so the first subroutine getError() simply extracts this information from the URL.

getDateTime() does just that - grabs the current date and time and formats it for easier reading.  Most web servers use Greenwich Mean Time (GMT), so we'll subtract six hours (21600 seconds) from the time to convert to Central time.  You can do the math to modify this line for your local time zone.

returnHTML() handles the user-friendly error message that is returned to the browser when the error occurs.  You can customize the HTML in this subroutine to display whatever you want.

Finally, writeMail() gathers information about the server, the error, and the browser that caused the error and compiles it into an e-mail message.  This file is then dropped into your SMTP server's pickup directory and you get an e-mail warning that something's happening on your server.

To configure IIS to use the Watcher script to handle server errors, go back into Internet Services Manager, select Properties for your web site, and go back to the Custom Errors tab.  Double-click on each entry that corresponds to the %errors code that you found in the Watcher script.  Change the Message Type to URL.  In the URL field, enter the relative URL to the Watcher script (e.g. /cgi/watcher.cgi).  Hit OK, hit Apply, and stop and start your web site just for good measure.

To test your configuration, start off by just applying the change to error code 404.  Modify the @trigger list to include 404 as a mail-triggering condition.  Then fire up a browser, point it to your web site, and request a page that doesn't exist (e.g. foo.htm).

If your test was successful, you should see the error page from the Watcher script come up in your browser, and you should have an e-mail in your inbox.  (Make sure that you remove 404 from the @trigger list.)  If you don't see the error page, you either didn't put the correct URL in the error mapping dialog box, you don't have permissions set up on your CGI directory, you forgot to map .cgi to the Perl interpreter, or you otherwise didn't follow instructions.

If you don't receive an e-mail, make sure that you put in the correct e-mail address, that your SMTP server is set up properly, and that you mapped to the correct SMTP pickup directory.  Beyond that, I'll have to leave it to you to figure out what you did wrong.

Spring Forward

Among the information that you receive by mail is the software used to access your site (usually a web browser, but sometimes an automated script), the bad HTTP request that generated the error, and the IP address of the would-be intruder.

Here's a sample, with IP addresses X'ed out for the sake of liability:

A server error occurred on $datetime. Details below.
------------------------------------------------------------------------------------
This error message was returned to the user:

Access Forbidden (403)

Access to this URL is not allowed. Please use the 'Back' button on your browser, or select a link from the navigation sidebar to the left.
------------------------------------------------------------------------------------

REQUEST INFO
----------------
Referrer:
Request: http://XXX.XXX.93.10/_vti_cnf/..%255c..%255c..%255c..%255c..%255c..%255cwinnt/system32/cmd.exe?/c+dir+e:\
Query String: 403;http://XXX.XXX.93.10/_vti_cnf/..%255c..%255c..%255c..%255c..%255c..%255cwinnt/system32/cmd.exe?/c+dir+e:\
Method: HEAD
Port: 80
Protocol: HTTP/1.0
                

USER INFO
----------------
Remote address: XX.130.93.214
Remote host: XX.103.93.214
User Agent: 
Remote Ident:
Remote User:
Authorization Type:


RESPONSE INFO
----------------
Script name: /errors/httperror.cgi
Content Length: 26791
Content Type: text/html
Path Info: /errors/httperror.cgi
Translated Path: C:\webroot\somesite\cgi\watcher.cgi

SERVER INFO
----------------
Server Name: XXX.XXX.93.10
Computer Name: SOMESERVER
Gateway Interface: CGI/1.1
Server Software: Microsoft-IIS/5.0
System Drive: C:
System Root: C:\WINNT
Windows Directory: C:\WINNT
User Profile: C:\Documents and Settings\ComProdSvc 
Path: C:\Perl\bin\;C:\WINNT\system32;C:\WINNT;C:\WINNT\System32\Wbem;C:\WINNT\System32\WBEM;C:\WINNT\System32\WBEM\SNMP

Notice what's in the Request line under REQUEST INFO.  Why, it's someone attempting a Unicode directory traversal exploit.  Gotcha.

You can use the user profile information to do a traceroute on the Remote Address IP address to find out where the attack is coming from.

Next, I recommend using whois.bw.org to find out who owns the IP.  Collect everything you'll need later, because odds are they won't be around for long.  Get on the phone with your provider (or your MIS staff) to block all traffic from the subnet of the attacker while you port scan the miscreant and, ummm.., do whatever you feel is justified.  (Hint to all script kiddies: make sure your box is secure before you go hunting for exploits.)

Room for Improvement

Watcher is a passive tool, very simple to implement, that will give you an early warning with just about every clumsy attempt to find and exploit a vulnerability in your IIS-based web site.

Having said that, there is a lot of room for improvement.

For one thing, when your site does come under attack, you're going to get a lot of e-mail.  Any Perl hacker worth his salt could extend Watcher to throttle the number of e-mails that it will send in a given period of time.  Logging all suspicious activity to a file wouldn't hurt either.  And many worms, viruses, and exploits leave a signature - like that garbage in the Request line we saw earlier - that can be used to identify the type of attack that is being attempted.

I've kept Watcher simple and clean for the sake of this article, but once you get familiar with the concept, there's a lot that you can do to extend it to suit your particular needs.

Best of all, you don't have to beg your boss to pay for it - it's free.

Watcher.cgi:

#!/usr/bin/perl
#
# Watcher.cgi / WormSign.cgi
# A passive intrusion detection tool
# Written by The Rev. Dr. Jackal-Headed God
# Configuration Stuff
#
$recipient = "admin\@opiwqeoip.com";
@cclist = ("someone\@opiwqeoip.com", "someone_else\@opiwqeoip.com");
$smtp_server_name = "xxx.xxx.146.8";
$smtp_pickup_path = "c:\\inetpub\\mailroot\\pickup\\";
$errorCode = "1013"; # Catch-all error code
$request = "unknown_error"; # Catch-all code part deux

# What HTTP errors should tripper an e-mail alert?
@mailtrigger = ("400", "401", "403", "405", "406", "407", "412", "414", "500", "502", "1013");

# Error codes and descriptions returned to the user.
%errors = (
  "400" => "Bad request|Due to malformed syntax, the request could not be understood by the server. The client should not repeat the request without modifications.",
  "401" => "Unauthorized: Logon Failed|This error indicates that the credentials passed to the server do not match the the credentials required to log on to the server. Please contact the Web server's administrator to verify that you have  permission to access the requested resource.",
  "401.1" => "Unauthorized: Logon Failed|This error indicates that the credentials passed to the server do not match the credentials required to log on to the server. Please contact the Web server's administrator to verify that you have permission to access the requested resource.",
  "401.2" => "Unauthorized: Logon Failed due to server configuration|This error indicates that the credentials passed to the server do not match the credentials required to log on to the server. This is usually caused by not sending the proper WWW-Authenticate header field. Please contact the Web server's administrator to verify that you have permission to access the requested resource.",
  "401.3" => "Unauthorized: Unauthorized due to ACL on resource|This error indicates that the credentials passed to the server do not match the credentials required to log on to the server. This resource could be either the page or file listed in the address line of the client, or it could be another file on the server that is needed to process the file listed on the address line of the client. Please make a note of the entire address you were trying to access and then contact the Web server's administrator to verify that you have permission to access the requested resource.",
  "401.4" => "Unauthorized: Authorization failed by filter|This error indicates that the Web server has a filter program installed to verify users connecting to the server. The authorization used to connect to the server was denied access by this filter program. Please make a  note of the entire address you were trying to access and then contact the Web server's administrator to verify that you have permission to access the requested resource.",
  "401.5" => "Unauthorized: Authorization failed by ISAPI/CGI app|This error indicates that the address on the Web server you attempted to use has an ISAPI or CGI program installed that verifies user credentials before proceeding. The authentication used to connect to the server was denied access by this program. Please make a  note of the entire address you were trying to access and then contact the Web server's administrator to verify that you have permission to access the requested resource.",
  "403" => "Access Forbidden|Access to this URL is not allowed. Please use the 'Back' button on your browser, or select a link from the navigation sidebar to the left.",
  "403.1" => "Forbidden: Execute Access Forbidden|This error can be caused if you try to execute a CGI, ISAPI, or other executable program from a directory that does not allow programs to be executed. Please contact the Web server's administrator if the problem persists.",
  "403.10" => "Access Forbidden: Invalid Configuration|There is a configuration problem on the Web server at this time. Please contact the Web server's administrator if the problem persists.",
  "403.11" => "Access Forbidden: Password Change|This error can be caused if the user has entered the wrong password during authentication. Please refresh the page and try again. Please contact the Web server's administrator if the problem persists.",
  "403.12" => "Access Forbidden: Mapper Denied Access|Your client certificate map has been denied access to this Web site. Please contact the site administrator to establish client certificate permissions. You can also change your client certificate and retry, if appropriate.",
  "403.2" => "Forbidden: Read Access Forbidden|This error can be caused if there is no default page available and directory browsing has not been enable for the directory, or if you are trying to display an HTML page that resides in a directory marked for Execute or Script permissions only. Please contact the Web server's administrator if the problem persists.",
  "403.3" => "Forbidden: Write Access Forbidden|This error can be caused if you attempt to upload to, or modify a file in, a directory that does not allow Write access. Please contact the Web server's administrator if the problem persists.",
  "403.4" => "Forbidden: SSL required|This error indicates that the page you are trying to access is secured with Secure Sockets Layer (SSL). In order to view it, you need to enable SSL by typing 'https://' at the beginning of the address you are attempting to reach. Please contact the Web server's administrator if the problem persists.",
  "403.5" => "Forbidden: SSL 128 required|This error message indicates that the resource you are trying to access is secured with a 128-bit version of Secure Sockets Layer (SSL). In order to view this resource, you need a browser that supports this level of SSL. Please confirm that your brower supports 128-bit SSL security. If it does, then contact the Web server's administrator and report the problem.",
  "403.6" => "Forbidden: IP address rejected|This error is caused when the server has a list of IP addresses that are not allowed to access the site, and the IP address you are using is in this list. Please contact the Web server's administrator if the problem persists.",
  "403.7" => "Forbidden: Client certificate required|This error occurts when the resource you are attempting to access requires your browser to have a client Secure Sockets Layer (SSL) certificate that the server recognizes. This is used for authenticating you as a valid user of the resource. Please contact the Web server's administrator if the problem persists.",
  "403.8" => "Forbidden: Site access denied|This error can be caused if the Web server is not servicing requests, or if you do not have permission to connect to the site. Please contact the Web server's administrator if the problem persists.",
  "403.9" => "Access Forbidden: Too many users are connected|This error can be caused if the Web server is busy and cannot process your request due to heavy traffic. Please try to connect again later. Please contact the Web server's administrator if the problem persists.",
  "403.14" => "Access Forbidden|Directory listings are not allowed. Please use the 'Back' button on your browser, or select a link from the navigation sidebar to the left.",
  "404" => "Page Not Found|The server could not locate the page that you requested.",
  "405" => "Method Not Allowed|The method specified in the Request Line is not allowed for the resource identified by the request. Please ensure that you have the proper MIME type set up for the resource you are requesting. Please contact the Web server's administrator if the problem persists.",
  "406" => "Not Acceptable|The resource identified by the request can only generate response entities that have content characteristics that are 'not acceptable' according to the Accept headers sent in the request. Please contact the Web server's administrator if the problem persists.",
  "407" => "Proxy Authentication Required|You must authenticate with a proxy server before this request can be serviced. Please log on to your proxy server, and then try again. Please contact the Web server's administrator if the problem persists.",
  "412" => "Preconditino Failed|The precondition given in one or more of the Request header fields evaluated to FALSE when it was tested on the server. The client placed perconditions on the current resource metainformation (header field data) to prevent the requested method from being applied to a resource other than the one intended. Please contact the Web server's administrator if the problem persists.",
  "414" => "Request-URL Too Long|The server is refusing to service the request because the Request-URI is too long. This rare condition is likely to occur only in the following situations:",
  "500" => "Internal Server Error|The Web server is incapable of performing the request. Please try your request again later. Please contact the Web server's administrator if the problem persists.",
  "501" => "Not Implemented|The Web server does not support the functionality required to fulfill the request. Please check your URL for errors, and contact the Web server's administrator if the problem persists.",
  "502" => "Bad Gateway|The server, while acting as a gateway or proxy, received an invalid response from the upstream server it accessed in attempted to fulfill the request. Please contact the Web server's administrator if the problem persists.",
  "1013" => "Something Bizarre Just Happened|A really bizarre error has occurred. I have no idea what you just did, but I'll certainly try to figure it out."
);

&getError;
&getDateTime;
&returnHTML;
&writeMail;

# Subroutines
sub getError {
  if ($ENV{'QUERY_STRING'}) { 
    ($errorCode, $request) = split /;/, $ENV{'QUERY_STRING'}, 2; 
  }

  $errorName = $errors{$errorCode};
  $errorName =~ s/\|.+//;
  $errorDesc = $errors{$errorCode};
  $errorDesc =~ s/.+\|//;
}

sub getDateTime {
  my ($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime((time - 21600)); # GMT - 6 hours (21600 seconds) = CST
  my $ampm;
  my $hrformat;
  $year = 1990 + $year;

  if ($min < 10) { 
    $min = "0$min"; 
  }

  if ($sec < 10) { 
    $sec = "0$sec"; 
  }

  if ($hour < 12) { 
    $ampm = "am"; 
  } 
  else { 
    $ampm = "pm"; 
  }

  if ($hour eq "0") { 
    $hour = "12"; 
  }

  $datetime = ($mon + 1) . "\/$mday\/$year at $hour:$min $ampm CST";
}

sub returnHTML {
  print "Content-type: text/html\r\n\r\n";
  print "<html>\n";
  print "<head>\n";
  print "<title>Error: $errorName ($errorCode)</title>\n";
  print "</head>\n";
  print "<body bgcolor=\"#ffffff\">\n";
  print "<br><br>\n";
  print "<blockquote>\n";
  print "<p>An error has occurred. Details are provided below:</p>\n";
  print "<table border=\"0\" cellspacing=\"1\" cellpadding=\"1\" bgcolor=\"#999999\">\n";
  print "<tr>\n";
  print "<td valign=\"top\" width=\"350\">\n";
  print "<table border=\"0\" cellspacing=\"0\" cellpadding=\"10\" bgcolor=\"#eeeeee\">\n";
  print "<tr>\n";
  print "<td valign=\"top\" width=\"350\">\n";
  print "<p><b>$errorName ($errorCode)<br><br><font color=\"#990000\">$errorDesc</font></b></p>\n";
  print "<br>\n";
  print "</td>\n";
  print "</tr>\n";
  print "</table>\n";
  print "</td>\n";
  print "</tr>\n";
  print "</table>\n";
  print "<br>\n";
  print "<p>If you continue having difficulties, please contact the webmaster. Be sure to specify the error code ($errorCode) and the page you were trying to access ($request).</p>\n";
  print "</blockquote>\n";
  print "</body>\n";
  print "</html>\n";
}

sub writeMail {
  # First, check the list of trigger errors
  my $found;
  foreach my $errorCode (@mailtrigger) { 
    if ($errorCode eq $errorCode) { 
      $found = "true"; last; 
    } 
  }

  # If this error condition is in our triggerlist, send an emai warning
  if ($found eq "true") {
    my $server_name = lc($ENV{'COMPUTERNAME'});
    my $from_name = "$server_name Watcher";
    my $from_email = "$server_name\@opiwqeoip.com";
    my $subject = "$server_name.opiwqeoip.com Server Error ($errorCode $errorName)";
    $random = int (rand(1000));
    $tempfile = "watcher_$random.txt";

    open TMP, ">>$smtp_pickup_path$tempfile" or die;

    print TMP "x-sender: $from_email\n";
    print TMP "x-receiver: $recipient\n";

    if (@cclist) { 
      foreach my $ccaddress (@cclist) { 
        print TMP "x-receiver: $ccaddress\n"; 
      } 
    }

    print TMP "To: $recipient\n";
    print TMP "CC: @cclist\n";
    print TMP "From: $from_name <$from_email>\n";
    print TMP "Subject: $subject\n";
    print TMP "\r\n";
    print TMP "A server error occurred on $datetime. Details below.\n";
    print TMP "------------------------------------------------------------------------------------\n";
    print TMP "This error message was returned to the user:\n\n";
    print TMP "$errorName ($errorCode)\n\n$errorDesc\n";
    print TMP "------------------------------------------------------------------------------------\n";
    print TMP "\nREQUEST INFO\n----------------\n";
    print TMP "Referrer: $ENV{'HTTP_REFERER'}\n";
    print TMP "Request: $request\n";
    print TMP "Query String: $ENV{'QUERY_STRING'}\n";
    print TMP "Method: $ENV{'REQUEST_METHOD'}\n";
    print TMP "Port: $ENV{'SERVER_PORT'}\n";
    print TMP "Protocol: $ENV{'SERVER_PROTOCOL'}\n";
    print TMP "\r\n";
    print TMP "\nUSER INFO\n----------------\n";
    print TMP "Remote address: $ENV{'REMOTE_ADDR'}\n";
    print TMP "Remote host: $ENV{'REMOTE_HOST'}\n";
    print TMP "User Agent: $ENV{'HTTP_USER_AGENT'}\n";
    print TMP "Remote Ident: $ENV{'REMOTE IDENT'}\n";
    print TMP "Remote User: $ENV{'REMOTE_USER'}\n";
    print TMP "Authorization Type: $ENV{'AUTH_TYPE'}\n";
    print TMP "\r\n";
    print TMP "\nRESPONSE INFO\n----------------\n";
    print TMP "Script name: $ENV{'SCRIPT_NAME'}\n";
    print TMP "Content Length: $ENV{'CONTENT_LENGTH'}\n";
    print TMP "Content Type: $ENV{'CONTENT_TYPE'}\n";
    print TMP "Path Info: $ENV{'PATH_INFO'}\n";
    print TMP "Translated Path: $ENV{'PATH_TRANSLATED'}\n";
    print TMP "\r\n";
    print TMP "\nSERVER INFO\n----------------\n";
    print TMP "Server Name: $ENV{'SERVER_NAME'}\n";
    print TMP "Computer Name: $ENV{'COMPUTERNAME'}\n";
    print TMP "Gateway Interface: $ENV{'GATEWAY_INTERFACE'}\n";
    print TMP "Server Software: $ENV{'SERVER SOFTWARE'}\n";
    print TMP "System Drive: $ENV{'SYSTEMDRIVE'}\n";
    print TMP "System Root: $ENV{'SYSTEMROOT'}\n";
    print TMP "Windows Directory: $ENV{'WINDIR'}\n";
    print TMP "User Profile: $ENV{'USERPROFILE'}\n";
    print TMP "Path: $ENV{'PATH'}\n";
    print TMP "\r\n";
    close TMP;
  }
}

Code: Watcher.cgi

Return to $2600 Index