Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| ############################################################################### | |
| # SPDX-License-Identifier: GPL-3.0-only | |
| # PURPOSE: Nikto core functionality | |
| ############################################################################### | |
| sub change_variables { | |
| my ($line, $mark, $checkid, $skip_lfi) = @_; | |
| # If no mark provided, use global mark | |
| $mark = $mark || $::mark; | |
| # If no variables to expand, check for LFI before returning | |
| my $at_index = index($line, '@'); | |
| if ($at_index == -1) { | |
| return ($line); | |
| } | |
| # Use cached $shname for efficiency | |
| my $shname = $mark->{'hostname'} || $mark->{'ip'}; | |
| my @subtests; | |
| # Replace JUNK(n) with random string | |
| $line =~ s/\@JUNK\((\d+)\)/LW2::utils_randstr($1)/e; | |
| # Replace static variables | |
| $line =~ s/\@IP/$mark->{'ip'}/g; | |
| $line =~ s/\@HOSTNAME/$shname/g; | |
| # Phase 1: Expand all non-LFI variables recursively | |
| # Keep expanding until no more non-LFI variables remain | |
| if (index($line, '@') == -1) { | |
| push @subtests, $line; | |
| } | |
| else { | |
| # Check for non-LFI variables and expand them | |
| my $found_variable = 0; | |
| foreach my $varname (keys %VARIABLES) { | |
| next if $varname =~ /^\@LFI/; | |
| next unless index($line, $varname) != -1; | |
| if ($line =~ /\Q$varname\E/) { | |
| # Expand this variable: split by whitespace and recursively process each value | |
| foreach my $value (split(/\s+/, $VARIABLES{$varname})) { | |
| my $cooked = $line; | |
| $cooked =~ s/\Q$varname\E/$value/g; | |
| # Recursively expand variables in the cooked line (skip LFI processing in recursive calls) | |
| my @expanded = change_variables($cooked, $mark, $checkid, 1); | |
| push @subtests, @expanded; | |
| } | |
| $found_variable = 1; | |
| last; # Break out of foreach loop after handling first variable found | |
| } | |
| } | |
| if (!$found_variable) { | |
| push(@subtests, $line); | |
| } | |
| } | |
| # Phase 2: Handle LFI (only if not skipping) | |
| if ($skip_lfi) { | |
| # In recursive call, just return the subtests without processing LFI | |
| return @subtests; | |
| } | |
| # Now that other expansions are done, handle LFI | |
| my @tests; | |
| foreach my $subtest (@subtests) { | |
| if ($subtest =~ /\@LFI\(([^)]*)\)/) { | |
| my $args = $1 || ''; | |
| my @temp_tests = lfi_function($subtest, $args, $checkid, $mark); | |
| push @tests, @temp_tests; | |
| } | |
| else { | |
| push @tests, $subtest; | |
| } | |
| } | |
| return @tests; | |
| } | |
| sub lfi_function { | |
| my ($test, $args, $checkid, $mark) = @_; | |
| my @options = split(/,/, $args); | |
| my @lfitests; | |
| # Get detected/forced platform (default to 'all' if not set) | |
| my $detected_platform = ($mark && $mark->{'platform'}) ? $mark->{'platform'} : 'all'; | |
| # Determine platform and path settings from test options | |
| my $is_nix = grep(/nix/i, @options); | |
| my $is_win = grep(/win/i, @options); | |
| my $use_url = grep(/url/i, @options); | |
| my $use_abs = grep(/abs/i, @options); | |
| # Determine which platforms to generate tests for | |
| my @platforms; | |
| my $test_specifies_platform = ($is_nix || $is_win); | |
| if ($test_specifies_platform) { | |
| # Test explicitly specifies platform(s) | |
| # Only run if platform matches or platform is 'all' | |
| if ($detected_platform eq 'all') { | |
| # Platform is 'all' - respect test's specification | |
| if ($is_nix && !$is_win) { | |
| push @platforms, 'nix'; | |
| } | |
| elsif ($is_win && !$is_nix) { | |
| push @platforms, 'win'; | |
| } | |
| else { | |
| # Both specified in test - generate both | |
| push @platforms, 'nix', 'win'; | |
| } | |
| } | |
| elsif ( ($is_nix && $detected_platform eq 'nix') | |
| || ($is_win && $detected_platform eq 'win')) { | |
| # Platform matches test specification - use test's choice | |
| if ($is_nix && !$is_win) { | |
| push @platforms, 'nix'; | |
| } | |
| elsif ($is_win && !$is_nix) { | |
| push @platforms, 'win'; | |
| } | |
| else { | |
| # Both specified in test - but only one matches platform | |
| push @platforms, $detected_platform; | |
| } | |
| } | |
| else { | |
| # Platform doesn't match test specification - return empty (no tests) | |
| return @lfitests; # Return empty array | |
| } | |
| } | |
| else { | |
| # Test doesn't specify platform - use detected platform | |
| if ($detected_platform eq 'all') { | |
| push @platforms, 'nix', 'win'; | |
| } | |
| elsif ($detected_platform eq 'nix') { | |
| push @platforms, 'nix'; | |
| } | |
| elsif ($detected_platform eq 'win') { | |
| push @platforms, 'win'; | |
| } | |
| else { | |
| # Unknown platform, default to both | |
| push @platforms, 'nix', 'win'; | |
| } | |
| } | |
| # Get depth (same for both platforms) | |
| my $depth = $VARIABLES{'@LFIDEPTH'} || 5; | |
| $depth =~ s/^\s+|\s+$//g; | |
| $depth = int($depth) if $depth =~ /^\s*\d+\s*$/; | |
| $depth = 5 if $depth < 1 || $depth > 20; # Sanity check | |
| # Generate tests for each platform | |
| foreach my $platform (@platforms) { | |
| my $is_nix_platform = ($platform eq 'nix'); | |
| # Get LFI variables (trim whitespace) | |
| my $target = | |
| $is_nix_platform | |
| ? ($VARIABLES{'@LFITGTNIX'} || '/etc/hosts') | |
| : ($VARIABLES{'@LFITGTWIN'} || '\\Windows\\win.ini'); | |
| $target =~ s/^\s+|\s+$//g; | |
| # Build the full path | |
| my $full_path; | |
| if ($use_abs) { | |
| # 'abs' means skip traversal, use absolute path directly | |
| $full_path = $target; | |
| # Ensure it starts with / for nix or \ for win | |
| if ($is_nix_platform) { | |
| $full_path = '/' . $full_path unless $full_path =~ /^\//; | |
| } | |
| else { | |
| # For Windows, ensure it starts with \ (unless it has a drive letter) | |
| $full_path = '\\' . $full_path unless $full_path =~ /^[A-Za-z]:|^\\/; | |
| } | |
| } | |
| else { | |
| # Build traversal sequence | |
| my $path = | |
| $is_nix_platform | |
| ? ($VARIABLES{'@LFIPATHNIX'} || '../') | |
| : ($VARIABLES{'@LFIPATHWIN'} || '..\\'); | |
| $path =~ s/^\s+|\s+$//g; | |
| my $traversal = $path x $depth; | |
| # Fix double slash if test is exactly @LFI(...) | |
| if ($test =~ /^\@LFI\([^)]*\)$/) { | |
| # Remove trailing / from traversal or leading / from target to avoid // | |
| if ($is_nix_platform && $traversal =~ /\/$/ && $target =~ /^\//) { | |
| $traversal =~ s/\/$//; | |
| } | |
| elsif (!$is_nix_platform && $traversal =~ /\\$/ && $target =~ /^\\/) { | |
| $traversal =~ s/\\$//; | |
| } | |
| } | |
| $full_path = $traversal . $target; | |
| } | |
| # Apply URL encoding if requested (do this after absolute path handling) | |
| if ($use_url) { | |
| $full_path =~ s/([^A-Za-z0-9])/sprintf("%%%02X", ord($1))/seg; | |
| } | |
| # Replace @LFI(...) in the test string with the generated path | |
| my $expanded_test = $test; | |
| $expanded_test =~ s/\@LFI\([^)]*\)/$full_path/g; | |
| push @lfitests, $expanded_test; | |
| } | |
| return @lfitests; | |
| } | |
| ############################################################################### | |
| sub unslash { | |
| my $line = $_[0] || return; # $line is the slash-escaped variable | |
| # Early return for empty strings | |
| return $line if $line eq ''; | |
| # Use a single regex with eval to handle all escape sequences at once | |
| $line =~ s/\\([abefnrt])|\\x([[:xdigit:]]{2})/defined($1) ? eval("qq{\\$1}") : chr(hex($2))/ge; | |
| return $line; | |
| } | |
| ############################################################################### | |
| sub is_404 { | |
| my ($mark, $uri, $response) = @_; | |
| return 0 unless defined $uri; | |
| return 0 unless defined $mark; | |
| return 0 unless defined $response; | |
| my $debug_404 = 0; | |
| my $code = $response->{'whisker'}{'code'}; | |
| if ($debug_404) { | |
| print "--------------------------------\n"; | |
| print "404: URI is: $uri\n"; | |
| print "404: URI code is: $code\n"; | |
| } | |
| # Check user-specified error codes first (highest priority) | |
| if (defined $VARIABLES{'ERRCODES'} && ref($VARIABLES{'ERRCODES'}) eq 'HASH') { | |
| if (exists $VARIABLES{'ERRCODES'}->{$code}) { | |
| print "404: 1\n" if $debug_404; | |
| return 1; | |
| } | |
| } | |
| # Check user-specified error strings (second priority, before expensive checks) | |
| if (defined $VARIABLES{'ERRSTRINGS'} && ref($VARIABLES{'ERRSTRINGS'}) eq 'HASH') { | |
| foreach my $pattern (keys %{ $VARIABLES{'ERRSTRINGS'} }) { | |
| if ($response->{'content'} =~ /$pattern/) { | |
| print "404: 2\n" if $debug_404; | |
| return 1; | |
| } | |
| } | |
| } | |
| # Trust 404, 406, and 410 codes | |
| if ($code =~ /^40[46]$/ || $code eq '410') { | |
| print "404: 3\n" if $debug_404; | |
| return 1; | |
| } | |
| # remove and capture the query string | |
| my $query_string = ""; | |
| if ($uri =~ /\?(.*)$/) { | |
| $query_string = $1; | |
| $uri =~ s/\?(.*)$//; | |
| } | |
| my @uri_parts = split("/", $uri); | |
| # If we don't have a real path (probably /) just return 0 | |
| # Filter out empty URI, just "/", or all empty parts | |
| if ($uri eq "" || $uri eq "/" || (scalar(@uri_parts) > 0 && !grep { $_ ne "" } @uri_parts)) { | |
| return 0; | |
| } | |
| my $ext = get_ext($uri); | |
| # Build base path (all parts except the last one) | |
| my @base_parts = @uri_parts; | |
| pop @base_parts if scalar(@base_parts) > 0; | |
| my $base_path = join("/", @base_parts); | |
| # Ensure base_path ends with / if it's not empty (for proper path construction) | |
| if ($base_path ne "" && $base_path ne "/") { | |
| $base_path .= "/"; | |
| } | |
| elsif ($base_path eq "") { | |
| $base_path = "/"; | |
| } | |
| # Determine the suffix pattern based on extension type | |
| my $suffix_pattern = ""; | |
| if ($ext eq "DIRECTORY") { | |
| $suffix_pattern = "/"; | |
| } | |
| elsif ($ext eq "DOTFILE") { | |
| $base_path .= "."; | |
| } | |
| elsif ($ext eq "NONE") { | |
| } | |
| else { | |
| $suffix_pattern = "." . $ext; | |
| } | |
| # Build cache key using "RAND" placeholder | |
| my $cache_key = $base_path . "RAND" . $suffix_pattern; | |
| # Check cache before making request | |
| my $cached_entry = $mark->{'nf_cache'}{$cache_key}; | |
| my ($err_res, $err_content, $err_error, $err_request, $err_response); | |
| if (defined $cached_entry) { | |
| print "404: Cached Entry\n" if $debug_404; | |
| # Use cached response data | |
| $err_res = $cached_entry->{'code'}; | |
| $err_content = ""; # Content not cached | |
| $err_error = ""; | |
| $err_request = {}; | |
| $err_response = { | |
| 'code' => $cached_entry->{'code'}, | |
| 'location' => exists $cached_entry->{'location'} ? $cached_entry->{'location'} : '' | |
| }; | |
| } | |
| else { | |
| print "404: No Cached Entry\n" if $debug_404; | |
| # Build nf_path by replacing "RAND" with actual random string | |
| my $rand_str = LW2::utils_randstr(8); | |
| my $nf_path = $base_path . $rand_str . $suffix_pattern; | |
| $nf_path .= "?" . $query_string if $query_string ne ""; | |
| ($err_res, $err_content, $err_error, $err_request, $err_response) = | |
| nfetch($mark, $nf_path, "GET", "", "", "", "is_404"); | |
| # Determine mode, type, match, and location | |
| my %response_map = (200 => "OK", | |
| 300 => "REDIR", | |
| 301 => "REDIR", | |
| 302 => "REDIR", | |
| 303 => "REDIR", | |
| 307 => "REDIR", | |
| 401 => "STD", | |
| 403 => "STD", | |
| 404 => "STD", | |
| 406 => "STD", | |
| 410 => "STD" | |
| ); | |
| my $mode = $response_map{$err_res} || "OTHER"; | |
| my $cache_entry = { 'code' => $err_res, | |
| 'mode' => $mode | |
| }; | |
| print "404: $nf_path returned code: $err_res\n" if $debug_404; | |
| print "404: mode is: $mode\n" if $debug_404; | |
| # Handle redirects - store location if present | |
| if ($err_response && $err_response->{'location'} ne '') { | |
| $cache_entry->{'location'} = get_base_host($err_response->{'location'}); | |
| } | |
| # Only determine type for OK/OTHER modes (STD and REDIR don't need type) | |
| if ($mode eq "OK" || $mode eq "OTHER") { | |
| if (length($err_content) == 0) { | |
| print "404: Type is BLANK\n" if $debug_404; | |
| $cache_entry->{'type'} = "BLANK"; | |
| $cache_entry->{'match'} = ""; | |
| } | |
| else { | |
| print "404: Type is HASH\n" if $debug_404; | |
| $cache_entry->{'type'} = "HASH"; | |
| $cache_entry->{'match'} = LW2::md5(rm_active_content($err_content, $nf_path)); | |
| print "404: Match is: $cache_entry->{'match'}\n" if $debug_404; | |
| } | |
| } | |
| # Store in cache | |
| $mark->{'nf_cache'}{$cache_key} = $cache_entry; | |
| } | |
| # Now determine if the actual response matches the "not found" pattern | |
| # Get the cached entry (should always be defined at this point) | |
| my $nf_entry = $mark->{'nf_cache'}{$cache_key}; | |
| return 0 unless defined $nf_entry; | |
| my $nf_mode = $nf_entry->{'mode'}; | |
| my $actual_code = $response->{'whisker'}{'code'}; # From the actual response being checked | |
| my $actual_content = $response->{'whisker'}{'data'} || ""; | |
| my $actual_location = $response->{'location'} || ""; | |
| # Check STD mode first (most common case - fastest check) | |
| if ($nf_mode eq "STD") { | |
| if ($actual_code =~ /^4\d\d$/) { | |
| print "404: 4\n" if $debug_404; | |
| return 1; | |
| } | |
| return 0; | |
| } | |
| # Check REDIR mode | |
| if ($nf_mode eq "REDIR") { | |
| if ($actual_location ne '' && exists $nf_entry->{'location'}) { | |
| my $actual_base = get_base_host($actual_location); | |
| if ($actual_base eq $nf_entry->{'location'}) { | |
| print "404: 5\n" if $debug_404; | |
| return 1; | |
| } | |
| } | |
| return 0; | |
| } | |
| # Check BLANK type (for OK/OTHER modes) | |
| if (exists $nf_entry->{'type'} && $nf_entry->{'type'} eq "BLANK") { | |
| if (length($actual_content) == 0) { | |
| print "404: 6\n" if $debug_404; | |
| return 1; | |
| } | |
| return 0; | |
| } | |
| # Check HASH type (most expensive, check last) | |
| if (exists $nf_entry->{'type'} && $nf_entry->{'type'} eq "HASH") { | |
| print "404: Checking HASH type\n" if $debug_404; | |
| print "404: nf_entry hash is: $nf_entry->{'match'}\n" if $debug_404; | |
| if (exists $nf_entry->{'match'} && $nf_entry->{'match'} ne '') { | |
| print "404: in exists\n" if $debug_404; | |
| if (length($actual_content) > 0) { | |
| print "404: length is > 0\n" if $debug_404; | |
| my $clean_content = rm_active_content($actual_content, $uri); | |
| print "404: Hash comparison is: \n" if $debug_404; | |
| print " " . LW2::md5($clean_content) . "\n" if $debug_404; | |
| print " " . $nf_entry->{'match'} . "\n" if $debug_404; | |
| if (LW2::md5($clean_content) eq $nf_entry->{'match'}) { | |
| print "404: 7\n" if $debug_404; | |
| return 1; | |
| } | |
| } | |
| else { print "length is 0\n" if $debug_404; } | |
| } | |
| print "404: 8\n" if $debug_404; | |
| return 0; | |
| } | |
| # If we get here, the cached entry doesn't match any known pattern | |
| print "404: 9\n" if $debug_404; | |
| return 0; | |
| } | |
| ############################################################################### | |
| sub scrub { | |
| # line to scrub | |
| my $line = shift; | |
| for my $val (@_) { | |
| next if $val eq ""; | |
| # Create a copy to avoid modifying read-only values | |
| my $val_copy = $val; | |
| # remove IPv6 brackets if present | |
| $val_copy =~ s/^\[([^\]]+)\]$/$1/; | |
| $val_copy = validate_and_fix_regex($val_copy); | |
| my ($validip, $internal, $loopback) = is_ip($val_copy); | |
| if ($validip) { | |
| if ($val_copy =~ /^$LW2::IPv6_re$/) { | |
| $line =~ s/$val_copy/\:\:/g; | |
| } | |
| else { | |
| $line =~ s/$val_copy/0.0.0.0/g; | |
| } | |
| } | |
| else { | |
| $line =~ s/$val_copy/example.com/ig; | |
| } | |
| } | |
| return $line; | |
| } | |
| ############################################################################### | |
| sub nprint { | |
| my ($line, $mode, $testid) = @_; | |
| chomp($line); | |
| # Deferred output? | |
| if ($VARIABLES{'deferout'}) { | |
| push @{ $VARIABLES{'defertxt'} }, $mode . "::" . ($testid // '') . "::" . $line; | |
| return; | |
| } | |
| # scrub values - only pass scrub values (everything after $line and $mode) | |
| if ($OUTPUT{'scrub'}) { | |
| my @scrub_values = @_[ 3 .. $#_ ]; | |
| $line = scrub($line, @scrub_values); | |
| } | |
| # don't print debug & verbose to output file... | |
| if ($mode ne '') { | |
| my %output_flags = ('d' => 'debug', 'v' => 'verbose', 'e' => 'errors'); | |
| if (exists $output_flags{$mode} && $OUTPUT{ $output_flags{$mode} }) { | |
| my $prefix = $mode eq 'd' ? "D:" : $mode eq 'v' ? "V:" : "E:"; | |
| my $output = $mode eq 'd' ? \*STDERR : \*STDOUT; | |
| my $testid_str = defined $testid ? "[$testid]" : "[000000]"; | |
| print $output $prefix . localtime() . " $testid_str - $line\n"; | |
| } | |
| return; | |
| } | |
| # print errors to STDERR | |
| if ($line =~ /^\t?\+ ERROR:/) { print STDERR "$line\n"; return; } | |
| # don't print to STDOUT if output file is "-" | |
| return if defined $CLI{'file'} && $CLI{'file'} eq "-"; | |
| $line =~ | |
| s/(CVE\-[12][0-9]{3}-[0-9]{4,5})/https:\/\/cve.mitre.org\/cgi-bin\/cvename.cgi?name\=$1/g; | |
| $line =~ s/(CA\-[12][0-9]{3}-[0-9]{2})/https:\/\/www.cert.org\/advisories\/$1.html/g; | |
| $line =~ | |
| s/(MS([0-9]{2})\-[0-9]{3})/https\:\/\/docs\.microsoft\.com\/en-us\/security-updates\/securitybulletins\/20$2\/$1/gi; | |
| print $line . "\n"; | |
| return; | |
| } | |
| ############################################################################### | |
| sub get_ext { | |
| my $uri = $_[0] || return; | |
| return "DIRECTORY" if $uri =~ /\/$/; | |
| $uri =~ s/^.*\///; | |
| return "DOTFILE" if $uri =~ /^\.[^.%]/; | |
| $uri =~ s/[?&%;\|].*$//; | |
| return "NONE" if index($uri, '.') == -1; | |
| $uri =~ s/\@[A-Z]+(\([^\)]*\))?//; # remove variables and functions | |
| $uri =~ s/".*$//; | |
| $uri =~ s/^.*\.//; | |
| return $uri; | |
| } | |
| ############################################################################### | |
| sub status_report { | |
| my ($mark) = shift; | |
| my $line; | |
| # without this we could face a div by 0 error | |
| if ( $COUNTERS{'totalrequests'} eq 0 | |
| || $COUNTERS{'total_checks'} eq 0 | |
| || $COUNTERS{'total_targets'} eq 0) { | |
| nprint("- STATUS: Starting up!"); | |
| return; | |
| } | |
| my $secleft = | |
| ((time() - $COUNTERS{'scan_start'}) / $COUNTERS{'totalrequests'}) * | |
| (($COUNTERS{'total_checks'} * $COUNTERS{'total_targets'}) - $COUNTERS{'totalrequests'}); | |
| my $timeleft; | |
| if ($secleft > 60) { | |
| my $minleft = $secleft / 60; | |
| $timeleft = sprintf("%.1f minutes", $minleft); | |
| if ($minleft > 60) { | |
| my $hrsleft = $minleft / 60; | |
| $timeleft = sprintf("%.1f hours", $hrsleft); | |
| } | |
| } | |
| else { $timeleft = sprintf("%.0f seconds", $secleft); } | |
| my $perc_compl = | |
| ($COUNTERS{'totalrequests'} / ($COUNTERS{'total_checks'} * $COUNTERS{'total_targets'}) * 100); | |
| $line = "- STATUS: Completed $COUNTERS{'totalrequests'} requests"; | |
| if ($COUNTERS{'total_targets'} > 1) { | |
| $line .= " (target " . ($COUNTERS{'hosts_completed'} + 1) . "/$COUNTERS{'total_targets'})"; | |
| } | |
| if (($perc_compl < 100) && ($secleft > 0)) { | |
| $line .= sprintf(" (~%.0f%% complete, ~$timeleft left)", $perc_compl); | |
| } | |
| if ($NIKTO{'current_plugin'} ne '') { | |
| $line .= ": currently in plugin '$NIKTO{'current_plugin'}'"; | |
| } | |
| nprint($line); | |
| nprint("- STATUS: " . running_average_print($mark)); | |
| return; | |
| } | |
| ############################################################################### | |
| sub date_disp { | |
| my $t = $_[0] || return; | |
| my @time = localtime($t); | |
| my $result = sprintf("%d-%02d-%02d %02d:%02d:%02d", | |
| $time[5] + 1900, | |
| $time[4] + 1, | |
| $time[3], $time[2], $time[1], $time[0]); | |
| return $result; | |
| } | |
| ############################################################################### | |
| sub get_base_host { | |
| my $uri = $_[0] || return; | |
| # uri, protocol, host, port, params, frag, user, password. | |
| my @hd = LW2::uri_split($uri); | |
| my $base = $hd[1] . "://" . $hd[2]; | |
| if (($hd[3] != 80) && ($hd[3] != 443)) { $base .= ":" . $hd[3]; } | |
| $base .= "/"; | |
| return $base; | |
| } | |
| ############################################################################### | |
| sub rm_active_content { | |
| # Try to remove active content which could mess up the file's signature | |
| my ($cont, $file) = @_; | |
| return "" if (length($cont) == 0); | |
| # Dates/Times | |
| $cont =~ s/[12]\d{3}[-.\/][1-3]?\d[-.\/][1-3]?\d//g; # 2001-12-12 | |
| $cont =~ s/[1-3]?\d[-.\/][1-3]?\d\d[-.\/][12]\d{3}//g; # 12-12-2002 | |
| $cont =~ s/\d{8,14}//g; # timestamp | |
| $cont =~ s/\d{6}//g; # timestamp | |
| $cont =~ s/\d{2}:\d{2}(?::\d{2})?//g; # 12:11:33 | |
| $cont =~ | |
| s/(?:mon|tue|wed|thu|fri|sat|sun)(?:day)?,? [1-3]?[0-9] (?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)//ig; | |
| $cont =~ s/[12][0-9]{3}\s?(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\s?[1-3]?[0-9]//gi | |
| ; # 2009 jan 29 | |
| $cont =~ | |
| s/[1-3]?[0-9]\s?(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[, ]?(?:[12][0-9]{3})?//gi | |
| ; # 29 Jan 2009 | |
| $cont =~ s/[\d.]+ (?:second|queries)//gi; # page load time | |
| # URI, if provided, plus encoded versions of it | |
| # $_[1] has unescaped file name, and $file has escaped. use appropriate one! | |
| if ($file ne '') { | |
| $file = quotemeta($file); | |
| $cont =~ s/$file//g; | |
| # base 64 | |
| my $e = LW2::encode_base64($_[1]); | |
| $cont =~ s/$e//gs; | |
| # hex encoded | |
| $e = LW2::encode_uri_hex($_[1]); | |
| $cont =~ s/$e//gs; | |
| # unicode encoded | |
| $e = LW2::encode_unicode($_[1]); | |
| $e = quotemeta($e); | |
| $cont =~ s/$e//gs; | |
| # url encoding, full url | |
| $e = $_[1]; | |
| $e =~ s/([^A-Za-z0-9])/sprintf("%%%02X", ord($1))/seg; | |
| $cont =~ s/$e//gs; | |
| # url encoding, query portion | |
| if ($file =~ /\?(.*$)/) { | |
| my $qs = $1; | |
| # match pages which link to themselves w/diff args | |
| $cont =~ s/$qs//gs; | |
| # url encoded | |
| $qs =~ s/([^A-Za-z0-9])/sprintf("%%%02X", ord($1))/seg; | |
| $cont =~ s/$qs//gs; | |
| } | |
| } | |
| return $cont; | |
| } | |
| ############################################################################### | |
| sub dump_target_info { | |
| my ($mark) = @_; | |
| my $sslprint = ""; | |
| if ($mark->{ssl}) { | |
| $sslprint = "$VARIABLES{'DIV'}\n"; | |
| $sslprint .= "+ SSL Info: Subject: $mark->{'ssl_cert_subject'}\n"; | |
| # Extract and display CN separately | |
| my $cn = ''; | |
| if ($mark->{'ssl_cert_subject'} =~ /CN=([^$ \/]+)/) { | |
| $cn = $1; | |
| $sslprint .= " CN: $cn\n"; | |
| } | |
| # Display SAN if present | |
| if ($mark->{'ssl_cert_altnames'} ne '') { | |
| $sslprint .= " SAN: $mark->{'ssl_cert_altnames'}\n"; | |
| } | |
| $sslprint .= " Ciphers: $mark->{'ssl_cipher'}\n"; | |
| $sslprint .= " Issuer: $mark->{'ssl_cert_issuer'}"; | |
| } | |
| if ($CLI{'plugins'} ne '@@NONE') { | |
| if ($mark->{ip} =~ /^$LW2::IPv4_re$/ || $mark->{ip} =~ /^$LW2::IPv6_re_inc_zoneid$/) { | |
| nprint("+ Target IP: $mark->{ip}", "", ($mark->{'ip'})); | |
| } | |
| else { | |
| nprint("+ Target IP: (proxied)", "", ($mark->{'ip'})); | |
| } | |
| nprint("+ Target Hostname: $mark->{hostname}", | |
| "", ($mark->{'hostname'}, $mark->{'ip'}, $mark->{'displayname'})); | |
| nprint("+ Target Port: $mark->{port}"); | |
| if (defined $CLI{'root'}) { | |
| nprint("+ Target Path: $CLI{'root'}", | |
| "", ($mark->{'hostname'}, $mark->{'ip'}, $mark->{'displayname'})); | |
| } | |
| if ((defined $CLI{'vhost'}) && ($CLI{'vhost'} ne $mark->{hostname})) { | |
| nprint("+ Virtual Host: $CLI{'vhost'}", | |
| "", ($mark->{'hostname'}, $mark->{'ip'}, $mark->{'displayname'})); | |
| } | |
| if ($request{'whisker'}->{'proxy_host'} ne '') { | |
| nprint( | |
| "+ Proxy: $request{'whisker'}->{'proxy_host'}:$request{'whisker'}->{'proxy_port'}", | |
| "", | |
| ($mark->{'hostname'}, $mark->{'ip'}, $mark->{'displayname'}) | |
| ); | |
| } | |
| if ($mark->{ssl}) { | |
| nprint($sslprint, "", ($mark->{'hostname'}, $mark->{'ip'}, $mark->{'displayname'})); | |
| } | |
| if (defined $NIKTO{'anti_ids'} && defined $CLI{'evasion'}) { | |
| for (my $i = 1 ; $i <= (keys %{ $NIKTO{'anti_ids'} }) ; $i++) { | |
| if ($CLI{'evasion'} =~ /$i/) { | |
| nprint("+ Using Encoding: $NIKTO{'anti_ids'}{$i}"); | |
| } | |
| } | |
| } | |
| if (defined $NIKTO{'mutate_opts'} && defined $CLI{'mutate'}) { | |
| for (my $i = 1 ; $i <= (keys %{ $NIKTO{'mutate_opts'} }) ; $i++) { | |
| if ($CLI{'mutate'} =~ /$i/) { | |
| nprint("+ Using Mutation: $NIKTO{'mutate_opts'}{$i}"); | |
| } | |
| } | |
| } | |
| if (defined $mark->{'messages'}) { | |
| my @msgs = @{ $mark->{'messages'} }; | |
| foreach my $m (@msgs) { | |
| nprint("+ Message: $m", | |
| "", ($mark->{'hostname'}, $mark->{'ip'}, $mark->{'displayname'})); | |
| } | |
| } | |
| if (defined $mark->{'platform'}) { | |
| my %platform_names = ('nix' => 'Linux/Unix', | |
| 'win' => 'Windows', | |
| 'all' => 'Unknown' | |
| ); | |
| my $platform_display = $platform_names{ $mark->{'platform'} } || $mark->{'platform'}; | |
| nprint("+ Platform: $platform_display"); | |
| } | |
| my $time = date_disp($mark->{start_time}); | |
| nprint("+ Start Time: $time (GMT$VARIABLES{'GMTOFFSET'})"); | |
| nprint($VARIABLES{'DIV'}); | |
| } | |
| if ($mark->{banner} ne "") { | |
| nprint("+ Server: $mark->{banner}", | |
| "", ($mark->{'hostname'}, $mark->{'ip'}, $mark->{'displayname'})); | |
| } | |
| else { | |
| nprint("+ Server: No banner retrieved"); | |
| } | |
| return; | |
| } | |
| ############################################################################### | |
| sub general_config { | |
| ## gotta set these first | |
| $| = 1; | |
| # internal array, this should never be used outside this sub | |
| my @options; | |
| # This is used in dump_target_info(), not just help output | |
| $NIKTO{'anti_ids'}{'1'} = "Random URI encoding (non-UTF8)"; | |
| $NIKTO{'anti_ids'}{'2'} = "Directory self-reference (/./)"; | |
| $NIKTO{'anti_ids'}{'3'} = "Premature URL ending"; | |
| $NIKTO{'anti_ids'}{'4'} = "Prepend long random string"; | |
| $NIKTO{'anti_ids'}{'5'} = "Fake parameter"; | |
| $NIKTO{'anti_ids'}{'6'} = "TAB as request spacer"; | |
| $NIKTO{'anti_ids'}{'7'} = "Change the case of the URL"; | |
| $NIKTO{'anti_ids'}{'8'} = "Use Windows directory separator (\\)"; | |
| $NIKTO{'anti_ids'}{'A'} = "Use a carriage return (0x0d) as a request spacer"; | |
| $NIKTO{'anti_ids'}{'B'} = "Use binary value 0x0b as a request spacer"; | |
| # This is used in dump_target_info(), not just help output | |
| $NIKTO{'mutate_opts'}{'1'} = "Test all files with all root directories"; | |
| $NIKTO{'mutate_opts'}{'2'} = "Guess for password file names"; | |
| $NIKTO{'mutate_opts'}{'3'} = "Enumerate user names via Apache (/~user type requests)"; | |
| $NIKTO{'mutate_opts'}{'4'} = | |
| "Enumerate user names via cgiwrap (/cgi-bin/cgiwrap/~user type requests)"; | |
| $NIKTO{'mutate_opts'}{'6'} = | |
| "Attempt to guess directory names from the supplied dictionary file"; | |
| ### CLI STUFF | |
| $CLI{'pause'} = $CLI{'html'} = $OUTPUT{'verbose'} = $CLI{'skiplookup'} = | |
| $COUNTERS{'totalrequests'} = $OUTPUT{'debug'} = $OUTPUT{'scrub'} = $OUTPUT{'errors'} = 0; | |
| $CLI{'all_options'} = join(" ", @ARGV); | |
| $CLI{'all_options'} =~ s/(\-id?\s[^\s:]+:)[^\s]+/$1****/i; | |
| GetOptions("ask=s" => \$CLI{'ask'}, | |
| "Add-header=s" => \@{ $CLI{'headers'} }, | |
| "check6" => \$CLI{'check6'}, | |
| "Cgidirs=s" => \$CLI{'forcecgi'}, | |
| "config=s" => \$CLI{'config'}, | |
| "dbcheck" => \&check_dbs, | |
| "Display=s" => \$CLI{'display'}, | |
| "evasion=s" => \$CLI{'evasion'}, | |
| "followredirects" => \$CLI{'followredirects'}, | |
| "Format=s" => \$CLI{'format'}, | |
| "Help" => \&usage, | |
| "host=s" => \$CLI{'host'}, | |
| "id=s" => \$CLI{'hostauth'}, | |
| "key=s" => \$CLI{'key'}, | |
| "list-plugins" => \&list_plugins, | |
| "maxtime=s" => \$CLI{'maxtime'}, | |
| "mutate-options=s" => \$CLI{'mutate-options'}, | |
| "mutate=s" => \$CLI{'mutate'}, | |
| "nointeractive" => \$CLI{'nointeractive'}, | |
| "nolookup" => \$CLI{'skiplookup'}, | |
| "nossl" => \$CLI{'nossl'}, | |
| "Option=s" => \@options, | |
| "output=s" => \$CLI{'file'}, | |
| "Pause=f" => \$CLI{'pause'}, | |
| "Plugins=s" => \$CLI{'plugins'}, | |
| "Platform=s" => \$CLI{'platform'}, | |
| "RSAcert=s" => \$CLI{'cert'}, | |
| "port=s" => \$CLI{'ports'}, | |
| "root=s" => \$CLI{'root'}, | |
| "ssl" => \$CLI{'ssl'}, | |
| "noslash" => \$CLI{'noslash'}, | |
| "Save=s" => \$CLI{'saveresults'}, | |
| "timeout=i" => \$CLI{'timeout'}, | |
| "Tuning=s" => \$CLI{'tuning'}, | |
| "Userdbs:s" => \$CLI{'userdbs'}, | |
| "nocheck" => \$CLI{'nocheck'}, | |
| "nocookies" => \$CLI{'nocookies'}, | |
| "useproxy:s" => \$CLI{'useproxy'}, | |
| "useragent=s" => \$CLI{'useragent'}, | |
| "url=s" => \$CLI{'host'}, | |
| "Version" => \&version, | |
| "vhost=s" => \$CLI{'vhost'}, | |
| "404string=s" => \$CLI{'404string'}, | |
| "404code=s" => \$CLI{'404code'}, | |
| "ipv6" => \$CLI{'ipv6'}, | |
| "ipv4" => \$CLI{'ipv4'}, | |
| ) | |
| or usage(); | |
| # Validate that -followredirects doesn't have an argument, a common confusion with -Format | |
| if ($CLI{'followredirects'}) { | |
| if ($CLI{'all_options'} =~ /\-f(ollowredirects)?\s+[^-]/) { | |
| nprint( | |
| "+ ERROR: -f (-followredirects) does not accept arguments. Use -F for output format (e.g., -F html)" | |
| ); | |
| exit 1; | |
| } | |
| } | |
| # Run a test for IPv6 connectivity | |
| if ($CLI{'check6'}) { | |
| check_ipv6(); | |
| } | |
| # both -host and -url | |
| if (($CLI{'host'} ne '') && ($CLI{'url'} ne '')) { | |
| nprint("+ ERROR: Cannot use -url and -host at the same time"); | |
| exit 1; | |
| } | |
| # -ipv4 and -ipv6 validations | |
| if ($CLI{'ipv4'} && $CLI{'ipv6'}) { | |
| nprint("+ ERROR: Cannot use -ipv4 and -ipv6 at the same time"); | |
| exit 1; | |
| } | |
| if ($CLI{'ipv6'}) { | |
| $CLI{'ipv4'} = 0; | |
| } | |
| else { | |
| $CLI{'ipv4'} = 1; | |
| } | |
| # 404string | |
| if ($CLI{'404string'} ne '') { | |
| my $s = validate_and_fix_regex($CLI{'404string'}); | |
| $VARIABLES{'ERRSTRINGS'}->{$s} = 1; | |
| } | |
| # 404code | |
| if ($CLI{'404code'} ne '') { | |
| foreach my $code (split(/\s?,\s?/, $CLI{'404code'})) { | |
| $code =~ s/^\s+|\s+$//g; # Trim whitespace | |
| if ($code =~ /[^\d]/) { | |
| nprint("+ ERROR: Invalid 404code, must be an integer"); | |
| exit 1; | |
| } | |
| $VARIABLES{'ERRCODES'}->{$code} = 1; | |
| } | |
| } | |
| # Maxtime must be seconds | |
| if ($CLI{'maxtime'} ne '') { | |
| $CLI{'maxtime'} = time_to_seconds($CLI{'maxtime'}); | |
| if ($CLI{'maxtime'} eq '') { | |
| nprint("+ ERROR: Invalid maxtime value, must be a valid time (e.g., 3600s, 60m, 1h)"); | |
| exit 1; | |
| } | |
| } | |
| # options allows overriding of nikto.conf entries on command line | |
| foreach my $option (@options) { | |
| my @optione = split("=", $option, 2); | |
| $CONFIGFILE{ $optione[0] } = $optione[1]; | |
| } | |
| # Userdb type: blank is db_tests only, so 'all' is only valid option | |
| if (defined($CLI{'userdbs'})) { | |
| if ($CLI{'userdbs'} =~ /^all$/i) { $CLI{'userdbs'} = 'all'; } | |
| else { $CLI{'userdbs'} = 'tests'; } | |
| } | |
| # CLI proxy overrides nikto.conf | |
| if ((defined($CLI{'useproxy'})) && ($CLI{'useproxy'} ne '')) { | |
| if ($CLI{'useproxy'} !~ /^https?:\/\//) { $CLI{'useproxy'} = "http://$CLI{'useproxy'}"; } | |
| my @prox = LW2::uri_split($CLI{'useproxy'}); | |
| $CONFIGFILE{'PROXYHOST'} = $prox[2]; | |
| $CONFIGFILE{'PROXYPORT'} = $prox[3]; | |
| $CONFIGFILE{'PROXYUSER'} = $prox[6]; | |
| $CONFIGFILE{'PROXYPASS'} = $prox[7]; | |
| } | |
| elsif (defined($CLI{'useproxy'})) { $CLI{'useproxy'} = 1; } | |
| else { | |
| undef $CONFIGFILE{'PROXYHOST'}; | |
| undef $CONFIGFILE{'PROXYPORT'}; | |
| undef $CONFIGFILE{'PROXYUSER'}; | |
| undef $CONFIGFILE{'PROXYPASS'}; | |
| } | |
| # Save Results | |
| if (defined($CLI{'saveresults'})) { | |
| if ($CLI{'saveresults'} eq '') { | |
| nprint("+ ERROR: -Save must have a directory name or '.' for auto-generated"); | |
| exit 1; | |
| } | |
| eval "require JSON::PP"; | |
| if ($@) { | |
| nprint("+ ERROR: Module JSON::PP missing."); | |
| exit 1; | |
| } | |
| } | |
| # Parse comma-separated formats early (before validation) | |
| my @formats_raw = (); | |
| if (defined $CLI{'format'} && $CLI{'format'} ne '') { | |
| @formats_raw = split(/,/, $CLI{'format'}); | |
| } | |
| # If no format specified, try to infer from file extension later | |
| my @formats = (); | |
| my %formats_hash = (); | |
| foreach my $fmt (@formats_raw) { | |
| $fmt =~ s/^\s+|\s+$//g; # Trim whitespace | |
| $fmt = lc($fmt); | |
| $fmt = 'txt' if $fmt eq 'text'; | |
| $fmt = 'htm' if $fmt eq 'html'; | |
| if ($fmt !~ /^(?:txt|htm|csv|json|sql|sqld|xml|none)$/) { | |
| nprint("+ ERROR: Invalid output format: $fmt"); | |
| exit 1; | |
| } | |
| # Avoid duplicates | |
| if (!exists $formats_hash{$fmt}) { | |
| push(@formats, $fmt); | |
| $formats_hash{$fmt} = 1; | |
| } | |
| } | |
| # Store formats array for later use | |
| $CLI{'formats'} = \@formats; | |
| # Keep first format for backward compatibility with single-format code paths | |
| $CLI{'format'} = $formats[0] if @formats > 0; | |
| # Check XML dependencies (check if xml is in formats) | |
| if (grep { $_ eq 'xml' } @formats) { | |
| eval "require XML::Writer"; | |
| if ($@) { | |
| nprint("+ ERROR: Module XML::Writer missing. Install with: cpan XML::Writer"); | |
| exit 1; | |
| } | |
| } | |
| # port(s) | |
| if (defined $CLI{'ports'}) { | |
| $CLI{'ports'} =~ s/^\s+//; | |
| $CLI{'ports'} =~ s/\s+$//; | |
| if ($CLI{'ports'} =~ /[^0-9\-\, ]/) { | |
| nprint("+ ERROR: Invalid port option '$CLI{'ports'}'"); | |
| exit 1; | |
| } | |
| } | |
| # output file - infer format from extension if not specified | |
| if (@formats == 0) { | |
| # No format specified, try to infer from file | |
| if (defined $CLI{'file'} && $CLI{'file'} ne '' && $CLI{'file'} ne '.') { | |
| my $ext = lc($CLI{'file'}); | |
| $ext =~ s/(^.*\.)([^.]*$)/$2/g; | |
| $ext = 'txt' if $ext eq 'text'; | |
| $ext = 'htm' if $ext eq 'html'; | |
| if ($ext =~ /^(?:txt|htm|csv|json|sql|sqld|xml)$/) { | |
| push(@formats, $ext); | |
| $CLI{'formats'} = \@formats; | |
| $CLI{'format'} = $ext; | |
| } | |
| else { | |
| $CLI{'format'} = 'none'; | |
| push(@formats, 'none'); | |
| $CLI{'formats'} = \@formats; | |
| } | |
| } | |
| else { | |
| $CLI{'format'} = 'none'; | |
| push(@formats, 'none'); | |
| $CLI{'formats'} = \@formats; | |
| } | |
| } | |
| # Check if we need files for any format | |
| my $needs_file = 0; | |
| foreach my $fmt (@formats) { | |
| if (($fmt ne "none") && ($fmt ne "sqld")) { | |
| $needs_file = 1; | |
| last; | |
| } | |
| } | |
| # Initialize files hash | |
| $CLI{'files'} = {}; | |
| # File naming logic | |
| if ($CLI{'file'} eq '.') { | |
| # Auto-generate file names for each format | |
| if (@formats == 0 || ($formats[0] eq '')) { | |
| nprint("+ ERROR: Output format must be used with auto file naming"); | |
| exit 1; | |
| } | |
| my $hn = $CLI{'host'}; | |
| $hn =~ s/[^a-zA-Z0-9\.\-\_]/_/g; | |
| $hn =~ s/_+/_/g; | |
| my $port = $CLI{'ports'}; | |
| $port =~ s/,/\-/g; | |
| $port =~ s/[^a-zA-Z0-9\.\-\_]/_/g; | |
| my $now = date_disp(time()); | |
| $now =~ s/[^0-9-]/-/g; | |
| my $base_name = "nikto_" . $hn . "_" . $port . "_" . $now; | |
| $base_name =~ s/_+/_/g; | |
| # Generate file names for each format | |
| foreach my $fmt (@formats) { | |
| next if ($fmt eq "none" || $fmt eq "sqld"); # These don't need files | |
| my $file_name = $base_name . "." . $fmt; | |
| # Check if file exists and add counter if needed | |
| if (-e $file_name) { | |
| $file_name =~ /^(.*)(\.[a-z]{3,4})/; | |
| my $fn = $1; | |
| my $ext = $2; | |
| my $ctr = 0; | |
| my $exists = 1; | |
| while ($exists) { | |
| $ctr++; | |
| my $new_name = $fn . "_" . $ctr . $ext; | |
| if (!-e $new_name) { | |
| $file_name = $new_name; | |
| $exists = 0; | |
| } | |
| } | |
| } | |
| $CLI{'files'}{$fmt} = $file_name; | |
| nprint("- Auto-generated save file: $file_name", | |
| "v", ($mark->{'hostname'}, $mark->{'ip'}, $mark->{'displayname'})); | |
| } | |
| } | |
| elsif (defined $CLI{'file'} && $CLI{'file'} ne '' && $CLI{'file'} ne '.') { | |
| # Use filename as prefix, append format extensions | |
| my $prefix = $CLI{'file'}; | |
| # Generate file names for each format | |
| foreach my $fmt (@formats) { | |
| if ($fmt eq "none" || $fmt eq "sqld") { | |
| $CLI{'files'}{$fmt} = ""; # No file for these formats | |
| next; | |
| } | |
| # Append format extension to prefix only if not already present | |
| my $file_name; | |
| if ($prefix =~ /\.\Q$fmt\E$/i) { | |
| $file_name = $prefix; | |
| } | |
| else { | |
| $file_name = $prefix . "." . $fmt; | |
| } | |
| $CLI{'files'}{$fmt} = $file_name; | |
| } | |
| } | |
| else { | |
| # No file specified - only sqld/none formats allowed | |
| foreach my $fmt (@formats) { | |
| if ($fmt eq "none" || $fmt eq "sqld") { | |
| $CLI{'files'}{$fmt} = ""; | |
| } | |
| } | |
| } | |
| # Validation: If file-based formats are specified but no output file, default to auto-generation | |
| if ((!defined $CLI{'file'} || $CLI{'file'} eq '') && $needs_file) { | |
| $CLI{'file'} = '.'; | |
| # Re-run the auto-generation logic above (simplified, since formats are already parsed) | |
| my $hn = $CLI{'host'}; | |
| $hn =~ s/[^a-zA-Z0-9\.\-\_]/_/g; | |
| $hn =~ s/_+/_/g; | |
| my $port = $CLI{'ports'}; | |
| $port =~ s/,/\-/g; | |
| $port =~ s/[^a-zA-Z0-9\.\-\_]/_/g; | |
| my $now = date_disp(time()); | |
| $now =~ s/[^0-9-]+/-/g; | |
| my $base_name = "nikto_" . $hn . "_" . $port . "_" . $now; | |
| $base_name =~ s/_+/_/g; | |
| foreach my $fmt (@formats) { | |
| next if ($fmt eq "none" || $fmt eq "sqld"); | |
| my $file_name = $base_name . "." . $fmt; | |
| if (-e $file_name) { | |
| $file_name =~ /^(.*)(\.[a-z]{3,4})/; | |
| my $fn = $1; | |
| my $ext = $2; | |
| my $ctr = 0; | |
| my $exists = 1; | |
| while ($exists) { | |
| $ctr++; | |
| my $new_name = $fn . "_" . $ctr . $ext; | |
| if (!-e $new_name) { | |
| $file_name = $new_name; | |
| $exists = 0; | |
| } | |
| } | |
| } | |
| $CLI{'files'}{$fmt} = $file_name; | |
| nprint("- Auto-generated save file: $file_name", | |
| "v", ($mark->{'hostname'}, $mark->{'ip'}, $mark->{'displayname'})); | |
| } | |
| } | |
| # Validation checks | |
| if ((defined $CLI{'file'}) && (@formats == 0 || ($formats[0] eq ""))) { | |
| nprint("+ ERROR: Output file specified without a format"); | |
| exit 1; | |
| } | |
| if ((!defined $CLI{'file'} || $CLI{'file'} eq '') && $needs_file && @formats > 0) { | |
| nprint("+ ERROR: Output file format specified without a name"); | |
| exit 1; | |
| } | |
| # verify readable dtd (check all formats for xml) | |
| if (grep { $_ eq 'xml' } @formats) { | |
| if (!defined $CONFIGFILE{'NIKTODTD'} || $CONFIGFILE{'NIKTODTD'} eq '') { | |
| nprint("+ ERROR: DTD not defined in configuration"); | |
| exit 1; | |
| } | |
| # Check if DTD file exists | |
| my $dtd_path = $CONFIGFILE{'NIKTODTD'}; | |
| if ($dtd_path !~ /^\// && defined $CONFIGFILE{'EXECDIR'}) { | |
| $dtd_path = "$CONFIGFILE{'EXECDIR'}/$dtd_path"; | |
| } | |
| if (!-f $dtd_path) { | |
| nprint("+ ERROR: DTD file not found: $dtd_path"); | |
| nprint("+ Please check your nikto.conf configuration"); | |
| exit 1; | |
| } | |
| } | |
| # screen output | |
| if (defined $CLI{'display'}) { | |
| if ($CLI{'display'} =~ /d/i) { $OUTPUT{'debug'} = 1; } | |
| if ($CLI{'display'} =~ /v/i) { $OUTPUT{'verbose'} = 1; } | |
| if ($CLI{'display'} =~ /s/i) { $OUTPUT{'scrub'} = 1; } | |
| if ($CLI{'display'} =~ /e/i) { $OUTPUT{'errors'} = 1; } | |
| if ($CLI{'display'} =~ /p/i) { $OUTPUT{'progress'} = 1; } | |
| if ($CLI{'display'} =~ /1/i) { $OUTPUT{'show_redirects'} = 1; } | |
| if ($CLI{'display'} =~ /2/i) { $OUTPUT{'show_cookies'} = 1; } | |
| if ($CLI{'display'} =~ /3/i) { $OUTPUT{'show_ok'} = 1; } | |
| if ($CLI{'display'} =~ /4/i) { $OUTPUT{'show_auth'} = 1; } | |
| } | |
| # Fixup | |
| if (defined $CLI{'root'}) { | |
| $CLI{'root'} =~ s/\/$//; | |
| if (($CLI{'root'} !~ /^\//) && ($CLI{'root'} ne "")) { $CLI{'root'} = "/$CLI{'root'}"; } | |
| } | |
| if (defined $CLI{'evasion'}) { | |
| $CLI{'evasion'} =~ s/[^1-8AB]//g; | |
| } | |
| if (!defined $CLI{'plugins'} || $CLI{'plugins'} eq "") { | |
| $CLI{'plugins'} = '@@DEFAULT'; | |
| } | |
| # Mapping for mutate for plugins | |
| if (defined $CLI{'mutate'}) { | |
| if ($CLI{'mutate'} =~ /1/ || $CLI{'mutate'} =~ /2/) { | |
| my $parameters; | |
| $parameters = "passfiles" if ($CLI{'mutate'} =~ /2/); | |
| $parameters .= ",all" if ($CLI{'mutate'} =~ /1/); | |
| $CLI{'plugins'} .= ';tests(' . $parameters . ')'; | |
| } | |
| if ($CLI{'mutate'} =~ /3/ || $CLI{'mutate'} =~ /4/) { | |
| my $parameters; | |
| $parameters = "enumerate"; | |
| $parameters .= ",home" if ($CLI{'mutate'} =~ /3/); | |
| $parameters .= ",cgiwrap" if ($CLI{'mutate'} =~ /4/); | |
| $parameters .= ",dictionary:" . $CLI{'mutate-options'} | |
| if (defined $CLI{'mutate-options'}); | |
| $CLI{'plugins'} .= ';apacheusers(' . $parameters . ')'; | |
| } | |
| if ($CLI{'mutate'} =~ /6/) { | |
| $CLI{'plugins'} .= ';dictionary(dictionary:' . $CLI{'mutate-options'} . ')'; | |
| } | |
| nprint( | |
| "- Mutate is deprecated, use -Plugins instead. The following option can be used in future: -Plugin $CLI{'plugins'}" | |
| ); | |
| } | |
| # Asking questions? | |
| if ($CLI{'ask'} =~ /^(?:auto|yes|no)$/) { | |
| $CONFIGFILE{'UPDATES'} = $CLI{'ask'}; # override nikto.conf setting | |
| undef($CLI{'ask'}); | |
| } | |
| $CLI{'timeout'} = $CLI{'timeout'} || 10; | |
| # RFI URL -- push it to VARIABLES | |
| if (defined $CONFIGFILE{'RFIURL'}) { | |
| $VARIABLES{'@RFIURL'} = $CONFIGFILE{'RFIURL'}; | |
| } | |
| else { | |
| nprint("- ***** RFIURL is not defined in nikto.conf--no RFI tests will run *****"); | |
| } | |
| # SSL Test | |
| if (!LW2::ssl_is_available()) { | |
| nprint("- ***** TLS/SSL support not available (see docs for SSL install) *****"); | |
| if ($CLI{'ssl'} || ($CLI{'host'} =~ /^https/i)) { | |
| nprint("- ERROR: -ssl was specified but TLS/SSL is not available."); | |
| exit 1; | |
| } | |
| } | |
| # get core version | |
| open(FI, "<$CONFIGFILE{'PLUGINDIR'}/nikto_core.plugin"); | |
| my @F = <FI>; | |
| close(FI); | |
| my @VERS = grep(/^#VERSION/, @F); | |
| $VARIABLES{'core_version'} = $VERS[0]; | |
| $VARIABLES{'core_version'} =~ s/\#VERSION,//; | |
| chomp($VARIABLES{'core_version'}); | |
| $VARIABLES{'TEMPL_HCTR'} = 0; | |
| if ($^O !~ /MSWin32/) { | |
| $NIKTO{'POSIX'}{'fd_stdin'} = fileno(STDIN); | |
| $NIKTO{'POSIX'}{'term'} = POSIX::Termios->new(); | |
| $NIKTO{'POSIX'}{'term'}->getattr($NIKTO{'POSIX'}{'fd_stdin'}); | |
| $NIKTO{'POSIX'}{'oterm'} = $NIKTO{'POSIX'}{'term'}->getlflag(); | |
| $NIKTO{'POSIX'}{'echo'} = ECHOE | ECHO | ECHOK | ICANON; | |
| $NIKTO{'POSIX'}{'noecho'} = $NIKTO{'POSIX'}{'oterm'} & ~$NIKTO{'POSIX'}{'echo'}; | |
| } | |
| if ($CLI{'pause'} > 0) { | |
| nprint("-***** Pausing $CLI{'pause'} second(s) per request"); | |
| } | |
| # Default values | |
| $COUNTERS{'totalrequests'} = 0; | |
| $COUNTERS{'total_checks'} = 0; | |
| $COUNTERS{'total_targets'} = 0; | |
| $VARIABLES{'GMTOFFSET'} = gmt_offset(); | |
| $VARIABLES{'DIV'} = "-" x 75; | |
| $VARIABLES{'deferout'} = 0; | |
| $VARIABLES{'defertxt'} = []; | |
| # Some Win versions can't use Time::HiRes correctly | |
| $VARIABLES{'MSWIN32'} = 0; | |
| if ($^O =~ /MSWin32/) { | |
| $VARIABLES{'MSWIN32'} = 1; | |
| } | |
| return; | |
| } | |
| ############################################################################### | |
| sub time_to_seconds { | |
| my $time = $_[0] || return; | |
| if ($time =~ /m$/i) { | |
| $time =~ s/m$//i; | |
| $time = ($time * 60); | |
| } | |
| elsif ($time =~ /h$/i) { | |
| $time =~ s/h$//i; | |
| $time = ($time * 3600); | |
| } | |
| elsif ($time =~ /s$/i) { | |
| $time =~ s/s$//i; | |
| } | |
| return $time; | |
| } | |
| ############################################################################### | |
| sub sleeper { | |
| sleep($CLI{'pause'}) if defined $CLI{'pause'}; | |
| } | |
| ############################################################################### | |
| sub safe_quit { | |
| my ($mark) = @_; | |
| # When called as a signal handler, $mark is the signal name (e.g. "INT"), not a hashref | |
| if (!ref($mark)) { | |
| $mark = $NIKTO{'current_mark'}; | |
| } | |
| if (ref($mark)) { | |
| $mark->{'end_time'} = time(); | |
| $mark->{'elapsed'} = $mark->{'end_time'} - $mark->{'start_time'}; | |
| $COUNTERS{'scan_elapsed'} = (time() - $COUNTERS{'scan_start'}); | |
| report_host_end($mark); | |
| report_summary($mark); | |
| report_close($mark); | |
| } | |
| $NIKTO{'POSIX'}{'term'}->setlflag($NIKTO{'POSIX'}{'oterm'}) if ($^O !~ /MSWin32/); | |
| exit 1; | |
| } | |
| ############################################################################### | |
| sub check_input { | |
| my ($mark) = @_; | |
| my $key = readkey(); | |
| return if $key eq ''; | |
| # Key to OUTPUT field mapping for toggles | |
| my %toggles = (v => 'verbose', | |
| d => 'debug', | |
| e => 'errors', | |
| p => 'progress', | |
| r => 'show_redirects', | |
| c => 'show_cookies', | |
| o => 'show_ok', | |
| a => 'show_auth', | |
| ); | |
| if ($key eq ' ') { | |
| status_report($mark); | |
| } | |
| elsif (exists $toggles{$key}) { | |
| $OUTPUT{ $toggles{$key} } = !$OUTPUT{ $toggles{$key} }; | |
| } | |
| elsif ($key eq 'q' || ord($key) == 3) { | |
| safe_quit($mark); | |
| } | |
| elsif ($key eq 'P') { | |
| status_report($mark); | |
| pause(); | |
| } | |
| elsif ($key eq 'N') { | |
| nprint("- Terminating host scan."); | |
| return 'term'; | |
| } | |
| return; | |
| } | |
| ############################################################################### | |
| sub pause { | |
| return if ($^O =~ /MSWin32/); | |
| nprint("- Pausing--press P to resume."); | |
| while (readkey() ne 'P') { sleep 1; } | |
| nprint("- Resuming."); | |
| } | |
| ############################################################################### | |
| sub readkey { | |
| return if $^O =~ /MSWin32/; # Early return for Windows | |
| my $key; | |
| $NIKTO{'POSIX'}{'term'}->setlflag($NIKTO{'POSIX'}{'noecho'}); | |
| $NIKTO{'POSIX'}{'term'}->setattr($NIKTO{'POSIX'}{'fd_stdin'}, TCSANOW); | |
| eval { | |
| local $SIG{ALRM} = sub { die; }; | |
| ualarm(1_000); | |
| sysread(STDIN, $key, 1); | |
| ualarm(0); | |
| }; | |
| $NIKTO{'POSIX'}{'term'}->setlflag($NIKTO{'POSIX'}{'oterm'}); | |
| $NIKTO{'POSIX'}{'term'}->setattr($NIKTO{'POSIX'}{'fd_stdin'}, TCSANOW); | |
| return $key; | |
| } | |
| ############################################################################### | |
| sub resolve { | |
| my $ident = $_[0] or return; | |
| my $report = defined $_[1] ? $_[1] : 1; | |
| my ($ip, $name, $ipcache) = ""; | |
| my (@addresses, @scrub); | |
| my $is6 = 0; | |
| if (($CONFIGFILE{'PROXYHOST'} ne '') && $CLI{'useproxy'}) { | |
| return $ident, $ident, $ident; | |
| } | |
| if ($ident =~ /^$LW2::IPv4_re$/) { # ident is IPv4 | |
| $ip = $name = $ident; | |
| } | |
| elsif ($ident =~ /^\[?($LW2::IPv6_re_inc_zoneid)\]?$/) { | |
| $ip = $1; | |
| $name = $ident; # HTTP host header uses [IPv6] rather than the raw IPv6 address | |
| } | |
| else # not an IP, assume name & resolve | |
| { | |
| if ($CLI{'skiplookup'}) { | |
| nprint("+ ERROR: -nolookup set, but given name\n"); | |
| exit 1; | |
| } | |
| if ($LW2::LW2_CAN_IPv6) { # IPv4/v6 resolve | |
| use Socket qw(:addrinfo SOCK_RAW); | |
| my ($err, @res) = Socket::getaddrinfo($ident, "", { socktype => SOCK_RAW }); | |
| if ($err) { | |
| my $msg = "ERROR: Cannot resolve hostname '$ident' because '$err'."; | |
| if ($ident =~ /^\[?($LW2::IPv6_re_inc_zoneid)\]?$/) { | |
| $msg .= " Use the -ipv6 flag if needed."; | |
| } | |
| if ($CLI{'ipv6'}) { | |
| $msg .= | |
| " Ensure you have IPv6 connectivity. Trying running Nikto with the '-check6' flag."; | |
| } | |
| return $ident, '', $ident, $msg; | |
| } | |
| foreach my $res (@res) { | |
| my ($err, $ip) = Socket::getnameinfo($res->{addr}, NI_NUMERICHOST, NIx_NOSERV); | |
| push @addresses, $ip unless $err; | |
| } | |
| } | |
| else { # Traditional IPv4 resolve | |
| if ($hent = gethostbyname($ident)) { | |
| my $addr_ref = $hent->addr_list; | |
| @addresses = map { inet_ntoa($_) } @$addr_ref; | |
| } | |
| } | |
| my @temp4_ipcache; | |
| my @temp6_ipcache; | |
| my %seen; | |
| foreach $temp_ip (@addresses) { | |
| if ($temp_ip =~ /:/) { | |
| push @temp6_ipcache, $temp_ip if !$seen{$temp_ip}++; | |
| } | |
| else { | |
| push @temp4_ipcache, $temp_ip if !$seen{$temp_ip}++; | |
| } | |
| } | |
| $ip = ($CLI{'ipv6'}) ? shift @temp6_ipcache : shift @temp4_ipcache; | |
| push(@scrub, $ip, @temp4_ipcache, @temp6_ipcache); | |
| $ipcache = join ", ", (@temp4_ipcache, @temp6_ipcache); | |
| if ($ip eq '') { | |
| if ($CLI{'ipv6'} && scalar @temp4_ipcache) { | |
| nprint( | |
| "+ ERROR: IPv6 scanning mode requested but only IPv4 addresses found ($ipcache)" | |
| ); | |
| } | |
| elsif ($CLI{'ipv4'} && scalar @temp6_ipcache) { | |
| nprint( | |
| "+ ERROR: IPv4 scanning mode requested but only IPv6 addresses found ($ipcache)" | |
| ); | |
| } | |
| exit 1; | |
| } | |
| if ($ipcache ne "" && $report) { | |
| nprint("+ Multiple IPs found: $ip, $ipcache", "", @scrub); | |
| } | |
| if ( $ip !~ /^$LW2::IPv4_re$/ | |
| && $ip !~ /^$LW2::IPv6_re$/) { | |
| nprint("+ ERROR: Invalid IP: $ip\n\n", | |
| "", ($ident, $ip, $ident)); | |
| exit 1; | |
| } | |
| $name = $ident; | |
| } | |
| my $displayname = ($name) ? $name : $ip; | |
| return $name, $ip, $displayname; | |
| } | |
| ############################################################################### | |
| sub set_targets { | |
| my ($hostlist, $portlist, $ssl, $root) = @_; | |
| my $host_ctr = 1; | |
| my @hosts = split(/,/, $hostlist); | |
| my @tempports = split(/,/, $portlist) if defined $portlist; | |
| my (@ports, @checkhosts, @results, @marks); | |
| my $defaultport = ($ssl) ? 443 : 80; | |
| nprint("- Getting targets", "v", "Init"); | |
| # Check for portlist and expand | |
| foreach my $port (@tempports) { | |
| if ($port =~ /-/) { | |
| my ($start, $end); | |
| my @temp = split(/-/, $port); | |
| $start = $temp[0]; | |
| $end = $temp[1]; | |
| if ($start eq "") { $start = 0; } | |
| if ($end eq "") { $end = 65535; } | |
| if ($start > $end) { | |
| nprint("+ ERROR port range $port doesn't make sense - assuming 80/tcp"); | |
| next; | |
| } | |
| for (my $i = $start ; $i <= $end ; $i++) { | |
| push(@ports, $i); | |
| } | |
| } | |
| else { | |
| push(@ports, $port); | |
| } | |
| } | |
| # no ports explicitly set, so use default port | |
| if (scalar(@ports) == 0) { | |
| push(@ports, $defaultport); | |
| } | |
| # check whether -h is a file or an entry | |
| foreach my $host (@hosts) { | |
| if (-f $host || $host eq "-") { | |
| @results = parse_hostfile($host); | |
| push(@checkhosts, @results); | |
| } | |
| else { | |
| push(@checkhosts, $host); | |
| } | |
| } | |
| # Now parse the list of checkhosts | |
| foreach my $host (@checkhosts) { | |
| $host =~ s/\s+//g; | |
| if ($host eq '') { next; } | |
| my $markhash = {}; | |
| $markhash->{'root'} = $root; | |
| $markhash->{'cookiejar'} = LW2::cookie_new_jar(); | |
| # is it a URL? | |
| if ($host =~ /^https?:\/\//) { | |
| if ($CLI{'ports'} ne '') { | |
| nprint("- ERROR: The -port option cannot be used with a full URI"); | |
| exit 1; | |
| } | |
| my @hostdata = LW2::uri_split($host); | |
| $markhash->{'ident'} = $hostdata[2]; | |
| $markhash->{'port'} = $hostdata[3]; | |
| if ($markhash->{'port'} eq '') { | |
| if ($host =~ /^https:/) { $markhash->{'port'} = 443; } | |
| else { $markhash->{'port'} = $defaultport; } | |
| } | |
| # If URL included a path, add that as the root unless -root was specified | |
| if (($hostdata[0] ne '/') && ($hostdata[0] ne '') && ($markhash->{'root'} eq '')) { | |
| $hostdata[0] =~ s/\/$//; | |
| $markhash->{'root'} = $hostdata[0]; | |
| nprint("- Added -root value of '$hostdata[0]' from URI", | |
| "v", ($mark->{'hostname'}, $mark->{'ip'}, $mark->{'displayname'})); | |
| } | |
| push(@marks, $markhash); | |
| } | |
| else { | |
| if ((index $host, '[') == 0) { # looks like accepted IPv6 format | |
| if ($host =~ /^(\[?$LW2::IPv6_re_inc_zoneid\]?)(?:[:](\d+))?$/) { | |
| $markhash->{'ident'} = $1; | |
| $markhash->{'port'} = $2; | |
| push(@marks, $markhash); | |
| } | |
| else { | |
| nprint("- ERROR: Unrecognised target host format: $host", | |
| "", ($mark->{'hostname'}, $mark->{'ip'}, $mark->{'displayname'})); | |
| } | |
| } | |
| else { | |
| my @h = split(/\:|\,/, $host); | |
| if (scalar @h > 2 || $h[0] eq '') { # Possible invalid IPv6 format has been supplied | |
| nprint( | |
| "- ERROR: Target host '$host' contains more than one colon (:). If specifying an IPv6 target, use the [IPv6] format.", | |
| "", | |
| ($mark->{'hostname'}, $mark->{'ip'}, $mark->{'displayname'}) | |
| ); | |
| } | |
| else { | |
| $markhash->{'ident'} = $h[0]; | |
| if ($h[1] !~ /[^0-9]/ && $h[1] ne '') { | |
| $markhash->{'port'} = $h[1]; | |
| push(@marks, $markhash); | |
| } | |
| else { | |
| # push unique array ref for each port | |
| my $ti = $markhash->{'ident'}; | |
| my $tr = $markhash->{'root'}; | |
| foreach my $p (@ports) { | |
| my $markhash = { 'port' => $p, | |
| 'root' => $tr, | |
| 'ident' => $ti | |
| }; | |
| $markhash->{'port'} = $p; | |
| push(@marks, $markhash); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| return @marks; | |
| } | |
| ############################################################################### | |
| sub platform_profiler { | |
| if (defined $CLI{'platform'}) { | |
| if ($CLI{'platform'} =~ /(nix|win|all)/i) { | |
| return $CLI{'platform'}; | |
| } | |
| else { | |
| $VARIABLES{'deferout'} = 0; | |
| nprint("+ ERROR: Invalid platform: $CLI{'platform'}"); | |
| exit 1; | |
| } | |
| } | |
| my ($mark) = @_; | |
| my @profile_pages = ("/", "/server-status", | |
| "/icons/", "/trace.axd", | |
| "/nosuchfile.asp", "/nosuchfile.aspx", | |
| "/localstart.asp", "/docs/", | |
| "/server" | |
| ); | |
| foreach my $file (@profile_pages) { | |
| my ($res, $content, $error, $request, $response) = | |
| nfetch($mark, $file, "GET", "", "", "", "platform_profiler"); | |
| # Look for indicators of the platform in the Server header | |
| if ( $mark->{'banner'} | |
| && $VARIABLES{'@PLATFORMNIX'} | |
| && $mark->{'banner'} =~ /$VARIABLES{'@PLATFORMNIX'}/i) { | |
| return 'nix'; | |
| } | |
| elsif ( $mark->{'banner'} | |
| && $VARIABLES{'@PLATFORMWIN'} | |
| && $mark->{'banner'} =~ /$VARIABLES{'@PLATFORMWIN'}/i) { | |
| return 'win'; | |
| } | |
| # check the response body | |
| if ($content && $content =~ /$VARIABLES{'@PLATFORMNIX'}/i) { | |
| return 'nix'; | |
| } | |
| elsif ($content && $content =~ /$VARIABLES{'@PLATFORMWIN'}/i) { | |
| return 'win'; | |
| } | |
| # check the response headers | |
| if ($response && ref($response) eq 'HASH') { | |
| foreach my $header (keys %$response) { | |
| my $value = $response->{$header}; | |
| next unless defined $value; | |
| # Check header name | |
| if ($header =~ /$VARIABLES{'@PLATFORMNIX'}/i) { | |
| return 'nix'; | |
| } | |
| elsif ($header =~ /$VARIABLES{'@PLATFORMWIN'}/i) { | |
| return 'win'; | |
| } | |
| # Check header value (handle arrays) | |
| my $header_value = ref($value) eq 'ARRAY' ? join(', ', @$value) : $value; | |
| if ($header_value =~ /$VARIABLES{'@PLATFORMNIX'}/i) { | |
| return 'nix'; | |
| } | |
| elsif ($header_value =~ /$VARIABLES{'@PLATFORMWIN'}/i) { | |
| return 'win'; | |
| } | |
| } | |
| } | |
| } | |
| return 'all'; | |
| } | |
| ############################################################################### | |
| sub load_databases { | |
| my @dbs = qw/db_useragents db_404_strings db_outdated db_variables db_headers_suggested/; | |
| my $prefix = $_[0] || ''; | |
| # Only load the right databases if -Userdbs is set | |
| if ((defined($CLI{'userdbs'})) && ($CLI{'userdbs'} eq 'all')) { | |
| if ($prefix eq '') { return; } | |
| else { push(@dbs, 'db_tests'); } | |
| } | |
| if (($prefix eq 'u') || (!defined($CLI{'userdbs'}))) { push(@dbs, 'db_tests'); } | |
| # verify required files | |
| for my $file (@dbs) { | |
| if (!-r "$CONFIGFILE{'DBDIR'}/$file") { | |
| nprint("+ ERROR: Can't find/read required file \"$CONFIGFILE{'DBDIR'}/$file\""); | |
| exit 1; | |
| } | |
| } | |
| for my $file (@dbs) { | |
| my $filename = $CONFIGFILE{DBDIR} . "/" . $prefix . $file; | |
| if (!-r $filename) { next; } | |
| nprint("- Loading DB: $filename", "d"); | |
| open(IN, "<$filename") || die nprint("+ ERROR: Can't open \"$filename\":$@\n"); | |
| # db_tests | |
| if ($file =~ /u?db_tests/) { push(@DBFILE, <IN>); next; } | |
| # all the other files require per-line processing | |
| else { | |
| my @file; | |
| # Cleanup | |
| while (<IN>) { | |
| chomp; | |
| $_ =~ s/#.*$//; | |
| $_ =~ s/\s+$//; | |
| $_ =~ s/^\s+//; | |
| if ($_ ne "") { push(@file, $_); } | |
| } | |
| # db_variables | |
| if ($file =~ /u?db_variables/) { | |
| foreach my $l (@file) { | |
| if ($l =~ /^@/) { | |
| next if $l eq ''; | |
| my @temp = split(/=/, $l, 2); # Limit to 2 parts to handle = in values | |
| if ( @temp >= 2 | |
| && defined($temp[0]) | |
| && defined($temp[1]) | |
| && $temp[0] ne '' | |
| && $temp[1] ne '') { | |
| $temp[0] =~ s/^\s+|\s+$//g; | |
| $VARIABLES{ $temp[0] } = $temp[1]; | |
| } | |
| } | |
| } | |
| } | |
| # db_headers_suggested | |
| elsif ($file =~ /u?db_headers_suggested/) { | |
| foreach my $l (@file) { | |
| my @T = parse_csv($l); | |
| next if $T[0] eq ''; | |
| $VARIABLES->{'SUGGESTED_HEADERS'}->{ $T[0] } = $T[1]; | |
| } | |
| } | |
| # db_404_strings | |
| elsif ($file =~ /u?db_404_strings/) { | |
| foreach my $l (@file) { | |
| if ($l =~ /^\@CODE=/) { | |
| $l =~ s/^\@CODE=//; | |
| $l = validate_and_fix_regex($l); | |
| $VARIABLES{'ERRCODES'}->{$l} = 1; | |
| } | |
| else { | |
| $l = validate_and_fix_regex($l); | |
| $VARIABLES{'ERRSTRINGS'}->{$l} = 1; | |
| } | |
| } | |
| } | |
| # db_outdated | |
| elsif ($file =~ /u?db_outdated/) { | |
| foreach my $l (@file) { | |
| my @T = parse_csv($l); | |
| next if $T[1] eq ''; | |
| $T[1] = validate_and_fix_regex($T[1]); | |
| $OVERS{ $T[1] }{ $T[2] } = $T[3]; | |
| $OVERS{ $T[1] }{'tid'} = $T[0]; | |
| } | |
| } | |
| # db_useragents | |
| elsif ($file =~ /u?db_useragents/) { | |
| $VARIABLES{'@USERAGENTS'} = []; | |
| foreach my $l (@file) { | |
| next if $l =~ /^\#/; | |
| next if $l eq ''; | |
| $l =~ s/^\s+//; | |
| $l =~ s/\s+$//; | |
| push @{ $VARIABLES{'@USERAGENTS'} }, $l; | |
| } | |
| } | |
| close(IN); | |
| } | |
| } | |
| return; | |
| } | |
| ############################################################################### | |
| # Get directory listing | |
| sub dirlist { | |
| my $DIR = $_[0] || return; | |
| my $PATTERN = $_[1] || ""; | |
| my @FILES_TMP = (); | |
| opendir(DIRECTORY, $DIR) || die print STDERR "+ ERROR: Can't open directory '$DIR': $@"; | |
| foreach my $file (readdir(DIRECTORY)) { | |
| if ($file =~ /^\./) { next; } # skip hidden files, '.' and '..' | |
| if ($PATTERN ne "") { | |
| if ($file =~ /$PATTERN/) { push(@FILES_TMP, $file); } | |
| } | |
| else { push(@FILES_TMP, $file); } | |
| } | |
| closedir(DIRECTORY); | |
| return @FILES_TMP; | |
| } | |
| ############################################################################### | |
| sub check_dbs { | |
| @dbs = dirlist($CONFIGFILE{'DBDIR'}, "^u?db_*"); | |
| my %ALL_IDS; | |
| for my $file (@dbs) { | |
| my $filename = $CONFIGFILE{DBDIR} . "/" . $prefix . $file; | |
| if (!-r $filename) { | |
| nprint("+ ERROR: Unable to read \"$filename\""); | |
| next; | |
| } | |
| open(IN, "<$filename") || die nprint("+ ERROR: Can't open \"$filename\":$@\n"); | |
| nprint("Syntax Check: $filename"); | |
| if ($file =~ /u?db_outdated/) { | |
| my $count = 0; | |
| my %BANNER; | |
| foreach $line (<IN>) { | |
| $line =~ s/^\s+//; | |
| if ($line =~ /^\#/) { next; } | |
| chomp($line); | |
| if ($line eq "" || $line =~ /"nikto_id"/) { next; } | |
| $count++; | |
| my @L = parse_csv($line); | |
| if ($#L ne 3) { nprint("\t+ ERROR: Invalid syntax ($#L): $line"); next; } | |
| if (($L[0] ne 0) && exists($ALL_IDS{ $L[0] })) { | |
| nprint("\t+ ERROR: Duplicate Test ID: $L[0]"); | |
| } | |
| else { $ALL_IDS{ $L[0] } = 1; } | |
| if (exists($BANNER{ $L[1] }) && $L[0] !~ /(600067|600068|601085)/i) { | |
| nprint("\t+ ERROR: Duplicate Server Banner: $line"); | |
| nprint( "\t+ If this expected/needed: Please add the ID $L[0] at line " | |
| . (__LINE__- 2) | |
| . " in the nikto_core.plugin."); | |
| } | |
| else { $BANNER{ $L[1] } = 1; } | |
| } | |
| nprint("\t$count entries"); | |
| } | |
| elsif ($file =~ /u?db_favicon/ || $file =~ /u?db_domino/) { | |
| my $counter = 0; | |
| my %ENTRY; | |
| foreach $line (<IN>) { | |
| $line =~ s/^\s+//; | |
| if ($line =~ /^\#/) { next; } | |
| chomp($line); | |
| if ($line eq "" || $line =~ /"nikto_id"/) { next; } | |
| $counter++; | |
| my @L = parse_csv($line); | |
| if ($#L ne 2) { nprint("\t+ ERROR: Invalid syntax ($#L): $line"); next; } | |
| if (($L[0] ne 0) && exists($ALL_IDS{ $L[0] })) { | |
| nprint("\t+ ERROR: Duplicate Test ID: $L[0]"); | |
| } | |
| else { $ALL_IDS{ $L[0] } = 1; } | |
| if (exists($ENTRY{ $L[1] })) { | |
| nprint("\t+ ERROR: Duplicate entry: $line"); | |
| } | |
| else { $ENTRY{ $L[1] } = 1; } | |
| } | |
| nprint("\t$counter entries"); | |
| } | |
| elsif ($file =~ /u?db_tests/) { | |
| my %ENTRIES; | |
| foreach my $line (<IN>) { | |
| chomp($line); | |
| $line =~ s/^\s+//; | |
| if ($line =~ /^\#|^$/) { next; } | |
| my @L = parse_csv($line); | |
| # Validate field count (should be 9 fields) | |
| if ((count_fields($line, 1) ne 8) && (count_fields($line) ne '')) { | |
| nprint( "\t+ ERROR: Invalid syntax - expected 9 fields, got " | |
| . (scalar(@L)) | |
| . ": $line"); | |
| next; | |
| } | |
| # Validate method | |
| if ( ($L[4] !~ /(GET|POST|TRACE|TRACK|OPTIONS|SEARCH|INDEX)/i) | |
| && ($L[0] ne '006433')) { | |
| nprint("\t+ ERROR: Possibly invalid method: $L[4] on ($line)"); | |
| } | |
| # Validate DSL field is not empty | |
| if ($L[5] eq "") { | |
| nprint("\t+ ERROR: blank DSL field: $line"); | |
| next; | |
| } | |
| # Validate DSL syntax | |
| my $dsl_to_validate = $L[5]; | |
| if (defined $dsl_to_validate && length $dsl_to_validate) { | |
| # Expand @LFI() placeholder before validation, since it needs to be expanded to be valid DSL | |
| $dsl_to_validate = expand_lfi_dsl($dsl_to_validate); | |
| if (defined $dsl_to_validate && length $dsl_to_validate) { | |
| eval { parse_dsl($dsl_to_validate); }; | |
| if ($@) { | |
| nprint( | |
| "\t+ ERROR: Invalid DSL syntax in test $L[0] field 5: \"$L[5]\", error: $@" | |
| ); | |
| } | |
| } | |
| else { | |
| nprint("\t+ ERROR: Empty DSL field test $L[0]"); | |
| } | |
| } | |
| # Validate URI format | |
| if (($L[3] =~ /^\@CG/) && ($L[3] !~ /^\@CGIDIRS/)) { | |
| nprint("\t+ ERROR: Possible \@CGIDIRS misspelling: $line"); | |
| } | |
| if ($L[3] =~ /[\s]/) { | |
| nprint("\t+ ERROR: space in file portion test #$L[0]: '$L[3]'"); | |
| } | |
| # Validate CSV format | |
| if ($line =~ /[^\\]"\s/) { | |
| nprint("\t+ ERROR: space after quote #$L[0]: $line"); | |
| } | |
| if ($line =~ /\s"/) { | |
| nprint("\t+ ERROR: space before quote #$L[0]: $line"); | |
| } | |
| # Check for duplicate entries | |
| $ENTRIES{"$L[3],$L[4],$L[5],$L[6],$L[7],$L[8]"}++; | |
| # Validate Test ID | |
| if (($L[0] ne 0) && exists($ALL_IDS{ $L[0] })) { | |
| nprint("\t+ ERROR: Duplicate Test ID: $L[0]"); | |
| } | |
| else { | |
| $ALL_IDS{ $L[0] } = 1; | |
| } | |
| # Validate Tuning Type | |
| if ($L[2] eq "" || $L[2] =~ /[^a-f0-9]/) { | |
| nprint("\t+ ERROR: Invalid Tuning Type: $line"); | |
| } | |
| # Validate URI patterns | |
| if ( $L[3] =~ '^(/@(?!JUNK)|//)' | |
| && $L[0] !~ | |
| /(000396|000447|000543|000544|000545|000928|000929|001208|001373|001497|002761|002762|003029|007152)/i | |
| ) { | |
| nprint("\t+ ERROR: Possible incorrect slashes: $line"); | |
| nprint( | |
| "\t+ If two or more slashes are needed for this test: Please add the ID $L[0] at line " | |
| . (__LINE__- 2) | |
| . " in the nikto_core.plugin."); | |
| } | |
| if ($L[3] =~ '^@(?!JUNK)[A-Z]+/' && $L[0] !~ /(003348|003349)/i) { | |
| nprint("\t+ ERROR: Possible incorrect slash after \@VARIABLE: $line"); | |
| nprint( | |
| "\t+ If this slash is needed for this test: Please add the ID $L[0] at line " | |
| . (__LINE__- 2) | |
| . " in the nikto_core.plugin."); | |
| } | |
| # Validate POST data usage | |
| if ((($L[4] ne 'POST') && ($L[4] ne 'SEARCH')) && ($L[7] ne '')) { | |
| # Some test IDs need this | |
| if ($L[0] !~ /(006992|000126|000291|001153)/i) { | |
| nprint( | |
| "\t+ ERROR: Possible incorrect use of POST data without POST method on line: $line" | |
| ); | |
| nprint( | |
| "\t+ If the POST data is needed for this test: Please add the ID $L[0] at line " | |
| . (__LINE__- 2) | |
| . " in the nikto_core.plugin."); | |
| } | |
| } | |
| } | |
| foreach $entry (keys %ENTRIES) { | |
| if ($ENTRIES{$entry} > 1) { | |
| nprint("\t+ ERROR: Duplicate Check Syntax ($ENTRIES{$entry}): $entry"); | |
| } | |
| } | |
| nprint("\t" . keys(%ENTRIES) . " entries"); | |
| } | |
| elsif ($file =~ /u?db_variables/) { | |
| my $ctr = 0; | |
| foreach $line (<IN>) { | |
| if ($line !~ /^\@/) { next; } | |
| if ($line !~ /^\@.+\=.+$/i) { nprint("\t+ ERROR: Invalid syntax: $line"); } | |
| $ctr++; | |
| } | |
| nprint("\t$ctr entries"); | |
| } | |
| elsif ($file =~ /u?db_404_strings/ || $file =~ /u?db_dictionary/) { | |
| my $ctr = 1; | |
| my %STRINGS; | |
| foreach $line (<IN>) { | |
| chomp($line); | |
| $line =~ s/\#.*$//; | |
| next if $line eq ''; | |
| my ($result, $bad) = validate_and_fix_regex($line, 1); | |
| if ($bad) { nprint("\t+ ERROR: Invalid regex on line $ctr: \"$line\""); } | |
| if (exists($STRINGS{$line})) { | |
| nprint("\t+ ERROR: Duplicate String: $line"); | |
| } | |
| else { $STRINGS{$line} = 1; } | |
| $ctr++; | |
| } | |
| $ctr--; | |
| nprint("\t$ctr entries"); | |
| } | |
| elsif ($file =~ /u?db_headers_suggested/) { | |
| my $ctr = 0; | |
| my %HEADERS; | |
| foreach $line (<IN>) { | |
| chomp($line); | |
| $line =~ s/\#.*$//; | |
| next if $line eq ''; | |
| my @fields = parse_csv($line); | |
| # Skip header line if present | |
| if ($fields[0] =~ /^header$/i) { next; } | |
| if (scalar(@fields) != 2) { | |
| nprint("\t+ ERROR: Invalid syntax (expected 2 fields): $line"); | |
| } | |
| if (exists($HEADERS{ $fields[0] })) { | |
| nprint("\t+ ERROR: Duplicate Header: $fields[0]"); | |
| } | |
| else { | |
| $HEADERS{ $fields[0] } = 1; | |
| } | |
| $ctr++; | |
| } | |
| nprint("\t$ctr entries"); | |
| } | |
| elsif ($file =~ /u?db_headers_common/) { | |
| my $ctr = 0; | |
| my %HEADERS; | |
| foreach $line (<IN>) { | |
| chomp($line); | |
| $line =~ s/\#.*$//; | |
| next if $line eq ''; | |
| if ((count_fields($line) ne 0) && (count_fields($line) ne '')) { | |
| nprint("\t+ ERROR: Invalid syntax: $line"); | |
| } | |
| if (exists($HEADERS{$line})) { | |
| nprint("\t+ ERROR: Duplicate Header: $line"); | |
| } | |
| else { $HEADERS{$line} = 1; } | |
| $ctr++; | |
| } | |
| nprint("\t$ctr entries"); | |
| } | |
| elsif ($file =~ /u?db_multiple_index/) { | |
| my $ctr = 0; | |
| foreach $line (<IN>) { | |
| if ((count_fields($line) ne 0) && (count_fields($line) ne '')) { | |
| nprint("\t+ ERROR: Invalid syntax: $line"); | |
| } | |
| $ctr++; | |
| } | |
| nprint("\t$ctr entries"); | |
| } | |
| elsif ($file =~ /u?db_useragents/) { | |
| my $ctr = 0; | |
| foreach $line (<IN>) { | |
| chomp($line); | |
| next if $line =~ /^\#/; | |
| next if $line eq ''; | |
| if ($line !~ /^\"[^"]+\"/) { | |
| nprint("\t+ ERROR: Invalid syntax: $line"); | |
| } | |
| $ctr++; | |
| } | |
| nprint("\t$ctr entries"); | |
| } | |
| else { | |
| # It's a file of standard DB type, we can do this intelligently | |
| my (@headers, @regex_fields); | |
| my $ctr = 0, $fields = 0; | |
| foreach $line (<IN>) { | |
| $line =~ s/^#.*//; | |
| next if $line eq ""; | |
| # first, grab the headers | |
| if ($fields == 0) { | |
| @headers = parse_csv($line); | |
| $fields = $#headers; | |
| # check regex fields for syntax | |
| for (my $i = 0 ; $i <= $#headers ; $i++) { | |
| if ( ($headers[$i] eq 'match') | |
| || ($headers[$i] eq 'matchstring') | |
| || ($headers[$i] eq 'server')) { | |
| push(@regex_fields, $i); | |
| } | |
| } | |
| next; | |
| } | |
| chomp($line); | |
| next if $line eq ""; | |
| my @entry = parse_csv($line); | |
| if ($regex_fields[0] ne '') { | |
| foreach my $f (@regex_fields) { | |
| my ($result, $bad) = validate_and_fix_regex($entry[$f], 1); | |
| if ($bad) { | |
| nprint("\t+ ERROR: Invalid regex in field $f on line $ctr: \"$line\""); | |
| } | |
| } | |
| } | |
| if ( (count_fields($line, 1) != $fields - 1) | |
| && (count_fields($line) ne '')) { | |
| nprint("\t+ ERROR: Invalid syntax: $line"); | |
| } | |
| if (($entry[0] ne 0) && exists($ALL_IDS{ $entry[0] })) { | |
| nprint("\t+ ERROR: Duplicate Test ID: $entry[0]"); | |
| } | |
| else { $ALL_IDS{ $entry[0] } = 1; } | |
| $ctr++; | |
| } | |
| nprint("\t$ctr entries"); | |
| } | |
| close(IN); | |
| } | |
| # Try to grab the test IDs from plugins to check for duplicates. Not foolproof. | |
| nprint("Checking plugins for duplicate test IDs"); | |
| my $found = 0; | |
| my @pluginlist = dirlist("$CONFIGFILE{'PLUGINDIR'}", '\.plugin$'); | |
| foreach my $pf (@pluginlist) { | |
| open(PF, "<$CONFIGFILE{'PLUGINDIR'}/$pf") | |
| || die print STDERR "+ ERROR: Unable to open '$pf': $@\n"; | |
| my @file = <PF>; | |
| close(PF); | |
| my @adds = grep(/add_vulnerability\(/, @file); | |
| foreach my $addv (@adds) { | |
| chomp($addv); | |
| my @bits = parse_csv($addv); | |
| $bits[2] =~ s/\s+//g; | |
| $bits[2] =~ s/\"//g; | |
| if ($bits[2] =~ /^[\d]+$/) { | |
| if (($bits[2] ne 0) && exists($ALL_IDS{ $bits[2] })) { | |
| $found++; | |
| nprint("\t+ ERROR: Duplicate Test ID: $bits[2]"); | |
| } | |
| else { $ALL_IDS{ $bits[2] } = 1; } | |
| } | |
| } | |
| } | |
| nprint("\t$found entries"); | |
| # Bad practice here but this one won't parse right above ¯\_(ツ)_/¯ | |
| $ALL_IDS{'000137'} = 1; # TLS issues | |
| # Look for bad/invalid IDs | |
| foreach my $id (keys %ALL_IDS) { | |
| chomp($id); | |
| next if (($id eq 0) || ($id eq '') || ($id eq 'nikto_id')); | |
| if ($id =~ /[^\d]/) { nprint("+ ERROR: Invalid test ID: $id"); next; } | |
| if (length($id) < 6) { nprint("+WARNING: Possibly invalid test ID: $id"); } | |
| } | |
| # Suggest some open IDs | |
| my @open; | |
| my $id = '000001'; | |
| while ($#open < 6) { | |
| if (!exists($ALL_IDS{$id})) { push(@open, $id); } | |
| $id++; | |
| } | |
| nprint("\nSome (probably) open IDs: " . join(", ", @open)); | |
| nprint("\n"); | |
| exit 1; | |
| } | |
| ############################################################################### | |
| sub count_fields { | |
| my $line = $_[0] || return; | |
| my $checkid = $_[1] || 0; | |
| if ($line !~ /^\"/) { return; } | |
| chomp($line); | |
| $line =~ s/\s+$//; | |
| if ($line eq '') { return; } | |
| my @L = parse_csv($line); | |
| if ($checkid && ($L[0] ne 'nikto_id') && (($L[0] =~ /[^0-9]/) || ($L[0] eq ''))) { return -1; } | |
| return $#L; | |
| } | |
| ############################################################################### | |
| sub port_check { | |
| my ($start_time, $hostname, $ip, $port, $key, $cert, $vhost) = @_; | |
| my $m = {}; | |
| $m->{'start_time'} = $start_time; | |
| $m->{'hostname'} = $vhost || $hostname; | |
| $m->{'ip'} = $ip; | |
| $m->{'port'} = $port; | |
| $m->{'ssl'} = 0; | |
| my @checktypes; | |
| if ($CLI{'nossl'}) { @checktypes = ('HTTP'); } | |
| elsif ($CLI{'ssl'} || $CLI{'host'} =~ /^https/i) { @checktypes = ('HTTPS'); } | |
| else { @checktypes = ('HTTP', 'HTTPS'); } | |
| foreach my $method (split(/ /, $CONFIGFILE{'CHECKMETHODS'})) { | |
| $request{'whisker'}->{'method'} = $method; | |
| foreach my $checkssl (@checktypes) { | |
| nprint("- Checking for $checkssl on " | |
| . ($m->{'hostname'} || $m->{'ip'}) | |
| . ":$port, using $method", | |
| "v", | |
| "CheckSSL", | |
| ($m->{'hostname'}, $m->{'ip'}, $m->{'displayname'}) | |
| ); | |
| $m->{ssl} = ($checkssl eq "HTTP") ? 0 : 1; | |
| if ($m->{'ssl'}) { | |
| $m->{'key'} = $key; | |
| $m->{'cert'} = $cert; | |
| } | |
| proxy_check($m); | |
| my ($res, $content, $error, $request, $response) = | |
| nfetch($m, "/", $method, "", "", { noerror => 1, noprefetch => 1, nopostfetch => 1 }, | |
| "PortCheck"); | |
| if ($res) { | |
| # Some Apache servers are annoying and answer non-TLS requests on a TLS server. | |
| if (defined $content | |
| && ($content =~ /plain HTTP (?:to an SSL|request was sent to HTTPS)/)) { | |
| dump_var("Result Hash", \%result, | |
| ($m->{'hostname'}, $m->{'ip'}, $m->{'displayname'})); | |
| next; | |
| } | |
| nprint("- $checkssl server found: " | |
| . ($m->{'hostname'} || $m->{'ip'}) | |
| . ":$port \t$response->{server}", | |
| "d", | |
| ($m->{'hostname'}, $m->{'ip'}, $m->{'displayname'}) | |
| ); | |
| return $m->{'ssl'} + 1; | |
| } | |
| } | |
| } | |
| my $msg = "Unable to connect to " . ($hostname || $ip) . ":$port"; | |
| if ($CLI{'ipv6'}) { | |
| $msg .= | |
| ". Ensure you have IPv6 connectivity. Trying running Nikto with the '-check6' flag."; | |
| } | |
| nprint($VARIABLES{'DIV'}); | |
| return $msg; | |
| } | |
| ############################################################################### | |
| sub load_plugins { | |
| my @pluginlist = dirlist("$CONFIGFILE{'PLUGINDIR'}", '\.plugin$'); | |
| my @all_names; | |
| # populate plugin macros | |
| $CONFIGFILE{'@@NONE'} = ""; | |
| # Check if running plugins is NONE - if so, don't bother initializing plugins | |
| if ($CLI{'plugins'} eq '@@NONE') { | |
| return; | |
| } | |
| foreach my $plugin (@pluginlist) { | |
| my $plugin_name = $plugin; | |
| $plugin_name =~ s/\.plugin$//; | |
| my $plugin_init = $plugin_name . "_init"; | |
| eval { require "$CONFIGFILE{'PLUGINDIR'}/$plugin"; }; | |
| if ($@) { | |
| nprint("- Could not load or parse plugin: $plugin_name\n Error: "); | |
| warn $@; | |
| nprint("- The plugin could not be run."); | |
| } | |
| else { | |
| nprint("- Initializing plugin $plugin_name", "v", "Init"); | |
| # Call initialisation method | |
| if (defined &$plugin_init) { | |
| my $pluginhash = &$plugin_init; | |
| # Add default weights if not already assigned | |
| while (my ($hook, $hook_params) = each(%{ $pluginhash->{'hooks'} })) { | |
| $hook_params->{$hook}->{'weight'} = 50 | |
| unless (defined $hook_params->{$hook}->{'weight'}); | |
| } | |
| $pluginhash->{report_weight} = 50 unless (defined $pluginhash->{report_weight}); | |
| push(@all_names, $pluginhash->{name}); | |
| push(@PLUGINS, $pluginhash); | |
| nprint("- Loaded \"$pluginhash->{full_name}\" plugin.", "v", "Init"); | |
| } | |
| else { | |
| nprint("WARNING: No init found for $plugin_name\n", "d"); | |
| } | |
| } | |
| } | |
| $CONFIGFILE{'@@ALL'} = join(';', @all_names); | |
| my @torun = split(/;/, expand_pluginlist($CLI{'plugins'}, 0)); | |
| # Force-enable report plugins if needed | |
| if ($CLI{'plugins'} =~ /\@NONE/ && defined $CLI{'formats'} && ref($CLI{'formats'}) eq 'ARRAY') { | |
| my %format_map = ('csv' => 'report_csv', | |
| 'json' => 'report_json', | |
| 'htm' => 'report_html', | |
| 'html' => 'report_html', | |
| 'sql' => 'report_sqlg', | |
| 'sqld' => 'report_sqld', | |
| 'txt' => 'report_text', | |
| 'xml' => 'report_xml' | |
| ); | |
| foreach my $fmt (@{ $CLI{'formats'} }) { | |
| if (exists $format_map{$fmt}) { | |
| push(@torun, $format_map{$fmt}) unless grep { $_ eq $format_map{$fmt} } @torun; | |
| } | |
| } | |
| } | |
| # Second pass to ensure that @@ALL is configured | |
| foreach my $plugin (@PLUGINS) { | |
| # Check that the plugin is to be run | |
| # Perl doesn't allow us to use "in", pity | |
| foreach my $torun_plugin (@torun) { | |
| next if ($torun_plugin eq ""); | |
| # split up into parameters | |
| my $name = my $suffix = $torun_plugin; | |
| if ($torun_plugin =~ /\(/) { | |
| $name =~ s/(.*)(\(.*\))/$1/; | |
| $suffix =~ s/(.*)(\(.*\))/$2/; | |
| } | |
| else { | |
| $name = $torun_plugin; | |
| $suffix = ""; | |
| } | |
| if ($plugin->{'name'} =~ /$name/i) { | |
| $plugin->{'run'} = 1; | |
| # Create parameters | |
| if ($suffix ne "") { | |
| my $parameters = {}; | |
| $suffix =~ s/(\()(.*[^\)])(\)?)/$2/; | |
| foreach my $parameter (split(/,/, $suffix)) { | |
| if ($parameter !~ /:/) { | |
| $parameters->{$parameter} = 1; | |
| } | |
| else { | |
| my $key = my $value = $parameter; | |
| $key =~ s/:.*//; | |
| $value =~ s/.*://; | |
| $parameters->{$key} = $value; | |
| } | |
| } | |
| $plugin->{'parameters'} = $parameters; | |
| } | |
| } | |
| } | |
| } | |
| # first build a temporary hash of all known hooks | |
| my %hooks; | |
| foreach my $plugin (@PLUGINS) { | |
| foreach my $hook (keys(%{ $plugin->{'hooks'} })) { | |
| $hooks{$hook} = (); | |
| } | |
| } | |
| # now we know the types of hooks, look through each plugin for them | |
| foreach my $hook (keys(%hooks)) { | |
| foreach my $plugin (@PLUGINS) { | |
| if ($plugin->{'run'} == 1) { | |
| if (defined $plugin->{'hooks'}->{$hook}->{'method'}) { | |
| push(@{ $hooks{$hook} }, $plugin); | |
| } | |
| } | |
| } | |
| } | |
| # Now sort each array by weight | |
| foreach my $hook (keys(%hooks)) { | |
| my @sorted = | |
| sort { $a->{'hooks'}->{$hook}->{'weight'} <=> $b->{'hooks'}->{$hook}->{'weight'} } | |
| @{ $hooks{$hook} }; | |
| $PLUGINORDER{$hook} = \@sorted; | |
| } | |
| } | |
| ############################################################################### | |
| sub run_hooks { | |
| my ($mark, $type, $request, $response) = @_; | |
| # Cache plugin array reference to avoid repeated hash access | |
| my $plugins = $PLUGINORDER{$type}; | |
| return ($request, $response) unless $plugins; | |
| foreach my $plugin (@$plugins) { | |
| return ($request, $response) if $mark->{'terminate'}; | |
| # Cache hook reference to avoid repeated hash access | |
| my $hook = $plugin->{'hooks'}->{$type}; | |
| next unless $hook; | |
| # Check conditionals more efficiently | |
| my $run = 1; | |
| if (my $condition = $hook->{'cond'}) { | |
| $run = eval($condition); | |
| next unless $run; | |
| } | |
| # Cache plugin parameters and full_name | |
| my $parameters = $plugin->{'parameters'}; | |
| my $full_name = $plugin->{'full_name'}; | |
| # Save current output states | |
| my $oldverbose = $OUTPUT{'verbose'}; | |
| my $olddebug = $OUTPUT{'debug'}; | |
| my $olderrors = $OUTPUT{'errors'}; | |
| # Set output flags based on parameters | |
| $OUTPUT{'verbose'} = 1 if $parameters && $parameters->{'verbose'} == 1; | |
| $OUTPUT{'debug'} = 1 if $parameters && $parameters->{'debug'} == 1; | |
| # Print status unless it's a prefetch/postfetch hook | |
| unless ($type eq "prefetch" || $type eq "postfetch") { | |
| nprint("- Running $type for \"$full_name\" plugin", "v", "Plugins"); | |
| $NIKTO{'current_plugin'} = $full_name; | |
| } | |
| # Execute the hook method | |
| &{ $hook->{'method'} }($mark, $parameters, $request, $response); | |
| # Restore output states | |
| $OUTPUT{'verbose'} = $oldverbose; | |
| $OUTPUT{'debug'} = $olddebug; | |
| $OUTPUT{'errors'} = $olderrors; | |
| } | |
| return ($request, $response); | |
| } | |
| ############################################################################### | |
| sub report_head { | |
| # Support multiple formats: use formats array | |
| my @formats_to_process = (); | |
| my %files_to_process = (); | |
| if (defined $CLI{'formats'} && ref($CLI{'formats'}) eq 'ARRAY' && @{ $CLI{'formats'} } > 0) { | |
| # Multiple formats from comma-separated list | |
| @formats_to_process = @{ $CLI{'formats'} }; | |
| if (defined $CLI{'files'} && ref($CLI{'files'}) eq 'HASH') { | |
| %files_to_process = %{ $CLI{'files'} }; | |
| } | |
| } | |
| else { | |
| # Fallback: no formats specified (should not happen, but handle gracefully) | |
| nprint("+ WARNING: No formats specified for reporting", "v", "Reports"); | |
| return; | |
| } | |
| nprint("- Opening reports (" . join(',', @formats_to_process) . ")", "v", "Reports"); | |
| # Process each format separately to ensure unique handles | |
| foreach my $format_to_use (@formats_to_process) { | |
| my $file_to_use = $files_to_process{$format_to_use} || ''; | |
| # Skip file-based formats that don't have a file (unless sqld/none) | |
| if ($file_to_use eq '' && $format_to_use ne 'none' && $format_to_use ne 'sqld') { | |
| nprint("+ ERROR: No file specified for format: $format_to_use", "v", "Reports"); | |
| next; | |
| } | |
| foreach my $i (1 .. 100) { | |
| foreach my $plugin (@PLUGINS) { | |
| if ( $plugin->{run} | |
| && defined $plugin->{report_item} | |
| && $plugin->{report_weight} == $i) { | |
| my $run = 1; | |
| # Check if this plugin handles this format | |
| if (defined $plugin->{report_format}) { | |
| $run = ($format_to_use eq $plugin->{report_format}); | |
| } | |
| if ($run) { | |
| nprint( | |
| "- Opening report for \"$plugin->{full_name}\" plugin ($format_to_use) -> $file_to_use", | |
| "v", "Reports" | |
| ); | |
| my $handle; | |
| if (defined $plugin->{report_head}) { | |
| # Each plugin gets its own unique lexical handle | |
| $handle = &{ $plugin->{report_head} }($file_to_use); | |
| # Ensure autoflush is enabled (already done in plugin, but double-check) | |
| if (defined $handle && ref($handle) eq 'GLOB') { | |
| $handle->autoflush(1); | |
| } | |
| } | |
| # Store this report entry with its unique handle | |
| my $report_entry = { | |
| host_start => $plugin->{report_host_start}, | |
| host_end => $plugin->{report_host_end}, | |
| item => $plugin->{report_item}, | |
| close => $plugin->{report_close}, | |
| summary => $plugin->{report_summary}, | |
| ssl_info => $plugin->{report_ssl_info}, # SSL info hook | |
| handle => $handle, | |
| format => $format_to_use, # Store for debugging | |
| file => $file_to_use, # Store for debugging | |
| }; | |
| push(@REPORTS, $report_entry); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| return; | |
| } | |
| ############################################################################### | |
| # Generate scanid for SQL reporting | |
| # Format: MD5 hash of "protocol://hostname:port/timestamp" | |
| # Example: LW2::md5("https://example.com:443/2025:12:16:03:30:44GMT") | |
| sub generate_scanid { | |
| my ($mark) = @_; | |
| # Determine protocol | |
| my $protocol = $mark->{'ssl'} ? 'https' : 'http'; | |
| # Get hostname (prefer vhost if present) | |
| my $hostname = $mark->{'vhost'} ? $mark->{'vhost'} : $mark->{'hostname'}; | |
| # Get port | |
| my $port = $mark->{'port'} || ($mark->{'ssl'} ? 443 : 80); | |
| # Get GMT timestamp in format YYYY:MM:DD:HH:MM:SSGMT | |
| my @gmt = gmtime(time); | |
| my $timestamp = sprintf("%04d:%02d:%02d:%02d:%02d:%02dGMT", | |
| $gmt[5] + 1900, # year | |
| $gmt[4] + 1, # month | |
| $gmt[3], # day | |
| $gmt[2], # hour | |
| $gmt[1], # minute | |
| $gmt[0] | |
| ); # second | |
| # Generate MD5 hash | |
| my $scanid = LW2::md5("$protocol://$hostname:$port/$timestamp"); | |
| return $scanid; | |
| } | |
| ############################################################################### | |
| sub report_host_start { | |
| my ($mark) = @_; | |
| # Generate scanid for SQL reporting plugins | |
| $mark->{'scanid'} = generate_scanid($mark); | |
| # Go through all reporting modules | |
| foreach my $reporter (@REPORTS) { | |
| if (defined $reporter->{host_start}) { | |
| &{ $reporter->{host_start} }($reporter->{handle}, $mark); | |
| } | |
| } | |
| } | |
| ############################################################################### | |
| sub report_host_end { | |
| my ($mark) = @_; | |
| # Go through all reporting modules | |
| foreach my $reporter (@REPORTS) { | |
| if (defined $reporter->{host_end}) { | |
| &{ $reporter->{host_end} }($reporter->{handle}, $mark); | |
| } | |
| } | |
| } | |
| ############################################################################### | |
| sub report_summary { | |
| my ($mark) = @_; | |
| # Go through all reporting modules | |
| foreach my $reporter (@REPORTS) { | |
| if (defined $reporter->{summary}) { | |
| &{ $reporter->{summary} }($reporter->{handle}, $mark); | |
| } | |
| } | |
| } | |
| ############################################################################### | |
| sub report_item { | |
| my ($mark, $item) = @_; | |
| if (($item->{'uri'} eq 'undef') || ($item->{'uri'} eq '')) { | |
| $item->{'uri'} = '/'; | |
| } | |
| # Go through all reporting modules | |
| foreach my $reporter (@REPORTS) { | |
| if (defined $reporter->{item}) { | |
| &{ $reporter->{item} }($reporter->{handle}, $mark, $item); | |
| } | |
| } | |
| } | |
| ############################################################################### | |
| sub report_ssl_info { | |
| my ($mark) = @_; | |
| # Only report SSL info if SSL is enabled and info is available | |
| return unless ($mark->{'ssl'} && defined $mark->{'ssl_cipher'}); | |
| # Go through all reporting modules | |
| foreach my $reporter (@REPORTS) { | |
| if (defined $reporter->{ssl_info}) { | |
| &{ $reporter->{ssl_info} }($reporter->{handle}, $mark); | |
| } | |
| } | |
| } | |
| ############################################################################### | |
| sub report_close { | |
| # Go through all reporting modules | |
| foreach my $reporter (@REPORTS) { | |
| # Explicitly flush the handle before closing to ensure all data is written | |
| if (defined $reporter->{handle}) { | |
| my $fh = $reporter->{handle}; | |
| # Only flush file handles (GLOB refs), not database handles or other types | |
| if (ref($fh) eq 'GLOB') { | |
| # Ensure autoflush is enabled | |
| $fh->autoflush(1); | |
| # Try to flush explicitly if method exists | |
| if ($fh->can('flush')) { | |
| eval { $fh->flush(); }; | |
| } | |
| } | |
| } | |
| # Call plugin's close function (it may close the handle itself) | |
| if (defined $reporter->{close}) { | |
| &{ $reporter->{close} }($reporter->{handle}); | |
| } | |
| # Explicitly close file handles after plugin's close (in case plugin didn't close it) | |
| # Only close GLOB file handles, not database handles (DBI) or STDOUT | |
| if (defined $reporter->{handle} && ref($reporter->{handle}) eq 'GLOB') { | |
| # Don't close STDOUT/STDERR | |
| if ($reporter->{handle} ne \*STDOUT && $reporter->{handle} ne \*STDERR) { | |
| eval { close($reporter->{handle}); }; | |
| } | |
| } | |
| } | |
| } | |
| ############################################################################### | |
| # portions of this sub were taken from the Term::ReadPassword module. | |
| # It has been modified to not require Term::ReadLine, but still requires | |
| # POSIX::Termios if it's a POSIX machine | |
| ############################################################################### | |
| sub read_data { | |
| if ($CONFIGFILE{PROMPTS} eq 'no') { return; } | |
| my ($prompt, $mode, $POSIX) = @_; | |
| my $input; | |
| my %SPECIAL = ("\x03" => 'INT', # Control-C, Interrupt | |
| "\x08" => 'DEL', # Backspace | |
| "\x7f" => 'DEL', # Delete | |
| "\x0d" => 'ENT', # CR, Enter | |
| "\x0a" => 'ENT', # LF, Enter | |
| ); | |
| local (*TTY, *TTYOUT); | |
| open TTY, "<&STDIN" or return; | |
| open TTYOUT, ">>&STDOUT" or return; | |
| # Don't buffer it! | |
| select((select(TTYOUT), $| = 1)[0]); | |
| print TTYOUT $prompt; | |
| # Remember where everything was | |
| my $fd_tty = fileno(TTY); | |
| my $term = POSIX::Termios->new(); | |
| $term->getattr($fd_tty); | |
| my $original_flags = $term->getlflag(); | |
| if ($mode eq "noecho") { | |
| my $new_flags = $original_flags & ~(ISIG | ECHO | ICANON); | |
| $term->setlflag($new_flags); | |
| } | |
| $term->setattr($fd_tty, TCSAFLUSH); | |
| KEYSTROKE: | |
| while (1) { | |
| my $new_keys = ''; | |
| my $count = sysread(TTY, $new_keys, 99); | |
| if ($count) { | |
| for my $new_key (split //, $new_keys) { | |
| if (my $meaning = $SPECIAL{$new_key}) { | |
| if ($meaning eq 'ENT') { last KEYSTROKE; } | |
| elsif ($meaning eq 'DEL') { chop $input; } | |
| elsif ($meaning eq 'INT') { last KEYSTROKE; } | |
| else { $input .= $new_key; } | |
| } | |
| else { $input .= $new_key; } | |
| } | |
| } | |
| else { last KEYSTROKE; } | |
| } | |
| # Done with waiting for input. Let's not leave the cursor sitting | |
| # there, after the prompt. | |
| print TTY "\n"; | |
| nprint("\n"); | |
| # Let's put everything back where we found it. | |
| $term->setlflag($original_flags); | |
| $term->setattr($fd_tty, TCSAFLUSH); | |
| close(TTY); | |
| close(TTYOUT); | |
| return $input; | |
| } | |
| ############################################################################### | |
| sub proxy_check { | |
| my ($mark) = @_; | |
| setup_hash(\%request, $mark, "Proxy Check"); | |
| if (($request{'whisker'}->{'proxy_host'} ne '') && ($CLI{'useproxy'})) # proxy is set up | |
| { | |
| LW2::http_close(\%request); # force-close any old connections | |
| $request{'whisker'}->{'method'} = "GET"; | |
| $request{'whisker'}->{'uri'} = "/"; | |
| LW2::http_fixup_request(\%request); | |
| sleeper(); | |
| LW2::http_do_request_timeout(\%request, \%response); | |
| $COUNTERS{'totalrequests'}++; | |
| dump_var("Request Hash", \%request, | |
| ($mark->{'hostname'}, $mark->{'ip'}, $mark->{'displayname'})); | |
| dump_var("Response Hash", | |
| \%response, ($mark->{'hostname'}, $mark->{'ip'}, $mark->{'displayname'})); | |
| # First check that we can connect to the proxy | |
| if (exists $response{'whisker'}{'error'}) { | |
| if ($response{'whisker'}{'error'} =~ /Transport endpoint is not connected/) { | |
| nprint("+ ERROR: Could not connect to the defined proxy $CONFIGFILE{PROXYHOST}"); | |
| } | |
| nprint("+ ERROR: Proxy error: $response{'whisker'}{'error'}"); | |
| exit 1; | |
| } | |
| if ($response{'whisker'}{'code'} eq "407") # proxy requires auth | |
| { | |
| # have id/pw? | |
| if ($CONFIGFILE{PROXYUSER} eq "") { | |
| $CONFIGFILE{PROXYUSER} = read_data("Proxy ID: ", ""); | |
| $CONFIGFILE{PROXYPASS} = read_data("Proxy Pass: ", "noecho"); | |
| } | |
| if ($response{'proxy-authenticate'} !~ /Basic/i) { | |
| my @x = split(/ /, $response{'proxy-authenticate'}); | |
| nprint( | |
| "+ Proxy server uses '$x[0]' rather than 'Basic' authentication. $VARIABLES{'name'} $VARIABLES{'version'} can't do that." | |
| ); | |
| exit 1; | |
| } | |
| # test it... | |
| LW2::http_close(\%request); # force-close any old connections | |
| LW2::auth_set("proxy-basic", \%request, $CONFIGFILE{PROXYUSER}, $CONFIGFILE{PROXYPASS}) | |
| ; # set auth | |
| LW2::http_fixup_request(\%request); | |
| sleeper(); | |
| LW2::http_do_request_timeout(\%request, \%response); | |
| $COUNTERS{'totalrequests'}++; | |
| dump_var("Request Hash", \%request, | |
| ($mark->{'hostname'}, $mark->{'ip'}, $mark->{'displayname'})); | |
| dump_var("Response Hash", | |
| \%response, ($mark->{'hostname'}, $mark->{'ip'}, $mark->{'displayname'})); | |
| if ($response{'proxy-authenticate'} ne "") { | |
| my @pauthinfo = split(/ /, $response{'proxy-authenticate'}); | |
| my @pauthinfo2 = split(/=/, $response{'proxy-authenticate'}); | |
| $pauthinfo2[1] =~ s/^\"//; | |
| $pauthinfo2[1] =~ s/\"$//; | |
| nprint( | |
| "+ Proxy requires authentication for '$pauthinfo[0]' realm '$pauthinfo2[1]', unable to authenticate." | |
| ); | |
| exit 1; | |
| } | |
| else { nprint("- Successfully authenticated to proxy.", "v", undef); } | |
| } | |
| } | |
| return; | |
| } | |
| ####################################################################### | |
| sub dump_var { | |
| return if !$OUTPUT{'debug'}; | |
| my $msg = $_[0]; | |
| my %hash_in = %{ $_[1] }; | |
| my @scrubs; | |
| for (my $i = 2 ; $i <= $#_ ; $i++) { | |
| push(@scrubs, $_[$i]); | |
| } | |
| my $display = LW2::dump('', \%hash_in); | |
| my $new; | |
| $display =~ s/^\$/'$msg'/; | |
| if ($OUTPUT{'scrub'}) { | |
| $new = ""; | |
| foreach my $line (split(/\n/, $display)) { | |
| $line = scrub($line, @scrubs); | |
| $new .= "$line\n"; | |
| } | |
| $display = $new; | |
| } | |
| nprint($display, "d"); | |
| return; | |
| } | |
| ####################################################################### | |
| sub get_ua { | |
| # Always honor command line | |
| if ($CLI{'useragent'}) { | |
| return $CLI{'useragent'}; | |
| } | |
| # Return a random User-Agent from @USERAGENTS array | |
| if ( defined($VARIABLES{'@USERAGENTS'}) | |
| && ref($VARIABLES{'@USERAGENTS'}) eq 'ARRAY' | |
| && @{ $VARIABLES{'@USERAGENTS'} }) { | |
| my $ua = $VARIABLES{'@USERAGENTS'}->[ rand(@{ $VARIABLES{'@USERAGENTS'} }) ]; | |
| $ua =~ s/^"//; | |
| $ua =~ s/"$//; | |
| return $ua; | |
| } | |
| return undef; | |
| } | |
| ####################################################################### | |
| sub setup_hash { | |
| my ($reqhash, $mark, $testid) = @_; | |
| # Clear the hash first (like LW2::http_init_request does) | |
| %$reqhash = (); | |
| # Initialize the whisker hash | |
| $reqhash->{'whisker'} = {}; | |
| # Cache whisker hash reference to avoid repeated dereferencing | |
| my $whisker = $reqhash->{'whisker'}; | |
| # Set all required whisker properties (matching LW2::http_init_request defaults) | |
| $whisker->{'http_space1'} = ' '; | |
| $whisker->{'http_space2'} = ' '; | |
| $whisker->{'version'} = $CONFIGFILE{'DEFAULTHTTPVER'} || '1.1'; | |
| $whisker->{'method'} = 'GET'; | |
| $whisker->{'protocol'} = 'HTTP'; | |
| $whisker->{'port'} = $mark->{'port'} || 80; | |
| $whisker->{'uri'} = '/'; | |
| $whisker->{'uri_prefix'} = ''; | |
| $whisker->{'uri_postfix'} = ''; | |
| $whisker->{'uri_param_sep'} = '?'; | |
| $whisker->{'host'} = $mark->{'hostname'} || $mark->{'ip'}; | |
| $whisker->{'timeout'} = $CLI{'timeout'} || 10; | |
| $whisker->{'include_host_in_uri'} = 0; | |
| $whisker->{'ignore_duplicate_headers'} = 0; | |
| $whisker->{'normalize_incoming_headers'} = 1; | |
| $whisker->{'lowercase_incoming_headers'} = 1; | |
| $whisker->{'require_newline_after_headers'} = 0; | |
| $whisker->{'invalid_protocol_return_value'} = 1; | |
| $whisker->{'ssl'} = $mark->{'ssl'} || 0; | |
| $whisker->{'ssl_save_info'} = 1; | |
| $whisker->{'http_eol'} = "\x0d\x0a"; | |
| $whisker->{'force_close'} = 0; | |
| $whisker->{'force_open'} = 0; | |
| $whisker->{'retry'} = 0; | |
| $whisker->{'trailing_slurp'} = 0; | |
| $whisker->{'force_bodysnatch'} = 0; | |
| $whisker->{'max_size'} = 750000; | |
| $whisker->{'MAGIC'} = 31339; | |
| # Set SSL-specific fields if needed | |
| if ($mark->{'ssl'}) { | |
| $whisker->{'ssl_rsacertfile'} = $mark->{'key'}; | |
| $whisker->{'ssl_certfile'} = $mark->{'cert'}; | |
| } | |
| # Set evasion only if needed | |
| $whisker->{'anti_ids'} = $CLI{'evasion'} if (length($CLI{'evasion'})); | |
| # Set default headers (like LW2::http_init_request does) | |
| $reqhash->{'Connection'} = 'Keep-Alive'; | |
| # Random User-Agent | |
| $reqhash->{'User-Agent'} = get_ua(); | |
| # Set Host header only if vhost is configured | |
| if ($mark->{'has_vhost'}) { | |
| $reqhash->{'Host'} = $mark->{'vhost'}; | |
| } | |
| # Proxy configuration | |
| if (length($CONFIGFILE{PROXYHOST}) && $CLI{'useproxy'}) { | |
| $whisker->{'proxy_host'} = $CONFIGFILE{'PROXYHOST'}; | |
| $whisker->{'proxy_port'} = $CONFIGFILE{'PROXYPORT'}; | |
| # Set proxy auth only if credentials are provided | |
| if (length($CONFIGFILE{'PROXYUSER'})) { | |
| LW2::auth_set("proxy-basic", $reqhash, | |
| $CONFIGFILE{'PROXYUSER'}, | |
| $CONFIGFILE{'PROXYPASS'}); | |
| } | |
| } | |
| return $reqhash; | |
| } | |
| ####################################################################### | |
| sub running_average { | |
| my $last = shift; | |
| my ($mark) = @_; | |
| # Use push instead of unshift for better performance | |
| push(@{ $mark->{'running_avg'} }, $last); | |
| # Only splice if we exceed the limit (more efficient than always splicing) | |
| if (@{ $mark->{'running_avg'} } > 100) { | |
| splice(@{ $mark->{'running_avg'} }, 0, @{ $mark->{'running_avg'} } - 100); | |
| } | |
| } | |
| ####################################################################### | |
| sub running_average_print { | |
| use List::Util qw(sum); | |
| my ($mark) = @_; | |
| my @data = @{ $mark->{'running_avg'} }; | |
| my $elements = @data; # More efficient than $#data + 1 | |
| return "Running average: Not enough data." if $elements == 0; | |
| my $message = ''; | |
| if ($elements == 100) { | |
| my $avg = sum(@data) / $elements; | |
| $message = sprintf("100 requests: %.5f sec, ", $avg); | |
| } | |
| if ($elements > 10) { | |
| my @recent_data = @data[ ($#data - 9) .. $#data ]; | |
| my $recent_count = @recent_data; | |
| my $avg = sum(@recent_data) / $recent_count; | |
| $message .= sprintf("10 requests: %.4f sec", $avg); | |
| } | |
| return "Running average: $message."; | |
| } | |
| ####################################################################### | |
| sub nfetch { | |
| my ($mark, $uri, $method, $data, $headers_send, $flags, $testid, $httpver) = @_; | |
| my (%request, %response); | |
| setup_hash(\%request, $mark, $testid); | |
| # Ensure $flags is a hash reference (handle cases where empty string is passed) | |
| if (!ref($flags) || ref($flags) ne 'HASH') { | |
| $flags = {}; | |
| } | |
| # Check for keyboard input & terminate flag | |
| if (!$CLI{'nointeractive'} && !(($COUNTERS{'totalrequests'} % 10))) { | |
| $mark->{'terminate'} = 1 if (check_input($mark) eq 'term'); | |
| } | |
| # Check execution time | |
| if (my $maxtime = $CLI{'maxtime'}) { | |
| # Cache start_time to avoid repeated hash access | |
| my $start_time = $mark->{'start_time'}; | |
| if ((time() - $start_time) > $maxtime) { | |
| nprint("+ ERROR: Host maximum execution time of $maxtime seconds reached"); | |
| $mark->{'terminate'} = 1; | |
| } | |
| } | |
| # Prepend -root option's value if set | |
| $request{'whisker'}->{'uri'} = $mark->{'root'} . $uri; | |
| # Remove trailing slash if requested | |
| $request{'whisker'}->{'uri'} =~ s/\/$// if ($CLI{'noslash'}); | |
| $request{'whisker'}->{'method'} = $method; | |
| # POST data? | |
| if (length($data)) { | |
| $data =~ s/\\\"/\"/g; | |
| $request{'whisker'}->{'data'} = $data; | |
| } | |
| # Default an unobtrusive headers to help WAF evasion | |
| $request{'whisker'}->{'Accept'} = | |
| 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'; | |
| $request{'whisker'}->{'Accept-Language'} = 'en-US,en;q=0.5'; | |
| $request{'whisker'}->{'Cache-Control'} = 'max-age=0'; | |
| $request{'whisker'}->{'Connection'} = 'keep-alive'; | |
| $request{'whisker'}->{'Upgrade-Insecure-Requests'} = '1'; | |
| $request{'whisker'}->{'Sec-Fetch-Dest'} = 'document'; | |
| $request{'whisker'}->{'Sec-Fetch-Mode'} = 'navigate'; | |
| $request{'whisker'}->{'Sec-Fetch-Site'} = 'none'; | |
| $request{'whisker'}->{'Sec-Fetch-User'} = '?1'; | |
| # Check for extra HTTP headers | |
| if (ref($headers_send) eq "HASH") { | |
| # Use explicit hash assignment instead of slice assignment | |
| foreach my $key (keys %$headers_send) { | |
| $request{$key} = $headers_send->{$key}; | |
| } | |
| } | |
| # Add custom headers from CLI if any | |
| if (defined $CLI{'headers'} && @{ $CLI{'headers'} }) { | |
| foreach my $header (@{ $CLI{'headers'} }) { | |
| if ($header =~ /^([^:]+):\s*(.+)$/) { | |
| my ($headername, $value) = ($1, $2); | |
| $request{$headername} = $value; | |
| } | |
| } | |
| } | |
| # Set auth | |
| if (my $realm = $mark->{'realms'}{'default'}) { | |
| if (length($realm->{'authtype'})) { | |
| LW2::auth_set($realm->{'authtype'}, $request, $realm->{'id'}, $realm->{'password'}); | |
| } | |
| } | |
| # Set cookies | |
| LW2::cookie_write($mark->{'cookiejar'}, \%request, 1) if defined($mark->{'cookiejar'}); | |
| # Override HTTP version | |
| $request{'whisker'}->{'version'} = $httpver if ($httpver ne ''); | |
| $request{'whisker'}->{'host'} = $mark->{'ip'} if ($flags->{'nohost'}); | |
| LW2::http_fixup_request(\%request) unless ($flags->{'noclean'}); | |
| # Run pre hooks | |
| unless ($flags->{'noprefetch'}) { | |
| (%$request, %$response) = run_hooks($mark, "prefetch", \%request, \%response); | |
| } | |
| # Do the request | |
| sleeper(); | |
| my $time = [gettimeofday]; | |
| LW2::http_do_request_timeout(\%request, \%response); | |
| $COUNTERS{'totalrequests'}++; | |
| if (!$VARIABLES{'MSWIN32'}) { | |
| running_average(tv_interval($time, [gettimeofday]), $mark); | |
| } | |
| # If we got an error, do 1 retry - optimized | |
| if (my $whisker = $response{'whisker'}) { | |
| if (defined $whisker->{'error'} || $whisker->{'code'} eq '') { | |
| $mark->{'failures'}++; | |
| sleeper(); | |
| LW2::http_do_request_timeout(\%request, \%response); | |
| $COUNTERS{'totalrequests'}++; | |
| } | |
| } | |
| # Get cookies from response & add to jar | |
| if (!$CLI{'nocookies'}) { | |
| my $tmpjar = LW2::cookie_new_jar(); | |
| LW2::cookie_read(\%tmpjar, \%response, \%request); | |
| # Cache cookiejar reference to avoid repeated hash access | |
| my $cookiejar = $mark->{'cookiejar'}; | |
| # Use more efficient array construction | |
| foreach my $c (keys %tmpjar) { | |
| my $cookie_data = $tmpjar{$c}; | |
| $cookiejar->{$c} = | |
| [ $cookie_data->[0], $cookie_data->[1], $cookie_data->[2], undef, $cookie_data->[4] ]; | |
| } | |
| } | |
| # follow redirects | |
| if ($CLI{'followredirects'} && ($response{'whisker'}->{'code'} =~ /^30[1278]/)) { | |
| my $newlocation = $response{'location'}; | |
| my $port = $mark->{'port'}; | |
| # Is a full URL redirect the same host? | |
| # Pre-compute host alternatives for better performance | |
| my $host_re = join '|', | |
| map { quotemeta $_ } ($mark->{'ip'}, $mark->{'hostname'}, $mark->{'display_name'}); | |
| # Only build port regex if port is specified | |
| if ($port && $port != 80 && $port != 443) { | |
| my $port_re = '(?:\:' . quotemeta($port) . ')?'; | |
| if ($response{'location'} =~ /^https?:\/\/($host_re)$port_re\//i) { | |
| $newlocation =~ s{^https?:\/\/(?:$host_re)$port_re/}{/}i; | |
| } | |
| } | |
| else { | |
| # No port needed in regex for standard ports | |
| if ($response{'location'} =~ /^https?:\/\/($host_re)\//i) { | |
| $newlocation =~ s{^https?:\/\/(?:$host_re)/}{/}i; | |
| } | |
| } | |
| # Cache whisker reference to avoid repeated hash access | |
| my $whisker = $request{'whisker'}; | |
| $whisker->{'uri'} = $newlocation; | |
| # Make redirect request | |
| LW2::http_fixup_request(\%request) unless ($flags->{'noclean'}); | |
| sleeper(); | |
| LW2::http_do_request_timeout(\%request, \%response); | |
| $COUNTERS{'totalrequests'}++; | |
| } | |
| # Check failures | |
| my $fail_limit = $CONFIGFILE{'FAILURES'}; | |
| if ($fail_limit > 0 && $mark->{'failures'} >= $fail_limit) { | |
| nprint( | |
| "+ ERROR: *** Error limit ($CONFIGFILE{'FAILURES'}) reached for host, giving up. Last error: " | |
| . $response{'whisker'}->{'error'} | |
| . ". ***\n+ ERROR: *** Consider using mitmproxy to avoid TLS fingerprinting. ***", | |
| "", | |
| ($mark->{'hostname'}, $mark->{'ip'}, $mark->{'displayname'}) | |
| ); | |
| $mark->{'terminate'} = 1; | |
| status_report(); | |
| } | |
| if ($OUTPUT{'debug'}) { | |
| dump_var("Request Hash", \%request, | |
| ($mark->{'ip'}, $mark->{'hostname'}, $mark->{'displayname'})); | |
| dump_var("Response Hash", | |
| \%response, ($mark->{'ip'}, $mark->{'hostname'}, $mark->{'displayname'})); | |
| } | |
| # Snarf what we can from the whisker hash and put in mark | |
| my $banner = \$mark->{'banner'}; | |
| my $whisker = $response{'whisker'}; | |
| if (!exists $whisker->{'error'}) { | |
| # Banner processing | |
| if ($$banner eq "") { | |
| $$banner = $response{'server'}; | |
| } | |
| elsif ( exists $response{'server'} | |
| && !exists $mark->{'bannerchanged'} | |
| && ($$banner ne $response{'server'}) | |
| && ($response{'server'} ne 'Microsoft-HTTPAPI/2.0')) { | |
| $request->{'whisker'}->{'uri'} = "/" | |
| if ( !defined $request->{'whisker'}->{'uri'} | |
| || $request->{'whisker'}->{'uri'} eq "" | |
| || $request->{'whisker'}->{'uri'} eq "."); | |
| add_vulnerability($mark, | |
| $request->{'whisker'}->{'uri'} | |
| . ": Server banner changed from '$$banner' to '$response{server}'", | |
| 999962, | |
| "", | |
| $method, | |
| $uri, | |
| $request, | |
| $response | |
| ); | |
| $mark->{'bannerchanged'} = 1; | |
| } | |
| # Also check X-Powered-By header for outdated version checking | |
| # Only add to components if it contains version-like information (digits, slashes, or dots) | |
| if (exists $response{'x-powered-by'} | |
| && $response{'x-powered-by'} =~ /(?:\d|\/|\.)/) { | |
| my $xpb_value = $response{'x-powered-by'}; | |
| $xpb_value =~ s/\s+.*$//; # Strip any trailing whitespace/content | |
| if (!exists $mark->{'components'}->{$xpb_value}) { | |
| $mark->{'components'}->{$xpb_value} = 1; | |
| } | |
| } | |
| # TLS | |
| if (!exists $mark->{'ssl_cipher'} && $mark->{'ssl'}) { | |
| # Cache SSL certificate array reference | |
| my $altnames = $whisker->{'ssl_cert_altnames'}; | |
| # Grab ssl details | |
| $mark->{'ssl_cipher'} = $whisker->{'ssl_cipher'}; | |
| $mark->{'ssl_cert_issuer'} = $whisker->{'ssl_cert_issuer'}; | |
| $mark->{'ssl_cert_subject'} = $whisker->{'ssl_cert_subject'}; | |
| # Process altnames correctly - Net::SSLeay::X509_get_subjectAltNames returns | |
| # an array where even indices are type codes and odd indices are the actual names | |
| if ($altnames && @$altnames) { | |
| my @valid_names; | |
| for (my $i = 1 ; $i < @$altnames ; $i += 2) { | |
| my $name = $altnames->[$i]; | |
| # Only include DNS names (type 2) and skip numeric-only names | |
| if ($altnames->[ $i - 1 ] == 2 && $name !~ /^[\d]+$/ && $name ne '') { | |
| push(@valid_names, $name); | |
| } | |
| } | |
| $mark->{'ssl_cert_altnames'} = join(', ', @valid_names); | |
| } | |
| } | |
| } | |
| nprint("- $response{'whisker'}{'code'} for $method:\t$response{'whisker'}->{'uri_requested'}", | |
| "v", $testid, ($mark->{'hostname'}, $mark->{'ip'}, $mark->{'displayname'})); | |
| # Check for errors to reduce false positives | |
| if (my $whisker = $response{'whisker'}) { | |
| if ((defined $whisker->{'error'} || $whisker->{'code'} eq '') | |
| && !exists $flags->{'noerror'}) { | |
| $mark->{'total_errors'}++; | |
| nprint("+ ERROR: $whisker->{'uri_requested'} returned an error: $whisker->{'error'}\n", | |
| "e", ($mark->{'hostname'}, $mark->{'ip'}, $mark->{'displayname'})); | |
| if ($whisker->{'code'} eq '502' && $CLI{'useproxy'}) { | |
| nprint("+ ERROR: Received 502 'Bad Gateway' from proxy\n"); | |
| } | |
| } | |
| } | |
| # Show cookies | |
| if ($OUTPUT{'show_cookies'} && (my $cookies = $response{'whisker'}->{'cookies'})) { | |
| # Cache frequently accessed values | |
| my $uri_requested = $response{'whisker'}->{'uri_requested'}; | |
| my $hostname = $mark->{'hostname'}; | |
| my $ip = $mark->{'ip'}; | |
| my $displayname = $mark->{'displayname'}; | |
| foreach my $c (@$cookies) { | |
| nprint("+ $uri_requested sent cookie: $c", "", ($hostname, $ip, $displayname)); | |
| } | |
| } | |
| # Run post hooks | |
| unless ($flags->{'nopostfetch'}) { | |
| ($request, %$response) = run_hooks($mark, "postfetch", \%request, \%response); | |
| } | |
| return $response{'whisker'}->{'code'}, $response{'whisker'}->{'data'}, | |
| $response{'whisker'}->{'error'}, \%request, \%response; | |
| } | |
| ####################################################################### | |
| sub set_scan_items { | |
| %TESTS = (); | |
| $COUNTERS{total_checks} = 0; | |
| my %SKIPLIST = (); | |
| if (defined $CONFIGFILE{SKIPIDS}) { | |
| foreach my $id (split(/ /, $CONFIGFILE{SKIPIDS})) { | |
| $SKIPLIST{$id} = 1; | |
| } | |
| } | |
| my ($includes, $excludes) = ""; | |
| foreach my $tune (split(//, $CLI{'tuning'})) { | |
| next if ($tune eq "x"); | |
| if ($CLI{'tuning'} !~ /(?<![x])$tune/gi) { | |
| $excludes .= $tune; | |
| } | |
| else { | |
| $includes .= $tune; | |
| } | |
| } | |
| # now load checks | |
| foreach my $line (@DBFILE) { | |
| if ($line =~ /^\"/) # check | |
| { | |
| chomp($line); | |
| my @item = parse_csv($line); | |
| my $add = 0; | |
| # check tuning options | |
| # $item[2] contains the test's types | |
| if (($CLI{'tuning'} ne "") && (defined $item[2])) { | |
| # Work out the required tuning from the CLI string | |
| if ($includes ne "") { | |
| foreach $tune (split(//, $includes)) { | |
| if ($item[2] =~ /$tune/i) { | |
| $add = 1; | |
| last; | |
| } | |
| } | |
| } | |
| if ($excludes ne "") { | |
| # if includes is null and excludes is not null, add all but excludes | |
| foreach $tune (split(//, $excludes)) { | |
| if ($item[2] =~ /$tune/i) { | |
| $add = 0; | |
| last; | |
| } | |
| else { | |
| $add = 1; | |
| } | |
| } | |
| } | |
| } | |
| else { | |
| $add = 1; | |
| } | |
| # Skip list | |
| if ($add && exists $SKIPLIST{ $item[0] }) { | |
| $add = 0; | |
| } | |
| # If $add is still true, then add it | |
| if ($add) { | |
| my $ext = get_ext($item[3]); | |
| $db_extensions{$ext} = 1; | |
| $COUNTERS{total_checks}++; | |
| $TESTS{ $item[0] }{'references'} = $item[1]; | |
| $TESTS{ $item[0] }{'tuning'} = $item[2]; | |
| $TESTS{ $item[0] }{'uri'} = $item[3]; | |
| $TESTS{ $item[0] }{'method'} = $item[4]; | |
| $TESTS{ $item[0] }{'dsl'} = $item[5]; | |
| $TESTS{ $item[0] }{'message'} = $item[6]; | |
| $TESTS{ $item[0] }{'data'} = $item[7]; | |
| $TESTS{ $item[0] }{'headers'} = $item[8]; | |
| $TESTS{ $item[0] }{'matcher'} = build_matcher($item[5], $item[0]); | |
| } | |
| } | |
| } | |
| nprint("- $COUNTERS{'total_checks'} server checks loaded", "v", "Init"); | |
| if ($COUNTERS{'total_checks'} eq 0 && !defined $CLI{'tuning'}) { | |
| nprint("+ Unable to load valid checks!"); | |
| $mark->{'terminate'} = 1; | |
| } | |
| return; | |
| } | |
| ####################################################################### | |
| # Check for updates to the program | |
| # Expects response like: | |
| # { "products": { "nikto": { "version": "2.6.0", "epoch": 1737935000 } } } | |
| sub check_updates { | |
| # Get epoch from program/.timestamp || 0 | |
| my $epoch = 0; | |
| my $timestamp_file = | |
| defined $CONFIGFILE{'EXECDIR'} ? "$CONFIGFILE{'EXECDIR'}/.timestamp" : 'program/.timestamp'; | |
| if (-f $timestamp_file) { | |
| if (open(my $fh, '<', $timestamp_file)) { | |
| $epoch = <$fh>; | |
| chomp($epoch); | |
| close($fh); | |
| } | |
| } | |
| # Request API to get manifest JSON using LibWhisker and configured proxy | |
| return unless defined $CONFIGFILE{'VERSION_API'} && $CONFIGFILE{'VERSION_API'} ne ''; | |
| eval "require JSON::PP"; | |
| return if $@; | |
| my %request; | |
| my %response; | |
| # Parse URL from config | |
| my @uridata = LW2::uri_split($CONFIGFILE{'VERSION_API'}); | |
| my $host = $uridata[2] || ''; | |
| my $port = $uridata[3] || ''; | |
| my $path = $uridata[0] || '/'; | |
| # Determine SSL and default port | |
| my $ssl = ($CONFIGFILE{'VERSION_API'} =~ /^https:/i) ? 1 : 0; | |
| if ($port eq '') { | |
| $port = $ssl ? 443 : 80; | |
| } | |
| # Build URI with query parameters | |
| my $uri = $path; | |
| $uri .= ($path =~ /\?/) ? '&' : '?'; | |
| $uri .= "p=nikto&v=$VARIABLES{'version'}&e=$epoch"; | |
| LW2::http_init_request(\%request); | |
| $request{'whisker'}->{'host'} = $host; | |
| $request{'whisker'}->{'port'} = $port; | |
| $request{'whisker'}->{'ssl'} = $ssl; | |
| $request{'whisker'}->{'uri'} = $uri; | |
| $request{'whisker'}->{'timeout'} = 5; | |
| $request{'User-Agent'} = "Nikto/$VARIABLES{'version'}"; | |
| # Configure proxy if enabled | |
| if (length($CONFIGFILE{PROXYHOST}) && $CLI{'useproxy'}) { | |
| $request{'whisker'}->{'proxy_host'} = $CONFIGFILE{'PROXYHOST'}; | |
| $request{'whisker'}->{'proxy_port'} = $CONFIGFILE{'PROXYPORT'}; | |
| if (length($CONFIGFILE{'PROXYUSER'})) { | |
| LW2::auth_set("proxy-basic", \%request, | |
| $CONFIGFILE{'PROXYUSER'}, | |
| $CONFIGFILE{'PROXYPASS'}); | |
| } | |
| } | |
| LW2::http_fixup_request(\%request); | |
| LW2::http_do_request_timeout(\%request, \%response); | |
| # Check if request succeeded | |
| if (($response{'whisker'}->{'code'} ne '200') || ($response{'whisker'}->{'data'} eq '')) { | |
| nprint("+ ERROR: Failed to check for updates: $response{'whisker'}->{'code'}"); | |
| return; | |
| } | |
| # Parse JSON response | |
| my $json_data; | |
| eval { $json_data = JSON::PP->new->utf8(1)->decode($response{'whisker'}->{'data'}); }; | |
| return if $@ || !$json_data; | |
| # Extract remote version and epoch | |
| my $remote_version = $json_data->{'products'}->{'nikto'}->{'version'} || ''; | |
| my $remote_epoch = $json_data->{'products'}->{'nikto'}->{'epoch'} || 0; | |
| # Compare epoch to manifest epoch | |
| # If remote epoch is greater OR remote version is greater: | |
| if ($remote_epoch > $epoch | |
| || ($remote_version ne '' && $remote_version ne $VARIABLES{'version'})) { | |
| my $defer = $VARIABLES{'deferout'}; | |
| $VARIABLES{'deferout'} = 0; | |
| # Check if git install | |
| my $is_git = (-d '.git' || -d '../.git' || -d '../../.git'); | |
| if ($is_git) { | |
| nprint( | |
| "+ Your Nikto installation is out of date. Please run 'git pull' to update to the latest version of Nikto." | |
| ); | |
| } | |
| else { | |
| nprint("+ Your Nikto installation is out of date."); | |
| } | |
| $VARIABLES{'deferout'} = $defer; | |
| } | |
| } | |
| ####################################################################### | |
| # Expand @LFI() in DSL with platform-specific matchers | |
| sub expand_lfi_dsl { | |
| my ($dsl) = @_; | |
| if ($dsl =~ /@?LFI\(\)/) { | |
| # Local variables for LFI matching | |
| my $lfi_match_win = $VARIABLES{'@LFIMATCHWIN'} || ''; | |
| my $lfi_match_nix = $VARIABLES{'@LFIMATCHNIX'} || ''; | |
| # If both are empty, return original DSL | |
| if (!$lfi_match_win && !$lfi_match_nix) { | |
| return $dsl; | |
| } | |
| # Build the OR pattern: (@LFIMATCHWIN|@LFIMATCHNIX) | |
| # Use string concatenation to preserve backslashes and avoid interpolation issues | |
| my $replacement; | |
| if ($lfi_match_win && $lfi_match_nix) { | |
| $replacement = '(' . $lfi_match_win . '|' . $lfi_match_nix . ')'; | |
| } | |
| elsif ($lfi_match_win) { | |
| $replacement = $lfi_match_win; | |
| } | |
| else { | |
| $replacement = $lfi_match_nix; | |
| } | |
| # Replace @LFI() with the expanded pattern | |
| # Use \Q...\E to quote the replacement and prevent any regex interpretation | |
| # But we need the | and && to work, so we can't quote everything | |
| # Instead, just do the substitution - the replacement side doesn't interpret regex | |
| $dsl =~ s/@?LFI\(\)/$replacement/g; | |
| } | |
| return $dsl; | |
| } | |
| sub build_matcher { | |
| my ($dsl, $checkid) = @_; | |
| # Return early if DSL is undefined or empty | |
| return sub { return (0, []); } | |
| unless defined $dsl && length $dsl; | |
| # Expand @LFI() if present | |
| my $expanded_dsl = expand_lfi_dsl($dsl); | |
| # Use cached parser for speed | |
| my $parsed = $DSL_CACHE{$expanded_dsl}; | |
| if (!$parsed) { | |
| $parsed = parse_dsl($expanded_dsl); | |
| $DSL_CACHE{$expanded_dsl} = $parsed; | |
| } | |
| return sub { | |
| my ($code, $body, $headers, $cookies) = @_; | |
| my @captured_groups = (); # Store captured groups for extraction | |
| # --- CODE NEGATIVES --- | |
| for my $re (@{ $parsed->{code_neg} }) { | |
| return (0, []) if $code =~ $re; | |
| } | |
| # --- CODE POSITIVES --- | |
| for my $re (@{ $parsed->{code_pos} }) { | |
| if ($code =~ $re) { | |
| # Capture groups if they exist | |
| push @captured_groups, $1, $2, $3, $4, $5, $6, $7, $8, $9; | |
| } | |
| else { | |
| return (0, []); | |
| } | |
| } | |
| # --- HEADER NEGATIVES --- | |
| for my $h (@{ $parsed->{header_neg} }) { | |
| if (exists $headers->{ $h->{name} }) { | |
| # If regex defined, header must NOT match | |
| return (0, []) if $h->{regex} && $headers->{ $h->{name} } =~ $h->{regex}; | |
| # If no regex, header must not exist | |
| return (0, []) unless $h->{regex}; | |
| } | |
| } | |
| # --- HEADER POSITIVES --- | |
| for my $h (@{ $parsed->{header_pos} }) { | |
| # Must exist | |
| return (0, []) unless exists $headers->{ $h->{name} }; | |
| # Must match regex if defined | |
| if ($h->{regex}) { | |
| if ($headers->{ $h->{name} } =~ $h->{regex}) { | |
| # Capture groups if they exist | |
| push @captured_groups, $1, $2, $3, $4, $5, $6, $7, $8, $9; | |
| } | |
| else { | |
| return (0, []); | |
| } | |
| } | |
| } | |
| # --- COOKIE NEGATIVES --- | |
| for my $c (@{ $parsed->{cookie_neg} }) { | |
| if (exists $cookies->{ $c->{name} }) { | |
| # If regex defined, cookie must NOT match | |
| return (0, []) if $c->{regex} && $cookies->{ $c->{name} } =~ $c->{regex}; | |
| # If no regex, cookie must not exist | |
| return (0, []) unless $c->{regex}; | |
| } | |
| } | |
| # --- COOKIE POSITIVES --- | |
| for my $c (@{ $parsed->{cookie_pos} }) { | |
| # Must exist | |
| return (0, []) unless exists $cookies->{ $c->{name} }; | |
| # Must match regex if defined | |
| if ($c->{regex}) { | |
| if ($cookies->{ $c->{name} } =~ $c->{regex}) { | |
| # Capture groups if they exist | |
| push @captured_groups, $1, $2, $3, $4, $5, $6, $7, $8, $9; | |
| } | |
| else { | |
| return (0, []); | |
| } | |
| } | |
| } | |
| # --- BODY NEGATIVES --- | |
| for my $re (@{ $parsed->{body_neg} }) { | |
| return (0, []) if $body =~ $re; | |
| } | |
| # --- BODY POSITIVES --- | |
| for my $re (@{ $parsed->{body_pos} }) { | |
| if ($body =~ $re) { | |
| # Capture groups if they exist | |
| push @captured_groups, $1, $2, $3, $4, $5, $6, $7, $8, $9; | |
| } | |
| else { | |
| return (0, []); | |
| } | |
| } | |
| # --- OR GROUPS --- | |
| # Each OR group must have at least one alternative that matches | |
| if (exists $parsed->{or_groups} && @{ $parsed->{or_groups} }) { | |
| for my $or_group (@{ $parsed->{or_groups} }) { | |
| my $or_matched = 0; | |
| foreach my $alt_parsed (@$or_group) { | |
| # Build a temporary matcher for this alternative | |
| my $alt_matcher = sub { | |
| my ($alt_code, $alt_body, $alt_headers, $alt_cookies) = @_; | |
| my @alt_captures = (); | |
| # Check all conditions in this alternative | |
| for my $re (@{ $alt_parsed->{code_neg} }) { | |
| return (0, []) if $alt_code =~ $re; | |
| } | |
| for my $re (@{ $alt_parsed->{code_pos} }) { | |
| return (0, []) unless $alt_code =~ $re; | |
| } | |
| for my $h (@{ $alt_parsed->{header_neg} }) { | |
| if (exists $alt_headers->{ $h->{name} }) { | |
| return (0, []) | |
| if $h->{regex} && $alt_headers->{ $h->{name} } =~ $h->{regex}; | |
| return (0, []) unless $h->{regex}; | |
| } | |
| } | |
| for my $h (@{ $alt_parsed->{header_pos} }) { | |
| return (0, []) unless exists $alt_headers->{ $h->{name} }; | |
| if ($h->{regex}) { | |
| return (0, []) unless $alt_headers->{ $h->{name} } =~ $h->{regex}; | |
| } | |
| } | |
| for my $c (@{ $alt_parsed->{cookie_neg} }) { | |
| if (exists $alt_cookies->{ $c->{name} }) { | |
| return (0, []) | |
| if $c->{regex} && $alt_cookies->{ $c->{name} } =~ $c->{regex}; | |
| return (0, []) unless $c->{regex}; | |
| } | |
| } | |
| for my $c (@{ $alt_parsed->{cookie_pos} }) { | |
| return (0, []) unless exists $alt_cookies->{ $c->{name} }; | |
| if ($c->{regex}) { | |
| return (0, []) unless $alt_cookies->{ $c->{name} } =~ $c->{regex}; | |
| } | |
| } | |
| for my $re (@{ $alt_parsed->{body_neg} }) { | |
| return (0, []) if $alt_body =~ $re; | |
| } | |
| for my $re (@{ $alt_parsed->{body_pos} }) { | |
| return (0, []) unless $alt_body =~ $re; | |
| } | |
| # Check nested OR groups in this alternative | |
| if (exists $alt_parsed->{or_groups} && @{ $alt_parsed->{or_groups} }) { | |
| for my $nested_or_group (@{ $alt_parsed->{or_groups} }) { | |
| my $nested_or_matched = 0; | |
| foreach my $nested_alt (@$nested_or_group) { | |
| my $nested_alt_matcher = sub { | |
| my ($n_code, $n_body, $n_headers, $n_cookies) = @_; | |
| for my $re (@{ $nested_alt->{code_neg} }) { | |
| return (0, []) if $n_code =~ $re; | |
| } | |
| for my $re (@{ $nested_alt->{code_pos} }) { | |
| return (0, []) unless $n_code =~ $re; | |
| } | |
| for my $h (@{ $nested_alt->{header_neg} }) { | |
| if (exists $n_headers->{ $h->{name} }) { | |
| return (0, []) | |
| if $h->{regex} | |
| && $n_headers->{ $h->{name} } =~ $h->{regex}; | |
| return (0, []) unless $h->{regex}; | |
| } | |
| } | |
| for my $h (@{ $nested_alt->{header_pos} }) { | |
| return (0, []) unless exists $n_headers->{ $h->{name} }; | |
| if ($h->{regex}) { | |
| return (0, []) | |
| unless $n_headers->{ $h->{name} } =~ $h->{regex}; | |
| } | |
| } | |
| for my $c (@{ $nested_alt->{cookie_neg} }) { | |
| if (exists $n_cookies->{ $c->{name} }) { | |
| return (0, []) | |
| if $c->{regex} | |
| && $n_cookies->{ $c->{name} } =~ $c->{regex}; | |
| return (0, []) unless $c->{regex}; | |
| } | |
| } | |
| for my $c (@{ $nested_alt->{cookie_pos} }) { | |
| return (0, []) unless exists $n_cookies->{ $c->{name} }; | |
| if ($c->{regex}) { | |
| return (0, []) | |
| unless $n_cookies->{ $c->{name} } =~ $c->{regex}; | |
| } | |
| } | |
| for my $re (@{ $nested_alt->{body_neg} }) { | |
| return (0, []) if $n_body =~ $re; | |
| } | |
| for my $re (@{ $nested_alt->{body_pos} }) { | |
| return (0, []) unless $n_body =~ $re; | |
| } | |
| return (1, []); | |
| }; | |
| my ($n_match, $n_caps) = | |
| $nested_alt_matcher->( | |
| $alt_code, $alt_body, $alt_headers, $alt_cookies | |
| ); | |
| if ($n_match) { | |
| $nested_or_matched = 1; | |
| push @alt_captures, @$n_caps if $n_caps; | |
| last; | |
| } | |
| } | |
| return (0, []) unless $nested_or_matched; | |
| } | |
| } | |
| return (1, \@alt_captures); | |
| }; | |
| # Test this alternative | |
| my ($alt_match, $alt_caps) = $alt_matcher->($code, $body, $headers, $cookies); | |
| if ($alt_match) { | |
| $or_matched = 1; | |
| push @captured_groups, @$alt_caps if $alt_caps; | |
| last; # One match is enough for OR | |
| } | |
| } | |
| # If no alternative matched, the OR group fails | |
| return (0, []) unless $or_matched; | |
| } | |
| } | |
| # Filter out undefined captured groups and build final array | |
| my @final_captures = (); | |
| for my $capture (@captured_groups) { | |
| push @final_captures, $capture if defined $capture; | |
| } | |
| return (1, \@final_captures); # All conditions passed, return captures | |
| }; | |
| } | |
| ####################################################################### | |
| sub path_matcher { | |
| my ($files_ref, $dirs_ref, $links_ref) = @_; | |
| # Map file patterns to variable names | |
| # var can be a string or array ref for multiple variables | |
| my @file_patterns = ({ pattern => qr/pass/i, var => '@PASSWORDFILES' },); | |
| # Process files | |
| if ($files_ref) { | |
| foreach my $file (keys %{$files_ref}) { | |
| my $raw = $file; | |
| $file = validate_and_fix_regex($file); | |
| foreach my $check (@file_patterns) { | |
| if ($file =~ $check->{pattern}) { | |
| my @vars = ref($check->{var}) eq 'ARRAY' ? @{ $check->{var} } : ($check->{var}); | |
| foreach my $var (@vars) { | |
| if (index(lc($VARIABLES{$var}), lc($file)) < 0) { | |
| $VARIABLES{$var} .= " $raw"; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| # Map directory patterns to variable names | |
| # var can be a string or array ref for multiple variables | |
| my @dir_patterns = ({ pattern => qr/cgi/i, var => '@CGIDIRS' }, | |
| { pattern => qr/forum/i, var => [ '@NUKE', '@VBULLETIN' ] }, | |
| { pattern => qr/pass/i, var => '@PASSWORDDIRS' }, | |
| { pattern => qr/nuke/i, var => '@NUKE' }, | |
| { pattern => qr/admin/i, var => '@ADMIN' }, | |
| { pattern => qr/phpmy/i, var => '@PHPMYADMIN' }, | |
| { pattern => qr/fck/i, var => '@FCKEDITOR' }, | |
| { pattern => qr/crystal/i, var => '@CRYSTALREPORTS' }, | |
| { pattern => qr/struts/i, var => '@STRUTSACTIONS' }, | |
| { pattern => qr/(wordpress|wp)/i, var => '@WORDPRESS' }, | |
| { pattern => qr/php/i, var => '@PHPINFODIRS' }, | |
| { pattern => qr/phpinfo/i, var => '@PHPINFOFILES' }, | |
| { pattern => qr/mantis/i, var => '@MANTIS' }, | |
| { pattern => qr/dokuwiki/i, var => '@DOKUWIKI' }, | |
| { pattern => qr/rockmongo/i, var => '@ROCKMONGO' }, | |
| { pattern => qr/magento/i, var => '@MAGENTO' }, | |
| { pattern => qr/(vb|vbulletin)/i, var => '@VBULLETIN' }, | |
| { pattern => qr/jenkins/i, var => '@JENKINS' }, | |
| { pattern => qr/symphony|contrib/i, var => '@SYMPHONY' }, | |
| ); | |
| # Process directories | |
| if ($dirs_ref) { | |
| foreach my $dir (keys %{$dirs_ref}) { | |
| # Directory is already validated before being stored, just use it directly | |
| foreach my $check (@dir_patterns) { | |
| if ($dir =~ $check->{pattern}) { | |
| my @vars = ref($check->{var}) eq 'ARRAY' ? @{ $check->{var} } : ($check->{var}); | |
| foreach my $var (@vars) { | |
| if (index(lc($VARIABLES{$var}), lc($dir)) < 0) { | |
| $VARIABLES{$var} .= " $dir"; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| # Map link patterns to variable names | |
| # var can be a string or array ref for multiple variables | |
| my @link_patterns = ({ pattern => qr/\.action(\?|$)/i, var => '@STRUTSACTIONS' },); | |
| # Process full links | |
| if ($links_ref) { | |
| foreach my $link (keys %{$links_ref}) { | |
| $link = validate_and_fix_regex($link); | |
| foreach my $check (@link_patterns) { | |
| if ($link =~ $check->{pattern}) { | |
| my @vars = ref($check->{var}) eq 'ARRAY' ? @{ $check->{var} } : ($check->{var}); | |
| foreach my $var (@vars) { | |
| if ($VARIABLES{$var} !~ /$link/i) { | |
| $VARIABLES{$var} .= " $link"; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| ####################################################################### | |
| # extract IP like strings and return an array | |
| sub get_ips { | |
| my $string = shift || return; | |
| my $ip_regex = qr/(?:\b|[^0-9v])($LW2::IPv4_re|$LW2::IPv6_re_inc_zoneid)(?:\b|[^0-9])/; | |
| return $string =~ /$ip_regex/g; | |
| } | |
| ####################################################################### | |
| # Check an IP's validity. Returns booleans for: validity, internal, loopback | |
| sub is_ip { | |
| my $ip = $_[0] || return 0, 0, 0; | |
| my $internal = 0; | |
| my $loopback = 0; | |
| # This is a little hacky but prevents Cloudflare cookies and headers from reporting | |
| if ($ip eq '1.0.1.1') { | |
| return 0, 0, 0; | |
| } | |
| if ($ip =~ /^$LW2::IPv4_re$/) { | |
| # check for internal | |
| if ($ip =~ /^(?:10|192\.168|172\.(?:1[6-9]|2\d|3[01]))\./) { $internal = 1; } | |
| # check for loopback | |
| if ($ip eq '127.0.0.1') { $loopback = 1; } | |
| } | |
| elsif ($ip =~ /^$LW2::IPv6_re_inc_zoneid(?:\/[0-9]+)?$/) { | |
| # check for internal | |
| if ($ip =~ /^(?:10|192\.168|172\.(?:1[6-9]|2\d|3[01]))\./) { $internal = 1; } | |
| if ($ip =~ /^(?:fe80 # is a link local unicast address | |
| |ff0[1-8] # is a multicast address | |
| |fc00 # private network | |
| ):/ix | |
| ) { | |
| $internal = 1; | |
| } | |
| # lastly, loopback? | |
| # This is a bit rough 'n' ready, could do with some finesse | |
| if ($ip =~ /^[01:]+(?:\/[0-9]+)?$/) { $loopback = 1; } | |
| } | |
| else { | |
| return 0, $internal, $loopback; | |
| } | |
| return 1, $internal, $loopback; | |
| } | |
| ####################################################################### | |
| sub parse_dsl { | |
| my ($dsl) = @_; | |
| die "Empty DSL string" unless defined $dsl && length $dsl; | |
| # Split top-level conditions on && and ||, but not inside parentheses | |
| # This preserves OR patterns like (PATTERN1|PATTERN2&&PATTERN3) | |
| # First, check if there's a top-level || (OR operator) | |
| my $has_top_level_or = 0; | |
| my $depth = 0; | |
| for (my $i = 0 ; $i < length($dsl) ; $i++) { | |
| my $char = substr($dsl, $i, 1); | |
| my $next_char = ($i < length($dsl) - 1) ? substr($dsl, $i + 1, 1) : ''; | |
| my $prev_char = ($i > 0) ? substr($dsl, $i - 1, 1) : ''; | |
| my $is_escaped = ($prev_char eq '\\'); | |
| if (!$is_escaped) { | |
| if ($char eq '(') { | |
| $depth++; | |
| } | |
| elsif ($char eq ')') { | |
| $depth--; | |
| } | |
| elsif ($char eq '|' && $next_char eq '|' && $depth == 0) { | |
| $has_top_level_or = 1; | |
| last; | |
| } | |
| } | |
| } | |
| # If we have a top-level ||, split on it and wrap in OR group | |
| if ($has_top_level_or) { | |
| my @or_alternatives = (); | |
| $depth = 0; | |
| my $current_alt = ''; | |
| for (my $i = 0 ; $i < length($dsl) ; $i++) { | |
| my $char = substr($dsl, $i, 1); | |
| my $next_char = ($i < length($dsl) - 1) ? substr($dsl, $i + 1, 1) : ''; | |
| my $prev_char = ($i > 0) ? substr($dsl, $i - 1, 1) : ''; | |
| my $is_escaped = ($prev_char eq '\\'); | |
| if (!$is_escaped) { | |
| if ($char eq '|' && $next_char eq '|' && $depth == 0) { | |
| push @or_alternatives, $current_alt if $current_alt ne ''; | |
| $current_alt = ''; | |
| $depth = 0; # Reset depth for new alternative | |
| $i += 1; # Skip first |, loop will auto-increment to skip second | | |
| next; # Continue to next iteration (will auto-increment i, skipping second |) | |
| } | |
| elsif ($char eq '(') { | |
| $depth++; | |
| $current_alt .= $char; | |
| } | |
| elsif ($char eq ')') { | |
| $depth--; | |
| $current_alt .= $char; | |
| } | |
| else { | |
| $current_alt .= $char; | |
| } | |
| } | |
| else { | |
| $current_alt .= $char; | |
| } | |
| } | |
| push @or_alternatives, $current_alt if $current_alt ne ''; | |
| # Parse each alternative and create an OR group | |
| my @or_parsed = (); | |
| foreach my $alt (@or_alternatives) { | |
| $alt =~ s/^\s+|\s+$//g; | |
| next unless $alt; | |
| # If alternative doesn't start with a known type prefix or OR group, assume it's a BODY pattern | |
| if ($alt !~ /^(CODE|BODY|HEADER|COOKIE|!CODE|!BODY|!HEADER|!COOKIE):/i && $alt !~ /^\(/) | |
| { | |
| $alt = "BODY:$alt"; | |
| } | |
| my $alt_parsed = parse_dsl($alt); | |
| push @or_parsed, $alt_parsed; | |
| } | |
| # Return a compiled structure with just the OR group | |
| my %compiled = (code_pos => [], | |
| code_neg => [], | |
| body_pos => [], | |
| body_neg => [], | |
| header_pos => [], | |
| header_neg => [], | |
| cookie_pos => [], | |
| cookie_neg => [], | |
| or_groups => [ \@or_parsed ], | |
| ); | |
| return \%compiled; | |
| } | |
| # No top-level ||, proceed with normal && splitting | |
| my @tokens = (); | |
| $depth = 0; | |
| my $current_token = ''; | |
| my $i = 0; | |
| my $len = length($dsl); | |
| while ($i < $len) { | |
| my $char = substr($dsl, $i, 1); | |
| my $next_char = ($i < $len - 1) ? substr($dsl, $i + 1, 1) : ''; | |
| my $prev_char = ($i > 0) ? substr($dsl, $i - 1, 1) : ''; | |
| # Check if && is escaped (previous char is backslash, similar to original regex) | |
| my $is_escaped = ($prev_char eq '\\'); | |
| if (!$is_escaped) { | |
| if ($char eq '(') { | |
| $depth++; | |
| $current_token .= $char; | |
| } | |
| elsif ($char eq ')') { | |
| $depth--; | |
| $current_token .= $char; | |
| } | |
| elsif ($char eq '&' && $next_char eq '&' && $depth == 0) { | |
| # Found top-level && delimiter | |
| push @tokens, $current_token if $current_token ne ''; | |
| $current_token = ''; | |
| $i += 2; # Skip both & characters | |
| next; | |
| } | |
| else { | |
| $current_token .= $char; | |
| } | |
| } | |
| else { | |
| # Escaped character, just add it | |
| $current_token .= $char; | |
| } | |
| $i++; | |
| } | |
| push @tokens, $current_token if $current_token ne ''; | |
| my %compiled = (code_pos => [], | |
| code_neg => [], | |
| body_pos => [], | |
| body_neg => [], | |
| header_pos => [], | |
| header_neg => [], | |
| cookie_pos => [], | |
| cookie_neg => [], | |
| or_groups => [], # Store OR patterns for special handling | |
| ); | |
| foreach my $token (@tokens) { | |
| $token =~ s/^\s+|\s+$//g; # trim whitespace | |
| $token =~ s/,\s*$// if length($token) > 1; # trim trailing comma | |
| next unless $token; | |
| # Check if this is an OR pattern (starts with ( and ends with )) | |
| # Use regex to check if it's a properly formed OR pattern | |
| if ($token =~ /^\((.*)\)$/) { | |
| # Verify balanced parentheses in the content (the outer parens are already matched) | |
| my $or_content = $1; | |
| my $depth = 0; | |
| my $valid_or = 1; | |
| for my $i (0 .. length($or_content) - 1) { | |
| my $char = substr($or_content, $i, 1); | |
| my $prev_char = ($i > 0) ? substr($or_content, $i - 1, 1) : ''; | |
| my $is_escaped = ($prev_char eq '\\'); | |
| if (!$is_escaped) { | |
| if ($char eq '(') { | |
| $depth++; | |
| } | |
| elsif ($char eq ')') { | |
| $depth--; | |
| if ($depth < 0) { | |
| $valid_or = 0; | |
| last; | |
| } | |
| } | |
| } | |
| } | |
| # Content must have balanced parentheses | |
| if ($valid_or && $depth == 0) { | |
| # Split on | but not escaped | | |
| my @alternatives = split /(?<!\\)\|/, $or_content; | |
| # Parse each alternative as a separate DSL expression | |
| my @or_parsed = (); | |
| foreach my $alt (@alternatives) { | |
| $alt =~ s/^\s+|\s+$//g; | |
| next unless $alt; | |
| # Recursively parse this alternative | |
| my $alt_parsed = parse_dsl($alt); | |
| push @or_parsed, $alt_parsed; | |
| } | |
| # Store the OR group | |
| push @{ $compiled{or_groups} }, \@or_parsed; | |
| next; | |
| } | |
| else { | |
| # Token looks like an OR pattern but has unbalanced parentheses | |
| die "Invalid DSL token: OR pattern has unbalanced parentheses: $token"; | |
| } | |
| } | |
| elsif ($token =~ /^\(/) { | |
| # Token starts with ( but doesn't match OR pattern format | |
| # Show token details for debugging | |
| my $token_len = length($token); | |
| my $first_char = substr($token, 0, 1); | |
| my $last_char = substr($token, -1); | |
| die | |
| "Invalid DSL token: token starts with '(' but is not a properly formed OR pattern (must start with '(' and end with ')'): length=$token_len, first='$first_char', last='$last_char', token='$token'"; | |
| } | |
| # Check negation | |
| my $neg = 0; | |
| if ($token =~ /^!/) { | |
| $neg = 1; | |
| $token = substr($token, 1); | |
| } | |
| # Skip @LFI() tokens - they are placeholders that will be expanded at runtime | |
| # This can happen if variables aren't loaded yet during validation | |
| if ($token =~ /^@?LFI\(\)$/) { | |
| next; | |
| } | |
| # TYPE:VALUE split (only first colon) | |
| my ($type, $pattern) = split /:/, $token, 2; | |
| die "Invalid DSL token: $token" unless defined $type && defined $pattern; | |
| $type = uc($type); | |
| if ($type eq 'CODE') { | |
| push @{ $neg ? $compiled{code_neg} : $compiled{code_pos} }, compile_regex($pattern); | |
| } | |
| elsif ($type eq 'BODY') { | |
| die "Empty BODY pattern in DSL" if $pattern eq ''; | |
| my $compiled = compile_regex($pattern); | |
| push @{ $neg ? $compiled{body_neg} : $compiled{body_pos} }, $compiled; | |
| } | |
| elsif ($type eq 'HEADER') { | |
| my ($hname, $hval) = split /:/, $pattern, 2; | |
| die "Missing HEADER name: $token" unless defined $hname && length $hname; | |
| $hname =~ s/^\s+|\s+$//g; | |
| $hval = '' unless defined $hval; | |
| $hval =~ s/^\s+//; | |
| my $re = length($hval) ? compile_regex($hval) : undef; | |
| push @{ $neg ? $compiled{header_neg} : $compiled{header_pos} }, | |
| { name => lc($hname), | |
| regex => $re, | |
| }; | |
| } | |
| elsif ($type eq 'COOKIE') { | |
| my ($cname, $cval) = split /:/, $pattern, 2; | |
| die "Missing COOKIE name: $token" unless defined $cname && length $cname; | |
| $cname =~ s/^\s+|\s+$//g; | |
| $cval = '' unless defined $cval; | |
| $cval =~ s/^\s+//; | |
| my $re = length($cval) ? compile_regex($cval) : undef; | |
| push @{ $neg ? $compiled{cookie_neg} : $compiled{cookie_pos} }, | |
| { name => lc($cname), | |
| regex => $re, | |
| }; | |
| } | |
| else { | |
| die "Unknown DSL type: $type"; | |
| } | |
| } | |
| return \%compiled; | |
| } | |
| ####################################################################### | |
| sub compile_regex { | |
| my ($pattern) = @_; | |
| # Remove escapes for our delimiters (but preserve other escapes like \. \d etc) | |
| # However, don't unescape pipes inside escaped parentheses \( \) as those are literal | |
| # Protect pipes inside \( \) blocks by temporarily replacing them | |
| my @protected_blocks = (); | |
| my $block_idx = 0; | |
| while ($pattern =~ /(\\\([^)]*\\\))/g) { | |
| my $block = $1; | |
| my $placeholder = "___PROTECTED_BLOCK_${block_idx}___"; | |
| $protected_blocks[$block_idx] = $block; | |
| $pattern =~ s/\Q$block\E/$placeholder/; | |
| $block_idx++; | |
| } | |
| # Now unescape delimiters outside protected blocks | |
| $pattern =~ s/\\([|!&:])/$1/g; | |
| # Restore protected blocks (with their escaped pipes intact) | |
| for (my $i = 0 ; $i < @protected_blocks ; $i++) { | |
| my $placeholder = "___PROTECTED_BLOCK_${i}___"; | |
| $pattern =~ s/\Q$placeholder\E/$protected_blocks[$i]/; | |
| } | |
| # Fix escaped quotes - convert \" to " (escaped quotes in CSV become literal quotes in regex) | |
| # This prevents "Trailing \ in regex" errors when patterns end with \" | |
| $pattern =~ s/\\"/"/g; | |
| # Fix trailing backslashes - if pattern ends with \ (not part of an escape sequence), double it | |
| # This prevents "Trailing \ in regex" errors (e.g., "c:\" becomes "c:\\") | |
| # Only do this if the backslash is not already escaped (not "\\") | |
| if ($pattern =~ /[^\\]\\$/) { | |
| $pattern =~ s/([^\\])\\$/$1\\\\/; # Double the trailing backslash | |
| } | |
| elsif ($pattern =~ /^\\$/) { | |
| # Pattern is just a single backslash | |
| $pattern = '\\\\'; | |
| } | |
| # Handle case-insensitive flag | |
| my $mod = ''; | |
| if ($pattern =~ s/^\(\?i\)//) { | |
| $mod = '(?i)'; | |
| } | |
| # Compile the regex pattern | |
| my $re = eval { qr/$mod$pattern/ }; | |
| die "Invalid regex '$pattern': $@" if $@; | |
| return $re; | |
| } | |
| ####################################################################### | |
| sub parse_csv { | |
| my $text = $_[0] || return; | |
| my @new = (); | |
| push(@new, $+) while $text =~ m{ | |
| "([^\"\\]*(?:\\.[^\"\\]*)*)",? | |
| | ([^,]+),? | |
| | , | |
| }gx; | |
| push(@new, undef) if substr($text, -1, 1) eq ','; | |
| return @new; | |
| } | |
| ####################################################################### | |
| sub check_ssl_support { | |
| LW2::init_ssl_engine(); | |
| my ($avail, $lib, $ver) = LW2::ssl_is_available(); | |
| if (!$avail) { | |
| nprint("+ WARNING: SSL: support not available."); | |
| } | |
| } | |
| ####################################################################### | |
| sub version { | |
| nprint("$VARIABLES{'name'} $VARIABLES{'version'} (LW $LW2::VERSION)"); | |
| exit 0; | |
| } | |
| ####################################################################### | |
| sub send_updates { | |
| return if ($CONFIGFILE{'UPDATES'} !~ /yes|auto/i); | |
| my (@MARKS) = @_; | |
| my ($updated_version, $answer, $code, $upd_enc); | |
| my $have_updates = 0; | |
| foreach my $mark (@MARKS) { | |
| foreach my $component (keys %{ $mark->{'components'} }) { | |
| if ($mark->{'components'}->{$component} eq 2) { | |
| if ($component !~ /\d/) { next; } | |
| elsif ($component =~ /^(?:\(?Win32\)?|Linux-Mandrake$)/) { next; } | |
| elsif ($component eq "") { next; } | |
| $have_updates = 1; | |
| $updated_version .= "$component "; | |
| } | |
| } | |
| } | |
| if ((!$have_updates) || ($updated_version eq "")) { return; } | |
| $updated_version =~ s/\s+$//; | |
| $updated_version =~ s/^\s+//; | |
| if ($CONFIGFILE{'UPDATES'} eq "auto") { | |
| $answer = "y"; | |
| } | |
| else { | |
| $answer = read_data( | |
| "\n | |
| ********************************************************************* | |
| Portions of the server's headers ($updated_version) are not in | |
| the Nikto " | |
| . $VARIABLES{'version'} | |
| . " database or are newer than the known string. Would you like | |
| to submit this information (*no server specific data*) to CIRT.net | |
| for a Nikto update (or you may email to sullo\@cirt.net) (y/n)? ", "" | |
| ); | |
| } | |
| if ($answer !~ /y/i) { return; } | |
| # set up our mark | |
| my %mark = ('ident' => $CONFIGFILE{CIRT}, | |
| 'ssl' => 1, | |
| 'port' => 443 | |
| ); | |
| ($mark{'hostname'}, $mark{'ip'}, $mark{'display_name'}) = resolve($CONFIGFILE{CIRT}, 0); | |
| $upd_enc = LW2::encode_base64($updated_version); | |
| chomp($upd_enc); | |
| # Use libwhisker directly instead of nfetch to avoid reporting on update target | |
| my (%request, %response); | |
| setup_hash(\%request, \%mark, ""); | |
| $request{'whisker'}->{'uri'} = "/nikto-updates.php?version=$upd_enc"; | |
| $request{'whisker'}->{'method'} = "GET"; | |
| $request{'whisker'}->{'host'} = $mark{'hostname'}; | |
| $request{'Accept'} = '*/*'; | |
| $request{'User-Agent'} = get_ua(); | |
| LW2::http_fixup_request(\%request); | |
| LW2::http_do_request_timeout(\%request, \%response); | |
| $code = $response{'whisker'}->{'code'}; | |
| $content = $response{'whisker'}->{'data'}; | |
| if ($code eq 407) { | |
| if ($CONFIGFILE{PROXYUSER} eq "") { | |
| $CONFIGFILE{PROXYUSER} = read_data("Proxy ID: ", ""); | |
| $CONFIGFILE{PROXYPASS} = read_data("Proxy Pass: ", "noecho"); | |
| } | |
| LW2::http_do_request_timeout(\%request, \%response); | |
| $code = $response{'whisker'}->{'code'}; | |
| $content = $response{'whisker'}->{'data'}; | |
| } | |
| if ($code eq "") { | |
| LW2::http_close(\%request); | |
| # Use CIRT config hostname for fallback | |
| my ($fallback_hostname, $fallback_ip, $fallback_display) = resolve($CONFIGFILE{CIRT}, 0); | |
| $mark{'ip'} = $fallback_ip; | |
| $request{'whisker'}->{'host'} = $fallback_hostname; | |
| LW2::http_fixup_request(\%request); | |
| LW2::http_do_request_timeout(\%request, \%response); | |
| $code = $response{'whisker'}->{'code'}; | |
| $content = $response{'whisker'}->{'data'}; | |
| } | |
| if (($code != 200) || ($content !~ /SUCCESS/)) { | |
| nprint("+ ERROR: $code -> " | |
| . $response{'location'} | |
| . "\n+ ERROR: Update failed, please notify sullo\@cirt.net of the previous line.", | |
| "", | |
| ($mark{'hostname'}, $mark{'ip'}, $mark{'display_name'}) | |
| ); | |
| } | |
| else { | |
| nprint("- Sent updated info to cirt.net -- Thank you!"); | |
| } | |
| return; | |
| } | |
| ####################################################################### | |
| sub usage { | |
| print " | |
| Options: | |
| -Add-header Add HTTP headers (can be used multiple times, one per header pair) | |
| -ask+ Whether to ask about submitting updates | |
| yes Ask about each (default) | |
| no Don't ask, don't send | |
| auto Don't ask, just send | |
| -check6 Check if IPv6 is working (connects to ipv6.google.com or value set in nikto.conf) | |
| -Cgidirs+ Scan these CGI dirs: \"none\", \"all\", or values like \"/cgi/ /cgi-a/\" | |
| -config+ Use this config file | |
| -Display+ Turn on/off display outputs: | |
| 1 Show redirects | |
| 2 Show cookies received | |
| 3 Show all 200/OK responses | |
| 4 Show URLs which require authentication | |
| D Debug output | |
| E Display all HTTP errors | |
| P Print progress to STDOUT | |
| S Scrub output of IPs and hostnames | |
| V Verbose output | |
| -dbcheck Check database and other key files for syntax errors | |
| -evasion+ Encoding technique:\n"; | |
| foreach my $k (sort keys %{ $NIKTO{'anti_ids'} }) { | |
| print " $k $NIKTO{'anti_ids'}{$k}\n"; | |
| } | |
| print " -followredirects Follow 3xx redirects to new location | |
| -Format+ Save file (-o) format: | |
| csv Comma-separated-value | |
| json JSON Format | |
| htm HTML Format | |
| sql Generic SQL (see docs for schema) | |
| txt Plain text | |
| xml XML Format | |
| (if not specified the format will be taken from the file extension passed to -output) | |
| -Help This help information | |
| -host+ Target host/URL | |
| -id+ Host authentication to use, format is id:pass or id:pass:realm | |
| -ipv4 IPv4 Only | |
| -ipv6 IPv6 Only | |
| -key+ Client certificate key file | |
| -list-plugins List all available plugins, perform no testing | |
| -maxtime+ Maximum testing time per host (e.g., 1h, 60m, 3600s) | |
| -mutate+ Guess additional file names:\n"; | |
| foreach my $k (sort keys %{ $NIKTO{'mutate_opts'} }) { | |
| print " $k $NIKTO{'mutate_opts'}{$k}\n"; | |
| } | |
| print " -mutate-options Provide information for mutates | |
| -nocheck Don't check for updates on startup | |
| -nocookies Do not use cookies from responses in requests | |
| -nointeractive Disables interactive features | |
| -nolookup Disables DNS lookups | |
| -nossl Disables the use of SSL | |
| -noslash Strip trailing slash from URL (e.g., '/admin/' to '/admin') | |
| -no404 Disables nikto attempting to guess a 404 page | |
| -Option Over-ride an option in nikto.conf, can be issued multiple times | |
| -output+ Write output to this file ('.' for auto-name) | |
| -Pause+ Pause between tests (seconds) | |
| -Platform+ Platform of target (nix, win, all) | |
| -Plugins+ List of plugins to run (default: ALL) | |
| -port+ Port to use (default 80) | |
| -RSAcert+ Client certificate file | |
| -root+ Prepend root value to all requests, format is /directory | |
| -Save Save positive responses to this directory ('.' for auto-name) | |
| -ssl Force ssl mode on port | |
| -Tuning+ Scan tuning: | |
| 1 Interesting File / Seen in logs | |
| 2 Misconfiguration / Default File | |
| 3 Information Disclosure | |
| 4 Injection (XSS/Script/HTML) | |
| 5 Remote File Retrieval - Inside Web Root | |
| 6 Denial of Service | |
| 7 Remote File Retrieval - Server Wide | |
| 8 Command Execution / Remote Shell | |
| 9 SQL Injection | |
| 0 File Upload | |
| a Authentication Bypass | |
| b Software Identification | |
| c Remote Source Inclusion | |
| d WebService | |
| e Administrative Console | |
| x Reverse Tuning Options (i.e., include all except specified) | |
| -timeout+ Timeout for requests (default 10 seconds) | |
| -Userdbs Load only user databases, not the standard databases | |
| all Disable standard dbs and load only user dbs | |
| tests Disable only db_tests and load udb_tests | |
| -useragent Force User-Agent instead of pulling from database | |
| -url+ Target host/URL (alias of -host) | |
| -useproxy Use the proxy defined in nikto.conf, or argument http://server:port | |
| -Version Print plugin and database versions | |
| -vhost+ Virtual host (for Host header) | |
| -404code Ignore these HTTP codes as negative responses (always). Format is \"302,301\". | |
| -404string Ignore this string in response body content as negative response (always). Can be a regular expression. | |
| + requires a value\n\n"; | |
| exit 0; | |
| } | |
| ####################################################################### | |
| sub init_db { | |
| my $dbname = shift; | |
| return if $dbname eq ""; | |
| my $filename = "$CONFIGFILE{'DBDIR'}/" . $dbname; | |
| my (@dbarray, @headers); | |
| my $hashref = {}; | |
| if ($CLI{'userdbs'} ne 'all') { | |
| # Check that the database exists | |
| unless (open(IN, "<$filename")) { | |
| nprint("+ ERROR: Unable to open database file $dbname: $@."); | |
| return $dbarray; | |
| } | |
| # Now read the header values | |
| while (<IN>) { | |
| chomp; | |
| s/\#.*$//; | |
| if ($_ eq "") { next } | |
| unless (@headers) { | |
| @headers = parse_csv($_); | |
| } | |
| else { | |
| # contents; so split them up and apply to hash | |
| my @contents = parse_csv($_); | |
| my $hashref = {}; | |
| for (my $i = 0 ; $i <= $#contents ; $i++) { | |
| $hashref->{ $headers[$i] } = $contents[$i]; | |
| } | |
| push(@dbarray, $hashref); | |
| } | |
| } | |
| close(IN); | |
| } | |
| # And the udb_* file | |
| $filename = "$CONFIGFILE{'DBDIR'}/u" . $dbname; | |
| if (open(IN, "<$filename")) { | |
| while (<IN>) { | |
| chomp; | |
| s/\#.*$//; | |
| if ($_ eq "") { next; } | |
| # contents; so split them up and apply to hash | |
| my @contents = parse_csv($_); | |
| my $hashref = {}; | |
| for (my $i = 0 ; $i <= $#contents ; $i++) { | |
| $hashref->{ $headers[$i] } = $contents[$i]; | |
| } | |
| push(@dbarray, $hashref); | |
| } | |
| } | |
| close(IN); | |
| return \@dbarray; | |
| } | |
| ####################################################################### | |
| sub add_vulnerability { | |
| my ($mark, $message, $nikto_id, $refs, $method, $uri, $request, $response, $reason) = @_; | |
| # Also normalize the response URI so it's consistent when used later | |
| if (defined $response && defined $response->{whisker}) { | |
| if ( !defined $response->{whisker}->{uri_requested} | |
| || $response->{whisker}->{uri_requested} eq "" | |
| || $response->{whisker}->{uri_requested} eq ".") { | |
| $response->{whisker}->{uri_requested} = "/"; | |
| } | |
| } | |
| $method = "GET" unless (defined $method); | |
| # Grammar matters | |
| if ($message !~ /\.$/) { | |
| $message .= "."; | |
| } | |
| # check to see if we've alerted already (can be from content search, etc.) | |
| foreach my $r (@RESULTS) { | |
| if ( ($uri eq $r->{'uri'}) | |
| && ($message eq $r->{'message'}) | |
| && ($method eq $r->{'method'}) | |
| && (${ $r->{'mark'} }{'ident'} eq $mark->{'ident'}) | |
| && (${ $r->{'mark'} }{'port'} eq $mark->{'port'})) { | |
| return; | |
| } | |
| } | |
| my $result = ""; | |
| if (defined $_[7]) { | |
| $result = $_[7]->{'whisker'}->{'data'}; | |
| } | |
| my $resulthash; | |
| %$resulthash = (mark => $mark, | |
| message => $message, | |
| nikto_id => $nikto_id, | |
| refs => $refs, | |
| method => $method, | |
| uri => $response->{whisker}->{uri_requested} || '/', | |
| result => $result, | |
| request => $request, | |
| response => $response, | |
| reason => $reason, | |
| ); | |
| push(@RESULTS, $resulthash); | |
| $mark->{total_vulns}++; | |
| if ($refs ne "") { | |
| $message .= " See: $refs"; | |
| } | |
| nprint("+ [$nikto_id] $message", | |
| "", ($mark->{'hostname'}, $mark->{'ip'}, $mark->{'displayname'})); | |
| # Save it | |
| if ($CLI{'saveresults'} ne '') { | |
| save_item($resulthash, $message, $request, $response); | |
| } | |
| # Now report it | |
| report_item($mark, $resulthash); | |
| } | |
| ############################################################################### | |
| sub rebuild_request { | |
| my ($req, $include_body, $truncate_length) = @_; | |
| return '' unless ref($req) eq 'HASH'; | |
| # Default to including body for backward compatibility | |
| $include_body = 1 unless defined $include_body; | |
| my $w = (ref($req->{whisker}) eq 'HASH') ? $req->{whisker} : {}; | |
| my $method = $w->{method} || 'GET'; | |
| my $uri = defined $w->{uri} ? $w->{uri} : '/'; | |
| my $proto = $w->{protocol} || 'HTTP'; | |
| my $ver = $w->{version} || '1.1'; | |
| my $eol = (defined $w->{http_eol} && $w->{http_eol} ne '') ? $w->{http_eol} : "\r\n"; | |
| # Things that are NOT HTTP headers in LW2 request structure | |
| my %NOT_HEADERS = map { $_ => 1 } qw( | |
| whisker data MAGIC host port ssl max_size | |
| method uri protocol version timeout | |
| http_eol http_space1 http_space2 retry | |
| uri_param_sep uri_prefix uri_postfix include_host_in_uri | |
| force_bodysnatch force_open force_close trailing_slurp | |
| ignore_duplicate_headers lowercase_incoming_headers normalize_incoming_headers | |
| require_newline_after_headers invalid_protocol_return_value | |
| ssl_certfile ssl_rsacertfile ssl_save_info | |
| ); | |
| # Allowlist pattern for header names | |
| my $is_header_name = sub { | |
| my ($k) = @_; | |
| return 0 if !defined $k; | |
| return 0 if $NOT_HEADERS{$k}; # hard stop | |
| return ($k =~ /^[A-Za-z][A-Za-z0-9-]*$/) ? 1 : 0; | |
| }; | |
| # Merge headers: whisker first, then top-level overrides | |
| my %h; | |
| for my $k (keys %$w) { | |
| next unless $is_header_name->($k); | |
| next unless defined $w->{$k}; | |
| next if ref($w->{$k}); | |
| $h{$k} = $w->{$k}; | |
| } | |
| for my $k (keys %$req) { | |
| next unless $is_header_name->($k); | |
| next unless defined $req->{$k}; | |
| next if ref($req->{$k}); | |
| $h{$k} = $req->{$k}; | |
| } | |
| # Determine Host header value (prefer explicit Host; else whisker 'host') | |
| my $host_val = ''; | |
| if (exists $h{Host} && defined $h{Host}) { | |
| $host_val = $h{Host}; | |
| } | |
| elsif (defined $w->{host} && $w->{host} ne '') { | |
| $host_val = $w->{host}; | |
| } | |
| # Build request line | |
| my $out = "$method $uri $proto/$ver$eol"; | |
| # Host header must be immediately after request line | |
| if ($host_val ne '') { | |
| $host_val =~ s/\r|\n/ /g; | |
| $out .= "Host: $host_val$eol"; | |
| delete $h{Host}; # avoid duplicate | |
| } | |
| # Other headers: stable order (case-insensitive) | |
| my @keys = sort { lc($a) cmp lc($b) } keys %h; | |
| for my $k (@keys) { | |
| my $v = $h{$k}; | |
| next unless defined $v; | |
| $v =~ s/\r|\n/ /g; | |
| $out .= "$k: $v$eol"; | |
| } | |
| $out .= $eol; | |
| # Include body if flag is set | |
| if ($include_body && defined $w->{data} && length($w->{data})) { | |
| $out .= $w->{data}; | |
| } | |
| # Truncate if length specified | |
| if (defined $truncate_length && $truncate_length > 0 && length($out) > $truncate_length) { | |
| my $original_length = length($out); | |
| $out = substr($out, 0, $truncate_length); | |
| $out .= "\n[Request truncated - original size: $original_length bytes]"; | |
| } | |
| return $out; | |
| } | |
| ############################################################################### | |
| sub rebuild_response { | |
| my ($resp, $include_body, $truncate_length) = @_; | |
| return '' unless ref($resp) eq 'HASH'; | |
| # Default to including body for backward compatibility | |
| $include_body = 1 unless defined $include_body; | |
| my $w = (ref($resp->{whisker}) eq 'HASH') ? $resp->{whisker} : {}; | |
| my $proto = $w->{protocol} || 'HTTP'; | |
| my $ver = $w->{version} || '1.1'; | |
| my $code = defined $w->{code} ? $w->{code} : '200'; | |
| my $msg = defined $w->{message} ? $w->{message} : 'OK'; | |
| my $eol = (defined $w->{http_eol} && $w->{http_eol} ne '') ? $w->{http_eol} : "\r\n"; | |
| # Build status line | |
| my $out = "$proto/$ver $code $msg$eol"; | |
| # Use header_order from whisker hash to maintain original header order | |
| if (defined $w->{header_order} && ref($w->{header_order}) eq 'ARRAY') { | |
| my %header_output; # Track which header values we've already output | |
| foreach my $header_name (@{ $w->{header_order} }) { | |
| next if ($header_name eq '' || $header_name eq 'whisker'); | |
| next unless defined $resp->{$header_name}; | |
| # Handle multiple values for the same header (array reference) | |
| if (ref($resp->{$header_name}) eq 'ARRAY') { | |
| my $value_idx = $header_output{$header_name} || 0; | |
| # Output one value per header_order entry to maintain order | |
| if ($value_idx < @{ $resp->{$header_name} }) { | |
| my $value = $resp->{$header_name}->[$value_idx]; | |
| if (defined $value) { | |
| $value =~ s/\r|\n/ /g; | |
| $out .= "$header_name: $value$eol"; | |
| $header_output{$header_name} = $value_idx + 1; | |
| } | |
| } | |
| } | |
| else { | |
| # Single value header - only output once even if in header_order multiple times | |
| unless ($header_output{$header_name}) { | |
| my $value = $resp->{$header_name}; | |
| $value =~ s/\r|\n/ /g; | |
| $out .= "$header_name: $value$eol"; | |
| $header_output{$header_name} = 1; | |
| } | |
| } | |
| } | |
| } | |
| $out .= $eol; | |
| # Include body if flag is set | |
| if ($include_body && defined $w->{data} && length($w->{data})) { | |
| $out .= $w->{data}; | |
| } | |
| # Truncate if length specified | |
| if (defined $truncate_length && $truncate_length > 0 && length($out) > $truncate_length) { | |
| my $original_length = length($out); | |
| $out = substr($out, 0, $truncate_length); | |
| $out .= "\n[Response truncated - original size: $original_length bytes]"; | |
| } | |
| return $out; | |
| } | |
| ############################################################################### | |
| sub list_plugins { | |
| # Just do a load_plugins, then loop through the array and print out name, | |
| # description and copyright | |
| load_plugins(); | |
| foreach my $plugin (@PLUGINS) { | |
| nprint("Plugin: $plugin->{'name'}"); | |
| nprint(" $plugin->{'full_name'} - $plugin->{'description'}"); | |
| nprint(" Written by $plugin->{'author'}, Copyright (C) $plugin->{'copyright'}"); | |
| if (defined $plugin->{'options'}) { | |
| nprint(" Options:"); | |
| while (my ($option, $description) = each(%{ $plugin->{'options'} })) { | |
| nprint(" $option: $description"); | |
| } | |
| } | |
| nprint("\n"); | |
| } | |
| # Plugin macros | |
| nprint("Defined plugin macros:"); | |
| foreach my $macro (keys %CONFIGFILE) { | |
| if ($macro =~ /^@@/) { | |
| nprint(" $macro = \"" . $CONFIGFILE{$macro} . "\""); | |
| if ($CONFIGFILE{$macro} =~ /@@/) { | |
| nprint(" (expanded) = \"" . expand_pluginlist($CONFIGFILE{$macro}, 0) . "\""); | |
| } | |
| } | |
| } | |
| exit 0; | |
| } | |
| ############################################################################### | |
| # This is overly complicated and jumps a lot between scalars and arrays. The REs are | |
| # probably dodgy, but it works! W00! | |
| sub expand_pluginlist { | |
| my ($pluginlist, $parent) = @_; | |
| my @macros; | |
| foreach my $config (keys %CONFIGFILE) { | |
| if ($config =~ /^@@/) { | |
| push(@macros, $config); | |
| } | |
| } | |
| # Now loop through each member of the list and expand it | |
| my $count = 0; | |
| my $npluginlist = $pluginlist; | |
| do { | |
| $count++; | |
| my @raw = split(/;/, $npluginlist); | |
| # cooked contains the processed list | |
| my @cooked; | |
| foreach my $entry (@raw) { | |
| # Is it +; if so remap to @@DEFAULT | |
| if ($entry eq "+") { | |
| $entry = '@@DEFAULT'; | |
| } | |
| # result contains the processed entry | |
| my $result = $original = $entry; | |
| # Is it a macro | |
| if ($entry =~ /^-?@@/) { | |
| # break up into components | |
| $prefix = ($entry =~ /^-/) ? "-" : ""; | |
| $name = $suffix = $entry; | |
| $name =~ s/(^-?)(@@[[:alpha:]]+)(\(?.*\)?$)/$2/; | |
| $suffix =~ s/(.*)(\(.*\))/$2/; | |
| if ($suffix eq $entry) { | |
| $suffix = ""; | |
| } | |
| foreach my $macro (@macros) { | |
| if ($entry =~ /-?$macro/) { | |
| # It's a macro, so replace the contents with the macro | |
| # Add prefix and suffix to each member of the macro | |
| my @temp; | |
| foreach my $child (split(/;/, $CONFIGFILE{$macro})) { | |
| push(@temp, "$prefix$child$suffix"); | |
| } | |
| $result = join(';', @temp); | |
| # stop an infinite loop | |
| last; | |
| } | |
| } | |
| } | |
| if ($result =~ /^-?@@/ && $result eq $original) { | |
| # macro not found or is itself - ignore | |
| $result = ""; | |
| } | |
| if ($count > 100) { | |
| # check for recurstion | |
| nprint("ERROR: Recursion found whilst expanding macros"); | |
| $result = ""; | |
| last; | |
| } | |
| push(@cooked, $result); | |
| } | |
| $npluginlist = join(';', @cooked); | |
| } while ($npluginlist =~ /@@/ && $count <= 100); | |
| #use re 'debug'; | |
| # Now we've expanded out macros, deal with duplicates and - | |
| my @raw = split(/;/, $npluginlist); | |
| # hash so we don't have to mess with duplicates | |
| my %cooked; | |
| foreach my $plugin (@raw) { | |
| # break out components | |
| my $minus; | |
| my $name = my $suffix = $plugin; | |
| $minus = (substr($plugin, 0, 1) eq '-'); | |
| $name =~ s/(^-?)([^\(]+)(\(?.*\)?$)/$2/; | |
| $suffix =~ s/(.*)(\(.*\))/$2/; | |
| if ($suffix eq $plugin) { | |
| $suffix = ""; | |
| } | |
| if ($minus) { | |
| # it's a minus - remove any previous entry | |
| if (exists $cooked{$name}) { | |
| delete $cooked{$name}; | |
| } | |
| } | |
| else { | |
| # else add it with the parameters as the value of the hash | |
| $cooked{$name} = $suffix; | |
| } | |
| } | |
| # Now rejoin into one happy whole | |
| my $output; | |
| foreach my $plugin (keys %cooked) { | |
| $output .= "$plugin" . $cooked{$plugin} . ";"; | |
| } | |
| # remove the last ; | |
| $output =~ s/;$//g; | |
| return $output; | |
| } | |
| ############################################################################### | |
| # Check a regex for validation & fix. If mode=1, return a flag which indicates | |
| # whether the regex was changed | |
| sub validate_and_fix_regex { | |
| my ($regex, $mode) = @_; | |
| my $fixed = 0; | |
| eval { qr/$regex/ }; | |
| if ($@) { | |
| $fixed = 1; | |
| $regex = rquote($regex); | |
| } | |
| return $mode ? ($regex, $fixed) : $regex; | |
| } | |
| ############################################################################### | |
| # Process captured groups in message strings | |
| # Replaces $1, $2, etc. with captured group values | |
| sub process_captured_groups { | |
| my ($message, $captures) = @_; | |
| # Quick exit if no message or captures | |
| return $message unless $message && $captures && @$captures; | |
| # Quick exit if message has no placeholder patterns | |
| return $message unless $message =~ /\$\d+/; | |
| # Replace $1, $2, etc. with captured values | |
| for my $i (1 .. @$captures) { | |
| my $value = $captures->[ $i - 1 ]; | |
| # Replace with captured value or empty string if undefined | |
| $value = '' unless defined $value; | |
| $message =~ s/\$$i(?!\d)/$value/g; | |
| } | |
| return $message; | |
| } | |
| ############################################################################### | |
| sub rquote { | |
| my $string = $_[0] || return; | |
| $string =~ s/([^A-Za-z_0-9 "'\\])/\\$1/g; | |
| return $string; | |
| } | |
| ############################################################################### | |
| sub gmt_offset { | |
| my @t = localtime(time); | |
| return (timegm(@t) - timelocal(@t)) / 3600; | |
| } | |
| ############################################################################### | |
| sub expand_range { | |
| local $" = '..'; | |
| my (@range); | |
| sort { $a <=> $b } | |
| map { | |
| map { ((@range = split /-/) == 2) ? eval('map {$_} ' . "@range") : $_ } | |
| split /\s/ | |
| } @_; | |
| } | |
| ############################################################################### | |
| sub check_ipv6 { | |
| nprint("Performing IPv6 connectivity tests:"); | |
| # Perform a series of tests and: exit 1 or 0 | |
| # Does the version of Socket even support IPv6? | |
| if (!$LW2::LW2_CAN_IPv6) { | |
| nprint( | |
| "+ ERROR: This version of Socket ($Socket::VERSION) has insufficient (or no) IPv6 support" | |
| ); | |
| exit 1; | |
| } | |
| nprint("+ This version of Socket ($Socket::VERSION) does support IPv6"); | |
| # Ensure switches are in a known state | |
| $CLI{'ipv6'} = 1; | |
| $CLI{'ipv4'} = 0; | |
| if ($CONFIGFILE{'CHECK6HOST'} eq "") { | |
| $CONFIGFILE{'CHECK6HOST'} = 'ipv6.google.com'; | |
| $CONFIGFILE{'CHECK6PORT'} = '443'; | |
| } | |
| # Try to resolve a known IPv6 hostname | |
| my ($name, $ip, $displayname) = resolve($CONFIGFILE{'CHECK6HOST'}); | |
| if (!$ip) { | |
| nprint("- DNS resolution of '$CONFIGFILE{'CHECK6HOST'}' using AF_INET6 failed"); | |
| nprint( | |
| "\t(Perhaps no DNS server set or server is incapable of resolving an IPv6 address for $CONFIGFILE{'CHECK6HOST'})" | |
| ); | |
| exit 1; | |
| } | |
| nprint("+ Successful DNS resolution of '$CONFIGFILE{'CHECK6HOST'}': $ip"); | |
| # Try to connect to the host | |
| my $res = LW2::utils_port_open($CONFIGFILE{'CHECK6HOST'}, $CONFIGFILE{'CHECK6PORT'}); | |
| if (!$res) { | |
| nprint( | |
| "+ ERROR: TCP connection to '$CONFIGFILE{'CHECK6HOST'}:$CONFIGFILE{'CHECK6PORT'}' using AF_INET6 failed" | |
| ); | |
| nprint("\t(Likely either no IPv6 connectivity or firewall blocking)"); | |
| exit 1; | |
| } | |
| nprint("+ Successful TCP connection to '$CONFIGFILE{'CHECK6HOST'}:$CONFIGFILE{'CHECK6PORT'}'"); | |
| nprint("----> All tests successful"); | |
| exit 0; | |
| } | |
| ############################################################################### | |
| sub nikto_core { return; } # trap for this plugin being called to run. lame. | |
| ############################################################################### | |
| 1; | |