ONLamp.com    
 Published on ONLamp.com (http://www.onlamp.com/)
 See this if you're having trouble printing code examples


Apache::CodeRed

by Reuven M. Lerner
08/16/2001

Like many programmers and system administrators, I like to know when something goes wrong with my web site, no matter how trivial the problem is. So when I moved my company's web server to mod_perl and HTML::Mason last year, one of the first things I did was write an automatic warning system to send me an e-mail message whenever a visitor encounters a broken link.

I usually expect to receive two or three such e-mail messages on any given day, with the majority coming from people who enter wrong URLs. But on the night of Saturday, August 4, I had hundreds of automatically generated "broken link" reports in my in-box, all of which came from requests for /default.ida. It didn't take long to figure out that my server was under attack by the Code Red 2 worm.

Code Red 2, in case you haven't heard, attacks Windows 2000 systems running Microsoft's IIS Web server. The worm enters via a very long HTTP request beginning with /default.ida. Once it has infected a system, the worm opens some security holes, and then begins trying to spread to other servers. The worm mostly attacks computers whose IP addresses are similar to that of its current host, but sometimes it chooses a new IP address at random.

Code Red 2 never should have infected more than a few dozen computers. A patch has been available from the Microsoft web site for some time, and the headlines that the original Code Red worm generated should have been enough to alert even the sleepiest system administrator. The unfortunate reality is that many system administrators, including those running Microsoft's own Hotmail system, didn't patch their systems in time, and were infected by this fast-spreading threat.

My own server wasn't vulnerable to these attacks, because it runs Apache and Linux. But Code Red 2 didn't check a server's identity when launching an attack; it sent the dangerous HTTP request to anyone who would listen, betting on the large number of IIS servers on the Internet. While I didn't have to worry about infection, I felt like I should do something to let people know that their computers had been infected.

My solution was to write a small module for mod_perl, originally called "CodeRed" and eventually renamed Apache::CodeRed. The module's job is to intercept any request for /default.ida, determine the host name of the HTTP client, and send a warning e-mail message to the administrator of that client.

Within a day of posting the first version of Apache::CodeRed on the mod_perl e-mail list, I began to get feedback, patches, and suggested improvements. I added a cache of IP addresses, to ensure that each system administrator would receive only one warning in a given 24-hour period. I sent e-mail to the designated address at SecurityFocus.com, which was keeping track of such things. And I added a list of IP addresses for which warnings were unnecessary, avoiding the embarrassing situation of turning a simple test into a false warning of a Code Red 2 infection on a Unix system.

A quick primer on mod_perl

Writing Apache Modules with Perl and CWriting Apache Modules with Perl and C
By Lincoln Stein & Doug MacEachern
1st Edition March 1999
1-56592-567-X, Order Number: 567X
743 pages, $39.95

Before looking at Apache::CodeRed, let's review how mod_perl works. The goal of Apache, like any Web server, is to produce an HTTP response for each incoming HTTP request. When Apache gets a request for foo.html, it will normally return the file verbatim. When it gets a request for foo.shtml in a directory that has been marked for server-side includes, it will execute each of those server-side includes, and then return the resulting file. And when it receives a request for foo.pl in a directory marked for CGI programs, Apache will execute the program, returning the program's output to the user's browser. Each of these behaviors is controlled by a different "handler." By assigning a handler to a directory (or to the files in a directory), you change the way Apache behaves.

Mod_perl lets us go one step further by writing our own handlers that can be attached to files, directories, or URLs. We do this by telling Apache that mod_perl will be handling things, and then by telling mod_perl which specific Perl module should handle the HTTP request. By default, mod_perl looks for a subroutine named "handler" in the named module.

After I wrote and installed Apache::CodeRed, I attached it to the URL /default.ida by inserting the following into my Apache configuration file, generally named httpd.conf:

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

The above tells Apache that when an incoming HTTP request asks for /default.ida. The response should be generated by the Perl subroutine Apache::CodeRed::handler.

Our handler receives one input, an instance of the Apache request/response object traditionally called $r. It will also have to return a value to its caller, indicating whether it handled the request (OK), is passing the buck to another handler (DECLINED), or refuses to allow access to the requested URL (FORBIDDEN). Remember that these constants, which are defined in the module Apache::Constants, are not HTTP response codes, but rather indications of whether Apache may return an HTTP response to the user.

Let's look through the overall logic of the code rather than a line-by-line analysis. You'll notice that I often use the $r->warn method, which sends a warning to the Apache error log. Chatty warnings in the error log allowed me to easily keep track of what was happening, which is often a good idea when working on server-side applications. I used $r->warn, rather than $r->log_error to allow Apache administrators to turn off the chattiness by modifying httpd.conf, rather than the module.

Let's review the module's logic, looking at some of the code that I used to make Apache::CodeRed work:

  1. 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();

  2. 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.

  3. 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.

  4. 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;
    }

  5. 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.

