#VERSION,1.31 #LASTMOD,01.20.2005 # Nikto "core" functions # This software is distributed under the terms of the GPL, which should have been received # with a copy of this software in the "LICENSE.txt" file. # --------------------------------------------------------------------# # Functions # # --------------------------------------------------------------------# sub test_target { dump_result_hash(); # this is the actual the looped code for all the checks for (my $CHECKID=1;$CHECKID<=$ITEMCOUNT;$CHECKID++) { (my $RES, $CONTENT) = fetch($FILES{$CHECKID},$METHD{$CHECKID},$DATAS{$CHECKID}); nprint("- $RES for $METHD{$CHECKID}:\t$request{whisker}{uri}","v"); # this is a kluge unti the DB is restructured for more conditional tests my @rcodes = split(/\!/,$RESPS{$CHECKID}); $RESPS{$CHECKID} = $rcodes[0]; # this will also have to change once we allow regex's in the DB $RESPS{$CHECKID} =~ s/([^a-zA-Z0-9\s])/\\$1/g; # do auth/redir first, independent of test pass/fail if (($RES eq 401) && !($NIKTO{suppressauth})) { my $R=$result{'www-authenticate'}; $R =~ s/^basic //i; $R =~ s/realm=//i; nprint("+ $FILES{$CHECKID} - Needs Auth: (realm $R)"); } elsif (($RES eq 302) || ($RES eq 301)) { nprint("+ $FILES{$CHECKID} - Redirects to " . $result{'location'} ." , $INFOS{$CHECKID}") unless !$OKTRAP; $NIKTO{totalmoved}++; } if ($RESPS{$CHECKID} =~ /[^0-9]/) # response has text to match { if ($CONTENT =~ /$RESPS{$CHECKID}/) # test passed { if (($rcodes[1] eq "") || ($CONTENT !~ /$rcodes[1]/)) # conditional passed or is blank { $VULS++; nprint("+ $request{whisker}{uri} - $INFOS{$CHECKID} ($METHD{$CHECKID})"); $NIKTO{totalokay}++; } } } elsif (($RES eq $RESPS{$CHECKID}) || ((($RES eq $TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{found}) || ($RES eq 200)) && ($RES !~ /$TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{notfound}/i)) ) { if (($rcodes[1] eq "") || ($CONTENT !~ /$rcodes[1]/)) # conditional passed or is blank { $VULS++; nprint("+ $request{whisker}{uri} - $INFOS{$CHECKID} ($METHD{$CHECKID})"); $NIKTO{totalokay}++; } } # verify we're not getting bogus 200/302 messages bogus_responses(); # end check loop if ($CLI{pause} > 0) { sleep $CLI{pause}; } } # print any cookies found if ($CLI{cookies}) { foreach my $cookie (@COOKIES) { $cookie =~ s/\n/ /g; my @C=split(/--=--/,$cookie); nprint("+ Got Cookie on file '$C[0]' - value '$C[1]'"); } } # do this again, at the end so it's obvious. reset OKTRAP. $OKTRAP=1; bogus_responses(); nprint("+ $ITEMCOUNT items checked - $VULS item(s) found on remote host(s)"); $TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{end_time} = time(); my $diff = $TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{end_time} - $TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{start_time}; my $end = localtime($TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{end_time}); nprint("+ End Time: $end ($diff seconds)"); nprint($DIV); return; } ################################################################################# sub bogus_responses { if ($OKTRAP) { if ($NIKTO{totalokay} > $CONFIG{MAX_WARN}) { $OKTRAP=0; nprint("\n+ Over 30 \"OK\" messages, this may be a by-product of the + server answering all requests with a \"200 OK\" message. You should + manually verify your results."); } elsif ($NIKTO{totalmoved} > $CONFIG{MAX_WARN}) { $OKTRAP=0; nprint("\n+ Over 30 \"Moved\" messages, this may be a by-product of the + server answering all requests with a \"302\" or \"301\" Moved message. You should + manually verify your results."); } } } ################################################################################# sub dump_target_info { # print out initial connection junk my $SSLPRINT=""; $TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{start_time}=time(); my $start=localtime($TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{start_time}); if ($TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{ssl}) { my $SSLCIPHERS=$result{whisker}{ssl_cipher} || "Enabled"; 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<=9;$i++) { if ($CLI{evasion} =~ /$i/) { nprint("+ Using IDS Evasion:\t$NIKTO{anti_ids}{$i}"); }} nprint("+ Start Time: $start"); nprint($DIV); if (!($CLI{forcegen})) { nprint("- Scan is dependent on \"Server\" string which can be faked, use -g to override"); } if ($TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{banner} ne "") { nprint("+ Server: $TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{banner}"); } else { nprint("+ Server ID string not sent"); } 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}="Random case sensitivity"; $NIKTO{anti_ids}{8}="Use Windows directory separator (\\)"; $NIKTO{anti_ids}{9}="Session splicing"; $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{options}=" Options: -Cgidirs+ Scan these CGI dirs: 'none', 'all', or a value like '/cgi/' -cookies print cookies found -evasion+ ids evasion technique (1-9, see below) -findonly find http(s) ports only, don't perform a full scan -Format save file (-o) Format: htm, csv or txt (assumed) -generic force full (generic) scan -host+ target host -id+ host authentication to use, format is userid:password -mutate+ mutate checks (see below) -nolookup skip name lookup -output+ write output to this file -port+ port to use (default 80) -root+ prepend root value to all requests, format is /directory -ssl force ssl mode on port -timeout timeout (default 10 seconds) -useproxy use the proxy defined in config.txt -Version print plugin and database versions -vhost+ virtual host (for Host header) + requires a value These options cannot be abbreviated: -debug debug mode -dbcheck syntax check scan_database.db and user_scan_database.db -update update databases and plugins from cirt.net -verbose verbose mode IDS Evasion Techniques: "; for (my $i=0;$i<=(keys %{$NIKTO{anti_ids}});$i++) { if ($NIKTO{anti_ids}{$i} eq "") { next; } $NIKTO{options} .= "\t$i\t$NIKTO{anti_ids}{$i}\n"; } $NIKTO{options} .= "\n Mutation Techniques:\n"; for (my $i=0;$i<=(keys %{$NIKTO{mutate_opts}});$i++) { if ($NIKTO{mutate_opts}{$i} eq "") { next; } $NIKTO{options} .= "\t$i\t$NIKTO{mutate_opts}{$i}\n"; } ### CLI STUFF $CLI{pause}=$NIKTO{suppressauth}=$CLI{html}=$OUTPUT{verbose}=$CLI{skiplookup}=$NIKTO{totalmoved}=$NIKTO{totalokay}=$NIKTO{totalrequests}=$ITEMCOUNT=0; @OPTS=@ARGV; # preprocess CLI options which cannot be abbreviated for (my $i=0;$i<=$#ARGV;$i++) { if ($ARGV[$i] =~ /\-dbcheck/) { dbcheck(); } elsif ($ARGV[$i] =~ /\-verbose/) { $OUTPUT{verbose}=1; $ARGV[$i]=""; } elsif ($ARGV[$i] =~ /\-debug/) { $OUTPUT{debug}=1; $ARGV[$i]=""; } elsif ($ARGV[$i] =~ /\-update/) { check_updates(); } } GetOptions( "nolookup" => \$CLI{skiplookup}, "generic" => \$CLI{forcegen}, "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}, "x=s" => \$CLI{pause}, "ssl" => \$CLI{ssl}, "useproxy" => \$CLI{useproxy}, "vhost=s" => \$CLI{vhost}, "host=s" => \$CLI{host}, "cookies" => \$CLI{cookies}, "output=s" => \$CLI{file}, "Format=s" => \$CLI{format}, "Verbose" => \$CLI{verbose} ); if ($CLI{verbose}) { version(); } # 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"; } 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"; } else { nprint("+ ERROR: Invalid output format '$CLI{format}'"); exit; } # 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; } # VARIABLES (STUFF) if (length($CONFIG{"\@CGIDIRS"}) < 1) { $CONFIG{"\@CGIDIRS"}="/cgi-bin/"; } # populate %VARIABLES foreach my $key (keys %CONFIG) { if ($key =~ /^\@/) { $VARIABLES{$key}=$CONFIG{"$key"}; } } # Fixup $CLI{root} =~ s/\/$//; if (($CLI{root} !~ /^\//) && ($CLI{root} ne "")) { $CLI{root} = "/$CLI{root}"; } $OKTRAP=1; 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'})"; # here's the fingerprint -- this should always be something which will NOT be found on the server! if ($CLI{evasion} ne "") # remove all refs to Nikto/LW { $NIKTO{useragent}="Mozilla/4.75"; $NIKTO{fingerprint}="/" . LW::utils_randstr() . ".htm"; } else { $NIKTO{fingerprint}="/$NIKTO{name}-$NIKTO{version}-" . LW::utils_randstr() . ".htm"; } # SSL Test if (!$LW::LW_HAS_SSL) { nprint("-***** SSL support not available (see docs for SSL install instructions) *****"); } # 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}); $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 { if ($CLI{host} eq "") { nprint("+ ERROR: No host specified"); usage(); } my $host_ctr=1; # if -p is not set, see if each line hast 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 "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 @h=split(/\:|\,/); $TARGETS{$host_ctr}{ident}=$h[0]; # preppend anything from CLI -- careful not to duplcate port 80 (default) my $p=""; my $has_eighty=0; for (my $i=1;$i<=$#h;$i++) { $p.="$h[$i],"; if ($h[$i] eq 80) { $has_eighty=1;} } if (($has_eighty eq 0) || ($CLI{ports} ne 80)) { $p = "$CLI{ports},$p"; } $p=~s/\,$//; $TARGETS{$host_ctr}{ports_in}=$p; } else { $TARGETS{$host_ctr}{ports_in} = $CLI{ports}; if ($TARGETS{$host_ctr}{ports_in} eq "") { $TARGETS{$host_ctr}{ports_in} = 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); } # if it's not a file, set it as the ident & set the ports. resolve() will figure out if its name or IP # if -p is not set, push 80 to ports_in else { $COUNTERS{hosts_total}++; $TARGETS{$host_ctr}{ident}=$CLI{host}; if ($CLI{ports} eq "") { $TARGETS{$host_ctr}{ports_in}=80;} else { $TARGETS{$host_ctr}{ports_in}=$CLI{ports}; } nprint("- Target id:$host_ctr:ident:$TARGETS{$host_ctr}{ident}:ports_in:$TARGETS{$host_ctr}{ports_in}:","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 $CONFIG{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 $CONFIG{NMAP}) && !$CLI{useproxy}) { nprint("- Calling nmap:$CONFIG{NMAP} $CONFIG{NMAPOPTS} -oG - -p $portopts $TARGETS{$CURRENT_HOST_ID}{ip}","v"); foreach my $line (split(/\n/,`$CONFIG{NMAP} $CONFIG{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... nprint("- Testing open ports for web servers","v"); foreach $p (sort keys %portlist) { if ($p !~ /[0-9]/) { next; } $p =~ s/\s+$//; $p =~ s/^\s+//; foreach my $skip (split(/ /,$CONFIG{SKIPPORTS})) { if ($skip eq $p) { $p=""; last; } } if ($p eq "") { next; } port_check($p); } return; } ################################################################################# sub get_banner { (my $RES, $CONTENT) = fetch("/","HEAD"); $TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{banner}=$result{'server'}; return; } ################################################################################# sub port_check { my $port=$_[0] || return 0; $port=~s/(^\s+|\s+$)//g; my $oldhost=$request{'whisker'}->{'host'}; $request{'whisker'}->{'uri'}="/"; $request{'whisker'}->{'method'}="HEAD"; # test for proxy proxy_check() unless $PROXYCHECKED; # try http nprint("- Checking for HTTP on port $TARGETS{$CURRENT_HOST_ID}{ip}:$port","v"); if (!$CLI{ssl}) { $request{'whisker'}->{'ssl'}=0; $request{'whisker'}->{'port'}= $port; LW::http_fixup_request(\%request); dump_request_hash(); if (!LW::http_do_request(\%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_result_hash(); return; } } else { dump_result_hash(); } } # try https nprint("- Checking for HTTPS on port $TARGETS{$CURRENT_HOST_ID}{ip}:$port","v"); $request{'whisker'}->{'ssl'}=1; $request{whisker}->{save_ssl_info}=1; $request{'whisker'}->{'port'}= $port; LW::http_fixup_request(\%request); dump_request_hash(); if (!LW::http_do_request(\%request,\%result)) { $TARGETS{$CURRENT_HOST_ID}{ports}{$port}{ssl}=1; $request{'whisker'}->{'host'}=$oldhost; nprint("- Server found: $TARGETS{$CURRENT_HOST_ID}{ip}:$port \t$result{'server'}","d"); dump_result_hash(); return; } else { dump_result_hash(); } return; } ################################################################################# sub open_output { if ($CLI{file} eq "") { return; } open(OUT,">>$CLI{file}") || die print "+ ERROR: Unable to open '$CLI{file}' for write: $@\n"; select(OUT); $|++; select(STDERR); $|++; select(STDOUT); $|++; if ($CLI{format} =~ /^htm/) { print OUT "\n\n"; print OUT "$NIKTO{name} Results\n"; print OUT "\n"; print OUT "
$NIKTO{name} v$NIKTO{version}/$NIKTO{core_version}
\n"; print OUT "CIRT.net
\n"; } elsif ($CLI{format} =~ /^txt/) { print OUT "- $NIKTO{name} v$NIKTO{version}/$NIKTO{core_version}\n"; } elsif ($CLI{format} =~ /^csv/) { print OUT "\"$NIKTO{name} v$NIKTO{version}\",\"Core v$NIKTO{core_version}\"\n"; } return; } ################################################################################# sub close_output { my $t=join(" ",@OPTS); if ($CLI{format} =~ /^htm/) { print OUT "Test Options: $t
\n"; print OUT "
\n\n"; } elsif ($CLI{format} =~ /^txt/) { print OUT "\nTest Options: $t\n"; print OUT "$DIV\n"; } elsif ($CLI{format} =~ /^csv/) { print OUT "\n\"Test Options\",\"$t\"\n"; } close(OUT); return; } ################################################################################# # print the output & write to the save file ################################################################################# sub nprint { my $line=$_[0]; chomp($line); # don't print debug & verbose to output file... if ($_[1] eq "d" && $OUTPUT{debug}) { print "D: $line\n"; return; } # debug if ($_[1] eq "v" && $OUTPUT{verbose}) { print "V: $line\n"; return; } # verbose if ($_[1] eq "v" || $_[1] eq "d") { return; } # print to STDOUT print "$line\n"; # if no file saving, return if ($CLI{file} eq "") { return; } if ($CLI{format} eq "txt") # plaintext output { $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} =~ /csv/) # if csv output { chomp($line); if ($line =~ /-------------/) { $line="\"$DIV\",\"$DIV\""; } $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; 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"; } elsif ($CLI{format} =~ /^htm/) # if htm(l) output { $protocol="http"; if ($TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{ssl}) { $protocol .= "s"; } if ($line =~ /------------/) { print OUT "
\n"; return; } elsif ($line =~ /^\+ Target IP/) { $line =~ s/$TARGETS{$CURRENT_HOST_ID}{ip}/$TARGETS{$CURRENT_HOST_ID}{ip}<\/a>/; } elsif ($line =~ /^\+ Target Host/) { $line =~ s/$TARGETS{$CURRENT_HOST_ID}{hostname}/$TARGETS{$CURRENT_HOST_ID}{hostname}<\/a>/; } elsif ($line =~ /^\+ Target Port/) { $line =~ s/$CURRENT_PORT/$CURRENT_PORT<\/a>/; } elsif ($line =~ /^\+ \//) # item { if ($line =~ /\((GET|TRACE|TRACK|Needs Auth)\)/) { my @TEMP=split(/ /,$line); my $r=$TEMP[1]; my $disp=$r; $disp =~ s/\/\>\;/g; $TEMP[1] =~ s/([^a-zA-Z0-9\s])/\\$1/g; $line =~ s/$TEMP[1]/$disp<\/a>/; } $line =~ s/[^\"]http(s)?\:\/\/[\w\.\!\@\~\#\$\%\^\&\*\(\)\_\-\=\+\'\.\,\/\?\\\{\}\;\:]+/$&<\/a>/ig; $line =~ s/to$1<\/a>/g; $line =~ s/(CA\-[0-9]{4}-[0-9]{2})/$1<\/a>/g; $line =~ s/BID\-([0-9]{4})/$1<\/a>/g; $line =~ s/(IN\-[0-9]{4}\-[0-9]{2})/$1<\/a>/gi; $line =~ s/(MS[0-9]{2}\-[0-9]{3})/$1<\/a>/gi; } $line =~ s/^\- /
  • /; $line =~ s/^\+ /
  • /; print OUT "$line
    \n"; } return; } ################################################################################# # run_plugins # load plugins & run them # 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; } require "$NIKTO{plugindir}/$pluginf\.plugin"; nprint("- Calling plugin:$pluginf\.plugin","d"); # just call it...hope it works...taint doesn't like this very much for obvious reasons &$pluginf; } return; } ################################################################################# # check_updates ################################################################################# sub check_updates { LW::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'}->{'http_ver'}="1.1"; $request{'whisker'}->{'port'}=80; $request{'whisker'}->{'anti_ids'}=""; $request{'User-Agent'}="Nikto Update Agent"; $request{'whisker'}->{'host'}="www.cirt.net"; for (my $i=0;$i<=$#ARGV;$i++) { if (($ARGV[$i] eq "-u") || ($ARGV[$i] eq "-useproxy")) { $CLI{useproxy}=1; last; } } if (($CONFIG{PROXYHOST} ne "") && ($CLI{useproxy})) { $request{'whisker'}->{'proxy_host'}=$CONFIG{PROXYHOST}; $request{'whisker'}->{'proxy_port'}=$CONFIG{PROXYPORT}; } my $ip=gethostbyname($server); if ($ip ne "") { $request{'whisker'}->{'host'}= inet_ntoa($ip); } else { $request{'whisker'}->{'host'}=$server; } # retrieve versions file LW::http_fixup_request(\%request); (my $RES, $CONTENT) = fetch("$serverdir/versions.txt","GET"); if ($RES eq 407) # requires Auth { if ($CONFIG{PROXYUSER} eq "") { $CONFIG{PROXYUSER}=read_data("Proxy ID: ",""); $CONFIG{PROXYPASS}=read_data("Proxy Pass: ","noecho"); } LW::auth_set_header("proxy-basic",\%request,$CONFIG{PROXYUSER},$CONFIG{PROXYPASS}); # and try again LW::http_fixup_request(\%request); ($RES, $CONTENT) = fetch("$serverdir/versions.txt","GET"); } if ($RES ne 200) { print "+ 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},"(^nikto|\.db\$)"); foreach my $file (@NIKTOFILES) { my $v=""; open(LOCAL,"<$NIKTO{plugindir}/$file") || print "+ 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 "+ 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 "+ ERROR: Unable to get $server$serverdir/$toget\n"; exit; } if ($CONTENT ne "") { open(OUT,">$NIKTO{plugindir}/$toget") || die print "+ 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 "+ 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; } ################################################################################# # auth_check # if the server requires authentication & we have it... ################################################################################# sub auth_check { my $REALM=$result{'www-authenticate'}; $REALM =~ s/^Basic //i; $REALM =~ s/realm=//i; if ($REALM eq "") { $REALM="unnamed"; } if (($result{'www-authenticate'} !~ /basic|ntlm/i) && ($result{'www-authenticate'} ne ""))# doh, not basic! { my $AUTHTYPE=$result{'www-authenticate'}; $AUTHTYPE =~ s/ .*$//; nprint("+ ERROR: Host uses '$AUTHTYPE'"); nprint("+ Continuing scan without authentication , but suppressing 401 messages."); $NIKTO{suppressauth}=1; } elsif ($NIKTO{hostid} eq "") { nprint("+ ERROR: No auth credentials for $REALM, please set."); nprint("+ Continuing scan without authentication, but suppressing 401 messages."); $NIKTO{suppressauth}=1; return; } else { nprint("- Attempting authorization to $REALM realm.","v"); # check for 'broken' web server, returns a blank www-auth header no matter what the id/pw sent my $tid=LW::utils_randstr(); LW::auth_set_header("basic",\%request,$tid,$tid,$NIKTO{hostdomain}); # set auth LW::http_fixup_request(\%request); LW::http_do_request(\%request,\%result); # test auth if ($result{'www-authenticate'} eq "") # broken { nprint("+ ERROR: Unable to verify authentication to $REALM works (server doesn't respond properly). Nikto is using it blindly."); } else # test { LW::auth_set_header("basic",\%request,$NIKTO{hostid},$NIKTO{hostpw},$NIKTO{hostdomain}); # set auth LW::http_fixup_request(\%request); LW::http_do_request(\%request,\%result); # test auth if ($result{'www-authenticate'} ne "") { nprint("+ ERROR: Unable to authenticate to $REALM"); nprint("+ Continuing scan without authentication, but suppressing 401 messages."); $NIKTO{suppressauth}=1; } 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 ($CONFIG{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); return $input; } else # non-POSIX { print $prompt; $input=; chomp($input); return $input; } return; } ################################################################################# # proxy_setuup # Setup the required proxy stuff ################################################################################# sub proxy_setup { if (!$CLI{useproxy}) { return; } # HTTP proxy $request{'whisker'}->{'proxy_host'}=$CONFIG{PROXYHOST}; $request{'whisker'}->{'proxy_port'}=$CONFIG{PROXYPORT}; # SOCKS return; } ################################################################################# # proxy_check # test whether proxy requires authentication, and if we can use it ################################################################################# sub proxy_check { $request{'whisker'}->{'method'}="HEAD"; $request{'whisker'}->{'uri'}="/"; if ($request{'whisker'}->{'proxy_host'} ne "") # proxy is set up { LW::http_fixup_request(\%request); LW::http_do_request(\%request,\%result); if ($result{'whisker'}{'http_resp'} eq "407") # proxy requires auth { # have id/pw? if ($CONFIG{PROXYUSER} eq "") { $CONFIG{PROXYUSER}=read_data("Proxy ID: ",""); $CONFIG{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... LW::auth_set_header("proxy-basic",\%request,$CONFIG{PROXYUSER},$CONFIG{PROXYPASS}); # set auth LW::http_fixup_request(\%request); LW::http_do_request(\%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; } ################################################################################# # directory listing # 'pattern' is an optional regex to match the file names against # written by Thomas Reucker for the SETI-Web project (GPL) ################################################################################# sub dirlist { my $DIR=$_[0] || return; my $PATTERN=$_[1] || ""; my @FILES_TEMP = (); # some basic security checks... REALLY basic # this should be better if ($DIR =~ /etc/) { return; } opendir(DIRECTORY,$DIR) || die print "+ 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_TEMP,$file); } } else { push (@FILES_TEMP,$file); } } closedir(DIRECTORY); return @FILES_TEMP; } ####################################################################### # dbcheck # checks the standard databases for duplicate entries ####################################################################### sub dbcheck { my (@L, @ENTRIES, %ENTRIES)=(); my ($line, $entry) =""; my $ctr=0; #### scan_database.db print "-->\tDB Syntax ($NIKTOFILES{dbfile})\n"; open(IN,"<$NIKTOFILES{dbfile}") || die print "\tERROR: Unable to open '$NIKTOFILES{dbfile}' for read: $@\n"; @ENTRIES=; close(IN); foreach $line (@ENTRIES) { if ($line !~ /^\"/) { next; } @L=parse_csv($line); if (($#L < 5) || ($#L > 6)) { print "\tERROR: Invalid syntax ($#L): $line"; next; } if ($line !~ /^\".*\",\".*\",\".*\",\".*\",\".*\"/) { print "\tERROR: Invalid syntax ($#L): $line\n"; next; } if (($L[1] =~ /^\@CGI/) && ($L[1] !~ /^\@CGIDIRS/)) { print "\tERROR: Possible \@CGIDIRS misspelling:$line"; } if ($line =~ /[^\\]\"\"/) { print "\tERROR: Possible double-quote syntax error:$line"; } # build entry based on all except output message $ENTRIES{"$L[0],$L[1],$L[2],$L[3],$L[5]"}++; } foreach $entry (keys %ENTRIES) { if ($ENTRIES{$entry} > 1) { print "\tERROR: Duplicate ($ENTRIES{$entry}): $entry\n"; } } $ctr=keys(%ENTRIES); print "\t$ctr entries\n"; #### user_scan_database.db if (-e $NIKTOFILES{userdbfile}) { print "--> DB Syntax ($NIKTOFILES{userdbfile})\n"; %ENTRIES=(); open(IN,"<$NIKTOFILES{userdbfile}") || die print "\tERROR: Unable to open '$NIKTOFILES{userdbfile}' for read: $@\n"; @ENTRIES=; close(IN); $ctr=0; foreach $line (@ENTRIES) { if ($line !~ /^\"/) { next; } @L=parse_csv($line); if (($#L < 5) || ($#L > 6)) { print "\tERROR: User DB: Invalid syntax ($#L): $line"; next; } if ($line !~ /^\".*\",\".*\",\".*\",\".*\",\".*\"/) { print "\tERROR: User DB: Invalid syntax ($#L): $line\n"; next; } if (($L[1] =~ /^\@CGI/) && ($L[1] !~ /^\@CGIDIRS/)) { print "\tERROR: User DB: Possible \@CGIDIRS misspelling:$line"; } if ($line =~ /[^\\]\"\"/) { print "\tERROR: User DB: Possible double-quote syntax error:$line"; } # build entry based on all except output message $ENTRIES{"$L[0],$L[1],$L[2],$L[3],$L[5]"}++; } foreach $entry (keys %ENTRIES) { if ($ENTRIES{$entry} > 1) { print "\tERROR: Duplicate ($ENTRIES{$entry}): $entry\n"; } } $ctr=keys(%ENTRIES); print "\t$ctr entries\n"; } #### outdated.db $ctr=0; print "-->\tDB Syntax ($NIKTO{plugindir}/outdated.db)\n"; %ENTRIES=(); open(IN,"<$NIKTO{plugindir}/outdated.db") || die print "\tERROR: Unable to open '$NIKTO{plugindir}/outdated.db' for read: $@\n"; @ENTRIES=; close(IN); foreach $line (@ENTRIES) { $line =~ s/^\s+//; if ($line =~ /^\#/) { next; } chomp($line); if ($line eq "") { next; } @L=parse_csv($line); if ($line !~ /^\".*\"\,\".*\"\,\".*\"$/) { print "\tERROR: Invalid syntax ($#L): $line\n"; next; } if ($#L ne 2) { print "\tERROR: Invalid syntax ($#L): $line\n"; next; } $ENTRIES{"$L[0]"}++; } foreach $entry (keys %ENTRIES) { if ($ENTRIES{$entry} > 1) { print "\tERROR: Duplicate ($ENTRIES{$entry}): $entry\n"; } } $ctr=keys(%ENTRIES); print "\t$ctr entries\n"; #### server_msgs.db $ctr=0; print "-->\tDB Syntax ($NIKTO{plugindir}/server_messages.db)\n"; %ENTRIES=(); open(IN,"<$NIKTO{plugindir}/server_msgs.db") || die print "\tERROR: Unable to open '$NIKTO{plugindir}/server_msgs.db' for read: $@\n"; @ENTRIES=; close(IN); foreach $line (@ENTRIES) { $line =~ s/^\s+//; if ($line =~ /^\#/) { next; } chomp($line); if ($line eq "") { next; } @L=parse_csv($line); if ($line !~ /^\".*\"\,\".*\"$/) { print "\tERROR: Invalid syntax ($#L): $line\n"; next; } if ($#L ne 1) { print "\tERROR: Invalid syntax ($#L): $line\n"; next; } # test regex "test" =~ /$L[0]/; $ENTRIES{"$L[0]"}++; } foreach $entry (keys %ENTRIES) { if ($ENTRIES{$entry} > 1) { print "\tERROR: Duplicate ($ENTRIES{$entry}): $entry\n"; } } $ctr=keys(%ENTRIES); print "\t$ctr entries\n"; #### check that all plugins are in nikto_plugin_order.txt print "-->\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 "\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 "\tERROR: plugin '$p' not in nikto_plugin_order.txt\n"; } } if (!$bad) { print "\tOrder file okay\n"; } #### check that all plugins are named properly print "-->\tPlugin conventions ($NIKTO{plugindir}/*.plugin)\n"; my $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 "\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 "\tERROR: file '$pluginf\.plugin' does not have 'sub $pluginf' defined.\n"; $bad++; } } if (!$bad) { print "\tPlugin syntax okay\n"; } exit; } ####################################################################### # spit out all the details ####################################################################### sub dump_result_hash { if (!$OUTPUT{debug}) { return; } # quick return nprint("- Result Hash:","d"); foreach my $item (sort keys %result) { if ($item eq "whisker") { next; } nprint("- $item \t\t$result{$item}","d"); } foreach my $item (sort keys %{$result{'whisker'}}) { nprint("- \$whisker-\>$item \t$result{'whisker'}->{$item}","d"); } return; } ####################################################################### # spit out all the details ####################################################################### sub dump_request_hash { if (!$OUTPUT{debug}) { return; } # quick return nprint("- Request Hash:","d"); foreach my $item (sort keys %request) { if ($item eq "whisker") { next; } nprint("- $item: $request{$item}","d"); } foreach my $item (sort keys %{$request{'whisker'}}) { if ($item eq "http_eol") { next; } nprint("- \$whisker-\>$item: $request{'whisker'}->{$item}","d"); } return; } ####################################################################### # check_responses # check what the 200/404 messages are... ####################################################################### sub check_responses { # get NOT FOUND response (404) # check for compliant 404 message # check for common strings to use as 'not found' matches from content ($TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{notfound}, $CONTENT)=fetch("$NIKTO{fingerprint}","GET"); if (($TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{notfound} eq "400") || ($TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{notfound} eq "")) # may need to use HTTP/1.? { my $old=$request{'whisker'}->{'http_ver'}; if ($request{'whisker'}->{'http_ver'} eq "1.1") { $request{'whisker'}->{'http_ver'}="1.0"; } else { $request{'whisker'}->{'http_ver'}="1.1"; } nprint("- Server did not understand HTTP $old, switching to HTTP $request{'whisker'}->{'http_ver'}"); ($TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{notfound}, $CONTENT)=fetch("$NIKTO{fingerprint}","GET"); } if (($TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{notfound} ne "404") && ($TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{notfound} ne "401")) { nprint("+ Server does not respond with '404' for error messages (uses '$TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{notfound}')."); nprint("+ This may increase false-positives."); if ($TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{notfound} eq "302") { nprint("+ Not found files redirect to: $result{'location'}"); } elsif ($TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{notfound} eq "301") { nprint("+ Not found files redirect to: $result{'location'}"); } if ($CONTENT =~ /not found/i) { $TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{notfound}="not found"; } # shorten it, content has "not found" in it elsif ($CONTENT =~ /404/i) { $TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{notfound}="404"; } # shorten it, content has "404" in it else { $TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{notfound} = $CONTENT; } } # get OK response (200) ($TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{found}, $CONTENT)=fetch("/","GET"); if ($TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{found} eq 404) # assume server does not actually have a / & set it to 200 { $TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{found}=200; nprint("+ No root document found, assuming 200 is OK response.","v"); } elsif ($TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{found} != 200) { if (($TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{found} eq "302") || ($TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{found} eq 301)) { nprint("+ The root file (/) redirects to: $result{'location'}"); # try to get redirected location to see if 200 is actually the valid response ($TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{found}, $CONTENT)=fetch($result{'location'},"GET"); #if ($TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{found} ne 200) # still no good... just a 302, stop going in circles # { $TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{found}=302; } } } if ($TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{found} eq "401") { auth_check(); } # if they're the same, something is amiss... just pick a 404/200 scheme, nothing better to do # except 403... which will cut down false positives if ($TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{notfound} eq $TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{found}) { if ($TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{notfound} ne "401") { nprint("+ The found & not found messages appear to be the same, be skeptical of positives."); } $TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{notfound}=404 unless $TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{notfound} eq 403; $TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{found}=200; } return; } ####################################################################### # figure out CGI directories # check_cgi ####################################################################### 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) || ($res eq 301)) { 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"} = @CFGCGI; } else { $VARIABLES{"\@CGIDIRS"} = join(" ",@POSSIBLECGI); } } # end !$CLI{forcecgi} nprint("- Checking for CGI in: $VARIABLES{\"\@CGIDIRS\"}","v"); return; } ####################################################################### # get a page # fetch URI, METHOD ####################################################################### sub fetch { if ($_[0] eq "") { return; } $request{'whisker'}->{'uri'} = "$CLI{root}$_[0]"; # Append -root option value $request{'whisker'}->{'method'} = $_[1]; LW::http_reset(); $request{'whisker'}->{'data'}=""; if ($_[2] ne "" && $_[2] ne " ") { $request{'whisker'}->{'data'} = $_[2]; $request{'whisker'}->{'data'} =~ s/\\\"/\"/g; } else { delete $request{'whisker'}->{'Content-Length'}; } $NIKTO{totalrequests}++; LW::http_fixup_request(\%request); dump_request_hash(); LW::http_do_request(\%request,\%result); dump_result_hash(); if (exists($result{'set-cookie'})) { push(@COOKIES,"/--=--$result{'set-cookie'}"); } return $result{'whisker'}->{'http_resp'}, $result{'whisker'}->{'data'}; } ####################################################################### # return $_[0] 'x' characters ####################################################################### sub junk { return "x" x $_[0]; } ####################################################################### # load the scan database ####################################################################### sub load_scan_items { open(IN,"<$NIKTOFILES{dbfile}") || die print "+ ERROR: Unable to open '$NIKTOFILES{dbfile}' for read: $@\n"; @DBFILE=; close(IN); open(IN,"<$NIKTOFILES{serverdbfile}") || die print "+ ERROR: Unable to open '$NIKTOFILES{serverdbfile}' for read: $@\n"; @SERVERFILE=; close(IN); # load a user database if it exists... if (-e $NIKTOFILES{userdbfile}) { open(IN,"<$NIKTOFILES{userdbfile}") || die print "+ ERROR: Unable to open '$NIKTOFILES{userdbfile}' for read: $@\n"; my @DBFILE_USER=; close(IN); # join them... foreach $line (@DBFILE_USER) { push(@DBFILE,$line); } } return; } ####################################################################### # get server categories ####################################################################### sub set_server_cats { # first figure out server type foreach $line (@SERVERFILE) { if ($line =~ /^\"/) { if ($line =~ /\#/) { $line=~s/\#.*$//; $line=~s/\s+$//; } chomp($line); @scat=parse_csv($line); nprint("servercat compare: '$TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{banner}' to '$scat[1]'","d"); if ($TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{banner} =~ /$scat[1]/i) { $TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{category}=$scat[0]; nprint("servercat match:$scat[0]","d"); last; } } } if ($TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{category} eq "") { $TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{category}="generic"; } return; } ####################################################################### # set up the scan database ####################################################################### sub set_scan_items { set_server_cats(); my $shname=$TARGETS{$CURRENT_HOST_ID}{hostname} || $TARGETS{$CURRENT_HOST_ID}{ip}; my ($line, $stype) = ""; my (@item, @scat, $RESPS, $METHD, $INFOS, $DATAS) = (); $ITEMCOUNT=0; # now load checks foreach $line (@DBFILE) { if ($line =~ /^\"/) # check { chomp($line); @item=parse_csv($line); # if the right category or cat is generic... if (($TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{category} eq $item[0]) || ($item[0] =~ /generic/i) || ($TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{banner} eq "") || ($CLI{forcegen})) { # substitute for @IP, @HOSTNAME in check $line =~ s/\@IP/$TARGETS{$CURRENT_HOST_ID}{ip}/g; $line =~ s/\@HOSTNAME/$shname/g; for (my $i=1;$i<=$#item;$i++) { chomp($item[$i]); if ($i eq 3) { next; } # skip method if ($item[$i] =~ /(JUNK\([0-9]+\))/) # junk text { my $j= my $m=$1; $j=~ s/^JUNK\(//; $j=~ s/\)//; $j=junk($j); $m=~s/([^a-zA-Z0-9])/\\$1/g; $item[$i] =~ s/$m/$j/; } } if ($item[1] eq "") { $item[2]="/"; } if (($#item < 4) || ($#item > 6)) { nprint("Invalid check syntax:@item:","d"); } # Build the check items. First check for any @ values to replace # this nasty set of loops allows for multiple values per line if ($item[1] =~ /^\@/) # multiple checks in one { my @clones=(); my $todelete=""; push(@clones,$item[1]); 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++) { $ITEMCOUNT++; $FILES{$ITEMCOUNT}=$clones[$i]; $RESPS{$ITEMCOUNT}=$item[2]; $METHD{$ITEMCOUNT}=$item[3]; $INFOS{$ITEMCOUNT}=$item[4]; $DATAS{$ITEMCOUNT}=$item[5]; } } else # normal, single check { $ITEMCOUNT++; $FILES{$ITEMCOUNT}=$item[1]; $RESPS{$ITEMCOUNT}=$item[2]; $METHD{$ITEMCOUNT}=$item[3]; $INFOS{$ITEMCOUNT}=$item[4]; $DATAS{$ITEMCOUNT}=$item[5]; } } } } nprint("- Server category identified as '$TARGETS{$CURRENT_HOST_ID}{ports}{$CURRENT_PORT}{category}', if this is not correct please use -g to force a generic scan.","v"); nprint("- $ITEMCOUNT server checks loaded","v"); if ($ITEMCOUNT eq 0) { nprint("+ Unable to load valid checks!"); exit; } if ($CLI{forcegen} eq 0) { nprint("+ Forcing full DB scan","v"); } return; } ####################################################################### # escape all non standard chars in a string (for regex's really) # char_escape 'string' ####################################################################### sub char_escape { my $text=$_[0]||return; $text =~ s/([^a-zA-Z0-9 ])/\\$1/g; return $text; } ####################################################################### # turn CSV data to an array # parse_csv 'string' ####################################################################### 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; } ####################################################################### # print version info ####################################################################### 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 $LW::VERSION\n"; my $col1=35; my $col2=13; foreach my $FILE (@NIKTOFILES) { open(FI,"<$NIKTO{plugindir}/$FILE") || die print "- 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=($col1-length($FILE)); my $ws2=($col2-length($VERS[0])); print "$FILE", " " x $ws1 , "$VERS[0]", " " x $ws2, "$MODS[0]\n"; } print "$DIV\n"; exit; } ####################################################################### # send version update info to CIRT.net, if desired ####################################################################### sub send_updates { if ($CONFIG{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 outdated.db isn't *too* old open(OD,"<$NIKTO{plugindir}/outdated.db") || die print "- ERROR: Unable to open '$NIKTO{plugindir}/outdated.db': $!\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 ($CONFIG{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; } LW::http_init_request(\%request); my $server="www.cirt.net"; $request{'whisker'}->{'http_ver'}="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 (($CONFIG{PROXYHOST} ne "") && ($CLI{useproxy})) { $request{'whisker'}->{'proxy_host'}=$CONFIG{PROXYHOST}; $request{'whisker'}->{'proxy_port'}=$CONFIG{PROXYPORT}; } # send data LW::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 "") && ($CONFIG{PROXYHOST} ne "")) { $request{'whisker'}->{'proxy_host'}=$CONFIG{PROXYHOST}; $request{'whisker'}->{'proxy_port'}=$CONFIG{PROXYPORT}; ($RES, $CONTENT) = fetch("/cgi-bin/versions?DATA=$updated_version","GET"); } if ($RES eq 407) # requires Auth { if ($CONFIG{PROXYUSER} eq "") { $CONFIG{PROXYUSER}=read_data("Proxy ID: ",""); $CONFIG{PROXYPASS}=read_data("Proxy Pass: ","noecho"); } LW::auth_set_header("proxy-basic",\%request,$CONFIG{PROXYUSER},$CONFIG{PROXYPASS}); # and try again LW::http_fixup_request(\%request); ($RES, $CONTENT) = fetch("/cgi-bin/versions?DATA=$updated_version","GET"); } if ($CONTENT !~ /SUCCESS/) { print "- 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; } ####################################################################### # print usage info ####################################################################### sub usage { nprint($NIKTO{options}); exit; } ####################################################################### sub nikto_core { return; } # trap for this plugin being called to run ####################################################################### 1;