#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;