Evolution in action

One of the first suggestions that I received for improving Apache::CodeRed came from Randal Schwartz, the well-known Perl author and lecturer. While some people on the mod_perl list thought that Code Red 2 was unlikely to repeat IP addresses, experience made it clear that my computer was being attacked repeatedly from the same computers.

Rather than bombard the same domain administrators with a new note each time my computer was attacked, I used the Cache::Cache module on CPAN (written by DeWitt Clinton) to keep track of which IP addresses I had seen in the last day. Cache::Cache is similar to hashes and DBM files, in that you can associate keys with values. But Cache::Cache keeps track of when you store an item, and automatically expires it after a set amount of time. You can also choose different back-end storage options for Cache::Cache, using the same API with caches in memory and a variety of types of disk files.

By creating a cache with a default expiration time of 86,400 seconds, I could be sure that any IP address stored in the cache would only remain for one day:

my %cache_options = ('default_expires_in' => 86400 );
my $file_cache = new Cache::FileCache(\%cache_options);

Each time Apache::CodeRed receives an incoming request, it stores the IP address in the cache:

$file_cache->set($remote_ip_address, 1);

To determine whether we saw this IP address in the last 24 hours, we simply try to retrieve it from the cache:

my $last_visited = $file_cache->get($remote_ip_address);

If $last_visited is true, then we have seen this IP address recently, and can ignore it.

Another improvement that I made came from David Young, who suggested that we be a bit more selective about which IP addresses are considered dangerous. After all, if I test my Apache::CodeRed installation while my laptop is dialed up to the Internet, should I really end up reporting myself to my ISP?

David's solution was to create @ignore_ip, a list of regular expressions that should be ignored by the system:

my @ignore_ip = ('192\.168\..*', '10\..*');

Before we send e-mail to SecurityFocus, Apache::CodeRed iterates over @ignore_ip, exiting (with a descriptive warning, of course) before we ever send e-mail to SecurityFocus or my ISP's administrators:

foreach my $ignore_ip (@ignore_ip)
{
if ($remote_ip_address =~ /^$ignore_ip$/) {
$r->warn("CodeRed: Detected known '$remote_ip_address'".
" (matched '$ignore_ip'). Exiting.");
return FORBIDDEN;
}
}

Conclusion

Writing Apache::CodeRed was both fun and interesting, and allowed me to feel like a good Internet citizen while exercising my mod_perl skills on a real-time problem.

At the same time, it demonstrated just how insecure many computers on the Internet are. Moreover, I was appalled to discover how many domains are misconfigured -- at a certain point, the amount of bounced e-mail I received from domains that had failed to configure "postmaster" accounts was so great that I collected it into a file rather than let it clog my in-box.

What's most amazing is that more than a week after Code Red 2 hit the Internet, my servers continue to get pounded by requests for /default.ida. True, the requests are now coming every 30 minutes, rather than once every 3 to 4 minutes -- but it means that there are still lots of uninformed system administrators out there, as well as a lot of infected, vulnerable computers.

Reuven M. Lerner is a Web/database consultant and a columnist for Linux Journal.


Return to the Apache DevCenter.

Copyright © 2009 O'Reilly Media, Inc.