Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| ############################################################################### | |
| # SPDX-License-Identifier: GPL-3.0-only | |
| # 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", | |
| } | |
| }; | |
| return $id; | |
| } | |
| sub nikto_tests { | |
| my ($mark, $parameters) = @_; | |
| return if $mark->{'terminate'}; | |
| my $data; | |
| # this is the actual the looped code for all the checks | |
| foreach my $checkid (sort keys %TESTS) { | |
| return if $mark->{'terminate'}; | |
| # replace variables in the uri | |
| my @urilist = change_variables($TESTS{$checkid}{'uri'}, $mark, $checkid); | |
| # Now repeat for each uri | |
| URI: foreach my $uri (@urilist) { | |
| return if $mark->{'terminate'}; | |
| my (%headrs, %flags, $data); | |
| if ($TESTS{$checkid}{'headers'} ne '') { | |
| my $header = unslash($TESTS{$checkid}{'headers'}); | |
| # Apply variable replacement to headers | |
| my @header_lines = split /\r\n/, $header; | |
| foreach my $h (@header_lines) { | |
| my ($key, $value) = split(/: /, $h); | |
| $key = lc($key); | |
| # Apply change_variables to the value part | |
| my @replaced_values = change_variables($value, $mark, $checkid); | |
| foreach my $replaced_value (@replaced_values) { | |
| $headrs{$key} = $replaced_value; | |
| } | |
| } | |
| $headrs{'host'} = $mark->{'hostname'} | |
| unless ($headrs{'host'}); # Kludge not to override host injection vectors | |
| $flags{'noclean'} = 1; | |
| } | |
| if ($TESTS{$checkid}{'data'} ne '') { | |
| $data = unslash($TESTS{$checkid}{'data'}); | |
| $headrs{'content-length'} = length($data) | |
| unless grep(/^(transfer-encoding|content-length)$/i, keys %headrs); | |
| } | |
| my ($res, $content, $error, $request, $response) = | |
| nfetch($mark, $uri, $TESTS{$checkid}{'method'}, $data, \%headrs, \%flags, $checkid); | |
| # DSL matcher expects response headers as a hashref | |
| my $response_string = rebuild_response($response, 0); | |
| my %response_headers; | |
| foreach my $line (split(/\r?\n/, $response_string)) { | |
| next if $line =~ /^HTTP\//; # Skip status line | |
| last if $line =~ /^\s*$/; # Stop at blank line (end of headers) | |
| if ($line =~ /^([^:]+):\s*(.*)$/) { | |
| my ($name, $value) = (lc($1), $2); | |
| if (exists $response_headers{$name}) { | |
| $response_headers{$name} .= ', ' . $value; | |
| } | |
| else { | |
| $response_headers{$name} = $value; | |
| } | |
| } | |
| } | |
| my $response_headers = \%response_headers; | |
| # Extract cookies as a hashref for matcher | |
| my %cookies = (); | |
| if (ref $response->{'whisker'}->{'cookies'} eq 'ARRAY') { | |
| foreach my $cookie (@{ $response->{'whisker'}->{'cookies'} }) { | |
| if ($cookie =~ /^([^=]+)=([^;]*)/) { | |
| $cookies{ lc($1) } = $2; | |
| } | |
| } | |
| } | |
| # Use the DSL matcher - now returns (match_status, captured_groups) | |
| my ($positive, $reason, $captures) = (0, '', []); | |
| my @matcher_result = | |
| $TESTS{$checkid}{'matcher'}->($res, $content, $response_headers, \%cookies); | |
| my ($match_status, $captured_groups) = @matcher_result; | |
| if ($match_status) { | |
| $positive = 1; | |
| $reason = 'DSL Match'; | |
| $captures = $captured_groups || []; | |
| } | |
| # matched on something, check fails/ands | |
| if ($positive) { | |
| # if it's an index.php, check for normal /index.php to see if it's a FP | |
| # if ($uri =~ /^\/index.php\?/i) { | |
| # my $clean_content = rm_active_content($content, $mark->{'root'} . $uri); | |
| # if (LW2::md5($clean_content) eq $mark->{'FoF'}{'index.php'}{'match'}) { | |
| # next; | |
| # } | |
| # } | |
| # Check user-specified error codes/strings first (highest priority, always wins) | |
| # This must be checked before any other 404 detection logic | |
| if (defined $VARIABLES{'ERRCODES'} && ref($VARIABLES{'ERRCODES'}) eq 'HASH') { | |
| my $code_str = "$res"; | |
| if (exists $VARIABLES{'ERRCODES'}->{$code_str}) { | |
| next URI; # Skip this test, it's a 404 | |
| } | |
| } | |
| if (defined $VARIABLES{'ERRSTRINGS'} && ref($VARIABLES{'ERRSTRINGS'}) eq 'HASH') { | |
| foreach my $pattern (keys %{ $VARIABLES{'ERRSTRINGS'} }) { | |
| if ($content =~ /$pattern/) { | |
| next URI; # Skip this test, it's a 404 | |
| } | |
| } | |
| } | |
| # Lastly check for a false positive based on file extension or type. | |
| # We check is_404 when the actual response code is 200, because 404 error pages | |
| # can return 200 status codes. However, we skip the check if: | |
| # 1. There's a positive BODY match (specific body content indicates real content) | |
| # 2. The DSL has an explicit non-200 CODE match (like CODE:404, CODE:403) | |
| # without including 200, as those are intentional matches for specific status codes. | |
| my $has_code_match = ($TESTS{$checkid}{'dsl'} =~ /CODE:/i); | |
| # Check if DSL has an explicit non-200 CODE match that doesn't include 200 | |
| # (e.g., CODE:404, CODE:403, but not CODE:200 or CODE:200|404) | |
| my $has_explicit_non200_code = 0; | |
| if ($has_code_match) { | |
| my $includes_code_200 = ($TESTS{$checkid}{'dsl'} =~ /\bCODE:\s*200(\D|$)/i); | |
| # If there's a CODE match but it doesn't include 200, skip is_404 check | |
| $has_explicit_non200_code = !$includes_code_200; | |
| } | |
| # Check if DSL has positive BODY patterns (BODY: but not !BODY:) | |
| my $has_body_match = ($TESTS{$checkid}{'dsl'} =~ /(?:^|[^!])BODY:/i); | |
| if ( $res == 200 | |
| && !$has_body_match | |
| && !$has_explicit_non200_code | |
| && is_404($mark, $mark->{'root'} . $uri, $response)) { | |
| next; | |
| } | |
| # Process message with captured groups (if any) | |
| my $message = $TESTS{$checkid}{'message'}; | |
| if (@$captures && $message =~ /\$\d+/) { | |
| $message = process_captured_groups($message, $captures); | |
| } | |
| # All checks passed, add vulnerability | |
| add_vulnerability($mark, "$mark->{'root'}$uri: $message", | |
| $checkid, $TESTS{$checkid}{'references'}, | |
| $TESTS{$checkid}{'method'}, $mark->{'root'} . $uri, | |
| $request, $response, | |
| $reason | |
| ); | |
| } | |
| } | |
| # Percentages | |
| if ( $OUTPUT{'progress'} | |
| && $parameters->{'report'} | |
| && ($COUNTERS{'totalrequests'} % $parameters->{'report'}) == 0) { | |
| status_report(); | |
| } | |
| } # end check loop | |
| # Perform mutation tests | |
| passchecks($mark) if $parameters->{'passfiles'}; | |
| allchecks($mark) if $parameters->{'all'}; | |
| 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); | |
| nprint("- Performing passfiles mutation.", "v", "tests"); | |
| # 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", "tests"); | |
| foreach my $checkid (keys %TESTS) { | |
| # Expand out vars so we get full matches | |
| my @uris = change_variables($TESTS{$checkid}{'uri'}, $mark, $checkid); | |
| 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 { | |
| my ($mark, $uri, $name, $tid) = @_; | |
| return if $mark->{'terminate'}; | |
| my ($res, $content, $error, $request, $response) = | |
| nfetch($mark, $uri, "GET", "", "", "", "Tests: $name"); | |
| nprint("- $res for $uri (error: $error)", | |
| "v", ($mark->{'hostname'}, $mark->{'ip'}, $mark->{'displayname'})); | |
| if ($error) { | |
| $mark->{'total_errors'}++; | |
| nprint("+ ERROR: $uri returned an error: $error", | |
| "e", ($mark->{'hostname'}, $mark->{'ip'}, $mark->{'displayname'})); | |
| return; | |
| } | |
| if ($res == 200) { | |
| add_vulnerability($mark, "$uri: file found during $name mutation", | |
| $tid, "", "GET", $uri, $request, $response); | |
| } | |
| } | |
| 1; | |