#VERSION,2.1.1 # $Id: nikto_core.plugin 295 2010-01-31 05:39:19Z sullo $ ############################################################################### # 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) = @_; $ext=get_ext($uri); my $pagenf=0; if (($FoF{$ext}{'mode'} eq "STD") && ($rescode =~ /4[0-9][0-9]/)) { $pagenf=1; } elsif ($FoF{$ext}{'mode'} eq "REDIR") { if ($result{'location'} eq $FoF{$ext}{'location'}) { $pagenf=1; } } elsif (($FoF{$ext}{'type'} eq "CONTENT") && ($content =~ /$FoF{$ext}{'match'}/i)) { $pagenf=1; } elsif (($FoF{$ext}{'type'} eq "BLANK") && ($content eq "")) { $pagenf=1; } elsif ($FoF{$ext}{'type'} eq "HASH") { my $content=rm_active_content($content, $uri); if (LW2::md4($content) eq $FoF{$ext}{'match'}) { $pagenf=1; } } return $pagenf; } ############################################################################### sub nprint { my $line=$_[0]; chomp($line); # don't print debug & verbose to output file... if (defined $_[1]) { if ($_[1] eq "d" && $OUTPUT{'debug'}) { print "D:" . localtime() . " $line\n"; } if ($_[1] eq "v" && $OUTPUT{'verbose'}) { print "V:" . localtime() . " $line\n"; } return; } # print errors to STDERR if ($line =~ /^\+ ERROR\:/) { print STDERR "$line\n"; return; } # don't print to STDOUT if output file is "-" if ((defined $CLI{'file'}) && ($CLI{'file'} eq "-")) { return; } # print to scan details to standard output if the users wants another format and is saving results to a file $line =~ s/((CVE|CAN)\-[0-9]{4}-[0-9]{4})/http:\/\/cve.mitre.org\/cgi-bin\/cvename.cgi?name\=$1/g; $line =~ s/(CA\-[0-9]{4}-[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/(IN\-[0-9]{4}\-[0-9]{2})/http:\/\/www.cert.org\/incident_notes\/$1.html/gi; $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/^.*\///; $uri =~ s/(\?|\&|\%).*$//; if ($uri =~ /^\.[^.%]/) { return "DOTFILE"; } if ($uri !~ /\./) { return "NONE"; } $uri =~ s/\".*$//; $uri =~ s/^.*\.//; return $uri; } ############################################################################### sub date_disp { my @time=localtime($_[0]); $time[5]+=1900; $time[4]++; if (length($time[4]) eq 1) { $time[4]="0$time[4]"; } $time[3]++; if (length($time[3]) eq 1) { $time[3]="0$time[3]"; } if (length($time[0]) eq 1) { $time[0]="0$time[0]"; } if (length($time[1]) eq 1) { $time[1]="0$time[1]"; } if (length($time[2]) eq 1) { $time[0]="0$time[2]"; } return "$time[5]-$time[4]-$time[3] $time[2]:$time[1]:$time[0]"; } ############################################################################### sub map_codes { my %REQS; my $rs=LW2::utils_randstr(8); # / for OK response $NIKTO{'totalrequests'}++; $request{'whisker'}->{'uri'} = "/"; delete $request{'whisker'}->{'data'}; $request{'whisker'}->{'method'} = GET; delete $request{'whisker'}->{'Content-Length'}; LW2::http_close(\%request); # force-close any old connections dump_var("Request Hash", \%request); if ($CLI{'pause'} > 0) { sleep $CLI{'pause'}; } LW2::http_do_request_timeout(\%request,\%result); $NIKTO{'totalrequests'}++; dump_var("Result Hash", \%result); if (defined $result{'location'}) { nprint("- Root page / redirects to: $result{'location'}"); if ($result{'location'} =~ /^$request{'whisker'}{'host'}/i) # same host { $request{'whisker'}->{'uri'} = $result{'location'}; $request{'whisker'}->{'uri'} =~ s/^http(s)?\:\/\/$request{'whisker'}{'host'}//i; LW2::http_close(\%request); # force-close any old connections LW2::http_fixup_request(\%request); dump_var("Request Hash", \%request); if ($CLI{'pause'} > 0) { sleep $CLI{'pause'}; } LW2::http_do_request_timeout(\%request,\%result); $NIKTO{'totalrequests'}++; dump_var("Result Hash", \%result); } else # different host... ugh... guess { $FoF{'okay'}{'response'}=200; $FoF{'okay'}{'type'}="STD"; } } else { $FoF{'okay'}{'response'}=$result{'whisker'}->{'code'}; my $content=rm_active_content($result{'whisker'}->{'data'}); $FoF{'okay'}{'type'}="HASH"; $FoF{'okay'}{'match'}=LW2::md4($content); } # 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 eq "DIRECTORY") { next; } # don't test generic type holder as real extension (added to db_extensions by get_ext) if ($ext eq "NONE") { next; } # don't test generic type holder as real extension (added to db_extensions by get_ext) if ($ext eq "DOTFILE") { next; } # don't test generic type holder as real extension (added to db_extensions by get_ext) $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"); $NIKTO{'totalrequests'}++; $request{'whisker'}->{'uri'} = $file; $request{'whisker'}->{'method'} = GET; LW2::http_close(\%request); # force-close any old connections delete $request{'whisker'}->{'data'}; delete $request{'Content-Encoding'}; delete $request{'Content-Length'}; LW2::http_fixup_request(\%request); dump_var("Request Hash", \%request); if ($CLI{'pause'} > 0) { sleep $CLI{'pause'}; } LW2::http_do_request_timeout(\%request,\%result); $NIKTO{'totalrequests'}++; dump_var("Result Hash", \%result); $ext=$REQS{$file}; $FoF{$ext}{'response'} = $result{'whisker'}->{'code'}; # handle .com to .org redirs or whatnot if (defined $result{'location'}) { $FoF{$ext}{'location'} = $result{'location'}; $file=char_escape($file); $FoF{$ext}{'location'} =~ s/$file//; } # 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 my $done=0; foreach my $string (keys %ERRSTRINGS) { nprint ("- Testing error string: $ERRSTRINGS{$string}","d"); if ($result{'whisker'}->{'data'} =~ /$ERRSTRINGS{$string}/i) { $FoF{$ext}{'type'}="CONTENT"; $FoF{$ext}{'match'}=$ERRSTRINGS{$string}; $done=1; last; } } if (!$done) # we have to get desperate... { if (length($result{'whisker'}->{'data'}) eq 0) # blank content { $FoF{$ext}{'type'}="BLANK"; $FoF{$ext}{'match'}=""; $done=1; } if (!$done) # md4! { my $content=rm_active_content($result{'whisker'}->{'data'}); $FoF{$ext}{'match'}=LW2::md4($content); $FoF{$ext}{'type'}="HASH"; } } } # lastly, get a hash of index.php so we can cut down on some false positives... $NIKTO{'totalrequests'}++; $request{'whisker'}->{'uri'} = "/index.php?"; $request{'whisker'}->{'method'} = GET; LW2::http_close(\%request); # force-close any old connections delete $request{'whisker'}->{'data'}; delete $request{'Content-Encoding'}; delete $request{'Content-Length'}; LW2::http_fixup_request(\%request); dump_var("Request Hash", \%request); if ($CLI{'pause'} > 0) { sleep $CLI{'pause'}; } LW2::http_do_request_timeout(\%request,\%result); $NIKTO{'totalrequests'}++; dump_var("Result Hash", \%result); my $content=rm_active_content($result{'whisker'}->{'data'}); $FoF{"index.php"}{'match'}=LW2::md4($content); $FoF{"index.php"}{'type'}="HASH"; # foreach $ext (keys %FoF) { nprint "$ext: mode $FoF{$ext}{'mode'}, response $FoF{$ext}{'response'}, type $FoF{$ext}{'type'}\n"; } return; } ############################################################################### sub rm_active_content { # Try to remove active content which could mess up the file's signature my $cont=$_[0]; # Dates $cont =~ s/([0-9]{4}|[0-9]{1,2})(\-|\.|\/)[0-9]{1,2}(\-|\.|\/)([0-9]{4}|[0-9]{1,2})//g; $cont =~ s/(([0-9]{2}:[0-9]{2}(:)?([0-9]{2})?)|([0-9]{8,14}|[0-9]{6}))//g; $cont =~ s/(mon|tue|wed|thu|fri|sat|sun)(,)? [0-9]{1,2} (jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec) [0-9]{4} ([0-9]{2}:[0-9]{2}(:)?([0-9]{2})?)?//ig; $cont =~ s/([0-9]{2,4})? ?(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)([0-9]{2,4})?(\/)?([0-9]{2})?([0-9]{2})?//gi; # Page load times $cont =~ s/[0-9\.]+ second//gi; $cont =~ s/[0-9]+ queries//gi; # wordpress # Advertising # URI, if provided, plus encoded versions of it if (defined $_[1]) { # match pages which link to themselves w/diff args my $e = $_[1]; $e =~ s/^\/$_[1]\??//; $e =~ s/([^a-zA-Z0-9\s])/\\$1/g; # escaping $cont =~ s/$e//gs; # again but with the index in place $e = $_[1]; $e =~ s/([^a-zA-Z0-9\s])/\\$1/g; # escaping $cont =~ s/$e//gs; # base 64 $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 ($_[1] =~ /\?/) { $e = $_[1]; $e =~ s/\?(.*$)//; my $qs = $1; $qs =~ s/([^A-Za-z0-9])/sprintf("%%%02X", ord($1))/seg; $e .= "?$qs"; $cont =~ s/$e//gs; } } return $cont; } ############################################################################### sub dump_target_info { my ($mark) = @_; my $sslprint=""; if ($mark->{ssl}) { my $sslcyphers=$result{whisker}->{ssl_cipher} || "Unknown"; my $sslissuers=$result{whisker}->{ssl_cert_issuer} || "Unknown"; my $sslinfo=$result{whisker}->{ssl_cert_subject} || "Unknown"; $sslprint="$NIKTO{'DIV'}\n"; $sslprint.="+ SSL Info: Ciphers: $sslcyphers\n Info: $sslissuers\n Subject: $sslinfo"; $mark->{sslcyphers}=$sslcyphers; $mark->{sslissuers}=$sslissuers; $mark->{sslinfo}=$sslinfo; } if ($mark->{ip} =~ /[a-z]/i) { nprint("+ Target IP: (proxied)"); } else { nprint("+ Target IP: $mark->{ip}"); } nprint("+ Target Hostname: $mark->{hostname}"); nprint("+ Target Port: $mark->{port}"); if ((defined $CLI{'vhost'}) && ($CLI{'vhost'} ne $mark->{hostname})) { nprint("+ Virtual Host: $CLI{'vhost'}"); } 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; $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"; $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"; $NIKTO{'display'}{1}="Show redirects"; $NIKTO{'display'}{2}="Show cookies received"; $NIKTO{'display'}{3}="Show all 200/OK responses"; $NIKTO{'display'}{4}="Show URLs which require authentication"; $NIKTO{'display'}{V}="Verbose Output"; $NIKTO{'display'}{D}="Debug Output"; $NIKTO{'tuning'}{1}="Interesting File / Seen in logs"; $NIKTO{'tuning'}{2}="Misconfiguration / Default File"; $NIKTO{'tuning'}{3}="Information Disclosure"; $NIKTO{'tuning'}{4}="Injection (XSS/Script/HTML)"; $NIKTO{'tuning'}{5}="Remote File Retrieval - Inside Web Root"; $NIKTO{'tuning'}{6}="Denial of Service"; $NIKTO{'tuning'}{7}="Remote File Retrieval - Server Wide"; $NIKTO{'tuning'}{8}="Command Execution / Remote Shell"; $NIKTO{'tuning'}{9}="SQL Injection"; $NIKTO{'tuning'}{0}="File Upload"; $NIKTO{'tuning'}{a}="Authentication Bypass"; $NIKTO{'tuning'}{b}="Software Identification"; $NIKTO{'tuning'}{c}="Remote Source Inclusion"; $NIKTO{'tuning'}{x}="Reverse Tuning Options (i.e., include all except specified)"; $NIKTO{'options_short'}= " -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) -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 "; $NIKTO{'options'}=" Options: -config+ Use this config file -Cgidirs+ Scan these CGI dirs: 'none', 'all', or values like \"/cgi/ /cgi-a/\" -Display+ Turn on/off display outputs:\n"; foreach my $k (sort keys %{$NIKTO{'display'}}) { $NIKTO{'options'} .= " $k $NIKTO{'display'}{$k}\n"; } $NIKTO{'options'}.=" -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'}}) { $NIKTO{'options'} .= " $k $NIKTO{'anti_ids'}{$k}\n"; } $NIKTO{'options'}.=" -findonly Find http(s) ports only, don't perform a full scan -Format+ Save file (-o) format: htm HTML Format csv Comma-separated-value 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'}}) { $NIKTO{'options'} .= " $k $NIKTO{'mutate_opts'}{$k}\n"; } $NIKTO{'options'}.=" -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)\n"; $NIKTO{'options'}.=" -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:\n"; foreach my $k (sort keys %{$NIKTO{'tuning'}}) { $NIKTO{'options'} .= " $k $NIKTO{'tuning'}{$k}\n"; } $NIKTO{'options'}.=" -useproxy Use the proxy defined in config.txt -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 "; ### CLI STUFF $CLI{'pause'}=$CLI{'html'}=$OUTPUT{'verbose'}=$CLI{'skiplookup'}=$NIKTO{'totalrequests'}=$OUTPUT{debug}=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'}, ) 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'} =~ /xml/i) { $CLI{'format'}="xml"; } 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; } } # 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'}="ALL"; } $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 ($NIKTOCONFIG{'RFIURL'} ne '') { $VARIABLES{'@RFIURL'} = $NIKTOCONFIG{'RFIURL'}; } # SSL Test if (!LW2::ssl_is_available()) { nprint("- ***** SSL support not available (see docs for SSL install instructions) *****"); } # 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; return; } ############################################################################### sub resolve { my $ident=$_[0] || return; my ($name, $ip, $dn)=""; # ident is name, lookup IP if ($ident =~ /[^0-9\.]/) # not an IP, assume name { if ($CLI{'skiplookup'}) { nprint("+ ERROR: -skiplookup set, but given name\n"); exit; } $ip=gethostbyname($ident); if (($ip eq "") && ($request{'whisker'}->{'proxy_host'} ne "")) # can't resolve name to IP, but using proxy { $name=$ident; $ip=$name; } elsif (($ip eq "") && ($request{'whisker'}->{'proxy_host'} eq "")) # can't resolve name to IP, no proxy set { 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}$/) && ($ip ne "")) # trap for proxy... { 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}$/) && ($ident ne "")) # trap for proxy... { 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; my @results; my @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"); 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"); push(@marks,$markhash); } } return @marks; } sub parse_hostfile { my ($file)=@_; my $nmap=0; my @results; my $hostdesc; 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 == 1) { # 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_realms db_tests db_variables/; my $prefix = $_[0]; unless (defined $prefix) { $prefix=""; } # 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') { my $db_404_ctr=keys %ERRSTRINGS; foreach my $l (@file) { $ERRSTRINGS{$db_404_ctr} = $l; $db_404_ctr++; } } # 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]; } } # db_realms elsif ($file eq 'db_realms') { my $db_realms_ctr= keys %REALMS; foreach my $l (@file) { my @t=parse_csv($l); $REALMS{$db_realms_ctr}{tid} = $t[0]; $REALMS{$db_realms_ctr}{realm} = $t[1]; $REALMS{$db_realms_ctr}{id} = $t[2]; $REALMS{$db_realms_ctr}{pw} = $t[3]; $REALMS{$db_realms_ctr}{msg} = $t[4]; $db_realms_ctr++; } # Cheat and add CLI directly to REALMS if (defined $CLI{'hostauth'}) { my @x=split(/:/,$CLI{'hostauth'}); $REALMS{$db_realms_ctr}{tid} = "700500"; $REALMS{$db_realms_ctr}{realm} = (defined $x[2])?$x[2]:'@ANY'; $REALMS{$db_realms_ctr}{pw} = $x[1]; $REALMS{$db_realms_ctr}{id} = $x[0]; $REALMS{$db_realms_ctr}{msg} = "Credentials provided by CLI."; } } 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/; my $prefix = $_[0]; if ($prefix eq "" ) { nprint "\n-->\tNikto Databases\n"; } if ($prefix eq "u" ) { nprint "\n-->\tUser Databases\n"; } 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\n"; 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) { print STDERR "\t+ ERROR: Invalid syntax ($#L): $line\n"; next; } $ENTRIES{"$L[0]"}++; } foreach $entry (keys %ENTRIES) { if ($ENTRIES{$entry} > 1) { print STDERR "\t+ ERROR: Duplicate ($ENTRIES{$entry}): $entry\n"; } } nprint "\t" . keys(%ENTRIES) . " entries\n"; } 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) { print STDERR "\t+ ERROR: Possibly invalid method: $L[4] on ($line)\n"; } if ($L[5] eq "") { print STDERR "\t+ ERROR: blank conditional: $line"; next; } if ($line !~ /^\".*\",\".*\",\".*\",\".*\",\".*\"/) { print STDERR "\t+ ERROR: Invalid syntax ($#L): $line\n"; next; } if ($line !~ /^(\".*\",){11}\".*\"/) { print STDERR "\t+ ERROR: Invalid syntax ($#L): $line\n"; next; } if (($L[3] =~ /^\@CG/) && ($L[3] !~ /^\@CGIDIRS/)) { print STDERR "\t+ ERROR: Possible \@CGIDIRS misspelling: $line"; } if ($L[1] =~ /[^0-9]/) { print STDERR "\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 '')) { print STDERR "\t+ ERROR: Invalid syntax: $line\n"; } } foreach $entry (keys %ENTRIES) { if ($ENTRIES{$entry} > 1) { print STDERR "\t+ ERROR: Duplicate ($ENTRIES{$entry}): $entry\n"; } } nprint "\t" . keys(%ENTRIES) . " entries\n"; } elsif ($file eq 'db_variables') { my $ctr=0; foreach $line () { if ($line !~ /^\@/) { next; } if ($line !~ /^\@.+\=.+$/i ) { print STDERR "\t+ ERROR: Invalid syntax: $line\n"; } $ctr++; } nprint "\t$ctr entries\n"; } elsif ($file eq 'db_realms') { my $ctr=0; foreach $line () { if ((count_fields($line, 1) ne 4) && (count_fields($line) ne '')) { print STDERR "\t+ ERROR: Invalid syntax: $line"; } $ctr++; } nprint "\t$ctr entries\n"; } elsif ($file eq 'db_404_strings') { my $ctr=0; foreach $line () { # not really any syntax to check $ctr++; } nprint "\t$ctr entries\n"; } elsif ($file eq 'db_headers') { my $ctr=0; foreach $line () { if ((count_fields($line) ne 0) && (count_fields($line) ne '')) { print STDERR "\t+ ERROR: Invalid syntax: $line"; } $ctr++; } nprint "\t$ctr entries\n"; } elsif ($file eq 'db_multiple_index') { my $ctr=0; foreach $line () { if ((count_fields($line) ne 0) && (count_fields($line) ne '')) { print STDERR "\t+ ERROR: Invalid syntax: $line"; } $ctr++; } nprint "\t$ctr entries\n"; } 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 '')) { print STDERR "\t+ ERROR: Invalid syntax: $line"; } $ctr++; } nprint "\t$ctr entries\n"; } 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 get_banner { my ($mark) = @_; my %headers; (my $res, $content) = nfetch($mark, "/", "GET", "", \%headers, "", "Get Banner"); return $headers{server}; } ############################################################################### sub port_check { my ($hostname, $ip, $port) = @_; my (%m, %headers); # 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->{ssl}=($checkssl eq "HTTP")?0:1; proxy_check($m); my ($res, $content)=nfetch($m, "/", $method, "", \%headers, "", "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"); return $m->{ssl}+1; } } } nprint("+ No web server found on $ip:$port"); nprint("---------------------------------------------------------------------------"); return 0; } ############################################################################### sub load_plugins { my @pluginlist=dirlist("$NIKTOCONFIG{PLUGINDIR}",'\.plugin$'); # Check if running plugins is NONE - if so, don't bother initalising # plugins if ($CLI{'plugins'} eq "NONE") { return; } my @torun=split(/,/,$CLI{'plugins'}); 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 $pluginhash->{recon_weight}=50 unless (defined $pluginhash->{recon_weight}); $pluginhash->{scan_weight}=50 unless (defined $pluginhash->{scan_weight}); $pluginhash->{report_weight}=50 unless (defined $pluginhash->{report_weight}); # Check that the plugin is to be run # Perl doesn't allow us to use "in", pity foreach my $torun_plugin (@torun) { if ($torun_plugin eq "ALL" || $pluginhash->{name} eq $torun_plugin) { $pluginhash->{run}="true"; } } push(@PLUGINS, $pluginhash); nprint("- Loaded \"$pluginhash->{full_name}\" plugin.","v"); } } } } ############################################################################### # this is ugly, and potentially dangerous if untrusted plugins are present ############################################################################### sub run_plugins { my ($mark) = @_; nprint("- Entering recon phase","v"); # 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->{recon_method} && $plugin->{recon_weight} == $i) { my $run=1; # first check for conditionals if (defined $plugin->{recon_cond}) { # Evaluate condition $run=eval($plugin->{recon_cond}); } if ($run) { nprint("- Running recon for \"$plugin->{full_name}\" plugin","v"); &{$plugin->{recon_method}}($mark); } } } } nprint("- Entering scan phase","v"); # 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->{scan_method} && $plugin->{scan_weight} == $i) { my $run=1; # first check for conditionals if (defined $plugin->{scan_cond}) { # Evaluate condition $run=eval "$plugin->{scan_cond}"; } if ($run) { nprint("- Running scan for \"$plugin->{full_name}\" plugin","v"); &{$plugin->{scan_method}}($mark); } } } } return; } sub report_head { my ($format,$file) = @_; nprint("- Opening reports","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'}"; my $server="www.cirt.net"; $request{'whisker'}->{'version'}="1.1"; $request{'whisker'}->{'port'}=80; $request{'whisker'}->{'anti_ids'}=""; $request{'User-Agent'}="Nikto Update Agent"; $request{'whisker'}->{'host'}=$server; for (my $i=0;$i<=$#ARGV;$i++) { if (($ARGV[$i] eq "-u") || ($ARGV[$i] eq "-useproxy")) { $CLI{'useproxy'}=1; last; } } if (($NIKTOCONFIG{PROXYHOST} ne "") && ($CLI{'useproxy'})) { $request{'whisker'}->{'proxy_host'}=$NIKTOCONFIG{PROXYHOST}; $request{'whisker'}->{'proxy_port'}=$NIKTOCONFIG{PROXYPORT}; } # retrieve versions file LW2::http_close(\%request); # force-close any old connections LW2::http_fixup_request(\%request); (my $RES, $CONTENT) = fetch("$serverdir/versions.txt","GET"); if ($RES eq 407) # requires Auth { if ($NIKTOCONFIG{PROXYUSER} eq "") { $NIKTOCONFIG{PROXYUSER}=read_data("Proxy ID: ",""); $NIKTOCONFIG{PROXYPASS}=read_data("Proxy Pass: ","noecho"); } LW2::auth_set("proxy-basic",\%request,$NIKTOCONFIG{PROXYUSER},$NIKTOCONFIG{PROXYPASS}); # and try again LW2::http_close(\%request); # force-close any old connections LW2::http_fixup_request(\%request); ($RES, $CONTENT) = fetch("$serverdir/versions.txt","GET"); } if ($RES eq "") # lookup failure? { LW2::http_close(\%request); # force-close any old connections $request{'whisker'}->{'host'}=$NIKTOCONFIG{CIRT}; $request{'Host'}="www.cirt.net"; LW2::http_fixup_request(\%request); ($RES, $CONTENT) = fetch("$serverdir/versions.txt","GET"); } if ($RES ne 200) { print STDERR "+ ERROR ($RES): Unable to get $request{'whisker'}->{'host'}$serverdir/versions.txt\n"; 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) { if ($remotefile eq "nikto") # main program version { if ($REMOTE{$remotefile} > $NIKTO{'version'}) { 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\n"; } exit; } next; } if (($LOCAL{$remotefile} eq "") || ($REMOTE{$remotefile} > $LOCAL{$remotefile})) { push(@DBTOGET,$remotefile); if ($remotefile !~ /^db_/) { $code_updates=1; } } elsif ($REMOTE{$remotefile} < $LOCAL{$remotefile}) # local is newer (!) { print STDERR "+ ERROR: Local '$remotefile' (ver $LOCAL{$remotefile}) is NEWER than remote (ver $REMOTE{$remotefile}).\n"; } } # replace local files if updated foreach my $toget (@DBTOGET) { nprint "+ Retrieving '$toget'\n"; (my $RES, $CONTENT) = fetch("$serverdir/$toget","GET"); if ($RES ne 200) { print STDERR "+ ERROR: Unable to get $server$serverdir/$toget\n"; 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'\n"; (my $RES, $CONTENT) = fetch("$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.\n"; } if ($remotemsg ne "") { nprint "+ $server message: $remotemsg\n"; } exit; } ############################################################################### # do_auth # Inputs: $request, $result # Returns: $request sub do_auth { my $request = $_[0]; my $result = $_[1]; my $mark = $_[2]; my $authtype = "basic"; my $realm = ""; unless (defined $result{'www-authenticate'}) { nprint("+ ERROR: No authentication header defined"); } # Split up www-authenticate to realm and method my @authenticate = split(/ /,$result{'www-authenticate'}); if ($#authenticate == 0) { # Only one parameter: realm $realm = $authenticate[0]; if ($realm =~ /^ntlm/i) { $realm=""; $authtype=$authenticate[0]; } } else { $authtype = $authenticate[0]; $realm = $authenticate[1]; $realm =~ s/^realm=//; } # Now we have this we can try guessing the password foreach my $REALM (keys %REALMS) { next unless ($realm =~ /$REALMS{$REALM}{realm}/i || $REALMS{$REALM}{realm} eq '@ANY'); if (($REALMS{$REALM}{id} eq "") && ($REALMS{$REALM}{pw} eq "")) { my $uridir=$request{whisker}->{uri}; $uridir =~ s#/[^/]*$#/#g; add_vulnerability($mark,"Blank credentials found at $uridir ($request{whisker}->{uri}), $REALMS{$REALM}{realm}: $REALMS{$REALM}{msg}", $REALMS{$REALM}{tid},0,"GET",$uridir,\%result); } else { my $save_auth=$result{'www-authenticate'}; LW2::http_close(\%request); # force-close any old connections LW2::auth_set($authtype,\%request,$REALMS{$REALM}{id},$REALMS{$REALM}{pw}); # Path to fix short reads $request{'whisker'}->{allow_short_reads}=1; LW2::http_fixup_request(\%request); if ($CLI{'pause'} > 0) { sleep $CLI{'pause'}; } LW2::http_do_request_timeout(\%request,\%result); # test auth $NIKTO{'totalrequests'}++; dump_var("Auth Request",\%request); dump_var("Auth Response",\%result); if ($result{'www-authenticate'} =~ /^ntlm/i) { # Deal with ntlm my @ntlm_x=split(/ /,$result{'www-authenticate'}); if ($#ntlm_x == 1) { LW2::http_do_request_timeout(\%request,\%result); $NIKTO{'totalrequests'}++; } } if ($result{'www-authenticate'} eq "" && !defined $result{'whisker'}->{'error'}) { unless ($REALMS{$REALM}{tid} == 700500 || $REALMS{$REALM}{checked} == 1) { my $uridir=$request{whisker}->{uri}; $uridir =~ s#/[^/]*$#/#g; add_vulnerability($mark,"Default account found for '$realm' at $uridir ($request{whisker}->{uri}) (ID '$REALMS{$REALM}{id}', PW '$REALMS{$REALM}{pw}'). $REALMS{$REALM}{msg}",$REALMS{$REALM}{tid},0,"GET",$uridir,\%result); $REALMS{$REALM}{checked}=1; } # Finally repeat the check LW2::http_do_request_timeout(\%request,\%result); # test auth $NIKTO{'totalrequests'}++; # and leave last; } else { $result->{'www-authenticate'}=$save_auth; } } } LW2::auth_unset(\%request); return $result; } ############################################################################### # read_data ( prompt, mode ) # read STDIN data from the user # portions of this (POSIX code) were taken from the # Term::ReadPassword module by Tom Phoenix (many thanks). # it has been modified to not require Term::ReadLine, but still requires # POSIX::Termios of it's a POSIX machine ############################################################################### sub read_data { if ($NIKTOCONFIG{PROMPTS} =~ /no/i) { return; } my($prompt, $mode, $POSIX) = @_; my $input = ""; if ($^O =~ /Win32/) { $POSIX=0; } else { $POSIX=1; } my %SPECIAL = ( "\x03" => 'INT', # Control-C, Interrupt "\x08" => 'DEL', # Backspace "\x7f" => 'DEL', # Delete "\x0d" => 'ENT', # CR, Enter "\x0a" => 'ENT', # LF, Enter ); # if we're on a non-POSIX machine we can't not-echo the # characters, so just use getc to avoid the dependency on # POSIX::Termios. We would be best to get rid of this # entirely and use another way... if ($POSIX) { 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_setup { if (!$CLI{'useproxy'}) { return; } # HTTP proxy $request{'whisker'}->{'proxy_host'}=$NIKTOCONFIG{PROXYHOST}; $request{'whisker'}->{'proxy_port'}=$NIKTOCONFIG{PROXYPORT}; return; } ############################################################################### 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}"); exit 1; } nprint("+ ERROR: Proxy error: $result{'whisker'}{'error'}", "v"); } 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'/; 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 fetch { if ($CLI{'pause'} > 0) { sleep $CLI{'pause'}; } LW2::http_close(\%request); # force-close any old connections $request{'whisker'}->{'uri'} = $_[0]; if (defined $CLI{'root'}) { $request{'whisker'}->{'uri'} = $CLI{'root'} . $_[0]; # prepend -root option value } # Set testid in UA my $temp_ua = $request{'User-Agent'}; my $testid = $_[3]; $request{'User-Agent'}=~s/\@TESTID/$testid/g; $request{'whisker'}->{'method'} = $_[1]; delete $request{'whisker'}->{'data'}; delete $request{'Content-Encoding'}; delete $request{'Content-Length'}; my $header_hash=$_[3]; if (defined $_[2]) { my $r=$_[2]; $r =~ s/\\\"/\"/g; $request{'whisker'}->{'data'} = $r; } # check for extra HTTP headers if (defined $_[3]) { # loop through the hash ref passed and add each header to request while (my($key, $value) = each(%$header_hash)) { $request{$key}=$value; } } unless ($_[4]) { LW2::http_fixup_request(\%request); } # Cache if (defined($CACHE{$request{whisker}->{uri}}) && !defined($CLI{'nocache'}) && ($CACHE{$request{whisker}->{uri}}{method} eq $request{whisker}->{method})) { # Get from cache nprint("- Retrieving $request{whisker}->{uri} from cache.","v"); $result{whisker}->{code}=$CACHE{$request{whisker}->{uri}}{res}; $result{whisker}->{data}=$CACHE{$request{whisker}->{uri}}{content}; } else { LW2::http_do_request_timeout(\%request,\%result); $NIKTO{'totalrequests'}++; unless (defined $CLI{'nocache'}) { $CACHE{$request{whisker}->{uri}}{method}=$request{whisker}->{method}; $CACHE{$request{whisker}->{uri}}{res}=$result{whisker}->{code}; $CACHE{$request{whisker}->{uri}}{content}=$result{whisker}->{data}; } if ($OUTPUT{debug}) { dump_var("Request Hash", \%request); dump_var("Result Hash", \%result); } } if (defined $CLI{'display'} && ($CLI{'display'} =~ /2/) && (defined($result{'whisker'}->{'cookies'}))) { foreach my $c (@{$result{'whisker'}->{'cookies'}}) { nprint("+ $request{'whisker'}->{'uri'} sent cookie: $c"); } } # Clean up extra headers if (defined $_[3]) { while (my($key, $value) = each(%$header_hash)) { delete $request{$key}; } } $request{'User-Agent'}=$temp_ua; # reset UA return $result{'whisker'}->{'code'}, $result{'whisker'}->{'data'}; } ####################################################################### 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/g; $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}; LW2::auth_set("proxy-basic",$reqhash,$NIKTOCONFIG{PROXYUSER}, $NIKTOCONFIG{PROXYPASS}); } return $reqhash; } ####################################################################### sub nfetch { my ($mark, $uri, $method, $data, $headers, $noclean, $testid) = @_; if ($CLI{'pause'} > 0) { sleep $CLI{'pause'}; } my (%request, %result); setup_hash(\%request, $mark, $testid); $request{whisker}->{uri} = $uri; if (defined $CLI{'root'}) { $request{whisker}->{uri} = $CLI{'root'} . $uri; # prepend -root option value } $request{whisker}->{method} = $method; my $header_hash=$headers; if (defined $data) { my $r=$data; $r =~ s/\\\"/\"/g; $request{whisker}->{data} = $r; } # 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(%$header_hash)) { $request{$key}=$value; } } unless ($noclean) { LW2::http_fixup_request(\%request); } # Cache if (defined $CACHE{$uri} && !defined $CLI{'nocache'} && $CACHE{$uri}{method} eq $method && $CACHE{$uri}{mark} eq $mark) { # Get from cache nprint("- Retrieving $uri from cache.","v"); $result{whisker}->{code}=$CACHE{$uri}{res}; $result{whisker}->{data}=$CACHE{$uri}{content}; } else { LW2::http_do_request_timeout(\%request,\%result); $NIKTO{'totalrequests'}++; unless (defined $CLI{'nocache'}) { $CACHE{$uri}{method}=$result{whisker}->{method}; $CACHE{$uri}{res}=$result{whisker}->{code}; $CACHE{$uri}{content}=$result{whisker}->{data}; $CACHE{$uri}{mark}=$mark; } if ($OUTPUT{debug}) { dump_var("Request Hash", \%request); dump_var("Result Hash", \%result); } } if (defined $CLI{'display'} && ($CLI{'display'} =~ /2/) && (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; } } } return $result{'whisker'}->{'code'}, $result{'whisker'}->{'data'}, $result{'whisker'}->{'error'}; } ####################################################################### sub set_scan_items { my ($mark) = @_; # load the tests my $shname=$mark->{hostname} || $mark->{ip}; %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) { # This nasty set of loops allows for multiple values per line my $ext = get_ext($item[3]); $db_extensions{$ext}=1; # This escapes regex characters in the conditionals. This will have to change if regex is ever allowed in the db for (my $y=5;$y<=9;$y++) { $item[$y] =~ s/([^a-zA-Z0-9\s])/\\$1/g; } $mark->{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]; } } } nprint("- $mark->{total_checks} server checks loaded","v"); if ($mark->{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 char_escape { $_[0] =~ s/([^a-zA-Z0-9 ])/\\$1/g; return $_[0]; } ####################################################################### 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'}\n"; nprint "$NIKTO{'name'} Versions\n"; nprint "$NIKTO{'DIV'}\n"; nprint "File Version Last Mod\n"; nprint "----------------------------- -------- ----------\n"; nprint "Nikto main $NIKTO{'version'}\n"; nprint "LibWhisker $LW2::VERSION\n"; 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]\n"; } nprint "$NIKTO{'DIV'}\n"; exit; } ####################################################################### sub send_updates { if ($NIKTOCONFIG{UPDATES} !~ /yes|auto/i) { return; } my $have_updates=0; my ($updated_version, $answer, $RES); foreach my $ver (keys %UPDATES) { if ($ver !~ /[0-9]/) { next; } # no version info...useless if ($ver eq "Win32") { next; } # also no use if ($ver eq "Linux-Mandrake") { next; } # just... usually garbage $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; } LW2::http_init_request(\%request); my $server="www.cirt.net"; $request{'whisker'}->{'version'}="1.1"; $request{'whisker'}->{'port'}=80; $request{'whisker'}->{'anti_ids'}=""; $request{'User-Agent'}="Nikto Update Agent"; $request{'Host'}=$server; for (my $i=0;$i<=$#ARGV;$i++) { if (($ARGV[$i] eq "-u") || ($ARGV[$i] eq "-useproxy")) { $CLI{'useproxy'}=1; last; } } my $ip=gethostbyname($server); if ($ip ne "") { $request{'whisker'}->{'host'}= inet_ntoa($ip); } else { $request{'whisker'}->{'host'}=$server; } if (($NIKTOCONFIG{PROXYHOST} ne "") && ($CLI{'useproxy'})) { $request{'whisker'}->{'proxy_host'}=$NIKTOCONFIG{PROXYHOST}; $request{'whisker'}->{'proxy_port'}=$NIKTOCONFIG{PROXYPORT}; } # send data LW2::http_close(\%request); # force-close any old connections LW2::http_fixup_request(\%request); ($RES, $CONTENT) = fetch("/cgi-bin/versions?DATA=$updated_version","GET"); # if res is blank... maybe only proxy to get to net? if (($RES eq "") && ($NIKTOCONFIG{PROXYHOST} ne "")) { $request{'whisker'}->{'proxy_host'}=$NIKTOCONFIG{PROXYHOST}; $request{'whisker'}->{'proxy_port'}=$NIKTOCONFIG{PROXYPORT}; ($RES, $CONTENT) = fetch("/cgi-bin/versions?DATA=$updated_version","GET"); } if ($RES eq 407) # requires Auth { if ($NIKTOCONFIG{PROXYUSER} eq "") { $NIKTOCONFIG{PROXYUSER}=read_data("Proxy ID: ",""); $NIKTOCONFIG{PROXYPASS}=read_data("Proxy Pass: ","noecho"); } LW2::auth_set("proxy-basic",\%request,$NIKTOCONFIG{PROXYUSER},$NIKTOCONFIG{PROXYPASS}); # and try again LW2::http_close(\%request); # force-close any old connections LW2::http_fixup_request(\%request); ($RES, $CONTENT) = fetch("/cgi-bin/versions?DATA=$updated_version","GET"); } if ($RES eq "") # lookup failure? { LW2::http_close(\%request); # force-close any old connections $request{'whisker'}->{'host'}=$NIKTOCONFIG{CIRT}; $request{'Host'}="www.cirt.net"; LW2::http_fixup_request(\%request); ($RES, $CONTENT) = fetch("/cgi-bin/versions?DATA=$updated_version","GET"); } if ($CONTENT !~ /SUCCESS/) { print STDERR "+ ERROR: ($RES, $CONTENT): Unable to send update info to CIRT.net\n"; } else { nprint "- Sent updated info to CIRT.net -- Thank you!\n"; } return; } ####################################################################### sub usage { if ($_[0] eq 2) { nprint($NIKTO{'options'}); } else { nprint($NIKTO{'options_short'}); } 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'}"); nprint(" $plugin->{'full_name'} - $plugin->{'description'}"); nprint(" Written by $plugin->{'author'}, Copyright (C) $plugin->{'copyright'}"); nprint("\n"); } exit(0); } ####################################################################### sub nikto_core { return; } # trap for this plugin being called to run. lame. ####################################################################### 1;