Let's review the module's logic, looking at some of the code that I
used to make Apache::CodeRed work:
Get the IP address of the incoming HTTP request.
This is easy to do in mod_perl. $r offers us a wealth of
information about the current HTTP transaction, including the IP
address from which the request originated:
my $remote_ip_address = $r->get_remote_host();
Try to look up the DNS name associated with this number.
Once I had the remote computer's IP address, I needed to retrieve
its host name. This "reverse DNS" lookup is normally quite easy to
do; from the Linux command line, I can simply say
nslookup 192.168.1.1
and my local DNS server will respond with the name for that
number. But I wanted to do this from within a Perl program,
without having to use "fork" or "system", which can cause problems when working with mod_perl.
So I decided to use the Net::DNS module, written by Michael Fuhr and available on CPAN. I first created a new instance of a DNS resolver object:
my $res = new Net::DNS::Resolver;
I then double-checked that $remote_ip_address really contained an
address, rather than a host name (which $r->get_remote_host() can
sometimes return):
if ($remote_ip_address =~ /^[\d.]+$/)
Finally, I looked up the host name, either putting it in
$remote_hostname or letting the user know that we didn't manage to
find one:
my $dns_query_response = $res->search($remote_ip_address);
if ($dns_query_response)
{
foreach my $rr ($dns_query_response->answer)
{
next unless $rr->type eq "PTR";
$remote_hostname = $rr->rdatastr;
}
}
else
{
my $dns_error = $res->errorstring;
$r->warn("CodeRed: Failed DNS lookup of " .
"'$remote_ip_address' ('$dns_error')");
}
Notice how we get a list of responses to our query, only some of
which (the "PTR" records) have the reverse DNS information we're
looking for. We thus loop through the result code until we find
one of type PTR, which will give us the host name associated with
$remote_ip_address.
Send e-mail to SecurityFocus with the host name or IP address.
We now have enough information to notify SecurityFocus of the
Code Red 2 infection. In fact, we're only missing one thing: the
name of the local time zone. SecurityFocus explicitly asks
people submitting reports to include a time zone; luckily, the
Time::Zone module has a tz_name() function that returns the local time zone name:
my $time_zone_name = tz_name();
Armed with this information, we composed an e-mail message to
SecurityFocus, and sent it to them with Milivoj Ivkovic's
Mail::Sendmail module from CPAN. Sending mail couldn't be easier
than Mail::Sendmail makes it; while it doesn't include all of the
bells and whistles of Mail::Sender, it's more than adequate for
sending simple text messages. (And it works just fine with qmail,
despite the slightly misleading name.)
Here's how we'll send the e-mail:
my %sf_mail =
( To => $security_focus_address,
CC => $cc_address,
From => $from_address,
Subject =>
"CodeRed infection on '$remote_hostname': Automatic report",
Message => $sf_message);
my $sf_sendmail_success = sendmail(%sf_mail);
If we have problems sending the e-mail, we send a warning to the
Apache error log with $r->log, and return DECLINED to Apache.
Returning DECLINED means our handler didn't do anything, and another handler (including Apache's default handler) may opt
to do something with this HTTP request.
Get the MX (mail exchanger) host associated with this domain.
In an ideal world, we would be able to send e-mail to the host
that originated the HTTP request. Unfortunately, life is more
complicated than that: Not all hosts run SMTP servers, and not all
e-mail servers are configured to accept e-mail for their domain.
To send e-mail to the proper authorities, we must use DNS
to find the MX host for a particular domain.
Note that domains have MX hosts, but regular hosts do not. This
means that we need to find the domain for a host, which can be a
tricky business.
Apache::CodeRed implemented a quick and dirty solution: We begin
with $remote_hostname, and remove successive parts until we
finally hit on an MX host. So we first try to get the MX for
www.example.com (the infected host), and then try to get the
MX for example.com (the domain). Here is how I implemented the
algorithm in Perl:
my @mx = ();
my @hostname_components = split /\./, $remote_hostname;
my $starting_index = 0;
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 (! @mx)
{
my $dns_error = $res->errorstring;
$r->warn("CodeRed: No MX records for '$remote_hostname':" .
"'$dns_error'. Exiting.");
return FORBIDDEN;
}
Finally, send e-mail to a number of administrative e-mail
addresses at the domain's MX host, warning the system administrators that one of their
computers has been infected by Code Red 2.
Now that we know the MX address, we can send e-mail to the
postmaster, webmaster, and administrator responsible for the site.
We compose another message with Mail::Sendmail, returning
FORBIDDEN to Apache. Apache, upon receiving this return code from
our handler, sends an appropriate response to the user's browser.