Well, I had already hacked Reuven's CodeRed.pm because I disagreed that
one should only send mail to the bozos once a day. 

So I hacked around some more and made a new module heavily based on
CodeRed.pm that recognizes CodeRed and this new worm (Nimda?) and can be
extended to carp about the new Micro$oft worms sure to come out in the
future.

By default the module behaves in the opposite way to Reuven's
original; that is, it uses the cache to count attacks per worm per IP
address, and sends mail each time with the updated count (the idea being
to bug ISP admins into taking action against their clueless users more
quickly!). But you can configure it to only send mail once per period if
you want.

I used a real ugly mod_rewrite hack to grab the requests (I didn't want to
lump all reqs for root.exe or cmd.exe into the same 'worm') ... I'm sure
others can improve on that. (BTW am I right in thinking that RewriteEngine
on needs to be specified for each virtual host?)

You can grab a gzip'ed copy of this at
http://www.tonkinresolutions.com/MSIISProbes.pm.tar.gz; the source follows
below fyi.

Comments/flames welcome, as this is the first public code I've dared to
post here :)

-- nick


package Apache::MSIISProbes;

use strict;
use vars qw($VERSION);

use Apache::Constants qw(OK DECLINED FORBIDDEN);
use Mail::Sendmail;
use Net::DNS;
use Cache::FileCache;
use Time::Zone;

# ------------------------------------------------------------
# What version of the module is this?
$VERSION = 1.01;

# Shall we be verbose? Set to '1' to log each attempt.
# Set to '2' to log all mail failures. Set to '0' for silence!
my $DEBUG = 1;

# Hash of URLs describing how to fix these problems.
# The key must be the same as defined for $worm_name in httpd.conf
my %security_url = (
  'CodeRed' => 
'http://www.microsoft.com/technet/treeview/default.asp?url=/technet/itsolutions/security/topics/codealrt.asp',
  'Nimda'   => 'http:[EMAIL PROTECTED]',
);

# What "From:" header should be inserted into outgoing e-mail?
my $from_address = '[EMAIL PROTECTED]';

# Do you want to know when one of these alerts has been sent?
# If so, put your address here.
my $cc_address = '';

# Define the Cache::Cache options we want to use.  If nothing else,
# indicate whether the cache should expire or not. If we want to track
# the number of attempts per IP address (and thus send multiple copies
# of the mail) we never expire the cache (this the default behavior).
# If we want to be less noisy we only send mail once per period. We must
# specify when to purge the cache in this case: default value is one day
# [86400 seconds]).
my $store = 1; # set to false to only send one message per IP per period
my %cache_options = $store ? '' : ('default_expires_in' => 86400);

# List of regexps that should be ignored
my @ignore_ip = '';# ('192\.168\..*', '10\..*');

# To what address at SecurityFocus do we report the attack?
my $security_focus_address = '[EMAIL PROTECTED]';

# Where to get information on this module
my $module_url = 'http://www.tonkinresolutions.com/MSIISProbes.pm.html';

# ------------------------------------------------------------

