#VERSION,2.1.4 # $Id$ ############################################################################### # Copyright (C) 2006 CIRT, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; version 2 # of the License only. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. ############################################################################### # PURPOSE: # Nikto core functionality ############################################################################### sub change_variables { # $line is the unfiltered variable my $line = $_[0]; my @subtests; # @subtests is the returned array of expanded variables my $cooked; my $shname = $mark->{'hostname'} || $mark->{'ip'}; $line =~ s/\@IP/$mark->{'ip'}/g; $line =~ s/\@HOSTNAME/$shname/g; $line =~ s/JUNK\(([0-9]+)\)/LW2::utils_randstr($1)/e; if ($line !~ "\@") { push(@subtests, $line); } else { foreach my $varname (keys %VARIABLES) { if ($line =~ /$varname/) { # We've found the variable; now to expand it! foreach my $value (split(/ /, $VARIABLES{$varname})) { $cooked = $line; $cooked =~ s/$varname/$value/g; push(@subtests, change_variables($cooked)); } } } } return @subtests; } ############################################################################### sub is_404 { my ($uri, $content, $rescode, $loc_header) = @_; $ext = get_ext($uri); if (($FoF{$ext}{'mode'} eq "STD") && ($rescode =~ /4[0-9][0-9]/)) { return 1; } elsif ($FoF{$ext}{'mode'} eq "REDIR") { if (get_base_host($loc_header) eq $FoF{$ext}{'location'}) { return 1; } } elsif (($FoF{$ext}{'type'} eq "BLANK") && ($content eq "")) { return 1; } elsif ($FoF{$ext}{'type'} eq "HASH") { my $content = rm_active_content($content, $uri); if (LW2::md4($content) eq $FoF{$ext}{'match'}) { return 1; } } else { foreach my $string (keys %ERRSTRINGS) { if ($content =~ /$string/i) { return 1; } } } return 0; } ############################################################################### sub nprint { my $line = shift; my $mode = shift; my ($mark) = @_; chomp($line); # scrub values if ($OUTPUT{'scrub'}) { # name $line =~ s/$mark->{'hostname'}/example.com/ig unless $mark->{'hostname'} eq ''; # ip $line =~ s/$mark->{'ip'}/0.0.0.0/ig unless $mark->{'ip'} eq ''; # vhost $line =~ s/$CLI{'vhost'}/example.com/ig unless $CLI{'vhost'} eq ''; # and in case we got here from set_target $line =~ s/$mark->{'ident'}/example.com/ig unless $mark->{'ident'} eq ''; } # don't print debug & verbose to output file... if ($mode ne '') { if ($mode eq "d" && $OUTPUT{'debug'}) { print "D:" . localtime() . " $line\n"; } if ($mode eq "v" && $OUTPUT{'verbose'}) { print "V:" . localtime() . " $line\n"; } if ($mode eq "e" && $OUTPUT{'errors'}) { print "E:" . localtime() . " $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 "-" if ((defined $CLI{'file'}) && ($CLI{'file'} eq "-")) { return; } $line =~ s/(CVE\-[12][0-9]{4}-[0-9]{4})/http:\/\/cve.mitre.org\/cgi-bin\/cvename.cgi?name\=$1/g; $line =~ s/(CA\-[12][0-9]{3}-[0-9]{2})/http:\/\/www.cert.org\/advisories\/$1.html/g; $line =~ s/BID\-([0-9]{4})/http:\/\/www.securityfocus.com\/bid\/$1/g; $line =~ s/(MS[0-9]{2}\-[0-9]{3})/http:\/\/www.microsoft.com\/technet\/security\/bulletin\/$1.asp/gi; print $line . "\n"; return; } ############################################################################### sub get_ext { my $uri = $_[0] || return; if ($uri =~ /\/$/) { return "DIRECTORY"; } $uri =~ s/^.*\///; if ($uri =~ /^\.[^.%]/) { return "DOTFILE"; } $uri =~ s/[?&%].*$//; if ($uri !~ /\./) { return "NONE"; } $uri =~ s/\".*$//; $uri =~ s/^.*\.//; return $uri; } ############################################################################### sub status_report { my $secleft = ((time() - $COUNTERS{'startsec'}) / $NIKTO{'totalrequests'}) * (($NIKTO{'total_checks'} * $NIKTO{'total_targets'}) - $NIKTO{'totalrequests'}); my $perc_compl = ($NIKTO{'totalrequests'} / ($NIKTO{'total_checks'} * $NIKTO{'total_targets'}) * 100); my $line; # This 'if' is because I am a lazy, bad programmer. # And also because total_checks only takes into account db_tests, not other stuff. I swear. if (($perc_compl < 100) && ($secleft > 0)) { $line = "- STATUS: Completed $NIKTO{'totalrequests'} tests"; if ($NIKTO{'total_targets'} > 1) { $line .= " (target " . ($COUNTERS{'hosts_completed'} + 1) . "/$NIKTO{'total_targets'})"; } $line .= sprintf(" (~%.0f%% complete, ~%d seconds left", $perc_compl, $secleft); if ($NIKTO{'current_plugin'} ne '') { $line .= ": currently in plugin '$NIKTO{'current_plugin'}'"; } $line .= ")"; } else { $line = "- STATUS: Finishing up!"; } nprint($line); return; } ############################################################################### sub date_disp { my @time = localtime($_[0]); my $result = sprintf("%d-%02d-%02d %02d:%02d:%02d", $time[5] + 1900, $time[4] + 1, $time[3] + 1, $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 map_codes { my ($mark) = @_; my %REQS; my $rs = LW2::utils_randstr(8); my ($res, $content, $error, %headers); # / for OK response ($res, $content, $error) = nfetch($mark, "/", "GET", "", \%headers, "", "map_codes"); if (defined $headers{'location'}) { nprint("+ Root page / redirects to: $headers{'location'}"); if ($headers{'location'} =~ /^$mark->{'hostname'}/i) # same host { my $uri = $headers{'location'}; %headers = (); ($res, $content, $error) = nfetch($mark, "/", "GET", "", \%headers, "", "map_codes"); } else # different host... ugh... just guess { $FoF{'okay'}{'response'} = 200; $FoF{'okay'}{'type'} = "STD"; } } else { $FoF{'okay'}{'response'} = $res; my $cooked = rm_active_content($content); $FoF{'okay'}{'type'} = "HASH"; $FoF{'okay'}{'match'} = LW2::md4($cooked); } # these are some used in mutate that may not be in the db_tests $db_extensions{'bak'} = 1; $db_extensions{'data'} = 1; $db_extensions{'dbc'} = 1; $db_extensions{'dbf'} = 1; $db_extensions{'lst'} = 1; $db_extensions{'htx'} = 1; foreach my $ext (keys %db_extensions) { if ( $ext ne "DIRECTORY" && $ext ne "NONE" && $ext ne "DOTFILE") { $REQS{"/$rs.$ext"} = $ext; } } # add those generic type holders back as real files $REQS{"/$rs/"} = "DIRECTORY"; $REQS{"/$rs"} = "NONE"; $REQS{"/.$rs"} = "DOTFILE"; foreach my $file (keys %REQS) { nprint("- Testing error for file: $file\n", "v"); %headers = (); ($res, $content, $error) = nfetch($mark, $file, "GET", "", \%headers, "", "map_codes"); $ext = $REQS{$file}; $FoF{$ext}{'response'} = $res; # handle .com to .org redirs or whatnot if (defined $headers{'location'}) { $FoF{$ext}{'location'} = get_base_host($headers{'location'}); } # if it is not specific type, figure out Content or HASH method... if ($FoF{$ext}{'response'} eq 404) { $FoF{$ext}{'mode'} = "STD"; next; } elsif ($FoF{$ext}{'response'} eq 200) { $FoF{$ext}{'mode'} = "OK"; } elsif ($FoF{$ext}{'response'} eq 410) { $FoF{$ext}{'mode'} = "STD"; next; } elsif ($FoF{$ext}{'response'} eq 401) { $FoF{$ext}{'mode'} = "STD"; next; } elsif ($FoF{$ext}{'response'} eq 403) { $FoF{$ext}{'mode'} = "STD"; next; } elsif ($FoF{$ext}{'response'} eq 300) { $FoF{$ext}{'mode'} = "REDIR"; next; } elsif ($FoF{$ext}{'response'} eq 301) { $FoF{$ext}{'mode'} = "REDIR"; next; } elsif ($FoF{$ext}{'response'} eq 302) { $FoF{$ext}{'mode'} = "REDIR"; next; } elsif ($FoF{$ext}{'response'} eq 303) { $FoF{$ext}{'mode'} = "REDIR"; next; } elsif ($FoF{$ext}{'response'} eq 307) { $FoF{$ext}{'mode'} = "REDIR"; next; } else { $FoF{$ext}{'mode'} = "OTHER"; } # if we've got an OK/OTHER response, look at content first # blank content, or hash... if (length($content) == 0) { $FoF{$ext}{'type'} = "BLANK"; $FoF{$ext}{'match'} = ""; } else { my $cooked = rm_active_content($content); $FoF{$ext}{'type'} = "HASH"; $FoF{$ext}{'match'} = LW2::md4($cooked); } } # lastly, get a hash of index.php so we can cut down on some false positives... %headers = (); ($res, $content, $error) = nfetch($mark, "/index.php?", "GET", "", \%headers, "", "map_codes"); my $cooked = rm_active_content($content); $FoF{'index.php'}{'match'} = LW2::md4($cooked); $FoF{'index.php'}{'type'} = "HASH"; return; } ############################################################################### sub rm_active_content { # Try to remove active content which could mess up the file's signature my ($cont, $file) = @_; # Dates/Times $cont =~ s/[12][0-9]{3}[-.\/][1-3]?[0-9][-.\/][1-3]?[0-9]//g; # 2001-12-12 $cont =~ s/[1-3]?[0-9][-.\/][1-3]?[0-9][-.\/][12][0-9]{3}//g; # 12-12-2002 $cont =~ s/[0-9]{8,14}//g; # timestamp $cont =~ s/[0-9]{6}//g; # timestamp $cont =~ s/[0-9]{2}:[0-9]{2}(?::[0-9]{2})?//g; #12:11:33 $cont =~ s/(?:mon|tue|wed|thu|fri|sat|sun),? [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/[0-9\.]+ second//gi; # page load time $cont =~ s/[0-9]+ queries//gi; # wordpress # 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]); $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 = "$NIKTO{'DIV'}\n"; $sslprint .= "+ SSL Info: Ciphers: $mark->{'ssl_cipher'}\n" . " Info: $mark->{'ssl_cert_issuer'}\n" . " Subject: $mark->{'ssl_cert_subject'}"; } if ($mark->{ip} =~ /[a-z]/i) { nprint("+ Target IP: (proxied)", "", $mark); } else { nprint("+ Target IP: $mark->{ip}", "", $mark); } nprint("+ Target Hostname: $mark->{hostname}", "", $mark); nprint("+ Target Port: $mark->{port}"); if ((defined $CLI{'vhost'}) && ($CLI{'vhost'} ne $mark->{hostname})) { nprint("+ Virtual Host: $CLI{'vhost'}", "", $mark); } if (defined $request{'whisker'}->{'proxy_host'}) { nprint( "+ Proxy: $request{'whisker'}->{'proxy_host'}:$request{'whisker'}->{'proxy_port'}" ); } if (defined $NIKTO{'hostid'}) { nprint( "+ Host Auth: ID: $NIKTO{'hostid'}, PW: $NIKTO{'hostpw'}, Realm: $NIKTO{'hostdomain'}", "v" ); } if ($mark->{ssl}) { nprint($sslprint); } if (defined $NIKTO{'anti_ids'} && defined $CLI{'evasion'}) { for (my $i = 1 ; $i <= (keys %{ $NIKTO{'anti_ids'} }) ; $i++) { if ($CLI{'evasion'} =~ /$i/) { nprint("+ Using IDS Evasion: $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}"); } } } my $time = date_disp($mark->{start_time}); nprint("+ Start Time: $time"); nprint($NIKTO{'DIV'}); if ($mark->{banner} ne "") { nprint("+ Server: $mark->{banner}"); } else { nprint("+ Server: No banner retrieved"); } return; } ############################################################################### sub general_config { ## gotta set these first $| = 1; # 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'}{'5'} = "Attempt to brute force sub-domain names, assume that the host name is the parent domain"; $NIKTO{'mutate_opts'}{'6'} = "Attempt to guess directory names from the supplied dictionary file"; ### CLI STUFF $CLI{'pause'} = $CLI{'html'} = $OUTPUT{'verbose'} = $CLI{'skiplookup'} = $NIKTO{'totalrequests'} = $OUTPUT{'debug'} = $OUTPUT{'scrub'} = $OUTPUT{'errors'} = 0; $CLI{'all_options'} = join(" ", @ARGV); # preprocess CLI options which cannot be abbreviated for (my $i = 0 ; $i <= $#ARGV ; $i++) { if ($ARGV[$i] eq '-dbcheck') { dbcheck(); } elsif ($ARGV[$i] eq '-update') { check_updates(); } } GetOptions("nolookup" => \$CLI{'skiplookup'}, "config=s" => \$CLI{'config'}, "Cgidirs=s" => \$CLI{'forcecgi'}, "mutate=s" => \$CLI{'mutate'}, "mutate-options=s" => \$CLI{'mutate-options'}, "id=s" => \$CLI{'hostauth'}, "evasion=s" => \$CLI{'evasion'}, "port=s" => \$CLI{'ports'}, "findonly" => \$CLI{'findonly'}, "root=s" => \$CLI{'root'}, "timeout=s" => \$CLI{'timeout'}, "Pause=s" => \$CLI{'pause'}, "ssl" => \$CLI{'ssl'}, "nocache" => \$CLI{'nocache'}, "nossl" => \$CLI{'nossl'}, "no404" => \$CLI{'nofof'}, "useproxy" => \$CLI{'useproxy'}, "Help" => \$CLI{'help'}, "vhost=s" => \$CLI{'vhost'}, "host=s" => \$CLI{'host'}, "output=s" => \$CLI{'file'}, "Format=s" => \$CLI{'format'}, "Display=s" => \$CLI{'display'}, "Single" => \$CLI{'Single'}, "Tuning=s" => \$CLI{'tuning'}, "Version" => \$CLI{'version'}, "Plugins=s" => \$CLI{'plugins'}, "list-plugins" => \$CLI{'list-plugins'}, "ask=s" => \$CLI{'ask'} ) or usage(0); if ($CLI{'help'}) { usage(2); } elsif ($CLI{'version'}) { version(); } elsif ($CLI{'Single'}) { single(); } elsif ($CLI{'list-plugins'}) { list_plugins(); } # output file if (!defined $CLI{'format'}) { # Check what output has $CLI{'format'} = "none"; if (defined $CLI{'file'}) { $CLI{'format'} = lc($CLI{'file'}); $CLI{'format'} =~ s/(^.*\.)([^.]*$)/$2/g; } } if ($CLI{'format'} =~ /te?xt/i) { $CLI{'format'} = "txt"; } elsif ($CLI{'format'} =~ /html?/i) { $CLI{'format'} = "htm"; } elsif ($CLI{'format'} =~ /csv/i) { $CLI{'format'} = "csv"; } elsif ($CLI{'format'} =~ /nbe/i) { $CLI{'format'} = "nbe"; } elsif ($CLI{'format'} =~ /xml/i) { $CLI{'format'} = "xml"; } elsif ($CLI{'format'} =~ /msf/i) { $CLI{'format'} = "msf"; } elsif ($CLI{'format'} eq 'none') { } else { nprint("+ ERROR: Invalid output format"); exit; } if ((defined $CLI{'file'}) && ($CLI{'format'} eq "")) { nprint("+ERROR: Output file specified without a format"); exit; } # verify readable dtd if ($CLI{'format'} eq 'xml' && !-r $NIKTOCONFIG{'NIKTODTD'}) { nprint("+ ERROR: reading DTD"); exit; } # 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; } } # 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; } } # Fixup if (defined $CLI{'root'}) { $CLI{'root'} =~ s/\/$//; if (($CLI{'root'} !~ /^\//) && ($CLI{'root'} ne "")) { $CLI{'root'} = "/$CLI{'root'}"; } } if (defined $CLI{'hostauth'}) { my @x = split(/:/, $CLI{'hostauth'}); if (($#x > 2) || ($x[0] eq "")) { nprint( "+ ERROR: \'$CLI{'hostauth'}\' (-i option) syntax is 'user:password' or 'user:password:domain' for host authentication." ); exit; } } 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'}) { nprint("- Mutate is deprecated, use -Plugins instead"); 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-opts'} if (defined $CLI{'mutate-opts'}); $CLI{'plugins'} .= ';apacheusers(' . $parameters . ')'; } if ($CLI{'mutate'} =~ /5/) { $CLI{'plugins'} .= ";subdomain"; } if ($CLI{'mutate'} =~ /6/) { $CLI{'plugins'} .= ';dictionary(dictionary:' . $CLI{'mutate-opts'} . ')'; } } # Asking questions? if ($CLI{'ask'} =~ /^(?:auto|yes|no)$/) { $NIKTOCONFIG{'UPDATES'} = $CLI{'ask'}; # override nikto.conf setting undef($CLI{'ask'}); } $NIKTO{'timeout'} = $CLI{'timeout'} || 10; # Set up User-Agent $NIKTO{'useragent'} = $NIKTOCONFIG{'USERAGENT'}; $NIKTO{'useragent'} =~ s/\@VERSION/$NIKTO{'version'}/g; my $ev = $CLI{'evasion'} || "None"; $NIKTO{'useragent'} =~ s/\@EVASIONS/$ev/g; # RFI URL -- push it to VARIABLES if (defined $NIKTOCONFIG{'RFIURL'}) { $VARIABLES{'@RFIURL'} = $NIKTOCONFIG{'RFIURL'}; } else { nprint("- ***** RFIURL is not defined in nikto.conf--no RFI tests will run *****"); } # SSL Test if (!LW2::ssl_is_available()) { nprint("- ***** SSL support not available (see docs for SSL install) *****"); } # Notices my $notice = ""; if (defined $CLI{'root'}) { $notice .= "Prepending \'$CLI{'root'}\' to requests"; } if ($CLI{'pause'} > 0) { $notice .= ", Pausing $CLI{'pause'} seconds per request"; } $notice =~ s/^, //; if ($notice ne '') { nprint("-***** $notice *****"); } # get core version open(FI, "<$NIKTOCONFIG{PLUGINDIR}/nikto_core.plugin"); my @F = ; close(FI); my @VERS = grep(/^#VERSION/, @F); $NIKTO{'core_version'} = $VERS[0]; $NIKTO{'core_version'} =~ s/\#VERSION,//; chomp($NIKTO{'core_version'}); $NIKTO{'TMPL_HCTR'} = 0; $NIKTO{'TMPL_SUMMARY'} = 0; # POSIX support for status? $NIKTO{'POSIX'}{'support'} = 0; eval "use POSIX qw(:termios_h)"; if (!$@) { eval "use Time::HiRes qw(ualarm)"; if (!$@) { $NIKTO{'POSIX'}{'support'} = 1; $NIKTO{'POSIX'}{'fd_stdin'} = fileno(STDIN); $NIKTO{'POSIX'}{'term'} = POSIX::Termios->new(); $NIKTO{'POSIX'}{'term'}->getattr($fd_stdin); $NIKTO{'POSIX'}{'oterm'} = $NIKTO{'POSIX'}{'term'}->getlflag(); $NIKTO{'POSIX'}{'echo'} = ECHOE | ECHO | ECHOK | ICANON; $NIKTO{'POSIX'}{'noecho'} = $oterm & ~$echo; } } return; } ############################################################################### sub reset_term { if (!$NIKTO{'POSIX'}{'support'}) { return; } $NIKTO{'POSIX'}{'term'}->setlflag($NIKTO{'POSIX'}{'oterm'}); $NIKTO{'POSIX'}{'support'} = 0; } ############################################################################### sub safe_quit { $mark->{'end_time'} = time(); report_host_end($mark); report_close($mark); reset_term(); exit(1); } ############################################################################### sub check_input { my $key = readkey(); if ($key eq '') { return; } lc($key); if ($key eq ' ') { status_report(); return; } elsif ($key eq 'v') { if ($OUTPUT{'verbose'}) { $OUTPUT{'verbose'} = 0; } else { $OUTPUT{'verbose'} = 1; } } elsif ($key eq 'd') { if ($OUTPUT{'debug'}) { $OUTPUT{'debug'} = 0; } else { $OUTPUT{'debug'} = 1; } } elsif ($key eq 'e') { if ($OUTPUT{'errors'}) { $OUTPUT{'errors'} = 0; } else { $OUTPUT{'errors'} = 1; } } elsif ($key eq 'p') { if ($OUTPUT{'progress'}) { $OUTPUT{'progress'} = 0; } else { $OUTPUT{'progress'} = 1; } } elsif ($key eq 'r') { if ($OUTPUT{'show_redirects'}) { $OUTPUT{'show_redirects'} = 0; } else { $OUTPUT{'show_redirects'} = 1; } } elsif ($key eq 'c') { if ($OUTPUT{'show_cookies'}) { $OUTPUT{'show_cookies'} = 0; } else { $OUTPUT{'show_cookies'} = 1; } } elsif ($key eq 'o') { if ($OUTPUT{'show_ok'}) { $OUTPUT{'show_ok'} = 0; } else { $OUTPUT{'show_ok'} = 1; } } elsif ($key eq 'a') { if ($OUTPUT{'show_auth'}) { $OUTPUT{'show_auth'} = 0; } else { $OUTPUT{'show_auth'} = 1; } } elsif (($key eq 'q') || (ord($key) eq 3)) { safe_quit(); } elsif ($key eq 'P') { nprint("- Pausing--press P to resume."); while (readkey() ne 'P') { } nprint("- Resuming."); } return; } ############################################################################### sub readkey { if (!$NIKTO{'POSIX'}{'support'}) { return; } 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] || return; my ($name, $ip, $dn) = ""; if ($request{'whisker'}->{'proxy_host'} ne '') { $name = $ident; $ip = $name; return $name, $ip, $name; } # ident is name, lookup IP if ($ident =~ /[^0-9\.]/) # not an IP, assume name { if ($CLI{'skiplookup'}) { print("+ ERROR: -skiplookup set, but given name\n"); exit; } $ip = gethostbyname($ident); # can't resolve name to IP if ($ip eq "") { nprint("+ ERROR: Cannot resolve hostname '$ident'\n"); return; } else { use IO::Socket; $ip = inet_ntoa($ip); if ($ip !~ /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/) { nprint("+ ERROR: Invalid IP '$ip'\n\n"); return; } $name = $ident; } } else # ident is IP, lookup name { if ($ident !~ /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/) { nprint("+ ERROR: Invalid IP '$ident'\n\n"); return; } $ip = $ident; if (!$CLI{'skiplookup'}) { use IO::Socket; my $temp_ip = inet_aton($ip); $name = gethostbyaddr($temp_ip, AF_INET); # check reverse dns to avoid an inet_aton error my $rdnsip = gethostbyname($name); if ($rdnsip ne "") { $rdnsip = inet_ntoa($rdnsip); if ($ip ne $rdnsip) { $name = $ip; } # Reverse DNS does not match } else { $name = $ip; } # Reverse DNS does not exist } if ($name eq "") { $name = $ip; } } # set displayname -- name takes precedence if ($name ne "") { $dn = $name; } else { $dn = $ip; } return $name, $ip, $dn; } ############################################################################### sub set_targets { my ($hostlist, $portlist, $ssl, $root) = @_; my $host_ctr = 1; my @hosts = split(/,/, $hostlist); my @ports = split(/,/, $portlist) if defined $portlist; my (@checkhosts, @results, @marks); my $defaultport = ($ssl) ? 443 : 80; # Check for old style portlist and expand my @newports; foreach my $port (@ports) { 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(@newports, $i); } } else { push(@newports, $port); } } @ports = @newports; nprint("- Getting targets", "v"); if (scalar(@ports) == 1) { # Only one port is set, assume that is the default port $defaultport = $ports[0]; } # check whether it's a file or an entry foreach my $host (@hosts) { if (-e $host || $host eq "-") { @results = parse_hostfile($host); push(@checkhosts, @results); } else { push(@checkhosts, $host); } } # Now parse the list of checkhosts foreach my $host (@checkhosts) { my $defhost; my $defport; $host =~ s/\s+//g; if (!defined $host) { next; } # is it a URL? if ($host =~ /^https?:\/\//) { my @hostdata = LW2::uri_split($host); $defhost = $hostdata[2]; $defport = $hostdata[3]; if ((!defined $root) && (defined $hostdata[0])) { $root = $hostdata[0]; nprint("- Added -root value of '$root'", "d"); } } else { my @h = split(/\:|\,/, $host); $defhost = $h[0]; $defport = $h[1]; } # Now skip through all ports if port hasn't been added if ($defport eq "" && scalar(@ports) > 0) { foreach $port (@ports) { my $markhash = {}; $markhash->{ident} = $defhost; $markhash->{port} = $port; nprint("- Target:$markhash->{ident} port:$markhash->{port}", "v", $markhash); push(@marks, $markhash); } } else { if ($defport eq "") { $defport = $defaultport; } my $markhash = {}; $markhash->{ident} = $defhost; $markhash->{port} = $defport; nprint("- Target:$markhash->{ident} port:$markhash->{port}", "v", $markhash); push(@marks, $markhash); } } return @marks; } ############################################################################### sub parse_hostfile { my ($file) = @_; my (@results, $hostdesc, $nmap); $nmap = 0; open(IN, $file) || die print STDERR "+ ERROR: Cannot open '$file':$@\n"; while () { my $found = 0; # Check whether this is an nmap oG file chomp; if (/^# Nmap [0-9.]* scan initiated/) { $nmap = 1; } s/\#.*$//; if ($_ eq "") { next; } # Parse for nmap files if ($nmap) { # First get the host name my @line = split(/ /); my @name = split(/\(|\)/, $line[2]); $hostdesc = ($name[1] ne "") ? $name[1] : $line[1]; # now parse the ports list for (my $i = 3 ; $i <= $#line ; $i++) { my @ports = split(/\//, $line[$i]); if ($ports[1] eq "open" && $ports[4] eq "http") { $found = 1; $hostdesc .= ":" . $ports[0]; } } } else { # just add it to the list $hostdesc = $_; $found = 1; } push(@results, $hostdesc) if ($found); } close(IN); return (@results); } ############################################################################### sub load_databases { my @dbs = qw/db_404_strings db_outdated db_tests db_variables db_content_search/; my $prefix = $_[0] || ""; # verify required files for my $file (@dbs) { if (!-r "$NIKTOCONFIG{PLUGINDIR}/$file") { die nprint("+ ERROR: Can't find/read required file \"$NIKTOCONFIG{PLUGINDIR}/$file\""); } } for my $file (@dbs) { my $filename = $NIKTOCONFIG{PLUGINDIR} . "/" . $prefix . $file; if (!-r $filename) { next; } open(IN, "<$filename") || die nprint("+ ERROR: Can't open \"$filename\":$!\n"); # db_tests if ($file eq 'db_tests') { push(@DBFILE, ); next; } # all the other files require per-line processing else { my @file; # Cleanup while () { chomp; $_ =~ s/#.*$//; $_ =~ s/\s+$//; $_ =~ s/^\s+//; if ($_ ne "") { push(@file, $_); } } # db_variables if ($file eq 'db_variables') { foreach my $l (@file) { if ($l =~ /^@/) { my @temp = split(/=/, $l); $VARIABLES{ $temp[0] } .= "$temp[1]"; } } } # db_404_strings elsif ($file eq 'db_404_strings') { foreach my $l (@file) { $ERRSTRINGS{$l} = 1; } } # db_content_search elsif ($file eq 'db_content_search') { foreach my $l (@file) { my @T = parse_csv($l); $CONTENTSEARCH{ $T[0] }{'osvdb'} = $T[1]; $CONTENTSEARCH{ $T[0] }{'string'} = $T[2]; $CONTENTSEARCH{ $T[0] }{'message'} = $T[3]; } } # db_outdated elsif ($file eq 'db_outdated') { foreach my $l (@file) { my @T = parse_csv($l); $OVERS{ $T[1] }{ $T[2] } = $T[3]; $OVERS{ $T[1] }{'tid'} = $T[0]; } } close(IN); } } return; } ############################################################################### sub dbcheck { my @dbs = qw/db_headers db_httpoptions db_multiple_index db_server_msgs db_subdomains db_favicon db_embedded db_404_strings db_outdated db_realms db_tests db_variables db_content_search/; my $prefix = $_[0]; if ($prefix eq "") { nprint("\n-->\tNikto Databases"); } if ($prefix eq "u") { nprint("\n-->\tUser Databases"); } for my $file (@dbs) { my $filename = $NIKTOCONFIG{PLUGINDIR} . "/" . $prefix . $file; if (!-r $filename) { next; } open(IN, "<$filename") || die nprint("+ ERROR: Can't open \"$filename\":$!\n"); nprint("Syntax Check: $filename"); if ($file eq 'db_outdated') { foreach $line () { $line =~ s/^\s+//; if ($line =~ /^\#/) { next; } chomp($line); if ($line eq "") { next; } my @L = parse_csv($line); if ($#L ne 3) { nprint("\t+ ERROR: Invalid syntax ($#L): $line"); next; } $ENTRIES{"$L[0]"}++; } foreach $entry (keys %ENTRIES) { if ($ENTRIES{$entry} > 1) { nprint("\t+ ERROR: Duplicate ($ENTRIES{$entry}): $entry"); } } nprint("\t" . keys(%ENTRIES) . " entries"); } elsif ($file eq 'db_tests') { my %ENTRIES; foreach my $line () { if ($line !~ /^\"/) { next; } my @L = parse_csv($line); if ($L[4] !~ /(GET|POST|TRACE|TRACK|OPTIONS|SEARCH|INDEX)/i) { nprint("\t+ ERROR: Possibly invalid method: $L[4] on ($line)"); } if ($L[5] eq "") { nprint("\t+ ERROR: blank conditional: $line"); next; } if ($line !~ /^\".*\",\".*\",\".*\",\".*\",\".*\"/) { nprint("\t+ ERROR: Invalid syntax ($#L): $line"); next; } if ($line !~ /^(\".*\",){11}\".*\"/) { nprint("\t+ ERROR: Invalid syntax ($#L): $line"); next; } if (($L[3] =~ /^\@CG/) && ($L[3] !~ /^\@CGIDIRS/)) { nprint("\t+ ERROR: Possible \@CGIDIRS misspelling: $line"); } if ($L[1] =~ /[^0-9]/) { nprint("\t+ ERROR: Invalid OSVDB ID: $line"); } $ENTRIES{"$L[3],$L[4],$L[5],$L[6],$L[7],$L[8],$L[9],$L[11],$L[12]"}++; if ((count_fields($line, 1) ne 12) && (count_fields($line) ne '')) { nprint("\t+ ERROR: Invalid syntax: $line"); } } foreach $entry (keys %ENTRIES) { if ($ENTRIES{$entry} > 1) { nprint("\t+ ERROR: Duplicate ($ENTRIES{$entry}): $entry"); } } nprint("\t" . keys(%ENTRIES) . " entries"); } elsif ($file eq 'db_variables') { my $ctr = 0; foreach $line () { if ($line !~ /^\@/) { next; } if ($line !~ /^\@.+\=.+$/i) { nprint("\t+ ERROR: Invalid syntax: $line"); } $ctr++; } nprint("\t$ctr entries"); } elsif ($file eq 'db_404_strings') { my $ctr = 0; foreach $line () { # not really any syntax to check $ctr++; } nprint("\t$ctr entries"); } elsif ($file eq 'db_headers') { my $ctr = 0; foreach $line () { if ((count_fields($line) ne 0) && (count_fields($line) ne '')) { nprint("\t+ ERROR: Invalid syntax: $line"); } $ctr++; } nprint("\t$ctr entries"); } elsif ($file eq 'db_multiple_index') { my $ctr = 0; foreach $line () { if ((count_fields($line) ne 0) && (count_fields($line) ne '')) { 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; my $ctr = 0, $fields = 0; foreach $line () { # first, grab the headers if ($fields == 0) { $line =~ s/\#.*//; next if ($line eq ""); @headers = parse_csv($line); $fields = $#headers; next; } if ( (count_fields($line, 1) != $fields - 1) && (count_fields($line) ne '')) { nprint("\t+ ERROR: Invalid syntax: $line"); } $ctr++; } nprint("\t$ctr entries"); } close(IN); } if ($_[0] eq "") { dbcheck('u'); } # do this once nprint("\n"); exit; } ############################################################################### 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 ($hostname, $ip, $port) = @_; my (%headers); my $m = {}; # Check SKIPPORTS if ($NIKTOCONFIG{'SKIPPORTS'} =~ /\b$port\b/) { nprint("+ ERROR: SKIPPORTS (nikto.conf) contains $port -- not checking"); return 0; } $m->{hostname} = $hostname; $m->{ip} = $ip; $m->{port} = $port; $m->{ssl} = 0; my @checktypes = ('HTTP', 'HTTPS'); if ($CLI{'ssl'}) { shift(@checktypes); } if ($CLI{'nossl'}) { pop(@checktypes); } foreach my $method (split(/ /, $NIKTOCONFIG{'CHECKMETHODS'})) { $request{'whisker'}->{'method'} = $method; foreach my $checkssl (@checktypes) { nprint("- Checking for $checkssl on port $ip:$port, using $method", "v", $m); $m->{ssl} = ($checkssl eq "HTTP") ? 0 : 1; proxy_check($m); my ($res, $content) = nfetch($m, "/", $method, "", \%headers, { noerror => 1, noprefetch => 1, nopostfetch => 1 }, "Port Check"); if ($res) { # this will fix for some Apaches that are smart enough to answer non ssl reqs on an ssl server if (defined $content && $content =~ /speaking plain HTTP to an SSL/) { dump_var("Result Hash", \%result); next; } nprint("- $checkssl Server found: $ip:$port \t$headers{server}", "d", $m); return $m->{ssl} + 1; } } } nprint("+ No web server found on $ip:$port"); nprint("---------------------------------------------------------------------------"); return 0; } ############################################################################### sub load_plugins { my @pluginlist = dirlist("$NIKTOCONFIG{PLUGINDIR}", '\.plugin$'); my @all_names; # populate plugin macros $NIKTOCONFIG{'@@NONE'} = ""; # Check if running plugins is NONE - if so, don't bother initalising # 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 "$NIKTOCONFIG{PLUGINDIR}/$plugin"; }; if ($@) { nprint("- Could not load or parse plugin: $plugin_name\n Error: "); warn $@; nprint("- The plugin could not be run."); } else { nprint("- Initialising plugin $plugin_name", "v"); # 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"); } } } $NIKTOCONFIG{'@@ALL'} = join(';', @all_names); my @torun = split(/;/, expand_pluginlist($CLI{'plugins'}, 0)); # 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) { # 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; } } } } # For speed in future, create a hash of active plugins ordered by plugin weight, for # each type of plugin # 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, $result) = @_; foreach my $plugin (@{ $PLUGINORDER{$type} }) { my ($run) = 1; # first check for conditionals my $condition = $plugin->{'hooks'}->{$type}->{'cond'}; if (defined $plugin->{'hooks'}->{$type}->{'cond'}) { # Evaluate condition $run = eval($condition); } if (!$run) { next; } my $oldverbose = $OUTPUT{'verbose'}; my $olddebug = $OUTPUT{'debug'}; my $olderrors = $OUTPUT{'errors'}; nprint("- Running $type for \"$plugin->{'full_name'}\" plugin", "v") unless ($type eq "prefetch" || $type eq "postfetch"); if (defined $plugin->{'parameters'}->{'verbose'} && $plugin->{'parameters'}->{'verbose'} == 1) { $OUTPUT{'verbose'} = 1; } if (defined $plugin->{'parameters'}->{'debug'} && $plugin->{'parameters'}->{'debug'} == 1) { $OUTPUT{'debug'} = 1; } unless ($type eq "prefetch" || $type eq "postfetch") { $NIKTO{'current_plugin'} = $plugin->{'full_name'}; } &{ $plugin->{'hooks'}->{$type}->{'method'} }($mark, $plugin->{'parameters'}, $request, $result); $OUTPUT{'verbose'} = $oldverbose; $OUTPUT{'debug'} = $olddebug; $OUTPUT{'errors'} = $olderrors; } return $request, $result; } ############################################################################### sub report_head { my ($format, $file) = @_; nprint("- Opening reports ($format, $file)", "v"); # For tuning set up a list of report methods, formats and handles # This is a frig until I can think of a better way of achieving it foreach my $i (1 .. 100) { foreach my $plugin (@PLUGINS) { if ($plugin->{run} && defined $plugin->{report_item} && $plugin->{report_weight} == $i) { my $run = 1; # first check for conditionals if (defined $plugin->{report_format}) { # Evaluate condition $run = ($format eq $plugin->{report_format}); } if ($run) { nprint("- Opening report for \"$plugin->{full_name}\" plugin", "v"); my $handle; if (defined $plugin->{report_head}) { $handle = &{ $plugin->{report_head} }($file); } # Now store this my $report_entry = { host_start => $plugin->{report_host_start}, host_end => $plugin->{report_host_end}, item => $plugin->{report_item}, close => $plugin->{report_close}, handle => $handle, }; push(@REPORTS, $report_entry); } } } } return; } ############################################################################### sub report_host_start { my ($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_item { my ($mark, $item) = @_; # Go through all reporting modules foreach my $reporter (@REPORTS) { if (defined $reporter->{item}) { &{ $reporter->{item} }($reporter->{handle}, $mark, $item); } } } ############################################################################### sub report_close { # Go through all reporting modules foreach my $reporter (@REPORTS) { if (defined $reporter->{close}) { &{ $reporter->{close} }($reporter->{handle}); } } } ############################################################################### sub check_updates { LW2::http_init_request(\%request); my (%REMOTE, %LOCAL, @DBTOGET) = (); my ($pluginmsg, $remotemsg) = ""; my $code_updates = 0; my $serverdir = "/nikto/UPDATES/$NIKTO{'version'}"; # set up our mark my %mark = ('ident' => 'cirt.net', 'ssl' => 0, 'port' => 80 ); for (my $i = 0 ; $i <= $#ARGV ; $i++) { if (($ARGV[$i] eq "-u") || ($ARGV[$i] eq "-useproxy")) { $CLI{'useproxy'} = 1; if (($NIKTOCONFIG{PROXYPORT} ne '') && ($NIKTOCONFIG{PROXYHOST} ne '')) { $request{'whisker'}->{'proxy_host'} = $NIKTOCONFIG{PROXYHOST}; $request{'whisker'}->{'proxy_port'} = $NIKTOCONFIG{PROXYPORT}; } proxy_check(); last; } } ($mark{'hostname'}, $mark{'ip'}, $mark{'display_name'}) = resolve('cirt.net'); # retrieve versions file (my $RES, $CONTENT) = nfetch(\%mark, "$serverdir/versions.txt", "GET"); if ($RES eq 407) { if ($NIKTOCONFIG{'PROXYUSER'} eq "") { $NIKTOCONFIG{'PROXYUSER'} = read_data("Proxy ID: ", ""); $NIKTOCONFIG{'PROXYPASS'} = read_data("Proxy Pass: ", "noecho"); } # and try again ($RES, $CONTENT) = nfetch(\%mark, "$serverdir/versions.txt", "GET"); } if ($RES eq "") { ($RES, $CONTENT) = nfetch(\%mark, "$serverdir/versions.txt", "GET"); } if ($RES ne 200) { nprint( "+ ERROR ($RES): Unable to get $mark{'hostname'}$serverdir/versions.txt"); exit; } # make hash for (split(/\n/, $CONTENT)) { my @l = parse_csv($_); if ($_ =~ /^msg/) { $remotemsg = "$l[1]"; next; } $REMOTE{ $l[0] } = $l[1]; } # get local versions of plugins/dbs my @NIKTOFILES = dirlist($NIKTOCONFIG{PLUGINDIR}, ""); foreach my $file (@NIKTOFILES) { my $v = ""; open(LOCAL, "<$NIKTOCONFIG{PLUGINDIR}/$file") || print STDERR "+ ERROR: Unable to open '$NIKTOCONFIG{PLUGINDIR}/$file' for read: $@\n"; my @l = ; close(LOCAL); my @VERS = grep(/^#VERSION/, @l); chomp($VERS[0]); $LOCAL{$file} = (parse_csv($VERS[0]))[1]; } # check main nikto versions foreach my $remotefile (keys %REMOTE) { my @l = split(/\./, $LOCAL{$remotefile}); my @r = split(/\./, $REMOTE{$remotefile}); my $update = 0; if ($LOCAL{$remotefile} eq '') { $update = 1; } elsif ($r[0] > $l[0]) { $update = 1; } elsif ($r[1] > $l[1]) { $update = 1; } elsif ($r[2] > $l[2]) { $update = 1; } if ($update) { if ($remotefile eq "nikto") { nprint "+ Nikto has been updated to $REMOTE{$remotefile}, local copy is $NIKTO{'version'}\n"; nprint "+ No update has taken place. Please upgrade Nikto by visiting http://$server/\n"; if ($remotemsg ne "") { nprint("+ $server message: $remotemsg"); } exit; } push(@DBTOGET, $remotefile); if ($remotefile !~ /^db_/) { $code_updates = 1; } } } # replace local files if updated foreach my $toget (@DBTOGET) { nprint("+ Retrieving '$toget'"); (my $RES, $CONTENT) = nfetch(\%mark, "$serverdir/$toget", "GET"); if ($RES ne 200) { nprint("+ ERROR: Unable to get $server$serverdir/$toget"); exit; } if ($CONTENT ne "") { open(OUT, ">$NIKTOCONFIG{PLUGINDIR}/$toget") || die print STDERR "+ ERROR: Unable to open '$NIKTOCONFIG{PLUGINDIR}/$toget' for write: $@\n"; print OUT $CONTENT; close(OUT); } } # CHANGES file if ($code_updates) { nprint("+ Retrieving 'CHANGES.txt'"); (my $RES, $CONTENT) = nfetch(\%mark, "$serverdir/CHANGES.txt", "GET"); if (($CONTENT ne "") && ($RES eq 200)) { open(OUT, ">$NIKTOCONFIG{DOCUMENTDIR}/CHANGES.txt") || die print STDERR "+ ERROR: Unable to open '$NIKTOCONFIG{DOCUMENTDIR}/CHANGES.txt' for write: $@\n"; print OUT $CONTENT; close(OUT); } } if ($#DBTOGET < 0) { nprint("+ No updates required."); } if ($remotemsg ne "") { nprint("+ $server message: $remotemsg"); } exit; } ############################################################################### # 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 ($NIKTOCONFIG{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 ); if ($NIKTO{'POSIX'}{'support'}) { 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); } else # non-POSIX { print $prompt; $input = ; chomp($input); } return $input; } ############################################################################### sub proxy_check { my ($mark) = @_; if (defined $request{'whisker'}->{'proxy_host'} && $CLI{'useproxy'}) # proxy is set up { LW2::http_close(\%request); # force-close any old connections setup_hash(\%request, $mark, "Proxy Check"); $request{'whisker'}->{'method'} = "GET"; $request{'whisker'}->{'uri'} = "/"; LW2::http_fixup_request(\%request); if ($CLI{'pause'} > 0) { sleep $CLI{'pause'}; } LW2::http_do_request_timeout(\%request, \%result); $NIKTO{'totalrequests'}++; dump_var("Request Hash", \%request); dump_var("Result Hash", \%result); # First check that we can connect to the proxy if (exists $result{'whisker'}{'error'}) { if ($result{'whisker'}{'error'} =~ /Transport endpoint is not connected/) { nprint("+ ERROR: Could not connect to the defined proxy $NIKTOCONFIG{PROXYHOST}"); } nprint("+ ERROR: Proxy error: $result{'whisker'}{'error'}"); exit 1; } if ($result{'whisker'}{'code'} eq "407") # proxy requires auth { # have id/pw? if ($NIKTOCONFIG{PROXYUSER} eq "") { $NIKTOCONFIG{PROXYUSER} = read_data("Proxy ID: ", ""); $NIKTOCONFIG{PROXYPASS} = read_data("Proxy Pass: ", "noecho"); } if ($result{'proxy-authenticate'} !~ /Basic/i) { my @x = split(/ /, $result{'proxy-authenticate'}); nprint( "+ Proxy server uses '$x[0]' rather than 'Basic' authentication. $NIKTO{'name'} $NIKTO{'version'} can't do that." ); exit; } # test it... LW2::http_close(\%request); # force-close any old connections LW2::auth_set("proxy-basic", \%request, $NIKTOCONFIG{PROXYUSER}, $NIKTOCONFIG{PROXYPASS}); # set auth LW2::http_fixup_request(\%request); if ($CLI{'pause'} > 0) { sleep $CLI{'pause'}; } LW2::http_do_request_timeout(\%request, \%result); $NIKTO{'totalrequests'}++; dump_var("Request Hash", \%request); dump_var("Result Hash", \%result); if ($result{'proxy-authenticate'} ne "") { my @pauthinfo = split(/ /, $result{'proxy-authenticate'}); my @pauthinfo2 = split(/=/, $result{'proxy-authenticate'}); $pauthinfo2[1] =~ s/^\"//; $pauthinfo2[1] =~ s/\"$//; nprint( "+ Proxy requires authentication for '$pauthinfo[0]' realm '$pauthinfo2[1]', unable to authenticate." ); exit; } else { nprint("- Successfully authenticated to proxy.", "v"); } } } return; } ############################################################################### 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 dump_var { return if !$OUTPUT{'debug'}; my $msg = $_[0]; my %hash_in = %{ $_[1] }; my $display = LW2::dump('', \%hash_in); $display =~ s/^\$/'$msg'/; if ($OUTPUT{'scrub'}) { $display =~ s/'host' => '.*',/'host' => 'example.com',/g; $display =~ s/'Host' => '.*'/'host' => 'example.com'/g; } nprint($display, "d"); return; } ###################################################################### sub content_present { my $result = FALSE; my $res = $_[0]; # perform an extra check just in case the web server lies about finds # basically assume that the value for a non-extension is the true # code for "File not Found". if ($res ne $FoF{'NONE'}{'response'}) { foreach $found (split(' ', $VARIABLES{"\@HTTPFOUND"})) { if ($res eq $found) { $result = TRUE; } } } return $result; } ####################################################################### sub setup_hash { my ($reqhash, $mark, $testid) = @_; # Do the standard set up for the hash LW2::http_init_request($reqhash); $reqhash->{'whisker'}->{'ssl_save_info'} = 1; $reqhash->{'whisker'}->{'lowercase_incoming_headers'} = 1; $reqhash->{'whisker'}->{'timeout'} = $NIKTO{'timeout'}; if (defined $CLI{'evasion'}) { $reqhash->{'whisker'}->{'encode_anti_ids'} = $CLI{'evasion'}; } $reqhash->{'User-Agent'} = $NIKTO{'useragent'}; $reqhash->{'User-Agent'} =~ s/\@TESTID/$testid/; $reqhash->{'whisker'}->{'retry'} = 0; $reqhash->{'whisker'}->{'host'} = $mark->{'hostname'} || $mark->{'ip'}; if ($mark->{'vhost'}) { $request{'Host'} = $mark->{'vhost'}; } $reqhash->{'whisker'}->{'port'} = $mark->{'port'}; $reqhash->{'whisker'}->{'ssl'} = $mark->{'ssl'}; # Proxy stuff if (defined $NIKTOCONFIG{PROXYHOST} && defined $CLI{'useproxy'}) { $reqhash->{'whisker'}->{'proxy_host'} = $NIKTOCONFIG{'PROXYHOST'}; $reqhash->{'whisker'}->{'proxy_port'} = $NIKTOCONFIG{'PROXYPORT'}; if ($NIKTOCONFIG{'PROXYUSER'} ne '') { LW2::auth_set("proxy-basic", $reqhash, $NIKTOCONFIG{'PROXYUSER'}, $NIKTOCONFIG{'PROXYPASS'}); } } return $reqhash; } ####################################################################### sub cache_add { my $method = shift; my $code = shift; my $content = shift; my $uri = shift; my $postdata = shift; my $flags_nocache = shift; my ($mark) = @_; if ((!defined $CLI{'nocache'}) && (!$flags_nocache)) { my $key = $mark->{'ip'} . $mark->{'hostname'} . $mark->{'port'} . $mark->{'ssl'} . $method . $uri . $postdata; $CACHE{$key}{'method'} = $method; $CACHE{$key}{'code'} = $code; $CACHE{$key}{'content'} = $content; } } ####################################################################### sub cache_fetch { my $method = shift; my $uri = shift; my $postdata = shift; my $flags_nocache = shift; my ($mark) = @_; if ((!defined $CLI{'nocache'}) && (!$flags_nocache)) { my $key = LW2::md5( $mark->{'ip'} . $mark->{'hostname'} . $mark->{'port'} . $mark->{'ssl'} . $method . $uri . $postdata); if ($CACHE{$key}{'code'} ne '') { return (1, $CACHE{$key}{'code'}, $CACHE{$key}{'content'}); } else { return 0; } } return 0; } ####################################################################### sub nfetch { my ($mark, $uri, $method, $data, $headers, $flags, $testid) = @_; if ($CLI{'pause'} > 0) { sleep $CLI{'pause'}; } my (%request, %result); setup_hash(\%request, $mark, $testid); # check for keyboard input if (($NIKTO{'totalrequests'} % 10) == 0) { check_input(); } if (defined $CLI{'root'}) { $request{'whisker'}->{'uri'} = $CLI{'root'} . $uri; # prepend -root option value } else { $request{'whisker'}->{'uri'} = $uri; } $request{'whisker'}->{'method'} = $method; if ($data ne "") { $data =~ s/\\\"/\"/g; $request{'whisker'}->{'data'} = $data; } # check for extra HTTP headers if (defined $headers) { # loop through the hash ref passed and add each header to request while (my ($key, $value) = each(%$headers)) { $request{$key} = $value; } } LW2::http_fixup_request(\%request) unless ($flags->{'noclean'}); # Run pre hooks unless ($flags->{'noprefetch'}) { (%$request, %$result) = run_hooks($mark, "prefetch", \%request, \%result); } # Check cache my $incache = 0; nprint("- Checking $uri in cache.", "d"); ($incache, my $code, my $content) = cache_fetch($request{'whisker'}->{'method'}, $uri, $data, $flags->{'nocache'}, $mark); if ($incache) { $result{'whisker'}->{'code'} = $code; $result{'whisker'}->{'data'} = $content; } if (!$incache) { LW2::http_do_request_timeout(\%request, \%result); $NIKTO{'totalrequests'}++; cache_add($request{'whisker'}->{'method'}, $result{'whisker'}->{'code'}, $result{'whisker'}->{'data'}, $uri, $data, $flags->{'nocache'}, $mark); if ($OUTPUT{'debug'}) { dump_var("Request Hash", \%request); dump_var("Result Hash", \%result); } # Snarf what we can from the whisker hash and put in mark if (!exists $result{'whisker'}->{'error'}) { if (!exists $mark->{'banner'}) { $mark->{'banner'} = $result{'server'}; } else { # Check banner hasn't changed if ( exists $result{'server'} && $mark->{'banner'} ne $result{'server'} && !exists $mark->{'bannerchanged'}) { nprint( "+ Server banner has changed from $mark->{banner} to $result{server}, this may suggest a WAF is in place" ); $mark->{'bannerchanged'} = 1; } } if (!exists $mark->{'ssl_cipher'} && $mark->{'ssl'}) { # Grab ssl details $mark->{'ssl_cipher'} = $result{'whisker'}->{'ssl_cipher'}; $mark->{'ssl_cert_issuer'} = $result{'whisker'}->{'ssl_cert_issuer'}; $mark->{'ssl_cert_subject'} = $result{'whisker'}->{'ssl_cert_subject'}; } } } nprint("- $result{'whisker'}{'code'} for $method:\t$uri", "v"); # Check for errors to reduce false positives if (defined $result{'whisker'}->{'error'} && !exists $flags->{'noerror'}) { $mark->{'total_errors'}++; nprint("+ ERROR: $uri returned an error: $result{'whisker'}{'error'}\n", "e"); if (($result{'whisker'}->{'code'} eq 502) && ($CLI{'useproxy'})) { nprint("+ ERROR: Revieved 502 'Bad Gateway' from proxy\n"); } } if ($OUTPUT{'show_cookies'} && (defined($result{'whisker'}->{'cookies'}))) { foreach my $c (@{ $result{'whisker'}->{'cookies'} }) { nprint("+ $uri sent cookie: $c"); } } # If headers is defined, copy the whisker headers to the hash if (defined $headers) { # First clear the hash foreach my $header (keys %$headers) { delete($headers->{$header}); } while (my ($key, $value) = each(%result)) { if ($key ne "whisker" && $key ne "connection") { $headers->{$key} = $value; } } } # Run post hooks unless ($flags->{'nopostfetch'}) { (%$request, %$result) = run_hooks($mark, "postfetch", \%request, \%result); } return $result{'whisker'}->{'code'}, $result{'whisker'}->{'data'}, $result{'whisker'}->{'error'}; } ####################################################################### sub set_scan_items { # load the tests %TESTS = (); my @SKIPLIST = (); if (defined $NIKTOCONFIG{SKIPIDS}) { @SKIPLIST = split(/ /, $NIKTOCONFIG{SKIPIDS}); } # now load checks foreach my $line (@DBFILE) { if ($line =~ /^\"/) # check { chomp($line); my @item = parse_csv($line); my $add = 1; # check tuning options if ((defined $CLI{'tuning'}) && (defined $item[2])) { # Work out the required tuning from the CLI string my $exclude = 0; foreach my $tune (split(//, $CLI{'tuning'})) { if ($tune eq "x") { $exclude = 1; next; } if ($exclude == 0) { if ($item[2] !~ /$tune/) { $add = 0; } next; } if ($exclude == 1) { if ($item[2] =~ /$tune/) { $add = 0; } } } } # Skip list foreach my $id (@SKIPLIST) { if ($id eq $item[0]) { $add = 0; } } # RFI URL Defined? if (($item[2] =~ /c/) && ($VARIABLES{'@RFIURL'} eq '')) { $add = 0; } if ($add) { my $ext = get_ext($item[3]); $db_extensions{$ext} = 1; # Escape chars in the conditionals. This must change if regexs are allowed in the db. for (my $y = 5 ; $y <= 9 ; $y++) { $item[$y] = quotemeta($item[$y]); } $NIKTO{total_checks}++; $TESTS{ $item[0] }{'uri'} = $item[3]; $TESTS{ $item[0] }{'osvdb'} = $item[1]; $TESTS{ $item[0] }{'method'} = $item[4]; $TESTS{ $item[0] }{'match_1'} = $item[5]; $TESTS{ $item[0] }{'match_1_or'} = $item[6]; $TESTS{ $item[0] }{'match_1_and'} = $item[7]; $TESTS{ $item[0] }{'fail_1'} = $item[8]; $TESTS{ $item[0] }{'fail_2'} = $item[9]; $TESTS{ $item[0] }{'message'} = $item[10]; $TESTS{ $item[0] }{'data'} = $item[11]; $TESTS{ $item[0] }{'headers'} = $item[12]; } } } undef @DBFILE; # this memory hog is no longer needed! nprint("- $NIKTO{'total_checks'} server checks loaded", "v"); if ($NIKTO{'total_checks'} eq 0 && !defined $CLI{'tuning'}) { nprint("+ Unable to load valid checks!"); exit; } return; } ####################################################################### sub max_test_id { return (sort { $a <=> $b } keys %TESTS)[-1]; } ####################################################################### 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 version { my @NIKTOFILES = dirlist($NIKTOCONFIG{PLUGINDIR}, "(^nikto|^db_)"); nprint($NIKTO{'DIV'}); nprint("$NIKTO{'name'} Versions"); nprint($NIKTO{'DIV'}); nprint("File Version Last Mod"); nprint("----------------------------- -------- ----------"); nprint("Nikto main $NIKTO{'version'}"); nprint("LibWhisker $LW2::VERSION"); foreach my $FILE (sort @NIKTOFILES) { open(FI, "<$NIKTOCONFIG{PLUGINDIR}/$FILE") || die print STDERR "+ ERROR: Unable to open '$NIKTOCONFIG{PLUGINDIR}/$FILE': $!\n"; my @F = ; close(FI); my @VERS = grep(/^#VERSION/, @F); my @MODS = grep(/^# \$Id:/, @F); chomp($VERS[0]); chomp($MODS[0]); my @modification = split(/ /, $MODS[0]); $VERS[0] =~ s/^#VERSION,//; my $ws1 = (35 - length($FILE)); my $ws2 = (13 - length($VERS[0])); nprint("$FILE" . " " x $ws1 . "$VERS[0]" . " " x $ws2 . "$modification[4]"); } nprint($NIKTO{'DIV'}); # Check dependencies eval "require RPC::XML"; if ($@) { nprint("Module RPC::XML missing. Logging to Metasploit is disabled."); } eval "require RPC::XML::Client"; if ($@) { nprint("Module RPC::XML::Client missing. Logging to Metasploit is disabled."); } nprint($NIKTO{'DIV'}); exit; } ####################################################################### sub send_updates { return if ($NIKTOCONFIG{'UPDATES'} !~ /yes|auto/i); my $have_updates = 0; my ($updated_version, $answer, $RES); foreach my $ver (keys %UPDATES) { # ignore useless ones... if ($ver !~ /[0-9]/) { next; } elsif ($ver eq "Win32") { next; } elsif ($ver eq "(Win32)") { next; } elsif ($ver eq "Linux-Mandrake") { next; } $have_updates = 1; $updated_version .= "$ver "; } if ((!$have_updates) || ($updated_version eq "")) { return; } # make sure the db_outdatedb isn't *too* old open(OD, "<$NIKTOCONFIG{PLUGINDIR}/db_outdated") || die print STDERR "+ ERROR: Unable to open '$NIKTOCONFIG{PLUGINDIR}/db_outdated': $!\n"; @F = ; close(OD); my @LASTUPDATED = grep(/^\# \$Id: db_outdated/, @F); $LASTUPDATED[0] =~ /([0-9]{4}\-[0-9]{2})/; $lm = $1; $lm =~ s/\-//g; my @NOW = localtime(time); $NOW[5] += 1900; $NOW[4]++; if ($NOW[4] < 10) { $NOW[4] = "0$NOW[4]"; } my $now = "$NOW[5]$NOW[4]"; if (($now - $lm) > 120) { return; } # DB is 4 months old... ignore the updates! $updated_version =~ s/\s+$//; $updated_version =~ s/^\s+//; if ($NIKTOCONFIG{'UPDATES'} eq "auto") { $answer = "y"; } else { $answer = read_data( "\n ********************************************************************* Portions of the server's ident string ($updated_version) are not in the Nikto database or is 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' => 'www.cirt.net', 'ssl' => 0, 'port' => 80 ); for (my $i = 0 ; $i <= $#ARGV ; $i++) { if (($ARGV[$i] eq "-u") || ($ARGV[$i] eq "-useproxy")) { $CLI{'useproxy'} = 1; last; } } ($mark{'hostname'}, $mark{'ip'}, $mark{'display_name'}) = resolve('cirt.net'); ($RES, $CONTENT) = nfetch(\%mark, "/cgi-bin/versions?DATA=$updated_version", "GET"); if ($RES eq 407) { if ($NIKTOCONFIG{PROXYUSER} eq "") { $NIKTOCONFIG{PROXYUSER} = read_data("Proxy ID: ", ""); $NIKTOCONFIG{PROXYPASS} = read_data("Proxy Pass: ", "noecho"); } ($RES, $CONTENT) = nfetch(\%mark, "/cgi-bin/versions?DATA=$updated_version", "GET"); } if ($RES eq "") { LW2::http_close(\%request); # force-close any old connections $mark{'ip'} = $NIKTOCONFIG{CIRT}; ($RES, $CONTENT) = nfetch(\%mark, "/cgi-bin/versions?DATA=$updated_version", "GET"); } if ($CONTENT !~ /SUCCESS/) { nprint("+ ERROR: ($RES, $CONTENT): Unable to send update info to cirt.net"); } else { nprint("- Sent updated info to CIRT.net -- Thank you!"); } return; } ####################################################################### sub usage { if ($_[0] eq 2) { print " Options: -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 -config+ Use this config file -Cgidirs+ Scan these CGI dirs: \"none\", \"all\", or values like \"/cgi/ /cgi-a/\" -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 V Verbose output -dbcheck Check database and other key files for syntax errors (cannot be abbreviated) -evasion+ IDS evasion technique:\n"; foreach my $k (sort keys %{ $NIKTO{'anti_ids'} }) { print " $k $NIKTO{'anti_ids'}{$k}\n"; } print " -findonly Find http(s) ports only, don't perform a full scan -Format+ Save file (-o) format: csv Comma-separated-value htm HTML Format msf+ Log to Metasploit nbe Nessus NBE format txt Plain text (default if not specified) xml XML Format -host+ Target host -Help Extended help information -id+ Host authentication to use, format is userid:password -list-plugins List all available plugins, perform no testing -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 -nocache Disables the URI cache -nossl Disables using SSL -no404 Disables nikto attempting to guess a 404 page -output+ Write output to this file -Plugins+ List of plugins to run (default: ALL) -port+ Port to use (default 80) -Pause+ Pause between tests (seconds) -root+ Prepend root value to all requests, format is /directory -ssl Force ssl mode on port -Single Single request mode -timeout+ Timeout (default 2 seconds) -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 x Reverse Tuning Options (i.e., include all except specified) -useproxy Use the proxy defined in nikto.conf -update Update databases and plugins from cirt.net (cannot be abbreviated) -Version Print plugin and database versions -vhost+ Virtual host (for Host header) + requires a value "; } else { print " -config+ Use this config file -Cgidirs+ scan these CGI dirs: 'none', 'all', or values like \"/cgi/ /cgi-a/\" -dbcheck check database and other key files for syntax errors (cannot be abbreviated) -evasion+ ids evasion technique -Format+ save file (-o) format -host+ target host -Help Extended help information -id+ host authentication to use, format is userid:password -list-plugins List all available plugins -mutate+ Guess additional file names -mutate-options+ Provide extra information for mutations -output+ Write output to this file -nocache Disables the URI cache -nossl Disables using SSL -no404 Disables 404 checks -Plugins+ List of plugins to run (default: ALL) -port+ Port to use (default 80) -root+ Prepend root value to all requests, format is /directory -Display+ Turn on/off display outputs -ssl Force ssl mode on port -Single Single request mode -timeout+ Timeout (default 2 seconds) -Tuning+ Scan tuning -update Update databases and plugins from cirt.net (cannot be abbreviated) -Version Print plugin and database versions -vhost+ Virtual host (for Host header) + requires a value "; } exit; } ####################################################################### sub init_db { my $dbname = $_[0]; my $filename = "$NIKTOCONFIG{PLUGINDIR}/" . $dbname; my (@dbarray, @headers); my $hashref = {}; # 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 () { 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); } } return \@dbarray; } ####################################################################### sub add_vulnerability { my ($mark, $message, $nikto_id, $osvdb, $method, $uri) = @_; $uri = "/" unless (defined $uri); $method = "GET" unless (defined $method); $osvdb = "0" unless (defined $osvdb); my $result = ""; if (defined $_[7]) { $result = $_[7]->{'whisker'}->{'data'}; } my $outmessage = $message; my $resulthash = {}; unless ($osvdb eq "0") { $outmessage = "OSVDB-$osvdb: $message"; } nprint("+ $outmessage"); %$resulthash = (mark => $mark, message => $message, nikto_id => $nikto_id, osvdb => $osvdb, method => $method, uri => $uri, result => $result, ); $mark->{total_vulns}++; push(@RESULTS, $resulthash); # Now report it report_item($mark, $resulthash); } ############################################################################### 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'}"); push(@all_names, $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 %NIKTOCONFIG) { if ($macro =~ /^@@/) { nprint(" $macro = \"" . $NIKTOCONFIG{$macro} . "\""); if ($NIKTOCONFIG{$macro} =~ /@@/) { nprint(" (expanded) = \"" . expand_pluginlist($NIKTOCONFIG{$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 %NIKTOCONFIG) { 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(/;/, $NIKTOCONFIG{$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 = ""; } #nprint("P:$plugin M:$minus N:$name S:$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; } ####################################################################### sub nikto_core { return; } # trap for this plugin being called to run. lame. ####################################################################### 1;