text: #VERSION,2.04
  ###############################################################################
  #  Copyright (C) 2007 Chris Sullo
  #
  #  This program is free software; you can redistribute it and/or
  #  modify it under the terms of the GNU General Public License
  #  as published by the Free Software Foundation; version 2
  #  of the License only.
  #
  #  This program is distributed in the hope that it will be useful,
  #  but WITHOUT ANY WARRANTY; without even the implied warranty of
  #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  #  GNU General Public License for more details.
  #
  #  You should have received a copy of the GNU General Public License
  #  along with this program; if not, write to
  #  Free Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  ###############################################################################
  # PURPOSE:
  # Perform the full database of nikto tests against a target
  ###############################################################################
  sub nikto_tests_init {
      my $id = { name        => "tests",
                 full_name   => "Nikto Tests",
                 author      => "Sullo, Tautology",
                 description => "Test host with the standard Nikto tests",
                 copyright   => "2008 Chris Sullo",
                 hooks       => {
                            scan => { method => \&nikto_tests,
                                      weight => 99,
                                      },
                              },
                 options => {
                           passfiles => "Flag to indicate whether to check for common password files",
                           all => "Flag to indicate whether to check all files with all directories",
                           report => "Report a status after the passed number of tests",
  						 tids => "A range of testids that will only be run",
                           }
                   };
      return $id;
  }
  
  sub nikto_tests {
      return if $mark->{'terminate'};
      my ($mark, $parameters) = @_;
  	my @tids;
  	
  	if (defined $parameters->{'tids'}) {
  		@tids = expand_range($parameters->{'tids'});
  	}
  
      # this is the actual the looped code for all the checks
      foreach my $checkid (sort keys %TESTS) {
          return if $mark->{'terminate'};
          if ($checkid >= 500000) { next; }    # skip TESTS added manually during run (for reports)
                                               # replace variables in the uri
  	my $intcheck=int($checkid);
  	if ((scalar(@tids) > 0) && !grep(/^$intcheck$/, @tids)) { next; }
          my @urilist = change_variables($TESTS{$checkid}{'uri'});
  
          # Now repeat for each uri
          foreach my $uri (@urilist) {
              return if $mark->{'terminate'};
              my %headrs;
              my %flags;
              if ($TESTS{$checkid}{'headers'}) {
                  %headrs=split /: /, $TESTS{$checkid}{'headers'};
                  $headrs{'host'} = $mark->{'hostname'} unless ($headrs{'host'}); # Kludge not to override host injection vectors
                  $flags{'noclean'} = 1;
              }
              my ($res, $content, $error, $request, $response) =
                nfetch($mark, $uri,
                       $TESTS{$checkid}{'method'},
                       $TESTS{$checkid}{'data'},
                       \%headrs,
                       \%flags, 
                       $checkid);
  
              # NOTE: auth is now done in nfetch
              if ($OUTPUT{'show_ok'} && ($res eq 200)) {
                  nprint("+ $mark->{'root'}$uri - 200/OK Response could be $TESTS{$checkid}{'message'}");
              }
              elsif ($OUTPUT{'show_redirects'} && ($res =~ /30(?:[0-3]|7)/)) {
                  nprint(  "+ $mark->{'root'}$uri - Redirects ($res) to "
                         . $response->{'location'} . " , " . $TESTS{$checkid}{'message'});
              }
  
              # If user puts a @CODE= string in db_404_strings, let it take precedence
              foreach my $code (keys %{ $VARIABLES->{'ERRCODES'} }) {
                  if ($res =~ /$code/) {
                      return;
                  }
              }
  
              my $m1_method = my $m1o_method = my $m1a_method = my $f2_method = my $f1_method =
                "content";
              my ($positive, $hashme) = 0;
              my ($hash, $reason) = '';
  
              # how to check each conditional
              if ($TESTS{$checkid}{'match_1'} =~ /^[0-9]{3}$/) { $m1_method = "code"; }
              elsif ($TESTS{$checkid}{'match_1'} =~ /^\@MD5/) {
                  $m1_method = "md5";
                  $hashme    = 1;
                  $TESTS{$checkid}{'match_1'} =~ s/^\@MD5//;
              }
  
              if ($TESTS{$checkid}{'match_1_or'} =~ /^[0-9]{3}$/) { $m1o_method = "code"; }
              elsif ($TESTS{$checkid}{'match_1_or'} =~ /^\@MD5/) {
                  $m1o_method = "md5";
                  $hashme     = 1;
                  $TESTS{$checkid}{'match_1_or'} =~ s/^\@MD5//;
              }
  
              if ($TESTS{$checkid}{'match_1_and'} =~ /^[0-9]{3}$/) { $m1a_method = "code"; }
              elsif ($TESTS{$checkid}{'match_1_and'} =~ /^\@MD5/) {
                  $m1a_method = "md5";
                  $hashme     = 1;
                  $TESTS{$checkid}{'match_1_and'} =~ s/^\@MD5//;
              }
  
              if ($TESTS{$checkid}{'fail_1'} =~ /^[0-9]{3}$/) { $f1_method = "code"; }
              elsif ($TESTS{$checkid}{'fail_1'} =~ /^\@MD5/) {
                  $f1_method = "md5";
                  $hashme    = 1;
                  $TESTS{$checkid}{'fail_1'} =~ s/^\@MD5//;
              }
  
              if ($TESTS{$checkid}{'fail_2'} =~ /^[0-9]{3}$/) { $f2_method = "code"; }
              elsif ($TESTS{$checkid}{'fail_2'} =~ /^\@MD5/) {
                  $f2_method = "md5";
                  $hashme    = 1;
                  $TESTS{$checkid}{'fail_2'} =~ s/^\@MD5//;
              }
  
              if ($hashme) {
                  $hash = LW2::md5($content);
              }
  
              # basic match for positive response
              if ($m1_method eq "content") {
                  if ($content =~ /$TESTS{$checkid}{'match_1'}/) {
                      $positive = 1;
                      $reason = 'Content Match';
                  }
              }
              elsif ($m1_method eq "md5") {
                  if ($hash eq $TESTS{$checkid}{'match_1'}) {
                      $positive = 1;
                      $reason = 'MD5 Hash';
                  }
              }
              else {
                  if ($res eq $TESTS{$checkid}{'match_1'}) {
                      $positive = 1;
                      $reason = 'Response Code Match';
                  }
                  elsif ($res eq $FoF{'okay'}{'response'}) {
                      $positive = 1;
                      $reason = 'Response Code Match - FoF OK Response)';
  		}
              }
  
              # no match, check optional match
              if ((!$positive) && ($TESTS{$checkid}{'match_1_or'} ne "")) {
                  if ($m1o_method eq "content") {
                      if ($content =~ /$TESTS{$checkid}{'match_1_or'}/) {
                          $positive = 1;
                      	$reason = 'Content Match - Match 1 (Or)';
                      }
                  }
                  elsif ($m1o_method eq "md5") {
                      if ($hash eq $TESTS{$checkid}{'match_1_or'}) {
                          $positive = 1;
                      	$reason = 'MD5 Hash - Match 1 (Or)';
                      }
                  }
                  else {
                      if ($res eq $TESTS{$checkid}{'match_1_or'}) {
                          $positive = 1;
                          $reason = 'Response Code Match - Match 1 (Or)';
  			}
                     elsif ($res eq $FoF{'okay'}{'response'}) {
                          $positive = 1;
                          $reason = 'Response Code Match - FoF OK Response / Match 1 (Or)';
                      }
                  }
              }
  
              # matched on something, check fails/ands
              if ($positive) {
                  if ($TESTS{$checkid}{'fail_1'} ne "") {
                      if ($f1_method eq "content") {
                          if ($content =~ /$TESTS{$checkid}{'fail_1'}/) { next; }
                      }
                      elsif ($f1_method eq "md5") {
                          if ($hash eq $TESTS{$checkid}{'fail_1'}) {
                              next;
                          }
                      }
                      else {
                          if ($res eq $TESTS{$checkid}{'fail_1'}) { next; }
                      }
                  }
                  if ($TESTS{$checkid}{'fail_2'} ne "") {
                      if ($f2_method eq "content") {
                          if ($content =~ /$TESTS{$checkid}{'fail_2'}/) { next; }
                      }
                      elsif ($f2_method eq "md5") {
                          if ($hash eq $TESTS{$checkid}{'fail_2'}) {
                              next;
                          }
                      }
                      else {
                          if ($res eq $TESTS{$checkid}{'fail_2'}) { next; }
                      }
                  }
                  if ($TESTS{$checkid}{'match_1_and'} ne "") {
                      if ($m1a_method eq "content") {
                          if ($content !~ /$TESTS{$checkid}{'match_1_and'}/) { next; }
  			else { $reason .= ' and content Match1 (And)'; }
                      }
                      elsif ($m1a_method eq "md5") {
                          if ($hash ne $TESTS{$checkid}{'match_1_and'}) {
                              next;
                          }
  			else { $reason .= ' and MD5 Match1 (And)'; }
                      }
                      else {
                          if ($res ne $TESTS{$checkid}{'match_1_and'}) { next; }
  			else { $reason .= ' and Response Code Match1 (And)'; }
                      }
                  }
  
                  # if it's an index.php, check for normal /index.php to see if it's a FP
                  if ($uri =~ /^\/index.php\?/) {
                      my $content = rm_active_content($content, $mark->{'root'} . $uri);
                      if (LW2::md4($content) eq $FoF{'index.php'}{'match'}) { next; }
                  }
  
                  # lastly check for a false positive based on file extension or type
                  if (($m1_method eq "code") || ($m1o_method eq "code")) {
                      if (is_404($mark->{'root'} . $uri, $content, $res, $response->{'location'}))
                      {
                          next;
                      }
                  }
  
                  $TESTS{$checkid}{'osvdb'} =~ s/\s+/ OSVDB\-/g;
  		if ($positive) {
                  	add_vulnerability($mark,
                                    "$mark->{'root'}$uri: $TESTS{$checkid}{'message'}",
                                    $checkid,
                                    $TESTS{$checkid}{'osvdb'},
                                    $TESTS{$checkid}{'method'},
                                    $mark->{'root'} . $uri,
                                    $request,
                                    $response,
                                    $reason
                                    );
  		}
              }
          }
  
          # Percentages
          if (($OUTPUT{'progress'}) && ($parameters->{'report'})) {
              if (($COUNTERS{'totalrequests'} % $parameters->{'report'}) == 0) {
                  status_report();
              }
          }
      }    # end check loop
  
      # Perform mutation tests
      if ($parameters->{'passfiles'}) {
          passchecks($mark);
      }
      if ($parameters->{'all'}) {
          allchecks($mark);
      }
  
      return;
  }
  
  sub passchecks {
      my ($mark) = @_;
      my @DIRS   = (split(/ /, $VARIABLES{"\@PASSWORDDIRS"}));
      my @PFILES = (split(/ /, $VARIABLES{"\@PASSWORDFILES"}));
      my @EXTS = qw(asp bak dat data dbc dbf exe htm html htx ini lst txt xml php php3 phtml);
  
      nprint("- Performing passfiles mutation", "v");
  
      # Update total requests for status reports
      my @CGIS = split(/ /, $VARIABLES{'@CGIDIRS'});
      $COUNTERS{'total_checks'} =
        $COUNTERS{'total_checks'} +
        (scalar(@DIRS) * scalar(@PFILES)) +
        (scalar(@DIRS) * scalar(@PFILES) * scalar(@EXTS)) +
        ((scalar(@DIRS) * scalar(@PFILES) * scalar(@EXTS) * scalar(@CGIS)) * 2);
  
      foreach my $dir (@DIRS) {
          return if $mark->{'terminate'};
          foreach my $file (@PFILES) {
              next if ($file eq "");
  
              # dir/file
              testfile($mark, "$dir$file", "passfiles", "299998");
  
              foreach my $ext (@EXTS) {
                  return if $mark->{'terminate'};
  
                  # dir/file.ext
                  testfile($mark, "$dir$file.$ext", "passfiles", "299998");
  
                  foreach my $cgi (@CGIS) {
                      $cgi =~ s/\/$//;
  
                      # dir/file.ext
                      testfile($mark, "$cgi$dir$file.$ext", "passfiles", "299998");
  
                      # dir/file
                      testfile($mark, "$cgi$dir$file", "passfiles", "299998");
                  }
              }
          }
      }
  }
  
  sub allchecks {
      my ($mark) = @_;
  
      # Hashes to temporarily store files/dirs in
      # We're using hashes to ensure that duplicates are removed
      my (%FILES, %DIRS);
  
      # build the arrays
      nprint("- Loading root level files", "v");
      foreach my $checkid (keys %TESTS) {
  
          # Expand out vars so we get full matches
          my @uris = change_variables($TESTS{$checkid}{'uri'});
  
          foreach my $uri (@uris) {
              my $dir  = LW2::uri_get_dir($uri);
              my $file = $uri;
  
              if ($dir ne "") {
                  $DIRS{$dir} = "";
                  $dir  =~ s/([^a-zA-Z0-9])/\\$1/g;
                  $file =~ s/$dir//;
              }
              if (($file ne "") && ($file !~ /^\?/)) {
                  $FILES{$file} = "";
              }
          }
      }
  
      # Update total requests for status reports
      $COUNTERS{'total_checks'} = $COUNTERS{'total_checks'} + (keys(%DIRS) * keys(%FILES));
  
      # Now do a check for each item - just check the return status, nothing else
      foreach my $dir (keys %DIRS) {
          foreach my $file (keys %FILES) {
              return if $mark->{'terminate'};
              testfile($mark, "$dir$file", "all checks", 299999);
          }
      }
  }
  
  sub testfile {
      return if $mark->{'terminate'};
      my ($mark, $uri, $name, $tid) = @_;
      my ($res, $content, $error, $request, $response) =
        nfetch($mark, $uri, "GET", "", "", "", "Tests: $name");
      nprint("- $res for $uri (error: $error)", "v");
      if ($error) {
          $mark->{'total_errors'}++;
          nprint("+ ERROR: $uri returned an error: $error", "e");
          return;
      }
      if ($res == 200) {
          add_vulnerability($mark, "$uri: file found during $name mutation", "$tid", "0", "GET", $uri, $request, $response);
      }
  }
  
  1;
  