#VERSION,2.03 #LASTMOD,01.09.2008 ############################################################################### # 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. ############################################################################### ############################################################################### # Nikto core functionality ############################################################################### sub test_target { dump_var("Result Hash", \%result); # this is the actual the looped code for all the checks foreach my $CHECKID (sort keys %TESTS) { if ($CHECKID >= 500000) { next; } # skip TESTS added manually during run (for reports) (my $RES, $CONTENT) = fetch($TESTS{$CHECKID}{uri},$TESTS{$CHECKID}{method},$TESTS{$CHECKID}{data}); nprint("- $RES for $TESTS{$CHECKID}{method}:\t$request{whisker}{uri}","v"); $NIKTO{resp_counts}{$RES}{total}++; # do auth/redir first, independent of test pass/fail if ($RES eq 401) { $result{'www-authenticate'} =~ /realm=\"(.+)\"/; my $R = $1; if ($R eq '') { $R = $result{'www-authenticate'} } my $old_creds = $request{'Authorization'}; # store any creds we have so we don't lose them if ($REALMS_TESTED{$R} ne 1) { auth_guess($R, $CHECKID); } if ($old_creds ne '') { $request{'Authorization'} = $old_creds; } # restore creds nprint("+ $TESTS{$CHECKID}{uri} - Requires Authentication for realm '$R'") if $CLI{display} =~ /4/; } elsif (($RES eq 300) || ($RES eq 301) || ($RES eq 302) || ($RES eq 303) || ($RES eq 307)) { nprint("+ $TESTS{$CHECKID}{uri} - Redirects ($RES) to " . $result{'location'} . " , $TESTS{$CHECKID}{message}") if $CLI{display} =~ /1/; } elsif ($RES eq 200) { nprint("+ $TESTS{$CHECKID}{uri} - 200/OK Response could be $TESTS{$CHECKID}{message}") if $CLI{display} =~ /3/; } my $m1_method= my $m1o_method= my $m1a_method= my $f2_method= my $f1_method ="content"; my $positive=0; # how to check each conditional if ($TESTS{$CHECKID}{match_1} =~ /^[0-9]{3}$/) { $m1_method="code"; } if ($TESTS{$CHECKID}{match_1_or} =~ /^[0-9]{3}$/) { $m1o_method="code"; } if ($TESTS{$CHECKID}{match_1_and} =~ /^[0-9]{3}$/) { $m1a_method="code"; } if ($TESTS{$CHECKID}{fail_1} =~ /^[0-9]{3}$/) { $f1_method="code"; } if ($TESTS{$CHECKID}{fail_2} =~ /^[0-9]{3}$/) { $f2_method="code"; } # basic match for positive result if ($m1_method eq "content") { if ($CONTENT =~ /$TESTS{$CHECKID}{match_1}/) { $positive=1; #print "testid:$CHECKID\n"; #print "match_1:$TESTS{$CHECKID}{match_1}:\n"; #exit; } } else { if (($RES eq $TESTS{$CHECKID}{match_1}) || ($RES eq $FoF{okay}{response})) { $positive=1; } } # no match, check optional match if ((!$positive) && ($TESTS{$CHECKID}{match_1_or} ne "")) { if ($m1o_method eq "content") { if ($CONTENT =~ /$TESTS{$CHECKID}{match_1_or}/) { $positive=1; } } else { if (($RES eq $TESTS{$CHECKID}{match_1_or}) || ($RES eq $FoF{okay}{response})) { $positive=1; } } } # matched on something, check fails/ands if ($positive) { if ($TESTS{$CHECKID}{fail_1} ne "") { if ($f1_method eq "content") { if ($CONTENT =~ /$TESTS{$CHECKID}{fail_1}/) { next; } } else { if ($RES eq $TESTS{$CHECKID}{fail_1}) { next; } } } if ($TESTS{$CHECKID}{fail_2} ne "") { if ($f2_method eq "content") { if ($CONTENT =~ /$TESTS{$CHECKID}{fail_2}/) { next; } } else { if ($RES eq $TESTS{$CHECKID}{fail_2}) { next; } } } if ($TESTS{$CHECKID}{match_1_and} ne "") { if ($m1a_method eq "content") { if ($CONTENT !~ /$TESTS{$CHECKID}{match_1_and}/) { next; } } else { if ($RES ne $TESTS{$CHECKID}{match_1_and}) { next; } } } # if it's an index.php, check for normal /index.php to see if it's a FP if ($TESTS{$CHECKID}{uri} =~ /^\/index.php\?/) { my $CONTENT=rm_active_content($CONTENT, $TESTS{$CHECKID}{uri}); if (LW2::md4($CONTENT) eq $FoF{'index.php'}{match}) { next; } } # lastly check for a false positive based on file extension or type if (($m1_method eq "code") || ($m1o_method eq "code")) { if (is_404($request{whisker}{uri},$CONTENT,$RES)) { next; } } $TARGETS{$CURRENT_HOST_ID}{total_vulns}++; $TESTS{$CHECKID}{osvdb} =~ s/\s+/ OSVDB\-/g; nprint("+ OSVDB-$TESTS{$CHECKID}{osvdb}: $TESTS{$CHECKID}{method} $request{whisker}{uri} : $TESTS{$CHECKID}{message}"); $TARGETS{$CURRENT_HOST_ID}{positives}{$CHECKID}=1; } } # end check loop nprint("+ $TARGETS{$CURRENT_HOST_ID}{total_checks} items checked: $TARGETS{$CURRENT_HOST_ID}{total_vulns} item(s) reported on remote host"); $TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{stop_time_epoch}=time(); $TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{stop_time_disp}=date_disp($TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{stop_time_epoch}); $TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{elapsed} = $TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{stop_time_epoch} - $TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{start_time_epoch}; nprint("+ End Time: $TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{stop_time_disp} ($TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{elapsed} seconds)"); nprint($DIV); return; } ############################################################################### sub is_404 { my ($uri, $content, $rescode) = @_; $ext=get_ext($uri); if (($FoF{$ext}{mode} eq "STD") && (($rescode eq 401) || ($rescode eq 403) || ($rescode eq 404) || ($rescode eq 410))) { return 1; } elsif ($FoF{$ext}{mode} eq "STD") { return 0; } elsif ($FoF{$ext}{mode} eq "REDIR") { if ($result{location} eq $FoF{$ext}{location}) { return 1; } } elsif (($FoF{$ext}{type} eq "CONTENT") && ($content =~ /$FoF{$ext}{match}/i)) { 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; } } return 0; } ############################################################################### sub nprint { my $line=$_[0]; chomp($line); # don't print debug & verbose to output file... if ($_[1] eq "d") { if ($OUTPUT{debug}) { print "D:" . localtime() . " $line\n"; } return; } elsif ($_[1] eq "v") { if ($OUTPUT{verbose}) { print "V:" . localtime() . " $line\n"; } return; } # print to STDOUT if ($line =~ /ERROR\:/) { print STDERR "$line\n"; } else { print "$line\n"; } # if no file saving, return if ($CLI{file} eq "") { return; } # HTML is handled elsewhere, sadly if ($CLI{format} eq "txt") { $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 OUT "$line\n"; } elsif ($CLI{format} eq "csv") { chomp($line); if ($line =~ /-------------/) { $line="\"$DIV\",\"$DIV\""; } if ($line =~ /^\+/) { $line =~ s/^\+ (.*) - (.*)/"$1","$2"/; } $line =~ s/Target IP: (.*)/"Target IP","$1"/; $line =~ s/Target Hostname: (.*)/"Target Hostname","$1"/; $line =~ s/Target Port: (.*)/"Target Port","$1"/; $line =~ s/Start Time: (.*)/"Start Time","$1"/; $line =~ s/End Time: (.*)/"End Time","$1"/; $line =~ s/ERROR: (.*)/"ERROR","$1"/; chomp($line); $line =~ s/\t/ /g; $line =~ s/Server: ([^ ]*)(.*$)/"Server","$1","$2"/; $line =~ s/Test Options: (.*)/"Test Options","$1\n"/; $line =~ s/Allowed HTTP Methods: (.*)/"Allowed HTTP Methods","$1"/; $line =~ s/^\+//; $line =~ s/^\-//; $line =~ s/^\s+//; if ($line !~ /^\"/) { $line = "\"$line\""; } $line =~ s/\n\r//g; $line =~ s/\"\"/\"/g; print OUT "$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; $request{'whisker'}->{'http_eol'}=$http_eol; 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); dump_var("Result Hash", \%result); if ($result{location} ne "") { 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; $request{'whisker'}->{'http_eol'}=$http_eol; 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); 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; $request{'whisker'}->{'http_eol'}=$http_eol; 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); dump_var("Result Hash", \%result); $ext=$REQS{$file}; $FoF{$ext}{response} = $result{'whisker'}->{'code'}; if ($result{location} ne "") { $FoF{$ext}{location} = $result{location}; $file=char_escape($file); $FoF{$ext}{location} =~ s/$file//; # if ($ext eq "php") # { #print "$ext $FoF{$ext}\n"; #print "loc is:$FoF{$ext}{location}\n"; exit; #} } # handle .com to .org redirs or whatnot # 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; $request{'whisker'}->{'http_eol'}=$http_eol; 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); 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) { print "$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 ($_[1] ne '') { my $e = $_[1]; # match pages which link to themselves w/diff args $e =~ s/^\/index.php\??//; $e =~ s/([^a-zA-Z0-9\s])/\\$1/g; $cont =~ s/$e//gs; # again but with the index.php in place $e = $_[1]; $e =~ s/([^a-zA-Z0-9\s])/\\$1/g; $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 { # print out initial connection info my $SSLPRINT=""; $TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{start_time_epoch}=time(); $TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{start_time_disp}=date_disp($TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{start_time_epoch}); if ($TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{ssl}) { my $SSLCIPHERS=$result{whisker}->{ssl_cipher} || "Unknown"; my $SSLISSUERS=$result{whisker}->{ssl_cert_issuer} || "Unknown"; my $SSLINFO=$result{whisker}->{ssl_cert_subject} || "Unknown"; $SSLPRINT="$DIV\n"; $SSLPRINT.="+ SSL Info: Ciphers: $SSLCIPHERS\n Info: $SSLISSUERS\n Subject: $SSLINFO"; } if ($TARGETS{$CURRENT_HOST_ID}{ip} =~ /[a-z]/i) { nprint("+ Target IP: (proxied)"); } else { nprint("+ Target IP: $TARGETS{$CURRENT_HOST_ID}{ip}"); } nprint("+ Target Hostname: $TARGETS{$CURRENT_HOST_ID}{hostname}"); nprint("+ Target Port: $CURRENT_PORT"); if (($CLI{vhost} ne $TARGETS{$CURRENT_HOST_ID}{hostname}) && ($CLI{vhost} ne "")) { nprint("+ Virtual Host: $CLI{vhost}"); } if ($request{'whisker'}->{'proxy_host'} ne "") { nprint("- Proxy: $request{'whisker'}->{'proxy_host'}:$request{'whisker'}->{'proxy_port'}"); } if ($NIKTO{hostid} ne "") { nprint("- Host Auth: ID: $NIKTO{hostid}, PW: $NIKTO{hostpw}, Realm: $NIKTO{hostdomain}","v"); } if ($TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{ssl}) { nprint($SSLPRINT); } for (my $i=1;$i<=(keys %{$NIKTO{anti_ids}});$i++) { if ($CLI{evasion} =~ /$i/) { nprint("+ Using IDS Evasion:\t$NIKTO{anti_ids}{$i}"); }} for (my $i=1;$i<=(keys %{$NIKTO{mutate_opts}});$i++) { if ($CLI{mutate} =~ /$i/) { nprint("+ Using Mutation:\t$NIKTO{mutate_opts}{$i}"); }} nprint("+ Start Time: $TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{start_time_disp}"); nprint($DIV); if ($TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{banner} ne "") { if ($CLI{format} =~ /^htm/) { nprint("+ Server: $TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{banner_disp}"); } # has < > escaped, JIC else { nprint("+ Server: $TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{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{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{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 -mutate+ Guess additional file names -output+ write output to this file -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 -mutate+ Guess additional file names:\n"; foreach my $k (sort keys %{$NIKTO{mutate_opts}}) { $NIKTO{options} .= " $k $NIKTO{mutate_opts}{$k}\n"; } $NIKTO{options}.=" -nolookup skip name lookup -output+ write output to this file -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}=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(); } elsif ($ARGV[$i] eq '-verbose') { print STDERR "-verbose is deprecated, please use '-D V' instead (verbose enabled)\n"; $OUTPUT{verbose}=1; $ARGV[$i]=""; } elsif ($ARGV[$i] eq '-debug') { print STDERR "-debug is deprecated, please use '-D D' instead (debug enabled)\n"; $OUTPUT{debug}=1; $ARGV[$i]=""; } } GetOptions( "nolookup" => \$CLI{skiplookup}, "config=s" => \$CLI{config}, "Cgidirs=s" => \$CLI{forcecgi}, "mutate=s" => \$CLI{mutate}, "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}, "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}, ); if ($CLI{help}) { usage(2); } elsif ($CLI{version}) { version(); } elsif ($CLI{Single}) { single(); } # output file if (($CLI{format} ne "") && ($CLI{file} eq "")) { nprint("+ ERROR: Output format specified without output file."); exit; } if ($CLI{format} eq "") { $CLI{format}="txt"; } # default to txt elsif ($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"; } else { nprint("+ ERROR: Invalid output format '$CLI{format}'"); exit; } # verify readable dtd if ($CLI{format} =~ /xml/i and !-r $NIKTOCONFIG{NIKTODTD}) { nprint("+ ERROR: reading DTD"); exit; } # screen output if ($CLI{display} =~ /d/i) { $OUTPUT{debug}=1; } if ($CLI{display} =~ /v/i) { $OUTPUT{verbose}=1; } # port(s) $CLI{ports}=~s/^\s+//; $CLI{ports}=~s/\s+$//; #if ($CLI{ports} eq "") { $CLI{ports}=80; } if ($CLI{ports} =~ /[^0-9\-\, ]/) { nprint("+ ERROR: Invalid port option '$CLI{ports}'"); exit; } # Fixup $CLI{root} =~ s/\/$//; if (($CLI{root} !~ /^\//) && ($CLI{root} ne "")) { $CLI{root} = "/$CLI{root}"; } if ($CLI{hostauth} ne "") { my @x=split(/:/,$CLI{hostauth}); if (($#x ne 1) || ($x[0] eq "")) { nprint("+ ERROR: '$CLI{hostauth}' (-i option) syntax is 'user:password' or 'user:password:domain' for host authentication.") } $NIKTO{hostid} = $x[0]; $NIKTO{hostpw} = $x[1]; $NIKTO{hostdomain} = $x[2]; } $CLI{evasion}=~s/[^0-9]//g; $NIKTO{useragent}="Mozilla/4.75 ($NIKTO{name}/$NIKTO{version} $request{'User-Agent'})"; # SSL Test if (!LW2::ssl_is_available()) { nprint("- ***** SSL support not available (see docs for SSL install instructions) *****"); } # Notices my $notice; if ($CLI{root} ne '') { $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,"<$NIKTO{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; $COUNTERS{hosts_total}=$COUNTERS{hosts_completed}=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"); delete $TARGETS{$CURRENT_HOST_ID}; 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"); delete $TARGETS{$CURRENT_HOST_ID}; 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"); delete $TARGETS{$CURRENT_HOST_ID}; 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; } # set this 'host' $request{'whisker'}{'host'} = $name; return $name,$ip,$dn; } ############################################################################### sub set_targets { my $host_ctr=1; if ($CLI{host} eq "") { nprint("+ ERROR: No host specified"); usage(); } # if -p is not set, see if each line has its own ports. if not push 80 to ports_in if (-e $CLI{host}) { nprint("Reading from file '$CLI{host}'","v"); open(IN,"<$CLI{host}") || die print STDERR "+ ERROR: Cannot open '$CLI{host}':$@\n"; while() { chomp; s/\#.*$//; if ($_ eq "") { next; } s/\s+//g; $COUNTERS{hosts_total}++; if (/(\:|\,)/) # at least one port is set for host { my $p=""; my @h=split(/\:|\,/); $TARGETS{$host_ctr}{ident}=$h[0]; # preppend anything from CLI, don't duplicate for (my $i=1;$i<=$#h;$i++) { $p.="$h[$i],"; } if ($p !~ /$CLI{ports}(,|$)/) { $p .= "$CLI{ports}"; } $p=~s/\,$//; $TARGETS{$host_ctr}{ports_in}=$p || 80; } else { $TARGETS{$host_ctr}{ports_in} = $CLI{ports} || 80; $TARGETS{$host_ctr}{ident} = $_; } nprint("- Target id:$host_ctr:ident:$TARGETS{$host_ctr}{ident}:ports_in:$TARGETS{$host_ctr}{ports_in}:","d"); $host_ctr++; } close(IN); } # Since it's not a file, it is either a host or an error # test for a full URL and parse, or treat like host and let resolve() sort it out else { $COUNTERS{hosts_total}++; if ($CLI{host} =~ /^https?:\/\//) { my @hostdata=LW2::uri_split($CLI{host}); $TARGETS{$host_ctr}{ident} = $hostdata[2]; $TARGETS{$host_ctr}{ports_in} = $hostdata[3]; if ($hostdata[1] eq "https") { $CLI{ssl}=1; if ($TARGETS{$host_ctr}{ports_in} eq "") { $TARGETS{$host_ctr}{ports_in}=443; } } if (($CLI{root} eq '') && ($hostdata[0] ne '')) { $CLI{root}=$hostdata[0]; nprint("- Added -root value of '$CLI{root}'","d"); } } else { $TARGETS{$host_ctr}{ident}=$CLI{host}; $TARGETS{$host_ctr}{ports_in}=$CLI{ports} || 80; } if ($CLI{vhost} ne '') { $TARGETS{$host_ctr}{vhost}=$CLI{vhost}; } nprint("- Target id:$host_ctr:ident:$TARGETS{$host_ctr}{ident}:ports_in:$TARGETS{$host_ctr}{ports_in}:vhost:$$TARGETS{$host_ctr}{vhost}=$CLI{vhost}:","d"); } return; } ############################################################################### sub host_config { ($TARGETS{$CURRENT_HOST_ID}{hostname}, $TARGETS{$CURRENT_HOST_ID}{ip}, $TARGETS{$CURRENT_HOST_ID}{display_name}) = resolve($TARGETS{$CURRENT_HOST_ID}{ident}); if ($TARGETS{$CURRENT_HOST_ID}{ident} eq "") { return; } port_scan($TARGETS{$CURRENT_HOST_ID}{ports_in}); # make sure we have open ports on this target if (keys(%{$TARGETS{$CURRENT_HOST_ID}{ports}}) eq 0) { nprint("+ No HTTP(s) ports found on $TARGETS{$CURRENT_HOST_ID}{ident} / $TARGETS{$CURRENT_HOST_ID}{ports_in}"); } return; } ############################################################################### # perform a port scan ############################################################################### sub port_scan { my $portopts=$_[0] || return; my (@t) = (); my %portlist; # if we're using nmap, skip this & let nmap handle port ranges... unless proxied if (!(-X $NIKTOCONFIG{NMAP}) || $CLI{useproxy}) { # break out , items if ($portopts =~ /,/) { foreach (split(/\,/,$portopts)) { push(@t,$_); } } else { push(@t,$portopts); } # ranges for (@t) { s/^\s+//; s/\s+$//; if ($_ !~ /-/) { $portlist{$_}=0; } else { my @x=split(/\-/,$_); for (my $i=$x[0];$i<=$x[1];$i++) { $portlist{$i}=0; } } } # last check for only null lists (i.e., user put in 4-1 as a range) my $invalid= my $have_valid = 0; foreach my $p (keys %portlist) { if (($p =~/[^0-9]/)||($p eq "")) { $invalid=1; last; } $have_valid++; } if ($invalid|!$have_valid) { nprint("+ ERROR: Invalid port option '$CLI{ports}'"); exit; } } # end if not NMAP # if NMAP is defined & no proxy, use that... if not, we do it the hard way if ((-X $NIKTOCONFIG{NMAP}) && !$CLI{useproxy}) { nprint("- Calling nmap:$NIKTOCONFIG{NMAP} $NIKTOCONFIG{NMAPOPTS} -oG - -p $portopts $TARGETS{$CURRENT_HOST_ID}{ip}","v"); foreach my $line (split(/\n/,`$NIKTOCONFIG{NMAP} $NIKTOCONFIG{NMAPOPTS} -oG - -p $portopts $TARGETS{$CURRENT_HOST_ID}{ip}`)) { if ($line !~ /^Host/) { next; } $line =~ s/^.*Ports: //; $line =~ s/Ignored.*$//; $line =~ s/^\s+//; $line =~ s/\s+$//; foreach my $PORTSTRING (parse_csv($line)) { $portlist{(split(/\//,$PORTSTRING))[0]}=0; } } } # test each port... foreach $p (sort keys %portlist) { nprint("- Testing open ports for web servers","v"); if ($p !~ /[0-9]/) { next; } $p =~ s/\s+$//; $p =~ s/^\s+//; foreach my $skip (split(/ /,$NIKTOCONFIG{SKIPPORTS})) { if ($skip eq $p) { next; } } if ($p eq "") { next; } port_check($p); } return; } ############################################################################### sub load_databases { my @dbs=qw/db_404_strings db_outdated db_realms db_tests db_server_msgs db_variables db_favicon/; my $prefix = $_[0]; # verify required files for my $file (@dbs) { if (!-r "$NIKTO{plugindir}/$file") { die nprint("+ ERROR: Can't find/read required file \"$NIKTO{plugindir}/$file\""); } } for my $file (@dbs) { my $filename = $NIKTO{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++; } } # db_server_msgs elsif ($file eq 'db_server_msgs') { foreach my $l (@file) { my @t=parse_csv($l); $VERSIONS{$t[1]}=$t[2]; $VERSIONS{$t[1]}{tid}=$t[0]; } } # db_favicons elsif ($file eq 'db_favicon') { foreach my $l (@file) { my @t=parse_csv($l); $FAVICONS{$t[1]}=$t[2]; $FAVICONS{$t[1]}{tid}=$t[0]; } } close(IN); } } return; } ############################################################################### sub dbcheck { my @dbs=qw/db_404_strings db_outdated db_realms db_tests db_server_msgs db_variables db_favicon/; my $prefix = $_[0]; if ($prefix eq "" ) { print "\n-->\tNikto Databases\n"; } if ($prefix eq "u" ) { print "\n-->\tUser Databases\n"; } for my $file (@dbs) { my $filename = $NIKTO{plugindir} . "/" . $prefix . $file; if (!-r $filename) { next; } open(IN,"<$filename") || die nprint("+ ERROR: Can't open \"$filename\":$!\n"); print "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 ($line !~ /^\".*\"\,\".*\"\,\".*\"$/) { print STDERR "\tERROR: Invalid syntax ($#L): $line\n"; next; } if ($#L ne 2) { print STDERR "\tERROR: Invalid syntax ($#L): $line\n"; next; } $ENTRIES{"$L[0]"}++; } foreach $entry (keys %ENTRIES) { if ($ENTRIES{$entry} > 1) { print STDERR "\tERROR: Duplicate ($ENTRIES{$entry}): $entry\n"; } } print "\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 "\tERROR: Possibly invalid method: $L[4] on ($line)\n"; } if ($L[5] eq "") { print STDERR "\tERROR: blank conditional: $line"; next; } if ($line !~ /^\".*\",\".*\",\".*\",\".*\",\".*\"/) { print STDERR "\tERROR: Invalid syntax ($#L): $line\n"; next; } if ($line !~ /^(\".*\",){11}\".*\"/) { print STDERR "\tERROR: Invalid syntax ($#L): $line\n"; next; } if (($L[3] =~ /^\@CGI/) && ($L[3] !~ /^\@CGIDIRS/)) { print STDERR "\tERROR: Possible \@CGIDIRS misspelling: $line"; } $ENTRIES{"$L[3],$L[4],$L[5],$L[6],$L[7],$L[8],$L[9],$L[10],$L[12]"}++; } foreach $entry (keys %ENTRIES) { if ($ENTRIES{$entry} > 1) { print STDERR "\tERROR: Duplicate ($ENTRIES{$entry}): $entry\n"; } } print "\t" . keys(%ENTRIES) . " entries\n"; } elsif ($file eq 'db_server_msgs') { foreach $line () { $line =~ s/^\s+//; if ($line =~ /^\#/) { next; } chomp($line); if ($line eq "") { next; } my @L=parse_csv($line); if ($line !~ /^\".*\"\,\".*\"$/) { print STDERR "\tERROR: Invalid syntax ($#L): $line\n"; next; } if ($#L ne 1) { print STDERR "\tERROR: Invalid syntax ($#L): $line\n"; next; } # test regex to look for errors "test" =~ /$L[0]/; $ENTRIES{"$L[0]"}++; } foreach $entry (keys %ENTRIES) { if ($ENTRIES{$entry} > 1) { print STDERR "\tERROR: Duplicate ($ENTRIES{$entry}): $entry\n"; } } print "\t" . keys(%ENTRIES) ." entries\n"; } elsif ($file eq 'db_variables') { my $ctr=0; foreach $line () { if ($line !~ /^\@/) { next; } if ($line !~ /^\@.+\=.+$/i ) { print STDERR "\tERROR: Invalid syntax: $line\n"; } $ctr++; } print "\t$ctr entries\n"; } elsif ($file eq 'db_realms') { my $ctr=0; foreach $line () { if ($line !~ /^\"/) { next; } chomp($line); my @L=parse_csv($line); if ($#L ne 3) { print STDERR "\tERROR: Invalid syntax: $line\n"; } $ctr++; } print "\t$ctr entries\n"; } elsif ($file eq 'db_404_strings') { my $ctr=0; foreach $line () { # not really any syntax to check $ctr++; } print "\t$ctr entries\n"; } elsif ($file eq 'db_favicon') { my $ctr=0; foreach $line () { if ($line !~ /^\"/) { next; } chomp($line); my @L=parse_csv($line); if ($#L ne 1) { print STDERR "\tERROR: Invalid syntax: $line\n"; } $ctr++; } print "\t$ctr entries\n"; } close(IN); } if ($_[0] eq "") { dbcheck('u'); } # do this once #### check that all plugins are in nikto_plugin_order.txt print "\n-->\tPlugin order ($NIKTO{plugindir}/nikto_plugin_order.txt)\n"; my @NIKTOFILES=dirlist($NIKTO{plugindir},"(\.plugin\$)"); my %PLUGS; foreach my $pluginf (@NIKTOFILES) { chomp($pluginf); $pluginf =~ s/\#.*$//; $pluginf =~ s/\..*$//; $pluginf =~ s/\s+//; if (($pluginf eq "") || ($pluginf eq "nikto_core")) { next; } $PLUGS{$pluginf}=0; } open(ORDERFILE,"<$NIKTO{plugindir}/nikto_plugin_order.txt") || die print STDERR "\tERROR: Unable to open '$NIKTO{plugindir}/nikto_plugin_order.txt' for read: $@\n"; foreach my $line () { chomp($line); $line =~ s/\#.*$//; $line =~ s/\s+/ /; if (($line eq "") || ($line eq " ")) { next; } $PLUGS{$line}=1; } close(ORDERFILE); my $bad=0; foreach my $p (sort keys %PLUGS) { if ($PLUGS{$p} eq 0) { $bad=1; print STDERR "\tERROR: plugin '$p' not in nikto_plugin_order.txt\n"; } } if (!$bad) { print STDERR "\tOrder file okay\n"; } #### check that all plugins are named properly print "-->\tPlugin conventions ($NIKTO{plugindir}/*.plugin)\n"; $bad=0; foreach my $pluginf (@NIKTOFILES) { chomp($pluginf); $pluginf =~ s/\#.*$//; $pluginf =~ s/\..*$//; $pluginf =~ s/\s+//; if (($pluginf eq "") || ($pluginf eq "nikto_core")) { next; } open(IN,"<$NIKTO{plugindir}/$pluginf.plugin") || die print STDERR "\tERROR: Unable to open '$NIKTO{plugindir}/$pluginf.plugin' for read: $@\n"; my @F=; close(IN); my $CT=grep(/sub $pluginf/,@F); if ($CT < 1) { print STDERR "\tERROR: file '$pluginf\.plugin' does not have 'sub $pluginf' defined.\n"; $bad++; } } if (!$bad) { print "\tPlugin syntax okay\n"; } print "\n"; exit; } ############################################################################### sub get_banner { (my $RES, $CONTENT) = fetch("/","HEAD"); $TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{banner}=$result{'server'}; $TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{banner_disp}=$result{'server'}; $TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{banner_disp} =~ s/{'host'}; $request{'whisker'}->{'uri'}="/"; $request{'whisker'}->{'method'}="HEAD"; # test for proxy proxy_check() unless $PROXYCHECKED; # try http if (!$CLI{ssl}) { nprint("- Checking for HTTP on port $TARGETS{$CURRENT_HOST_ID}{ip}:$port","v"); $request{'whisker'}->{'ssl'}=0; $request{'whisker'}->{'port'}= $port; $request{'whisker'}->{'http_eol'}=$http_eol; dump_var("Request Hash", \%request); LW2::http_close(\%request); # force-close any old connections LW2::http_fixup_request(\%request); if ($CLI{pause} > 0) { sleep $CLI{pause}; } if (!LW2::http_do_request_timeout(\%request,\%result)) { # this will fix for some Apaches that are smart enough to answer non ssl reqs on an ssl server if ($result{'whisker'}->{'data'} !~ /speaking plain HTTP to an SSL/) { $TARGETS{$CURRENT_HOST_ID}{ports}{$port}{ssl}=0; nprint("- Server found: $TARGETS{$CURRENT_HOST_ID}{ip}:$port \t$result{'server'}","d"); $request{'whisker'}->{'host'}=$oldhost; dump_var("Result Hash", \%result); return; } } else { dump_var("Result Hash", \%result); } } # try https nprint("- Checking for HTTPS on port $TARGETS{$CURRENT_HOST_ID}{ip}:$port","v"); $request{'whisker'}->{'ssl'}=1; $request{'whisker'}->{'port'}= $port; $request{'whisker'}->{'http_eol'}=$http_eol; dump_var("Request Hash", \%request); LW2::http_close(\%request); # force-close any old connections LW2::http_fixup_request(\%request); if ($CLI{pause} > 0) { sleep $CLI{pause}; } if (LW2::http_do_request_timeout(\%request,\%result) eq 0) { $TARGETS{$CURRENT_HOST_ID}{ports}{$port}{ssl}=1; dump_var("Result Hash", \%result); $request{'whisker'}->{'host'}=$oldhost; nprint("- Server found: $TARGETS{$CURRENT_HOST_ID}{ip}:$port \t$result{'server'}","d"); } dump_var("Result Hash", \%result); return; } ############################################################################### # this ugly, and potentially dangerous if untrusted plugins are present ############################################################################### sub run_plugins { open(ORDERFILE,"<$NIKTO{plugindir}/nikto_plugin_order.txt"); my @ORDER=; close(ORDERFILE); foreach my $pluginf (@ORDER) { if ($pluginf =~ /^\#/) { next; } chomp($pluginf); $pluginf =~ s/\s+//; if ($pluginf eq "") { next; } eval { require "$NIKTO{plugindir}/$pluginf\.plugin"; }; if ($@) { nprint("- Could not load or parse plugin: $pluginf\.plugin\nError: ",""); warn $@; nprint("- The plugin could not be run.",""); } else { nprint("- Calling plugin: $pluginf\.plugin","d"); # just call it...hope it works...taint doesn't like this very much for obvious reasons &$pluginf; } } return; } ############################################################################### 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($NIKTO{plugindir},""); foreach my $file (@NIKTOFILES) { my $v=""; open(LOCAL,"<$NIKTO{plugindir}/$file") || print STDERR "+ ERROR: Unable to open '$NIKTO{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}) { print "+ Nikto has been updated to $REMOTE{$remotefile}, local copy is $NIKTO{version}\n"; print "+ No update has taken place. Please upgrade Nikto by visiting http://$server/\n"; if ($remotemsg ne "") { print "+ $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) { print "+ 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,">$NIKTO{plugindir}/$toget") || die print STDERR "+ ERROR: Unable to open '$NIKTO{plugindir}/$toget' for write: $@\n"; print OUT $CONTENT; close(OUT); } } # CHANGES file if ($code_updates) { print "+ Retrieving 'CHANGES.txt'\n"; (my $RES, $CONTENT) = fetch("$serverdir/CHANGES.txt","GET"); if (($CONTENT ne "") && ($RES eq 200)) { open(OUT,">$NIKTO{plugindir}/../docs/CHANGES.txt") || die print STDERR "+ ERROR: Unable to open '$NIKTO{plugindir}/../CHANGES.txt' for write: $@\n"; print OUT $CONTENT; close(OUT); } } if ($#DBTOGET < 0 ) { print "+ No updates required.\n"; } if ($remotemsg ne "") { print "+ $server message: $remotemsg\n"; } exit; } ############################################################################### sub auth_guess { my ($realm, $checkid) = @_; my $successful=0; # check for 'broken' web server, returns a blank www-auth header no matter what the id/pw sent my $tid=LW2::utils_randstr(); LW2::http_close(\%request); # force-close any old connections LW2::auth_set("basic",\%request,$tid,$tid); $request{'whisker'}->{'http_eol'}=$http_eol; LW2::http_fixup_request(\%request); if ($CLI{pause} > 0) { sleep $CLI{pause}; } LW2::http_do_request_timeout(\%request,\%result); # test auth if ($result{'www-authenticate'} ne "") { foreach my $REALM (keys %REALMS) { if (($REALMS{$REALM}{id} eq "") && ($REALMS{$REALM}{pw} eq "") && ($result{'www-authenticate'} =~ /$REALMS{$REALM}{realm}/i)) { nprint("+ $REALMS{$REALM}{realm}: $REALMS{$REALM}{msg}"); $TARGETS{$CURRENT_HOST_ID}{positives}{$REALMS{$REALM}{tid}}=1; $TARGETS{$CURRENT_HOST_ID}{total_vulns}++; } if (($result{'www-authenticate'} =~ /$REALMS{$REALM}{realm}/i) || ($REALMS{$REALM}{realm} eq "\@ANY")) { my $realm_temp=$result{'www-authenticate'}; # grab name LW2::http_close(\%request); # force-close any old connections LW2::auth_set("basic",\%request,$REALMS{$REALM}{id},$REALMS{$REALM}{pw}); # set auth $request{'whisker'}->{'http_eol'}=$http_eol; LW2::http_fixup_request(\%request); if ($CLI{pause} > 0) { sleep $CLI{pause}; } LW2::http_do_request_timeout(\%request,\%result); # test auth if ($result{'www-authenticate'} eq "") { nprint("+ Default account found for '$realm' at $TESTS{$checkid}{uri} (ID '$REALMS{$REALM}{id}', PW '$REALMS{$REALM}{pw}'). $REALMS{$REALM}{msg}"); $TARGETS{$CURRENT_HOST_ID}{positives}{$REALMS{$REALM}{tid}}=1; $TARGETS{$CURRENT_HOST_ID}{total_vulns}++; #set auth stuff & run auth_check again $NIKTO{hostid}=$REALMS{$REALM}{id}; $NIKTO{hostpw}=$REALMS{$REALM}{pw}; $result{'www-authenticate'}=$realm_temp; # set it back so auth_check properly checks it auth_check(); # and lastly, redo the check $successful=1; (my $RES, $CONTENT) = fetch($TESTS{$checkid}{uri},$TESTS{$checkid}{method},$TESTS{$checkid}{data}); nprint("- $RES for $TESTS{$checkid}{method}:\t$request{whisker}{uri}","v"); last; } } } } else { print "+ ERROR: Authorization is required, but bogus auth test appeared to work. Server is a bit whacked.\n"; } $REALMS_TESTED{$realm}=1; my $pa = $request{'Proxy-Authorization'}; LW2::auth_unset(\%request); if ($pa ne '') { $request{'Proxy-Authorization'} = $pa; } } ############################################################################### # auth_check # if the server requires authentication & we have it... ############################################################################### sub auth_check { my $REALM=$result{'www-authenticate'} || return; $REALM =~ s/^Basic //i; $REALM =~ s/realm=//i; if ($REALM eq "") { $REALM="unnamed"; } if ($result{'www-authenticate'} !~ /basic|ntlm/i) # doh, not basic! { my $AUTHTYPE=$result{'www-authenticate'}; $AUTHTYPE =~ s/ .*$//; nprint("+ ERROR: Host uses '$AUTHTYPE'"); return; } elsif ($NIKTO{hostid} eq "") { nprint("+ ERROR: No auth credentials for $REALM, please set."); return; } else { nprint("- Attempting authorization to $REALM realm.","v"); LW2::auth_set("basic",\%request,$NIKTO{hostid},$NIKTO{hostpw},$NIKTO{hostdomain}); # set auth LW2::http_fixup_request(\%request); if ($CLI{pause} > 0) { sleep $CLI{pause}; } $request{'whisker'}->{'http_eol'}=$http_eol; LW2::http_do_request_timeout(\%request,\%result); # test auth dump_var("Request Hash", \%request); dump_var("Result Hash", \%result); if ($result{'www-authenticate'} ne "") { nprint("+ ERROR: Unable to authenticate to $REALM"); } else { nprint("- Successfully authenticated to realm $REALM."); } } return; } ############################################################################### # 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"; print "\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 { $request{'whisker'}->{'method'}="HEAD"; $request{'whisker'}->{'uri'}="/"; if ($request{'whisker'}->{'proxy_host'} ne "") # proxy is set up { LW2::http_close(\%request); # force-close any old connections $request{'whisker'}->{'http_eol'}=$http_eol; LW2::http_fixup_request(\%request); if ($CLI{pause} > 0) { sleep $CLI{pause}; } LW2::http_do_request_timeout(\%request,\%result); 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 $request{'whisker'}->{'http_eol'}=$http_eol; LW2::http_fixup_request(\%request); if ($CLI{pause} > 0) { sleep $CLI{pause}; } LW2::http_do_request_timeout(\%request,\%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"); } } } $PROXYCHECKED=1; 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 check_cgi { my ($gotvalid,$gotinvalid)=0; my @POSSIBLECGI=(); my @CFGCGI=(split(/ /,$VARIABLES{"\@CGIDIRS"})); my ($res, $possiblecgidir) =""; if ($CLI{forcecgi} eq "all") # all possible CGI dirs to be "true" { nprint("Using 'all' CGI directories\n","d"); $VARIABLES{"\@CGIDIRS"} = join(" ",@CFGCGI); } elsif ($CLI{forcecgi} eq "none") # scan no CGI directories { nprint("Using no CGI directories\n","d"); $VARIABLES{"\@CGIDIRS"} = ""; } elsif ($CLI{forcecgi} =~ /[a-zA-Z0-9]/) # scan a specific directory { nprint("Using CGI dir '$CLI{forcecgi}'\n","d"); $VARIABLES{"\@CGIDIRS"} = $CLI{forcecgi}; } else # or normal testing of each dir { foreach $possiblecgidir (@CFGCGI) { ($res, $CONTENT)=fetch($possiblecgidir,"GET"); nprint("Checked for CGI dir\t$possiblecgidir\tgot:$res","d"); if (($res eq 302) || ($res eq 200) || ($res eq 403)) { push(@POSSIBLECGI,$possiblecgidir); $gotvalid++; } } if ($gotvalid eq 0) { nprint("+ No CGI Directories found (use '-C all' to force check all possible dirs)"); $VARIABLES{"\@CGIDIRS"} = ""; } elsif ($#CFGCGI eq $#POSSIBLECGI) { nprint("+ All CGI directories 'found', use '-C none' to test none"); $VARIABLES{"\@CGIDIRS"} = join(" ",@CFGCGI); } else { $VARIABLES{"\@CGIDIRS"} = join(" ",@POSSIBLECGI); } } # end !$CLI{forcecgi} nprint("- Checking for CGI in: $VARIABLES{\"\@CGIDIRS\"}","v"); return; } ####################################################################### sub fetch { if ($CLI{pause} > 0) { sleep $CLI{pause}; } LW2::http_close(\%request); # force-close any old connections $request{'whisker'}->{'uri'} = $CLI{root} . $_[0]; # prepend -root option value $request{'whisker'}->{'method'} = $_[1]; $request{'whisker'}->{'http_eol'}=$http_eol; delete $request{'whisker'}->{'data'}; delete $request{'Content-Encoding'}; delete $request{'Content-Length'}; if ($_[2] ne "") { my $r=$_[2]; $r =~ s/\\\"/\"/g; $request{'whisker'}->{'data'} = $r; } $NIKTO{totalrequests}++; LW2::http_fixup_request(\%request); LW2::http_do_request_timeout(\%request,\%result); dump_var("Request Hash", \%request); dump_var("Result Hash", \%result); if (($CLI{display} =~ /2/) && (defined($result{'whisker'}->{'cookies'}))) { foreach my $c (@{$result{'whisker'}->{'cookies'}}) { nprint("+ $request{'whisker'}->{'uri'} sent cookie: $c"); } } return $result{'whisker'}->{'code'}, $result{'whisker'}->{'data'}; } ####################################################################### sub set_scan_items { # load the tests my $shname=$TARGETS{$CURRENT_HOST_ID}{hostname} || $TARGETS{$CURRENT_HOST_ID}{ip}; %TESTS = (); $TARGETS{$CURRENT_HOST_ID}{total_checks}=0; my @SKIPLIST = split(/ /,$NIKTOCONFIG{SKIPIDS}); # now load checks foreach my $line (@DBFILE) { if ($line =~ /^\"/) # check { chomp($line); # substitute for @IP, @HOSTNAME in check $line =~ s/\@IP/$TARGETS{$CURRENT_HOST_ID}{ip}/g; $line =~ s/\@HOSTNAME/$shname/g; my @item=parse_csv($line); my $add=1; # check tuning options if (($CLI{tuning} ne "") && ($item[2] ne "")) { if ($CLI{tuning} =~ /x/) { # don't include checks that have the tuning set if ($CLI{tuning} =~ /$item[2]/) { $add=0; } } else { # only include checks that have the tuning set if ($CLI{tuning} !~ /$item[2]/) { $add=0; } } } # Skip list foreach my $id (@SKIPLIST) { if ($id eq $item[0]) { $add=0; } } # Add random text if ($add) { for (my $i=3;$i<=$#item;$i++) { if ($item[$i] =~ /JUNK\([0-9]+\)/) # junk text { $item[$i]=~/JUNK\(([0-9]+)\)/; $item[$i] =~ s/JUNK\(([0-9]+)\)/LW2::utils_randstr($1)/e; } } # Build the check items. First check for any @ values to replace. 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; } if ($item[3] =~ /^\@/) # multiple checks in one { my @clones=(); my $todelete=""; push(@clones,$item[3]); foreach my $varname (keys %VARIABLES) { for (my $i=0;$i<=$#clones;$i++) { if ($clones[$i] =~ /$varname/) { my @values=split(/ /,$VARIABLES{$varname}); foreach my $val (@values) { my $temp=$clones[$i]; $temp =~ s/$varname/$val/g; push(@clones,$temp); $todelete=$i; } splice(@clones,$todelete,1); $i--; # step back in the @clones 'cause we deleted one. more work, but it guarantees it's complete } } } # now actually populate the checks for (my $i=0;$i<=$#clones;$i++) { $TARGETS{$CURRENT_HOST_ID}{total_checks}++; $TESTS{$item[0]}{uri}=$clones[$i]; $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]; } } else # normal, single check { $TARGETS{$CURRENT_HOST_ID}{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("- $TARGETS{$CURRENT_HOST_ID}{total_checks} server checks loaded","v"); if ($TARGETS{$CURRENT_HOST_ID}{total_checks} eq 0) { 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($NIKTO{plugindir},"(^nikto|^db_)"); print "$DIV\n$NIKTO{name} Versions\n$DIV\n"; print "File Version Last Mod\n"; print "----------------------------- -------- ----------\n"; print "Nikto main $NIKTO{version}\n"; print "LibWhisker $LW2::VERSION\n"; foreach my $FILE (sort @NIKTOFILES) { open(FI,"<$NIKTO{plugindir}/$FILE") || die print STDERR "- ERROR: Unable to open '$NIKTO{plugindir}/$FILE': $!\n";;; my @F=; close(FI); my @VERS=grep(/^#VERSION/,@F); my @MODS=grep(/^#LASTMOD/,@F); chomp($VERS[0]); chomp($MODS[0]); $VERS[0] =~ s/^#VERSION,//; $MODS[0] =~ s/^#LASTMOD,//; my $ws1=(35-length($FILE)); my $ws2=(13-length($VERS[0])); print "$FILE", " " x $ws1 , "$VERS[0]", " " x $ws2, "$MODS[0]\n"; } print "$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 ($UPDATES{$ver} eq 1) { 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) { return; } if ($updated_version eq "") { return; } # make sure the db_outdatedb isn't *too* old open(OD,"<$NIKTO{plugindir}/db_outdated") || die print STDERR "- ERROR: Unable to open '$NIKTO{plugindir}/db_outdated': $!\n";; @F=; close(OD); my @LASTUPDATED=grep(/^#LASTMOD/,@F); chomp($LASTUPDATED[0]); $LASTUPDATED[0]=~s/^.*,//; my @lu=split(/\./,$LASTUPDATED[0]); my $lm="$lu[2]$lu[0]"; 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) > 4) { 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'}="www.cirt.net"; 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 updated version string(s) to CIRT.net\n"; } else { print "- Sent updated version string(s) to CIRT.net\n"; } return; } ####################################################################### sub usage { if ($_[0] eq 2) { nprint($NIKTO{options}); } else { nprint($NIKTO{options_short}); } exit; } ####################################################################### sub nikto_core { return; } # trap for this plugin being called to run. lame. ####################################################################### 1;