sub handler {
    # Get Apache request/response object
    my $r = shift;

    # Get the server name
    my $s = $r->server();
    my $server_name = $s->server_hostname();

    # Create a DNS resolver, which we'll need no matter what.
    my $res = new Net::DNS::Resolver;

    # ------------------------------------------------------------
    # Open the cache of already-responded-to IP addresses,
    # which we're going to keep in /tmp, just for simplicity.
    #
    # Use the environment var set by Apache to decide which part
    # of the cache to use
    my $worm_name = $r->dir_config('worm_name');
    $cache_options{'namespace'} = $worm_name || 'Default';
    my $file_cache = new Cache::FileCache(\%cache_options);

    unless ($file_cache) {
        $r->log_error("MSIISProbes: Could not instantiate FileCache.  Exiting.");
        return DECLINED;
    }

    # Get the HTTP client's IP address.  We'll use this to
    #  send mail to the people who run the domain.
    my $remote_ip_address = $r->get_remote_host();

    # If we don't have the remote IP address, then we cannot send mail
    # to the remote server, can we?  Let's just stop now, while we're at it.
    unless (defined $remote_ip_address) {
        $r->warn("MSIISProbes: Undefined remote IP address!  Exiting.");
        return DECLINED;
    }

    # If we have the remote IP address, then check to see
    # if it's in our cache.
    my $count = $file_cache->get($remote_ip_address);

    # We update the cache with the new count no matter what,
    # although the count may be cleared (if the mail fails)
    $file_cache->set($remote_ip_address, ++$count);

    if ($count > 1) {
        if ($store) {
            # We go ahead anyway
            $DEBUG && $r->warn("MSIISProbes: Attack number [$count] with [$worm_name] 
from [$remote_ip_address].  Re-mailing.");
        } else {
            $DEBUG && $r->warn("MSIISProbes: Attack number [$count] with [$worm_name] 
in the current cache period from [$remote_ip_address].  Exiting.");
            return FORBIDDEN;
        }
    } else {
        $DEBUG && $r->warn("MSIISProbes: Attack number [$count] with [$worm_name] from 
[$remote_ip_address]. Mailing.");
    }

    # If the remote address matches our ignore list, then ignore it
    foreach my $ignore_ip (@ignore_ip) {
        if ($remote_ip_address =~ /^$ignore_ip$/) {
            $DEBUG && $r->warn("MSIISProbes: Detected known IP [$remote_ip_address] 
(matched [$ignore_ip]).  Exiting.");
            return FORBIDDEN;
        }
    }

    # ------------------------------------------------------------
    # If we only have the IP address (rather than the hostname), then get the
    # hostname.  (We can't look up the MX host for a number, only a name.)

    my $remote_hostname = $remote_ip_address;

    # If the IP address is numeric, then look up its name
    if ($remote_ip_address =~ /^[\d.]+$/) {
        my $dns_query_response = $res->search($remote_ip_address);
        if ($dns_query_response) {
            foreach my $rr ($dns_query_response->answer) {
                # All of the records we retrieve should be PTR records,
                # since we're doing an IP-to-hostname lookup.
                next unless $rr->type eq "PTR";
                # Once we know this is a PTR, we can grab its name
                $remote_hostname = $rr->rdatastr;
            }
        } else {
            my $dns_error = $res->errorstring;
            $DEBUG && $r->warn("MSIISProbes: Failed DNS lookup of [$remote_ip_address] 
(error: [$dns_error])");
        }
    }

    # ------------------------------------------------------------
    # Send e-mail to SecurityFocus.com, which is going to
    # deal with all of this stuff automatically
    #
    # If we are storing a count per IP, we only send this mail the first time.
    # If we are purging the cache, we send it once per period.
    my $now = scalar localtime;
    if ($count > 1) {
        $DEBUG && $r->warn("MSIISProbes: Not mailing Security Focus about duplicate IP 
[$remote_ip_address]");
    } else {
        $DEBUG && $r->warn("MSIISProbes: Sending e-mail to SecurityFocus about IP 
[$remote_ip_address]");
        my $time_zone_name = uc(tz_name());
        my $sf_message = <<EOT;
$remote_ip_address\t$now $time_zone_name

Brought to you by Apache::MSIISProbes version $VERSION for mod_perl and
Apache running on
$server_name.
Information at <$module_url>.
EOT

        my %sf_mail = (
                        To      => $security_focus_address,
                        CC      => $cc_address,
                        From    => $from_address,
                        Subject => "$worm_name infection on [$remote_hostname]: 
Automatic report",
                        Message => $sf_message
                      );

        my $sf_sendmail_success = sendmail(%sf_mail);

        unless ($sf_sendmail_success) {
            $DEBUG > 1 && $r->warn("MSIISProbes: Mail::Sendmail returned 
[$Mail::Sendmail::error].  Exiting.");
            # We want to make sure Security Focus gets the report, so clear the cache 
entry for this IP
            $file_cache->set($remote_ip_address, 0);
            return DECLINED;
        }
    }

    # ------------------------------------------------------------
    # Get the MX for this domain.  This is trickier than you might
    # think, since some DNS servers (like my ISP's) give accurate
    # answers for domains, but not for hosts.  So www.lerner.co.il
    # doesn't have an MX, while lerner.co.il does.  So we're going to
    # do an MX lookup -- and if it doesn't work, we're going to break
    # off everything up to and including the first . in the hostname,
    # and try again.  We shouldn't have to get to the top-level
    # domain, but we'll try that anyway, just in case the others don't
    # work.

    my @mx = ();
    my @hostname_components = split /\./, $remote_hostname;
    my $starting_index = 0;

    # Loop around until our starting index begins at the same location as it would end
    while ($starting_index < @hostname_components) {
        my $host_for_mx_lookup = join('.', @hostname_components[$starting_index .. 
$#hostname_components]);
        @mx = mx($res, $host_for_mx_lookup);

        if (@mx) {
            last;
        } else {
            $starting_index++;
        }
    }

    # If we still haven't found any records, then simply return FORBIDDEN, and log an 
error message
    if (! @mx) {
        my $dns_error = $res->errorstring;
        $DEBUG > 1 && $r->warn("MSIISProbes: No MX records for 
[$remote_hostname](error: [$dns_error]).  Exiting.");
        return FORBIDDEN;
    }

    # Grab the first MX record, and assume that it'll work.
    my $mx_host = $mx[0]->exchange;
    $DEBUG > 1 && $r->warn("MSIISProbes: Using MX host [$mx_host]");

    # ------------------------------------------------------------
    # Send e-mail to the webmaster, postmaster, and administrator, since
    # the webmaster and/or postmaster addresses often doesn't work
    my $remote_webmaster_address = "webmaster\@$mx_host, postmaster\@$mx_host, 
administrator\@$mx_host";

    # Set the outgoing message
    my $outgoing_message = <<EOT;

Your Microsoft IIS server (at $remote_ip_address) appears to have been
infected with the $worm_name worm.  It attempted to spread to
our Web server, despite the fact that we run Apache, which is immune.

This was attempt number $count from the server at $remote_ip_address
to infect our server. You should immediately view the latest information
and download any available security patches, from
<$security_url{$worm_name}>.

Automatically generated by Apache::MSIISProbes version $VERSION for
mod_perl and Apache running on $server_name. Information at <$module_url>.
EOT

    # ------------------------------------------------------------
    # Also send e-mail to the people running the offending host,
    # just in case SecurityFocus takes a while.

    $DEBUG > 1 && $r->warn("MSIISProbes: Sending e-mail to 
[$remote_webmaster_address]");

    my %mail = (
                To      => $remote_webmaster_address,
                CC      => $cc_address,
                From    => $from_address,
                Subject => "$worm_name infection on [$remote_hostname]: Automatic 
report",
                Message => $outgoing_message
              );

    my $sendmail_success = sendmail(%mail);

    if ($sendmail_success) {
        return FORBIDDEN;
    } else {
        $DEBUG > 1 && $r->warn("MSIISProbes: Mail::Sendmail returned 
[$Mail::Sendmail::error]. Exiting.");
        return DECLINED;
    }
}

# All modules must return a true value
1;

__END__

=pod

=head1 NAME

    Apache::MSIISProbes - Responds to worm attacks on Microsoft
    Internet Information Servers with e-mail warnings.

=head1 SYNOPSIS

    In your httpd.conf, put something similar to the following:

        <Location /default.ida>
            SetHandler perl-script
            PerlHandler Apache::MSIISProbes
            PerlSetVar worm_name CodeRed
        </Location>

=head1 DESCRIPTION

    This Perl module should be invoked whenever the worms it
    knows about attack. We don't have to worry about such
    attacks on non-Windows boxes, but we can be good Internet
    citizens, warning the webmasters on infected machines of the
    problem and how to solve it.

    The module allows the user to add new configuration
    directives as new worms are discovered.

=head1 USAGE

    In your httpd.conf, put directives similar to the following:

    <Location /default.ida>
        SetHandler perl-script
        PerlHandler Apache::MSIISProbes
        PerlSetVar worm_name CodeRed
    </Location>

    RewriteCond %{REQUEST_URI} !nimda
    RewriteCond %{QUERY_STRING} /c.dir
    RewriteRule .* /nimda? [R,L]

    <LocationMatch /nimda*>
        SetHandler perl-script
        PerlHandler NPT::MSIISProbes
        PerlSetVar worm_name Nimda
    </LocationMatch>

B<Duplicates>

        Although rumor has it that CodeRed and other similar worms
        only attack a given IP once from a given host, experience
        shows this to be false. You can control the behavior of
        MSIISProbes.pm when it encounters a second or subsequent
        attempt from a given IP address. By default MSIISProbes.pm
        keeps a cache of IP addresses from which an attempt has
        originated, counting attempts per worm from the IP and
        including the count in each message it mails.

        You can override this behavior and send a message only the
        first time a given host attempts to spread the worm in a given
        period by setting the variable $store to a false value. This
        will cause the cache to be cleared at a given interval (by
        default, one day). Mail alerts to the IIS server's
        administrators will be sent only once per cache period.

=head1 BUGS

    If the remote IP address fails a reverse DNS lookup, we don't
    send e-mail to anyone associated with that host.  (We do,
    however, submit the IP address to SecurityFocus.)  It would be
    nice to automatically determine which ISP is responsible for a
    particular IP address, and contact them automatically.

=head1 LICENSE

    You may distribute this module under the same license as Perl itself.

=head1 AUTHOR

    Author, current version: Nick Tonkin, [EMAIL PROTECTED]

    Original author: Reuven M. Lerner, [EMAIL PROTECTED]

    Thanks to Randal Schwartz, David Young, and Salve J. Nilsen for
    their suggestions.

=head1 SEE ALSO

L<mod_perl>.

=cut


~~~~~~~~~~~
Nick Tonkin


Reply via email to