[unisog] IIS vulernerability scanner tool

Anne Bennett anne at alcor.concordia.ca
Tue Jul 31 03:32:37 GMT 2001


Russell Fulton <r.fulton at auckland.ac.nz> writes:
> 
> I am having my second go at looking for machines that are vulnerable to 
> attack via MS index services using the script as modified by Anne Bennet

I have modified it again to add the non-English strings per David
Moore, and a few other minor tweaks.  New version appended.

> RESULT 130.216.7.41: Possibly UNPATCHED IIS 4.0
> DETAIL 130.216.7.41: : HTTP/1.1 404 Object Not Found  Server: 
> Microsoft-IIS/4....
> 
> I am guessing that this is caused by removal of the mappings for .ida, 
> which I have been advising people to do.  In anycase it would appear 
> that such machine are not vulnerable to attack via .ida or .idq 
> mappings.
> 
> Anyone have any other ideas?

I'm sure hoping someone will speak up; as a Unix geek, I definitely
need education on the quirks of IIS.  :-(

Also, I am told that Windows ME is *not* vulnerable, because it can
run only a stripped-down version of IIS called "Personal Web Server".
However, machines whose admins swear are running exactly this combination
ID as "IIS 4.0" in the HTTP header, and nmap 2.53 thinks they are
Windows 2000 hosts, making it apparently impossible to weed these out
programmatically (the script returns "possibly unpatched").

Ideas?

Anne.
-- 
Ms. Anne Bennett, Senior Analyst, IITS, Concordia University, Montreal H3G 1M8
anne at alcor.concordia.ca                                        +1 514 848-7606
----------------------------------------------------------------------------
#!/usr/bin/perl -wT

# This program scans hosts on port 80 to check for unpatched versions
# of the Microsoft IIS web server, which are vulnerable to the
# "Code Red" worm released in July 2001.
#
# On stdin, it accepts a list of lines whose first element is 
# an IP address or a hostname to be scanned -- the rest of each line
# is ignored.

# 2001/07/20 David Dandar <ddandar at odu.edu>
#  - Original as per posting below.
# 2001/07/27 Anne Bennett <anne at alcor.concordia.ca>
#  - Patched for -wT and "use strict"
# 2001/07/30 Anne Bennett <anne at alcor.concordia.ca>
#  - Added mods from David Moore <dmoore at ipn.caida.org>, for
#    multi-lingual recognition of patterns.

# Date: Fri, 20 Jul 2001 15:45:54 -0400
# From: David Dandar <ddandar at odu.edu>
# To: unisog at sans.org
# Message-ID: <20010720154554.B8261 at kataan.usg.odu.edu>
# Subject: [unisog] IIS vulernerability scanner tool
# 
# Below is a rough-cut perl script I whipped up to test for vulnerable IIS
# servers.  I can offer no guarantees, but it seems to be reporting good info. 
# It's based on a report on Bugtraq that fingerprinted some different
# responses to an exploit attempt.  I know Microsoft has something out there
# that reports patch information, but this is an alternative method.
# 
# I recommend using nmap or some other method of finding hosts listening on
# port 80, then feed the IP addresses in on stdin, or via a file listing on
# the command-line to this script.  The output lines contain the first 4k of
# the response from the server, whitespace squished, so they can be very long. 
# The "cannot determine address" and "bad file descriptor" errors are failed
# connects.
# 
# It shouldn't crash anything, but you have been warned. :-)  Use at your own
# risk.  I recommend trying it on a few known hosts first.
# 
# David

use strict;

use IO::Socket;
use IO::Select;

my ( $parallel, $connect_timeout, $response_timeout, $max_batch_timeout );
my ( @hosts, %hosts, %timeouts, $s );
my ( $codered_query, $ida_file_path, @vul_patterns );


# ----------------------- configuration section ----------------------

# How many hosts to scan in parallel:
$parallel=50;

# How many seconds to wait for a connection to each host:
$connect_timeout=10;

# How many seconds to wait for an answer to our HTTP query:
$response_timeout=15;

# How many seconds to wait, maximum, per batch of tests:
$max_batch_timeout=30;

# ----------------------- configuration section ends -----------------

$codered_query = 
  "GET /NULL.ida?".("x"x200)."=X HTTP/1.1\nHost: IITS-test\n\n";

$ida_file_path =
  "(?:[A-Za-z]:\\\\(?:[^\\\\]+\\\\)*)?NULL.ida";

@vul_patterns  = (
        # English
    "The IDQ file $ida_file_path could not be found\.",
    "File $ida_file_path\.    The system cannot find the path specified\.",
    "The file $ida_file_path is on a network share. IDQ, IDA and HTX files cannot be placed on  a network share\.",
        # Chinese
    "\xd5\xd2\xb2\xbb\xb5\xbd IDQ \xce\xc4\xbc\xfe $ida_file_path\xa1\xa3",
        # Korean
    "IDQ \xc6\xc4\xc0\xcf $ida_file_path\xc0\xbb\\(\xb8\xa6\\) \xc3\xa3\xc1\xf6 \xb8\xf8\xc7\xdf\xbd\xc0\xb4\xcf\xb4\xd9\.",
        # Taiwan
    "\xa7\xe4\xa4\xa3\xa8\xec IDQ \xc0\xc9\xae\xd7 $ida_file_path\xa1C",
        # Japanese
    "IDQ \x83t\x83@\x83C\x83\x8b $ida_file_path \x82\xaa\x8c\xa9\x82\xc2\x82\xa9\x82\xe8\x82\xdc\x82\xb9\x82\xf1\x82\xc5\x82\xb5\x82\xbd\x81B",
        # German
    "IDQ-Datei $ida_file_path nicht gefunden\.",
        # Italian
    "Impossibile trovare il file IDQ $ida_file_path\.",
        # French
    "Le fichier IDQ $ida_file_path n'a pas pu \xeatre trouv\xe9\.",
    "Le fichier IDQ $ida_file_path n'a pas pu \\&ecirc;tre trouv\\&eacute;\.",
        # Spanish ?
    "No se encuentra el archivo IDQ $ida_file_path\.",
        # Dutch ?
    "Kan het IDQ-bestand $ida_file_path niet vinden\.",
        # ?
    "O arquivo IDQ $ida_file_path n\xe3o p\xf4de ser encontrado\.",
    "O arquivo IDQ $ida_file_path n\\&atilde;o p\\&ocirc;de ser encontrado\.",
    "\xcd\xe5 \xf3\xe4\xe0\xeb\xee\xf1\xfc \xed\xe0\xe9\xf2\xe8 IDQ-\xf4\xe0\xe9\xeb $ida_file_path\.",
    "Det gick inte att hitta IDQ-filen $ida_file_path\.",
    "Nie mo\xbfna odnale\x9f\xe6 pliku IDQ $ida_file_path\.",
    "$ida_file_path\.   \xc6\xc4\xc0\xcf  \xc1\xf6\xc1\xa4\xb5\xc8 \xb0\xe6\xb7\xce\xb8\xa6 \xc3\xa3\xc0\xbb \xbc\xf6 \xbe\xf8\xbd\xc0\xb4\xcf\xb4\xd9\.",
    "$ida_file_path IDQ dosyas\xfd bulunamad\xfd\.",
    "Soubor IDQ $ida_file_path nebyl nalezen\.",
    "IDQ \xc6\xc4\xc0\xcf $ida_file_path\xc0\xbb\\(\xb8\xa6\\) \xc3\xa3\xc1\xf6 \xb8\xf8\xc7\xdf\xbd\xc0\xb4\xcf\xb4\xd9\.",
    "Finner ikke IDQ-filen $ida_file_path\.",
    "\xce\xc4\xbc\xfe $ida_file_path\xa1\xa3    \xcf\xb5\xcd\xb3\xd5\xd2\xb2\xbb\xb5\xbd\xd6\xb8\xb6\xa8\xb5\xc4\xc2\xb7\xbe\xb6\xa1\xa3",
    "IDQ \xc6\xc4\xc0\xcf $ida_file_path\xc0\xbb(\xb8\xa6) \xc3\xa3\xc1\xf6 \xb8\xf8\xc7\xdf\xbd\xc0\xb4\xcf\xb4\xd9\.",
    "\xc0\xc9\xae\xd7 $ida_file_path\xa1C    \xa8t\xb2\xce\xa7\xe4\xa4\xa3\xa8\xec\xab\xfc\xa9w\xaa\xba\xb8\xf4\xae|\xa1C",
    "N\xe3o foi poss\xedvel encontrar  ficheiro de IDQ $ida_file_path\.",
    "IDQ-filen $ida_file_path blev ikke fundet\.",
    "Az IDQ f\xe1jl \\($ida_file_path\\) nem tal\xe1lhat\xf3\.",
);



# ----------------------- subroutines --------------------------------

# Connect to the given host and send it the query.  Add it to
# the list of hosts we'll be checking for a response.
sub sendtest($) {
  my $host=shift;
  my $fd;
  print "DEBUG  $host: contacting...\n";

  eval {
    local $SIG{ALRM}=sub { die("TIMEOUT on connect\n")};
    alarm($connect_timeout);
    if($fd=IO::Socket::INET->new(PeerAddr => "$host",
                                 PeerPort => 80,
                                 Proto=>"TCP")) {
      $fd->send($codered_query);
      $fd->autoflush(1);
      alarm(0);
      } else {
        die("Failed to connect: $!"); # dies inside eval only
        }
    };
  unless($@) {  # i.e. if the eval above was successful
    $s->add($fd);          # add the resulting filehandle to our list
    $hosts{$fd}=$host;     # remember which host that handle is for
    $timeouts{$fd}=time()+$response_timeout; # the host should respond
                                             # in the next XX seconds.
    } else {    # if the eval (connect) failed, say why.
      print "RESULT $host: $@\n";
      }
  }


# Figure out whether we have a patched or unpatched system based on
# the response to the crafted "code red" query.  This code should be
# called only for IIS servers.
sub classify_codered_response($$) {
  my ( $msg, $version ) = @_;
  my ( $pattern );

  if($msg=~/0x80040e14/) {
    return "patched IIS";
    }
  foreach $pattern ( @vul_patterns ) {
    if($msg =~ /$pattern/) {
      return "UNPATCHED IIS";
      }
    }
  if($msg=~/404 Object Not Found/ && ($version ne "5.0")) {
    return "Possibly UNPATCHED IIS";
    }

  if($msg=~/HTTP\/1\.[01] 401 Access Denied/) {
    return "UNKNOWN IIS (401 Access Denied)";
    }
  else {
    return "UNKNOWN IIS";
    }

  }


# Read a host's response to our query, and delete it from the
# list of hosts we're still waiting for.
sub recvtest($) {
   my $fd=shift;
   my ( $raw_msg, $msg, $version, $result, $type, $detail );
   print "DEBUG  $hosts{$fd}: receiving...\n";

   $result = "RESULT $hosts{$fd}: ";
   $type   = "TYPE   $hosts{$fd}: ";
   $detail = "DETAIL $hosts{$fd}: ";

   $fd->recv($raw_msg,4096);
   $msg=$raw_msg;
   $msg=~s/\s/ /g;
   chomp $msg;

   if ($raw_msg=~/^Server: ([\S\t ]+).*$/m) {
     $type .= "$1";
     } else {
       $type .= "UNKNOWN";
       }

   if($msg=~/Microsoft-IIS\/(\d+\.\d+)/) {
     $version=$1;
     $result .= classify_codered_response($msg, $version) . " " . $version;
     } else { # Not an IIS server
         $result .= "ignore -- not an IIS server";
       }
   print "$result\n";
   print "$type\n";
   print "$detail: $msg\n";
   $s->remove($fd);
   $fd->close;
   delete $hosts{$fd};
   delete $timeouts{$fd};
   }

# ------------ main program ------------

# --- set up list of hosts to scan ---

# autoflush on stdout:
$|=1;

while(<>) {	# Take a list of hosts to scan on stdin.
  chomp;
  my $host;
  # Keep only the first element of the line, then check it properly.
  if( /^\s*(\S+)(\s+.*$|$)/ ) {
    $host = $1;  # WARNING: this is not correctly untainted yet!
    if( $host =~ /^([\d\.]+)$/ ) {  # Looks like an IP address
      if( $host =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/ ) {
        if( ( $1 >= 0 ) && ( $1 <= 255 ) &&
            ( $2 >= 0 ) && ( $2 <= 255 ) &&
            ( $3 >= 0 ) && ( $3 <= 255 ) &&
            ( $4 >= 0 ) && ( $4 <= 255 )    ) {
          $host = "$1.$2.$3.$4";  # untainted
          } else {
            print "ERROR  $host line $.: IP addr element out of range 0-155\n";
            next;
            }
        } else {
          print "ERROR  $host line $.: malformatted IP address\n";
          next;
          }
      } else {                       # Looks liek a hostname
        if( $host =~ /^([\w\-\.]+)$/ ) {
          $host = $1;  # untainted
          } else {
            print "ERROR  $host line $.: bad characters in hostname\n";
            next;
          }
        }
    } else {
      print "ERROR  UNKNOWN line $.: unparsable line: $_\n";
      next;
      }
  push(@hosts, $host);
  }

$s=IO::Select->new();
{
  my ( $next, $fd, @ready );

  while(@hosts) {
    while(($s->handles)<$parallel && @hosts) {
      # start a bunch of parallel tests
      sendtest(shift(@hosts));
      }
  
    # Timeout management: our next batch of reads should wait only as long
    # as the earliest of our queued up timeouts.
    $next=time()+$max_batch_timeout;
    foreach $fd ($s->handles) {
      $next=$timeouts{$fd} if($timeouts{$fd}<$next);
      }
  
    # Read a batch of responses.
    @ready=$s->can_read($next-time());
    if(@ready) {
      foreach $fd (@ready) {
        recvtest($fd);
        }
      }
    
    # If any handles were not processed in the above read batch,
    # check them for timeouts, and if they are timed out, report and
    # delete them.
    foreach $fd ($s->handles) {
      if(time()>$timeouts{$fd}) {
        print "$hosts{$fd}: TIMEOUT on read\n";
        $s->remove($fd);
        delete $hosts{$fd};
        delete $timeouts{$fd};
        }
      }
    } # while
  
  # Make sure we don't get any leftovers after all our batches...
  sleep $max_batch_timeout;

  # Read last batch of responses.
  @ready=$s->can_read($max_batch_timeout);
  if(@ready) {
    foreach $fd (@ready) {
      recvtest($fd);
      }
    }

  # Deal with last batch (we hope!) of timeouts:
  foreach $fd ($s->handles) {
    if(time()>$timeouts{$fd}) {
      print "$hosts{$fd}: TIMEOUT on read\n";
      $s->remove($fd);
      delete $hosts{$fd};
      delete $timeouts{$fd};
      }
    }

  # There should be no more, but...
  foreach $fd ($s->handles) {
    print "RESULT $hosts{$fd}: LEFTOVER at end of processing\n";
    }
  }
----------------------------------------------------------------------------



More information about the unisog mailing list