#VERSION,1.04
#LASTMOD,02.23.2003
# 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.
# this searches google for terms listed in the config file for the given domain
# and summarizes the findings in the output.
# This code based upon the idea in 'mass-scan' by mjm, www.codito.de
# --------------------------------------------------------------------#
# Functions #
# --------------------------------------------------------------------#
sub test_target
{
# print connection details
if ($OUTPUT{debug})
{
dprint("- Initial Server Connect Details:\n");
&dump_result_hash;
}
# this is 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});
vprint("- $RES for $METHD{$CHECKID}:\t$request{whisker}{uri}\n");
if ($RESPS{$CHECKID} =~ /[^0-9]/) # response has text to match--before response code, just in case
{
$RESPS{$CHECKID} =~ s/([^a-zA-Z0-9\s])/\\$1/g; # escaping...
if ($CONTENT =~ /$RESPS{$CHECKID}/) { $VULS++; iprint($CHECKID,$request{whisker}{uri}); $NIKTO{totalokay}++; }
}
# is response 200 or 'found' response, and not match notfound message? or matches checkid response?
elsif ((($RES eq ("200" || $SERVER{found})) && ($RES !~ /$SERVER{notfound}/i)) || ($RES eq $RESPS{$CHECKID}))
{ $VULS++; iprint($CHECKID,$request{whisker}{uri}); $NIKTO{totalokay}++; }
elsif ($RES eq "302")
{
fprint("+ $FILES{$CHECKID} Redirects to '" . $result{'location'} ."', $INFOS{$CHECKID}\n");
$NIKTO{totalmoved}++;
}
elsif (($RES eq "401") && !($NIKTO{suppressauth}))
{
my $R=$result{'www-authenticate'};
$R =~ s/^Basic //i;
$R =~ s/realm=//i;
fprint("+ $FILES{$CHECKID} Needs Auth: (realm $R)\n");
}
# verify we're not getting bogus 200/302 messages
&fp_warning;
# end check loop
sleep $PAUSE;
}
# print any cookies found
if ($OUTPUT{cookies})
{
foreach my $cookie (@COOKIES)
{
$cookie =~ s/\n/ /g;
my @C=split(/--=--/,$cookie);
fprint("+ Got Cookie on file '$C[0]' with value '$C[1]'\n");
}
}
# do this again, at the end so it's obvious. reset OKTRAP.
$OKTRAP=1;
&fp_warning;
if ($VULS eq 1) { fprint("- $ITEMCOUNT items checked, $VULS item found on remote host\n"); }
else { fprint("- $ITEMCOUNT items checked, $VULS items found on remote host\n"); }
&save_output;
}
#################################################################################
sub fp_warning
{
if ($OKTRAP)
{
if ($NIKTO{totalokay} > 30)
{
$OKTRAP=0;
fprint("\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.\n");
}
elsif ($NIKTO{totalmoved} > 30)
{
$OKTRAP=0;
fprint("\n+ Over 30 \"Moved\" messages, this may be a by-product of the
+ server answering all requests with a \"302\" Moved message. You should
+ manually verify your results.\n");
}
}
}
#################################################################################
sub dump_target_info
{
# print out initial connection junk
my $SSLPRINT="";
if ($SERVER{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\n";
}
fprint("$DIV\n");
if ($SERVER{ip} =~ /[a-z]/i) { fprint("+ Target IP: ?? (proxied)\n"); }
else { fprint("+ Target IP: $SERVER{ip}\n"); }
if ($SERVER{hostname} ne "") { fprint("+ Target Hostname: $SERVER{hostname}\n"); }
else { fprint("+ Target Hostname: ?? (unable to resolve)\n"); }
fprint("+ Target Port: $request{'whisker'}{'port'}\n");
if (($SERVER{vhost} ne $SERVER{hostname}) && ($SERVER{vhost} ne ""))
{ fprint("+ Virtual Host: $SERVER{vhost}\n"); }
if ($request{'whisker'}->{'proxy_host'} ne "")
{ fprint("- Proxy: $request{'whisker'}->{'proxy_host'}:$request{'whisker'}->{'proxy_port'}\n"); }
if ($NIKTO{hostid} ne "") { vprint("- Host Auth: $NIKTO{hostid}/$NIKTO{hostpw}\n"); }
if ($SERVER{ssl}) { fprint($SSLPRINT); }
for (my $i=1;$i<=9;$i++) { if ($NIKTO{evasion} =~ /$i/) { fprint("+ Using IDS Evasion:\t$NIKTO{anti_ids}{$i}\n"); }}
fprint("$DIV\n");
if (!($SERVER{forcegen})) { fprint("- Scan is dependent on \"Server\" string which can be faked, use -g to override\n"); }
if ($SERVER{servertype} ne "") { fprint("+ Server: $SERVER{servertype}\n"); }
else { fprint("+ Server ID string not sent\n"); }
return;
}
#################################################################################
sub general_config
{
my ($HOSTAUTH) ="";
## 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 Apache user names (via /~user type requests)";
$CLIOPTS="
Options:
-allcgi force scan of all possible CGI directories
-cookies print cookies found
-evasion+ ids evasion technique (1-9, see below)
-findonly find http(s) ports only, don't perform a full scan
-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+ also 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 is 10 seconds)
-useproxy use the proxy defined in config.txt
-vhost+ virtual host (for Host header)
-webformat write to file in web HTML format
+ requires a value
These options cannot be abbreviated:
-debug debug mode
-dbcheck syntax check scan_database.db and user_scan_database.db
-google use Google search to find files
-update update databases and plugins from cirt.net
-verbose verbose mode
IDS Evasion Techniques:
";
for (my $i=0;$i<=9;$i++) { if ($NIKTO{anti_ids}{$i} eq "") { next; }
$CLIOPTS .= "\t$i\t$NIKTO{anti_ids}{$i}\n"; }
$CLIOPTS .= "\n Mutation Techniques:\n";
for (my $i=0;$i<=9;$i++) { if ($NIKTO{mutate_opts}{$i} eq "") { next; }
$CLIOPTS .= "\t$i\t$NIKTO{mutate_opts}{$i}\n"; }
### CLI STUFF
$PAUSE=$NIKTO{google}=$NIKTO{suppressauth}=$OUTPUT{html}=$OUTPUT{verbose}=$SKIPLOOKUP=$NIKTO{totalmoved}=$NIKTO{totalokay}=$NIKTO{totalrequests}=$ITEMCOUNT=0;
@OPTS=@ARGV;
# preprocess some CLI options
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] =~ /\-google/) { $NIKTO{google}=1; $ARGV[$i]=""; }
elsif ($ARGV[$i] =~ /\-update/) { &check_updates; }
}
GetOptions(
"nolookup" => \$SKIPLOOKUP,
"generic" => \$SERVER{forcegen},
"allcgi" => \$SERVER{forcecgi},
"cookies" => \$OUTPUT{cookies},
"output=s" => \$OUTPUT{file},
"mutate=s" => \$NIKTO{mutate},
"web" => \$OUTPUT{html},
"id=s" => \$HOSTAUTH,
"evasion=s"=> \$NIKTO{evasion},
"port=s" => \$SERVER{port},
"findonly" => \$SERVER{findonly},
"root=s" => \$SERVER{root},
"ssl" => \$SERVER{ssl},
"timeout=s"=> \$TIME,
"x=s" => \$PAUSE,
"useproxy" => \$SERVER{useproxy},
"vhost=s" => \$SERVER{vhost},
"host=s" => \$HOST);
### VARIABLES (STUFF)
@CGIDIRS=split(/ /,$CONFIG{CGIDIRS});
if ($#CGIDIRS < 0) { $CGIDIRS[0]="/cgi-bin/"; }
$OKTRAP=1;
if ($HOSTAUTH ne "")
{
my @t=split(/:/,$HOSTAUTH);
if (($#t ne 1) || ($t[0] eq ""))
{ fprint("+ ERROR: '$HOSTAUTH' (-i option) syntax is 'user:password' for host authentication.\n") }
$NIKTO{hostid}=$t[0];
$NIKTO{hostpw}=$t[1];
}
$NIKTO{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!
$NIKTO{fingerprint}="$NIKTO{name}-$NIKTO{version}-" . LW::utils_randstr() . ".htm";
if ($NIKTO{evasion} ne "") # remove all refs to Nikto/LW
{
$NIKTO{useragent}="Mozilla/4.75";
$NIKTO{fingerprint}=LW::utils_randstr() . ".htm";
}
# SSL Test
if (!$LW::LW_HAS_SSL)
{ fprint("-***** SSL support not available (see docs for SSL install instructions) *****\n"); }
return;
}
#################################################################################
sub host_config
{
if ($SERVER{useproxy}) {
$request{'whisker'}->{'proxy_host'}=$CONFIG{PROXYHOST};
$request{'whisker'}->{'proxy_port'}=$CONFIG{PROXYPORT};
}
my @ports = ();
### HOST STUFF
if ($HOST eq "") { &usage; } # if no target
elsif ($HOST =~ /[^0-9\.]/) # if hostname
{
$SERVER{hostname}=$HOST;
my $ip=gethostbyname($SERVER{hostname});
if (($ip eq "") && ($request{'whisker'}->{'proxy_host'} ne ""))
{ $SERVER{ip}=$SERVER{hostname}; }
elsif (($ip eq "") && ($request{'whisker'}->{'proxy_host'} eq ""))
{ print("+ ERROR: Cannot resolve hostname to IP\n");
exit;
}
else
{ $SERVER{ip}=inet_ntoa($ip); }
}
else # if IP
{
$SERVER{ip}=$HOST;
if (!$SKIPLOOKUP) { my $ip=inet_aton($SERVER{ip}); $SERVER{hostname}=gethostbyaddr($ip,AF_INET); }
}
if (($SERVER{ip} !~ /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/) && ($SERVER{ip} ne $SERVER{hostname})) # trap for proxy...
{
fprint("+ ERROR: Invalid IP '$SERVER{ip}'\n");
exit;
}
# port(s)
$SERVER{port}=~s/^\s+//;
$SERVER{port}=~s/\s+$//;
if ($SERVER{port} eq "") { $SERVER{port}=80; }
if ($SERVER{port} =~ /[^0-9\-\, ]/) { fprint("+ ERROR: Invalid port option '$SERVER{port}'\n"); exit; }
if ($SERVER{port} =~ /[\,\-]/) # need a port scan
{ portscan($SERVER{ip},$SERVER{port}); }
else # just 1 port
{ port_check($SERVER{ip},$SERVER{port}); }
# if no open ports found
my $VALID=0;
foreach $target (keys %TARGETS) { $VALID++; last; }
if (!$VALID) { fprint("+ No HTTP(s) ports were found open on the server.\n"); }
if ($SERVER{findonly}) # no scan!
{
foreach $target (keys %TARGETS)
{
foreach $port ( sort keys %{$TARGETS{$target}} )
{
if ($TARGETS{$target}{$port})
{
if ($BANNERS{$target}{$port} eq "") { $BANNERS{$target}{$port}="(no identification could be made)"; }
fprint("+ Server: https://$target:$port\t$BANNERS{$target}{$port}\n");
}
else { fprint("+ Server: http://$target:$port\t$BANNERS{$target}{$port}\n"); }
}
}
&save_output;
exit;
}
return;
}
#################################################################################
# perform a port scan
#################################################################################
sub portscan
{
my $portopts=$_[1] || return;
my $target=$_[0] || return;
my (@t) = ();
my %portlist;
# if we're using nmap, skip this & let nmap handle port ranges...
if (!(-X $CONFIG{NMAP}))
{
# break out , items
if ($portopts =~ /,/)
{
my @u=split(/\,/,$portopts);
foreach my $x (@u) { push(@t,$x); }
}
else { push(@t,$portopts); }
# ranges
foreach my $x (@t)
{
$x=~s/^\s+//;
$x=~s/\s+$//;
if ($x !~ /-/) { $portlist{$x}=0; }
else
{
my @u=split(/\-/,$x);
for (my $i=$u[0];$i<=$u[1];$i++) { $portlist{$i}=0; }
}
}
# last check for only null lists (i.e., user put in 4-1 as a range)
my $invalid=1;
foreach $p (keys %portlist) { if ($p =~/[0-9]/) { $invalid=0; } last; }
if ($invalid) { fprint("+ ERROR: Invalid port option '$SERVER{port}'\n"); exit; }
} # end if not NMAP
# if NMAP is defined, use that... if not, we do it the hard way
if (-X $CONFIG{NMAP})
{
vprint("- Calling nmap:$CONFIG{NMAP} -oG - -p $portopts $SERVER{ip}\n");
foreach my $line (split(/\n/,`$CONFIG{NMAP} -oG - -p $portopts $SERVER{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...
vprint("- Testing open ports for web servers\n");
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($target,$p);
}
return;
}
#################################################################################
sub port_check
{
my $host=$_[0] || return 0;
my $port=$_[1] || return 0;
$port =~ s/(^\s+|\s+$)//g;
# we don't really need to do this (now), but...
my $oldhost= $request{'whisker'}->{'host'};
$request{'whisker'}->{'host'}=$host;
dprint("- Checking for open port: $port\n");
# if no proxy, try regular socket connection & if it fails, return
if ($request{'whisker'}->{'proxy_host'} eq "")
{
if (!LW::utils_port_open($host,$port)) { $request{'whisker'}->{'host'}=$oldhost; return; }
}
# if the connect succeeded OR there is a proxy set up...
# try http
if (!$SERVER{ssl}) # no force-ssl
{
vprint("- Checking for HTTP on port $port\n");
$request{'whisker'}->{'ssl'}=0;
$request{'whisker'}->{'port'}= $port;
LW::http_fixup_request(\%request);
$request{'whisker'}->{'lowercase_incoming_headers'}=1;
if (!LW::http_do_request(\%request,\%result))
{ $TARGETS{$host}{$port}=0;
$BANNERS{$host}{$port}=$result{'server'};
dprint("- Server found: $host:$port \t$result{'server'}\n");
$request{'whisker'}->{'host'}=$oldhost;
return;
}
}
# if that fails, try https
vprint("- Checking for HTTPS on port $port\n");
$request{'whisker'}->{'ssl'}=1;
$request{whisker}->{save_ssl_info}=1;
$request{'whisker'}->{'port'}= $port;
LW::http_fixup_request(\%request);
$request{'whisker'}->{'lowercase_incoming_headers'}=1;
if (!LW::http_do_request(\%request,\%result))
{ $TARGETS{$host}{$port}=1;
$BANNERS{$host}{$port}=$result{'server'};
$request{'whisker'}->{'host'}=$oldhost;
$SERVER{ssl}=1;
dprint("- Server found: $host:$port \t$result{'server'}\n");
return;
}
return;
}
#################################################################################
# save output to a file
#################################################################################
sub save_output
{
my $linkhost=$SERVER{hostname} || $SERVER{ip};
my $httpstring="http";
if ($SERVER{ssl}) { $httpstring="https"; }
if ($OUTPUT{file} ne "")
{
open(OUT,">>$OUTPUT{file}") || die print "+ ERROR: Unable to open '$OUTPUT{file}' for write: $@\n";
if ($OUTPUT{html}) { print OUT "\n
\n"; }
my $t=join(" ",@OPTS);
if ($OUTPUT{html}) { push(@PRINT,"
CLI Options Executed: $t
\n"); }
else { push(@PRINT,"\nCLI Options Executed: $t\n"); }
if (!$OUTPUT{html})
{
foreach my $line (@PRINT)
{
chomp($line);
$line =~ s/((CVE|CAN)\-[0-9]{4}-[0-9]{4})/http\:\/\/icat\.nist\.gov\/icat\.cfm\?cvename=$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 ($OUTPUT{html}) { print OUT ""; }
print OUT "$line\n";
}
print OUT "$DIV\n";
close(OUT);
}
if ($OUTPUT{html})
{
print OUT "\n";
print OUT "\n";
print OUT "\n";
close(OUT);
}
}
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";
dprint("- Calling plugin:$pluginf\.plugin\n");
# 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 $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{'Host'}="www.cirt.net";
for (my $i=0;$i<=$#ARGV;$i++)
{ if (($ARGV[$i] eq "-u") || ($ARGV[$i] eq "-useproxy")) { $SERVER{useproxy}=1; last; } }
if (($CONFIG{PROXYHOST} ne "") && ($SERVER{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 to get $request{'whisker'}->{'host'}$serverdir/versions.txt\n";
exit; }
# parse into an array
my @CON=split(/\n/,$CONTENT);
foreach my $line (@CON)
{ my @l=parse_csv($line);
if ($line =~ /^msg/) { $remotemsg="$l[1]"; next; }
$REMOTE{$l[0]}=$l[1];
}
# get local versions of plugins/dbs
my @FILES=dirlist($NIKTO{plugindir},"(^nikto|\.db\$)");
foreach my $file (@FILES)
{
my $v="";
open(LOCAL,"<$NIKTO{plugindir}/$file") || print "+ ERROR: Unable to open '$NIKTO{plugindir}/$file' for read: $@\n";
my @l=;
close(LOCAL);
foreach my $line (@l) { if ($line =~ /^#VERSION/) { $v=$line; last; } }
chomp($v);
my @x=parse_csv($v);
$LOCAL{$file}=$x[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); }
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)
{
$updates++;
print "+ Retrieving '$toget'\n";
(my $RES, $CONTENT) = fetch("$serverdir/$toget","GET");
if ($RES ne 200) { print "+ ERROR: Unable to 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);
}
}
if ($updates eq 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/i) && ($result{'www-authenticate'} ne ""))# doh, not basic!
{
my $AUTHTYPE=$result{'www-authenticate'};
$AUTHTYPE =~ s/ .*$//;
fprint("+ ERROR: Host uses '$AUTHTYPE'\n");
fprint("+ Continuing scan without authentication , but suppressing 401 messages.\n");
$NIKTO{suppressauth}=1;
}
elsif ($NIKTO{hostid} eq "")
{
fprint("+ ERROR: No auth credentials for $REALM, please set.\n");
fprint("+ Continuing scan without authentication, but suppressing 401 messages.\n");
$NIKTO{suppressauth}=1;
return;
}
else
{
vprint("- Attempting authorization to $REALM realm.\n");
# 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); # set auth
LW::http_fixup_request(\%request);
LW::http_do_request(\%request,\%result); # test auth
if ($result{'www-authenticate'} eq "") # broken
{ fprint("+ ERROR: Unable to verify authentication to $REALM works (server doesn't respond properly). Nikto is using it blindly.\n"); }
else # test
{
LW::auth_set_header("basic",\%request,$NIKTO{hostid},$NIKTO{hostpw}); # set auth
LW::http_fixup_request(\%request);
LW::http_do_request(\%request,\%result); # test auth
if ($result{'www-authenticate'} ne "")
{
fprint("+ ERROR: Unable to authenticate to $REALM\n");
fprint("+ Continuing scan without authentication, but suppressing 401 messages.\n");
$NIKTO{suppressauth}=1;
}
else { fprint("- Successfully authenticated to realm $REALM.\n"); }
}
}
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
{
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";
# 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_check
# test whether proxy requires authentication, and if we can use it
#################################################################################
sub proxy_check
{
if ($PROXYCHECKED) { return; }
if ($request{'whisker'}->{'proxy_host'} ne "") # proxy is set up
{
LW::http_fixup_request(\%request);
LW::http_do_request(\%request,\%result);
if ($result{'proxy-authenticate'} ne "") # 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 @pauthinfo=split(/ /,$result{'proxy-authenticate'});
fprint("+ Proxy server uses '$pauthinfo[0]' rather than 'Basic' authentication. $NIKTO{name} $NIKTO{version} can't do that.\n");
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/\"$//;
fprint("+ Proxy requires authentication for '$pauthinfo[0]' realm '$pauthinfo2[1]', unable to authenticate.\n");
exit;
}
else { vprint("- Successfully authenticated to proxy.\n"); }
}
}
# these may be duplicates...
$request{'whisker'}->{'method'}="HEAD";
$request{'whisker'}->{'uri'}="/";
$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 = ();
# 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,$file); } }
else { push (@FILES,$file); }
}
closedir(DIRECTORY);
return @FILES;
}
#######################################################################
# dbcheck
# checks the standard databases for duplicate entries
#######################################################################
sub dbcheck {
my (@L, @ENTRIES, %ENTRIES)=();
my ($line, $entry) ="";
my $ctr=0;
print "-->\t$FILES{dbfile}\n";
open(IN,"<$FILES{dbfile}") || die print "\tERROR: Unable to open '$FILES{dbfile}' for read: $@\n";
@ENTRIES=; close(IN);
foreach $line (@ENTRIES)
{
if ($line !~ /^\"/) { next; }
@L=parse_csv($line);
if (($#L < 5) || ($#L > 6)) { print "Invalid syntax ($#L): $line"; next; }
if ($line !~ /^\".*\",\".*\",\".*\",\".*\",\".*\"/) { print "\tERROR: Invalid syntax ($#L): $line\n"; next; }
if (($L[1] =~ /^\@C/) && ($L[1] !~ /^\@CGIDIRS/)) { chomp($line); print "\tERROR: Possible \@CGIDIRS misspelling:$line\n"; }
# build entry based on all except output message
$ENTRIES{"$L[0],$L[1],$L[2],$L[3],$L[4]"}++;
$ctr++;
}
foreach $entry (keys %ENTRIES) { if ($ENTRIES{$entry} > 1) { print "\tERROR: Duplicate ($ENTRIES{$entry}): $entry\n"; } }
print "\t$ctr entries\n";
# user_scan_database.db
if (-e $FILES{userdbfile}) {
print "--> $FILES{userdbfile}\n";
%ENTRIES=();
open(IN,"<$FILES{userdbfile}") || die print "\tERROR: Unable to open '$FILES{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 "Invalid syntax ($#L): $line"; next; }
if ($line !~ /^\".*\",\".*\",\".*\",\".*\",\".*\"/) { print "\tERROR: Invalid syntax ($#L): $line\n"; next; }
if (($L[1] =~ /^\@C/) && ($L[1] !~ /^\@CGIDIRS/)) { chomp($line); print "\tERROR: Possible \@CGIDIRS misspelling:$line\n"; }
# build entry based on all except output message
$ENTRIES{"$L[0],$L[1],$L[2],$L[3],$L[4]"}++;
$ctr++;
}
foreach $entry (keys %ENTRIES) { if ($ENTRIES{$entry} > 1) { print "\tERROR: Duplicate ($ENTRIES{$entry}): $entry\n"; } }
print "\t$ctr entries\n";
}
# outdated.db
$ctr=0;
print "-->\t$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]"}++;
$ctr++;
}
foreach $entry (keys %ENTRIES) { if ($ENTRIES{$entry} > 1) { print "\tERROR: Duplicate ($ENTRIES{$entry}): $entry\n"; } }
print "\t$ctr entries\n";
#server_msgs.db
$ctr=0;
print "-->\t$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; }
$ENTRIES{"$L[0]"}++;
$ctr++;
}
foreach $entry (keys %ENTRIES) { if ($ENTRIES{$entry} > 1) { print "\tERROR: Duplicate ($ENTRIES{$entry}): $entry\n"; } }
print "\t$ctr entries\n";
exit;
}
#######################################################################
# spit out all the details
#######################################################################
sub dump_result_hash
{
dprint("- Result Hash:\n");
foreach my $item (sort keys %result) {if ($item eq "whisker") { next; } dprint("- $item \t\t$result{$item}\n"); }
foreach my $item (sort keys %{$result{'whisker'}}) { dprint("- \$whisker-\>$item \t$result{'whisker'}->{$item}\n"); }
}
#######################################################################
#######################################################################
# spit out all the details
#######################################################################
sub dump_request_hash
{
dprint("- Request Hash:\n");
foreach my $item (sort keys %request) { if ($item eq "whisker") { next; } dprint("- $item \t$request{$item}\n"); }
foreach my $item (sort keys %{$request{'whisker'}}) { dprint("- $item \t$request{'whisker'}->{$item}\n"); }
}
#######################################################################
# 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
#
($SERVER{notfound}, $CONTENT)=fetch("/$NIKTO{fingerprint}","GET");
if (($SERVER{notfound} eq "400") || ($SERVER{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"; }
fprint("- Server did not understand HTTP $old, switching to HTTP $request{'whisker'}->{'http_ver'}\n");
($SERVER{notfound}, $CONTENT)=fetch("/$NIKTO{fingerprint}","GET");
}
if (($SERVER{notfound} ne "404") && ($SERVER{notfound} ne "401"))
{
fprint("+ Server does not respond with '404' for error messages (uses '$SERVER{notfound}').\n");
fprint("+ This may increase false-positives.\n");
if ($SERVER{notfound} eq "302") { fprint("+ Not found files redirect to: $result{'location'}\n"); }
if ($CONTENT =~ /not found/i) { $SERVER{notfound}="not found"; } # shorten it, content has "not found" in it
elsif ($CONTENT =~ /404/i) { $SERVER{notfound}="404"; } # shorten it, content has "404" in it
else { $SERVER{notfound} = $CONTENT; }
}
# get OK response (200)
($SERVER{found}, $CONTENT)=fetch("/","GET");
if ($SERVER{found} eq 404) # assume server does not actually have a / & set it to 200
{
$SERVER{found}=200;
vprint("+ No root document found, assuming 200 is OK response.\n");
}
elsif ($SERVER{found} != 200)
{
if ($SERVER{found} eq "302")
{
fprint("+ The root file (/) redirects to: $result{'location'}\n");
# try to get redirected location to see if 200 is actually the valid response
($SERVER{found}, $CONTENT)=fetch($result{'location'},"GET");
if ($SERVER{found} ne 200) # still no good... just a 302, stop going in circles
{ $SERVER{found}=302; }
}
}
if ($SERVER{found} eq "401") { &auth_check; }
# if they're the same, something is amiss... just pick a 404/200 scheme, nothing better to do
if ($SERVER{notfound} eq $SERVER{found})
{
if ($SERVER{notfound} ne "401") {
fprint("+ The found & not found messages appear to be the same, be skeptical of positives.\n");
}
$SERVER{notfound}=404; $SERVER{found}=200;
}
return;
}
#######################################################################
# just get the banner, nothing major
# banner_get
#######################################################################
sub banner_get
{
(my $TEMP, $CONTENT)=fetch("/","GET");
$SERVER{servertype}=$result{'server'}||$result{'proxy-agent'};
return;
}
#######################################################################
# figure out CGI directories
# check_cgi
#######################################################################
sub check_cgi
{
my ($gotvalid,$gotinvalid)=0;
my @POSSIBLECGI=();
my ($res, $possiblecgidir) ="";
#force all possible CGI directories to be "true"
if (!$SERVER{forcecgi})
{
foreach $possiblecgidir (@CGIDIRS)
{
($res, $CONTENT)=fetch($possiblecgidir,"GET");
dprint("Checked for CGI dir\t$possiblecgidir\tgot:$res\n");
if (($res eq 302) || ($res eq 200) || ($res eq 403)) {
push(@POSSIBLECGI,$possiblecgidir);
$gotvalid++;
}
}
if ($gotvalid eq 0)
{
fprint("+ No CGI Directories found (use -a to force check all possible dirs)\n");
@CGIDIRS=();
}
elsif ($#CGIDIRS eq $#POSSIBLECGI)
{
fprint("+ All CGI directories 'found'--assuming invalid responses and using none (use -a to force check all possible dirs)\n");
@CGIDIRS=();
}
else { @CGIDIRS=@POSSIBLECGI; }
} # end !$SERVER{forcecgi}
vprint("- Checking for CGI in: @CGIDIRS\n");
return @CGIDIRS;
}
#######################################################################
# get a page
# fetch URI, METHOD
#######################################################################
sub fetch
{
my $uri=$_[0] || return;
&LW::http_reset;
delete $result{'whisker'}->{'data'};
if ($uri eq "//") { $uri="/"; } # trap for some weird ones
$request{'whisker'}->{'method'} = $_[1] || "GET";
$request{'whisker'}->{'uri'} = $uri;
if (($_[2] ne "") && ($_[2] ne " "))
{ my $x=$_[2];
$x=~s/\\\"/\"/g;
$request{'whisker'}->{'data'} = $x;
}
else { delete $request{'whisker'}->{'Content-Length'}; }
$NIKTO{totalrequests}++;
LW::http_fixup_request(\%request);
$request{'whisker'}->{'uri_orig'}=$request{'whisker'}->{'uri'}; # for anti-ids encoding
&dump_request_hash;
LW::http_do_request(\%request,\%result);
&dump_result_hash;
if (exists($result{'set-cookie'})) { push(@COOKIES,"/--=--$result{'set-cookie'}"); }
$request{'whisker'}->{'data'}="";
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,"<$FILES{dbfile}") || die print "+ ERROR: Unable to open '$FILES{dbfile}' for read: $@\n";
@DBFILE=;
close(IN);
open(IN,"<$FILES{serverdbfile}") || die print "+ ERROR: Unable to open '$FILES{serverdbfile}' for read: $@\n";
@SERVERFILE=;
close(IN);
# load a user database if it exists...
if (-e $FILES{userdbfile})
{
open(IN,"<$FILES{userdbfile}") || die print "+ ERROR: Unable to open '$FILES{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);
dprint("servercat compare: '$SERVER{servertype}' to '$scat[1]'\n");
if ($SERVER{servertype} =~ /$scat[1]/i)
{
$SERVER{category}=$scat[0];
dprint("servercat match:$scat[0]\n");
last;
}
}
}
if ($SERVER{category} eq "") { $SERVER{category}="generic"; }
return;
}
#######################################################################
# set up the scan database
#######################################################################
sub set_scan_items
{
&set_server_cats;
my $shname=$SERVER{hostname} || $SERVER{ip};
my ($line, $stype) = "";
my (@item, @scat, $FILES, $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 (($SERVER{category} =~ /$item[0]/i) || ($item[0] =~ /generic/i) || ($SERVER{servertype} eq "") || ($SERVER{forcegen}))
{
# substitute for @IP, @HOSTNAME in check
for (my $i=1;$i<=$#item;$i++)
{
chomp($item[$i]);
if ($i eq 3) { next; } # skip method
$item[$i] =~ s/\@IP/$SERVER{ip}/g;
$item[$i] =~ s/\@HOSTNAME/$shname/g;
if ($item[$i] =~ /(JUNK\([0-9]+\))/) # junk overflow
{
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)) { dprint("Invalid check syntax:@item:\n"); }
if ($item[1] =~ /^\@CGIDIRS/) # multiple checks in one
{
$item[1] =~ s/^\@CGIDIRS//;
foreach my $CGI (@CGIDIRS)
{
$ITEMCOUNT++;
$FILES{$ITEMCOUNT}="$SERVER{root}$CGI$item[1]";
$RESPS{$ITEMCOUNT}=$item[2];
$METHD{$ITEMCOUNT}=$item[3];
$INFOS{$ITEMCOUNT}=$item[4];
$DATAS{$ITEMCOUNT}=$item[5];
}
}
else # normal, single check
{
$ITEMCOUNT++;
$FILES{$ITEMCOUNT}="$SERVER{root}$item[1]";
$RESPS{$ITEMCOUNT}=$item[2];
$METHD{$ITEMCOUNT}=$item[3];
$INFOS{$ITEMCOUNT}=$item[4];
$DATAS{$ITEMCOUNT}=$item[5];
}
}
}
}
vprint("- Server category identified as '$SERVER{category}', if this is not correct please use -g to force a generic scan.\n");
vprint("- $ITEMCOUNT server checks loaded\n");
if ($ITEMCOUNT eq 0) { fprint("+ Unable to load valid checks!\n"); exit; }
if ($SERVER{forcegen} eq 0) { vprint("+ Forcing full DB scan"); }
return;
}
#######################################################################
# Print standard item information
# iprint CHECKID
#######################################################################
sub iprint
{
print "+ $_[1] - $INFOS{$_[0]} ($METHD{$_[0]})\n";
push(@PRINT,"+ $_[1] - $INFOS{$_[0]} ($METHD{$_[0]})\n");
return;
}
#######################################################################
# print debug information
# dprint 'string'
#######################################################################
sub dprint
{
if ($OUTPUT{debug}) { print $_[0]; }
return;
}
#######################################################################
# print verbose information
# vprint 'string'
#######################################################################
sub vprint
{
if ($OUTPUT{verbose}) { print $_[0]; }
return;
}
#######################################################################
# basic print (but pushes to @PRINT for file save)
# fprint 'string'
#######################################################################
sub fprint
{
push(@PRINT,$_[0]);
print $_[0];
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];
my @new = ();
push(@new, $+) while $text =~ m{
"([^\"\\]*(?:\\.[^\"\\]*)*)",?
| ([^,]+),?
| ,
}gx;
push(@new, undef) if substr($text, -1,1) eq ',';
return @new;
}
#######################################################################
# print usage info
#######################################################################
sub usage
{
fprint("$CLIOPTS");
exit;
}
#######################################################################
sub nikto_core { return; } # trap for this plugin being called to run
#######################################################################
1;