.*?| References<\/td>.*? | #TEMPL_REFERENCES#<\/td>.*?<\/tr>/#TEMPL_REFERENCES_ROW_REPLACE#/s;
}
# Now process main template with all variables (including processed SSL row replacements)
foreach my $var (keys %variables) {
my $replacement = $variables{$var};
# Escape $ in replacement to prevent backreference interpretation ($1, $&, etc.)
$replacement =~ s/\$/\$\$/g;
$template =~ s/\Q$var\E/$replacement/g;
}
return $template;
}
###############################################################################
sub linkify_refs {
my $refs = $_[0] || return;
my @rs = split(/ /, $refs);
for (my $i = 0 ; $i <= $#rs ; $i++) {
$r = $rs[$i];
my $escaped_r = simple_enc($r);
if ($r =~ /^OSVDB-(\d+)$/) {
my $id = $1;
$r = "$escaped_r";
}
elsif ($r =~ /^CVE-\d{4}-\d{3,4}/) {
my $escaped_url_r = simple_enc($r);
$r =
"$escaped_r";
}
elsif ($r =~ /^MS-\d+-\d+/i) {
my $escaped_url_r = simple_enc($r);
$r =
"$escaped_r";
}
elsif ($r =~ /^(CA-\d{4}-\d{2})/) {
my $escaped_ca = simple_enc($1);
$r =
"$escaped_r";
}
elsif ($r =~ /^CWE-\d+/) {
my $escaped_url_r = simple_enc($r);
$r =
"$escaped_r";
}
elsif ($r =~ /^http/) {
my $escaped_url_r = simple_enc($r);
$r = "$escaped_r";
}
$rs[$i] = $r;
}
my $out = join("", @rs);
$out =~ s/ $//;
return $out;
}
###############################################################################
sub simple_enc {
my $var = $_[0] || return;
$var =~ s/&/&/g;
$var =~ s/</g;
$var =~ s/>/>/g;
$var =~ s/"/"/g;
$var =~ s/'/'/g;
return $var;
}
1;
================================================
FILE: program/plugins/nikto_report_json.plugin
================================================
###############################################################################
# SPDX-License-Identifier: GPL-3.0-only
# PURPOSE: JSON Reporting - Multi-host support
###############################################################################
our $JSONRPT_ALL = []; # Arrayref to hold all hosts' reports
our $JSONRPT_CURR = undef; # Scalarref to current host's report
use JSON;
use Time::Piece;
use Time::Seconds;
###############################################################################
sub nikto_report_json_init {
use JSON;
my $id = { name => "report_json",
full_name => "JSON reports",
author => "Sullo",
description => "Produces a JSON report.",
report_head => \&json_open,
report_host_start => \&json_host_start,
report_host_end => \&json_host_end,
report_close => \&json_close,
report_item => \&json_item,
report_ssl_info => \&json_ssl_info,
report_format => 'json',
copyright => "2025 Chris Sullo"
};
return $id;
}
###############################################################################
# open output file
sub json_open {
my ($file) = @_;
# Open file with lexical handle (read-write mode for JSON)
my $fh;
open($fh, "+>", $file) || die "+ ERROR: Unable to open '$file' for write: $@\n";
# Enable autoflush
$fh->autoflush(1);
return $fh;
}
###############################################################################
# start output for a host
sub json_host_start {
my ($handle, $mark) = @_;
# Get current time with timezone
my $current_time = localtime;
my $start_time = $mark->{'start_time'} ? localtime($mark->{'start_time'}) : $current_time;
$JSONRPT_CURR = { host => $mark->{'vhost'} ? $mark->{'vhost'} : $mark->{'hostname'},
ip => $mark->{'ip'},
port => $mark->{'port'},
server_banner => $mark->{'banner'},
start_time => $start_time->strftime('%Y-%m-%d %H:%M:%S %z'),
vulnerabilities => []
};
# Add current host report to the array of all hosts
push @$JSONRPT_ALL, $JSONRPT_CURR;
return;
}
###############################################################################
# write SSL info
sub json_ssl_info {
my ($handle, $mark) = @_;
# Update the current host report with SSL info
return unless defined $JSONRPT_CURR;
# Extract CN from subject
my $cn = '';
if ($mark->{'ssl_cert_subject'} =~ /CN=([^$ \/]+)/) {
$cn = $1;
}
$JSONRPT_CURR->{'ssl_info'} = { ciphers => $mark->{'ssl_cipher'} || '',
issuer => $mark->{'ssl_cert_issuer'} || '',
subject => $mark->{'ssl_cert_subject'} || '',
cn => $cn,
altnames => $mark->{'ssl_cert_altnames'} || ''
};
}
###############################################################################
# end output for a host
sub json_host_end {
my ($handle, $mark) = @_;
my $end_time;
if ($mark->{'end_time'} eq '') {
$end_time = localtime;
}
else {
$end_time = localtime($mark->{'end_time'});
}
$JSONRPT_CURR->{'end_time'} = $end_time->strftime('%Y-%m-%d %H:%M:%S %z');
return;
}
###############################################################################
# close output file
sub json_close {
my ($handle, $mark) = @_;
my $json_encoder = JSON->new->utf8->canonical->pretty->convert_blessed;
my $json_output = $json_encoder->encode($JSONRPT_ALL);
print $handle $json_output;
close($handle);
return;
}
###############################################################################
# print an item
sub json_item {
my ($handle, $mark, $item) = @_;
my $uri = $item->{'uri'};
if (($uri ne '') && ($mark->{'root'} ne '') && ($uri !~ /^$mark->{'root'}/)) {
$uri = $mark->{'root'} . $uri;
}
my $msg = $item->{'message'};
my $uri2 = quotemeta($uri);
my $root = quotemeta($mark->{'root'});
$msg =~ s/^$uri2:\s//;
$msg =~ s/^$root$uri2:\s//;
push @{ $JSONRPT_CURR->{'vulnerabilities'} },
{ id => $item->{'nikto_id'},
references => $item->{'refs'},
method => $item->{'method'},
url => $uri,
msg => $msg
};
}
1;
================================================
FILE: program/plugins/nikto_report_sqlg.plugin
================================================
###############################################################################
# SPDX-License-Identifier: GPL-3.0-only
# PURPOSE: SQL Reporting
###############################################################################
# Sample table create (mysql):
# create table nikto_table (id int(11) not null auto_increment primary key,
# scanid varchar(32), testid varchar(6) not null, ip varchar(15),
# hostname text, port int(5), tls tinyint(1), refs text, httpmethod text,
# uri text, message text, request blob, response mediumblob);
# See documentation/nikto_schema_mysql.sql and nikto_schema_postgresql.sql for complete schemas
###############################################################################
sub nikto_report_sqlg_init {
my $id = { name => "report_sqlg",
full_name => "Generic SQL reports",
author => "Sullo",
description => "Produces SQL inserts into a generic database.",
report_head => \&sqlg_open,
report_host_start => \&sqlg_host_start,
report_item => \&sqlg_item,
report_ssl_info => \&sqlg_ssl_info,
report_format => 'sql',
copyright => "2013 Chris Sullo"
};
return $id;
}
###############################################################################
# open output file
sub sqlg_open {
my ($file) = @_;
print STDERR "+ ERROR: Output file not specified.\n" if $file eq '';
# Open file with lexical handle and produce header
my $fh;
open($fh, ">>", $file) || die print STDERR "+ ERROR: Unable to open '$file' for write: $@\n";
# Enable autoflush
$fh->autoflush(1);
# Write header
my $opt = $CLI{'all_options'};
$opt =~ s/'/\\'/g;
print $fh "# $VARIABLES{'name'} - v$VARIABLES{'version'}/$VARIABLES{'core_version'}\n";
print $fh "# Options: $opt\n";
print $fh "# Start Time: " . localtime($COUNTERS{'scan_start'}) . "\n";
print $fh "# End Time: " . localtime($COUNTERS{'scan_end'}) . "\n";
print $fh "\n";
return $fh;
}
###############################################################################
# start output
sub sqlg_host_start {
my ($handle, $mark) = @_;
my $banner = $mark->{'banner'} || '';
my $ssl = 0;
if (defined $mark->{'ssl_cipher'}) { $ssl = 1; }
my $hostname = $mark->{'vhost'} ? $mark->{'vhost'} : $mark->{'hostname'};
my $scanid = $mark->{'scanid'} || '';
my $msg = $banner ne '' ? "Server banner: $banner" : "Host scan started";
my $ip = $mark->{'ip'};
$ip =~ s/'/\\'/g;
$hostname =~ s/'/\\'/g;
$msg =~ s/'/\\'/g;
$scanid =~ s/'/\\'/g;
my $sql =
"insert into nikto_table (scanid, testid, ip, hostname, port, tls, refs, httpmethod, uri, message) values(";
$sql .=
"'$scanid','999958','$ip','$hostname','$mark->{'port'}','$ssl','0','GET','/','$msg');\n";
print $handle $sql;
return;
}
###############################################################################
# write SSL info
sub sqlg_ssl_info {
my ($handle, $mark) = @_;
my $hostname = $mark->{'vhost'} ? $mark->{'vhost'} : $mark->{'hostname'};
my $ssl_cipher = $mark->{'ssl_cipher'} || '';
my $ssl_subject = $mark->{'ssl_cert_subject'} || '';
my $ssl_issuer = $mark->{'ssl_cert_issuer'} || '';
my $ssl_altnames = $mark->{'ssl_cert_altnames'} || '';
my $ssl = defined $mark->{'ssl_cipher'} ? 1 : 0;
# Extract CN from subject
my $ssl_cn = '';
if ($ssl_subject =~ /CN=([^$ \/]+)/) {
$ssl_cn = $1;
}
# Build combined SSL info message
my $ssl_message = "SSL/TLS Information - ";
$ssl_message .= "Subject: $ssl_subject" if $ssl_subject ne '';
$ssl_message .= "; CN: $ssl_cn" if $ssl_cn ne '';
$ssl_message .= "; SAN: $ssl_altnames" if $ssl_altnames ne '';
$ssl_message .= "; Ciphers: $ssl_cipher" if $ssl_cipher ne '';
$ssl_message .= "; Issuer: $ssl_issuer" if $ssl_issuer ne '';
$ssl_message =~ s/'/\\'/g;
# Single INSERT statement with all SSL info
my $sql =
"insert into nikto_table (scanid, testid, ip, hostname, port, tls, refs, httpmethod, uri, message) values(";
$sql .=
"'$mark->{'scanid'}','000137','$mark->{'ip'}','$hostname','$mark->{'port'}','$ssl','0','GET','/','$ssl_message');\n";
print $handle $sql;
}
###############################################################################
# print an item
sub sqlg_item {
my ($handle, $mark, $item) = @_;
foreach my $uri (split(' ', $item->{'uri'})) {
my $hostname = $mark->{'vhost'} ? $mark->{'vhost'} : $mark->{'hostname'};
$hostname = quotemeta($hostname);
my $httpmethod = quotemeta($item->{'method'});
my $msg = quotemeta($item->{'message'});
my $root = quotemeta($mark->{'root'});
my $rootq = quotemeta($mark->{'root'}); # temporary, just for regex
$uri = quotemeta($uri);
my $ssl = $mark->{'ssl_cipher'} ? 1 : 0;
# Get scanid (generated by core in report_host_start)
my $scanid = $item->{'mark'}->{'scanid'} || '';
$scanid =~ s/'/\\'/g; # Escape single quotes
my $sql =
"insert into nikto_table (scanid, testid, ip, hostname, port, tls, refs, httpmethod, uri, message, request, response) values(";
$sql .=
"'$scanid','$item->{'nikto_id'}','$item->{'mark'}->{'ip'}','$hostname','$item->{'mark'}->{'port'}','$ssl',";
$sql .= "'$item->{'refs'}','$httpmethod',";
if (($uri ne '') && ($root ne '') && ($uri !~ /^$rootq/)) {
$sql .= "'" . $root . $uri . "',";
}
else {
$sql .= "'$uri',";
}
$msg =~ s/^$uri:\s//;
$msg =~ s/^$rootq$uri:\s//;
$sql .= "'$msg',";
# Rebuild the request from the hash -- no need to escape as it's base64 encoded
my $req = rebuild_request($item->{'request'}, 1, 48000);
$sql .= "'" . LW2::encode_base64($req, '') . "',";
# response content
my $response = rebuild_response($$item{'response'}, 1, 12000000);
$sql .= "'" . LW2::encode_base64($response, '') . "');";
print $handle "$sql\n";
}
}
1;
================================================
FILE: program/plugins/nikto_report_text.plugin
================================================
###############################################################################
# SPDX-License-Identifier: GPL-3.0-only
# PURPOSE: Text Reporting
###############################################################################
sub nikto_report_text_init {
my $id = { name => "report_text",
full_name => "Text reports",
author => "Tautology",
description => "Produces a text report.",
report_head => \&text_open,
report_host_start => \&text_host,
report_item => \&text_item,
report_ssl_info => \&text_ssl_info,
report_format => 'txt',
copyright => "2008 Chris Sullo"
};
return $id;
}
sub text_open {
my ($file) = @_;
print STDERR "+ ERROR: Output file not specified.\n" if $file eq '';
# Open file with lexical handle and produce header
my $fh;
open($fh, ">>", $file) || die print STDERR "+ ERROR: Unable to open '$file' for write: $@\n";
# Enable autoflush
$fh->autoflush(1);
# Write header
print $fh "- $VARIABLES{'name'} v$VARIABLES{'version'}/$VARIABLES{'core_version'}\n";
return $fh;
}
sub text_host {
my ($handle, $mark) = @_;
my ($curr_host, $curr_port);
my $hostname = $mark->{'vhost'} ? $mark->{'vhost'} : $mark->{'hostname'};
print $handle "+ Target Host: $hostname\n";
print $handle "+ Target Port: $mark->{port}\n";
}
sub text_ssl_info {
my ($handle, $mark) = @_;
print $handle "+ SSL Info: Subject: $mark->{'ssl_cert_subject'}\n";
# Extract and display CN separately
my $cn = '';
if ($mark->{'ssl_cert_subject'} =~ /CN=([^$ \/]+)/) {
$cn = $1;
print $handle " CN: $cn\n";
}
# Display SAN if present
if ($mark->{'ssl_cert_altnames'} ne '') {
print $handle " SAN: $mark->{'ssl_cert_altnames'}\n";
}
print $handle " Ciphers: $mark->{'ssl_cipher'}\n";
print $handle " Issuer: $mark->{'ssl_cert_issuer'}\n";
}
sub text_item {
my ($handle, $mark, $item) = @_;
foreach my $uri (split(' ', $item->{uri})) {
my $line = "+ ";
if ($item->{method}) { $line .= $item->{method} . " " }
if (($uri ne '') && ($uri !~ /^$mark->{'root'}/)) {
$line .= $mark->{'root'} . $uri . ": ";
}
$line .= $item->{message};
if ($item->{refs} ne "") { $line .= " See: " . $item->{refs} . ": " }
print $handle "$line\n";
}
}
1;
================================================
FILE: program/plugins/nikto_report_xml.plugin
================================================
###############################################################################
# SPDX-License-Identifier: GPL-3.0-only
# PURPOSE: XML Reporting - Multi-host support with proper XML handling
###############################################################################
our $XMLRPT_ALL = []; # Arrayref to hold all hosts' reports
our $XMLRPT_CURR = undef; # Scalarref to current host's report
our $XML_WRITER = undef; # XML::Writer instance
our $XML_HANDLE = undef; # File handle
use XML::Writer;
use Time::Piece;
use Time::Seconds;
###############################################################################
sub nikto_report_xml_init {
my $id = { name => "report_xml",
full_name => "XML reports (v2)",
author => "Sullo",
description => "Produces a proper XML report with validation.",
report_head => \&xml_open,
report_host_start => \&xml_host_start,
report_host_end => \&xml_host_end,
report_close => \&xml_close,
report_item => \&xml_item,
report_ssl_info => \&xml_ssl_info,
report_format => 'xml',
copyright => "2025 Chris Sullo"
};
return $id;
}
###############################################################################
# open output file
sub xml_open {
my ($file) = @_;
print STDERR "+ ERROR: Output file not specified.\n" if $file eq '';
# Open file with lexical handle for writing
my $fh;
open($fh, ">>", $file) || die print STDERR "+ ERROR: Unable to open '$file' for write: $@\n";
# Enable autoflush
$fh->autoflush(1);
# Store lexical handle reference in package variable
$XML_HANDLE = $fh;
# Initialize XML writer with proper settings
$XML_WRITER = XML::Writer->new(OUTPUT => $fh,
DATA_MODE => 1,
DATA_INDENT => 2,
ENCODING => 'UTF-8',
UNSAFE => 0 # Ensure proper escaping
);
# Write XML declaration
$XML_WRITER->xmlDecl('UTF-8');
# Write DOCTYPE if DTD is defined
if (defined $CONFIGFILE{'NIKTODTD'} && $CONFIGFILE{'NIKTODTD'} ne '') {
# Resolve DTD path relative to Nikto execution directory
my $dtd_path = $CONFIGFILE{'NIKTODTD'};
if ($dtd_path !~ /^\// && defined $CONFIGFILE{'EXECDIR'}) {
$dtd_path = "$CONFIGFILE{'EXECDIR'}/$dtd_path";
}
$XML_WRITER->doctype('niktoscans', 'SYSTEM', $dtd_path);
}
# Start root element
$XML_WRITER->startTag('niktoscans');
nprint("- XML report initialized with proper encoding and structure", "v", "report_xml");
return $fh;
}
###############################################################################
# start host entry
sub xml_host_start {
my ($handle, $mark) = @_;
# Initialize current host report
$XMLRPT_CURR = { targetip => $mark->{'ip'},
targethostname => $mark->{'hostname'},
targetport => $mark->{'port'},
targetbanner => $mark->{'banner'} || '',
starttime => date_disp($mark->{'start_time'}),
sitename => '',
siteip => '',
hostheader => $mark->{'vhost'} || $mark->{'hostname'},
errors => $mark->{'total_errors'} || 0,
checks => $COUNTERS{'total_checks'} || 0,
items => [],
ssl_info => undef
};
# Build site URLs
my $protocol = $mark->{'ssl'} ? 'https' : 'http';
my $hostname = $mark->{'vhost'} || $mark->{'hostname'};
$XMLRPT_CURR->{'siteip'} = "$protocol://$mark->{'ip'}:$mark->{'port'}$mark->{'root'}";
$XMLRPT_CURR->{'siteip'} .= '/' unless $XMLRPT_CURR->{'siteip'} =~ /\/$/;
if ($hostname ne '') {
$XMLRPT_CURR->{'sitename'} = "$protocol://$hostname:$mark->{'port'}$mark->{'root'}";
$XMLRPT_CURR->{'sitename'} .= '/' unless $XMLRPT_CURR->{'sitename'} =~ /\/$/;
}
else {
$XMLRPT_CURR->{'sitename'} = 'N/A';
}
push(@$XMLRPT_ALL, $XMLRPT_CURR);
# Write niktoscan start tag with all required attributes
$XML_WRITER->startTag('niktoscan',
hoststest => $COUNTERS{'hosts_completed'} || 0,
options => $CLI{'all_options'} || '',
version => $VARIABLES{'version'} || 'unknown',
scanstart => localtime($COUNTERS{'scan_start'}) || '',
scanend => localtime($COUNTERS{'scan_end'}) || '',
scanelapsed => ($COUNTERS{'scan_elapsed'} || 0),
nxmlversion => "1.2"
);
# Write scandetails start tag
$XML_WRITER->startTag('scandetails',
targetip => $mark->{'ip'},
targethostname => $mark->{'hostname'},
targetport => $mark->{'port'},
targetbanner => $mark->{'banner'} || '',
starttime => date_disp($mark->{'start_time'}),
sitename => $XMLRPT_CURR->{'sitename'},
siteip => $XMLRPT_CURR->{'siteip'},
hostheader => $XMLRPT_CURR->{'hostheader'},
errors => $XMLRPT_CURR->{'errors'},
checks => $XMLRPT_CURR->{'checks'}
);
nprint("- XML host entry started for $mark->{'hostname'}", "v", "report_xml");
}
###############################################################################
# write SSL info
sub xml_ssl_info {
my ($handle, $mark) = @_;
# Extract CN from subject for separate reporting
my $cn = '';
if ($mark->{'ssl_cert_subject'} =~ /CN=([^$ \/]+)/) {
$cn = $1;
}
# Store SSL info in current host report
$XMLRPT_CURR->{'ssl_info'} = { ciphers => $mark->{'ssl_cipher'},
issuers => $mark->{'ssl_cert_issuer'} || '',
info => $mark->{'ssl_cert_subject'} || '',
cn => $cn,
altnames => $mark->{'ssl_cert_altnames'} || ''
};
# Write SSL info tag immediately
$XML_WRITER->emptyTag('ssl',
ciphers => $XMLRPT_CURR->{'ssl_info'}->{'ciphers'},
issuers => $XMLRPT_CURR->{'ssl_info'}->{'issuers'},
info => $XMLRPT_CURR->{'ssl_info'}->{'info'},
cn => $XMLRPT_CURR->{'ssl_info'}->{'cn'},
altnames => $XMLRPT_CURR->{'ssl_info'}->{'altnames'}
);
}
###############################################################################
# end host entry
sub xml_host_end {
my ($handle, $mark) = @_;
# Update host data with end time and elapsed time
$XMLRPT_CURR->{'endtime'} = date_disp($mark->{'end_time'});
$XMLRPT_CURR->{'elapsed'} = $mark->{'end_time'} - $mark->{'start_time'};
$XMLRPT_CURR->{'itemsfound'} = $mark->{'total_vulns'} || 0;
# Write statistics
$XML_WRITER->emptyTag('statistics',
elapsed => $XMLRPT_CURR->{'elapsed'},
itemsfound => $XMLRPT_CURR->{'itemsfound'},
itemstested => $XMLRPT_CURR->{'checks'},
endtime => $XMLRPT_CURR->{'endtime'}
);
# Close scandetails and niktoscan tags
$XML_WRITER->endTag('scandetails');
$XML_WRITER->endTag('niktoscan');
nprint("- XML host entry completed for $mark->{'hostname'}", "v", "report_xml");
}
###############################################################################
# add item to current host
sub xml_item {
my ($handle, $mark, $item) = @_;
# Add item to current host's items array
push(@{ $XMLRPT_CURR->{'items'} },
{ id => $item->{'nikto_id'},
method => $item->{'method'},
description => $item->{'message'},
uri => $item->{'uri'},
namelink => $item->{'namelink'} || '',
iplink => $item->{'iplink'} || '',
references => $item->{'refs'} || ''
}
);
# Write item tag with proper structure
$XML_WRITER->startTag('item',
id => $item->{'nikto_id'},
method => $item->{'method'}
);
# Write item content with CDATA for potentially problematic content
$XML_WRITER->startTag('description');
$XML_WRITER->cdata($item->{'message'});
$XML_WRITER->endTag('description');
$XML_WRITER->startTag('uri');
$XML_WRITER->cdata($item->{'uri'});
$XML_WRITER->endTag('uri');
$XML_WRITER->startTag('namelink');
$XML_WRITER->cdata($item->{'namelink'} || '');
$XML_WRITER->endTag('namelink');
$XML_WRITER->startTag('iplink');
$XML_WRITER->cdata($item->{'iplink'} || '');
$XML_WRITER->endTag('iplink');
$XML_WRITER->startTag('references');
$XML_WRITER->cdata($item->{'refs'} || '');
$XML_WRITER->endTag('references');
$XML_WRITER->endTag('item');
}
###############################################################################
# close output file
sub xml_close {
my ($handle) = @_;
# Close root element
$XML_WRITER->endTag('niktoscans');
# End the XML writer
$XML_WRITER->end();
# Close file handle
close($XML_HANDLE) if $XML_HANDLE;
# Validate XML if possible
xml_validate_output($handle);
nprint("- XML report completed with proper structure", "v", "report_xml");
}
###############################################################################
# Validate XML output
sub xml_validate_output {
my ($filename) = @_;
# Only validate if XML::LibXML is available
eval {
require XML::LibXML;
my $parser = XML::LibXML->new();
my $doc = $parser->parse_file($filename);
# Validate against DTD if available
if (defined $CONFIGFILE{'NIKTODTD'} && $CONFIGFILE{'NIKTODTD'} ne '') {
$doc->validate();
nprint("- XML output validated against DTD successfully", "v", "report_xml");
}
else {
nprint("- XML output is well-formed", "v", "report_xml");
}
};
if ($@) {
nprint("+ WARNING: XML validation failed: $@", "e");
}
}
sub nikto_reports { } # so core doesn't freak
1;
================================================
FILE: program/plugins/nikto_robots.plugin
================================================
###############################################################################
# SPDX-License-Identifier: GPL-3.0-only
# PURPOSE: Check out the robots.txt file
###############################################################################
sub nikto_robots_init {
my $id = {
name => "robots",
full_name => "Robots",
author => "Sullo",
description =>
"Checks whether there's anything within the robots.txt file and analyses it for other paths to pass to other scripts.",
hooks => { recon => { method => \&nikto_robots,
weight => 49,
},
},
copyright => "2008 Chris Sullo",
options => { nocheck => "Flag to disable checking entries in robots file.", }
};
return $id;
}
sub nikto_robots {
my ($mark, $parameters) = @_;
return if $mark->{'terminate'};
my ($code, $content, $errors, $request, $response) =
nfetch($mark, "/robots.txt", "GET", "", "", "", "robots");
my $has_non_root_entries = 1;
# Validate content-type: should be text/* or not present
if (defined($response->{'content-type'})) {
if ( $response->{'content-type'} !~ /^text\//i
|| $response->{'content-type'} =~ /^text\/html/i) {
return; # Not a text type, or is HTML - skip processing
}
}
# Accept any 2xx success code (except 204 No Content) or custom "okay" response
if ($code =~ /^2\d\d$/) {
if (is_404($mark, "/robots.txt", $response)) {
return;
}
my (%DIRS, %RFILES);
my $DISCTR = 0;
my @DOC = split(/\n/, $content);
my $tocheck;
foreach my $line (@DOC) {
$line =~ s/(?:^\s+|\s+$)//g;
$line = quotemeta($line);
if ($line =~ /allow/i) {
chomp($line);
# Report if Allow
$has_non_root_entries = 0 if ($line =~ /^allow/i);
$line =~ s/\#.*$//;
$line =~ s/\s+/ /g;
$line =~ s/\t/ /g;
$line =~ s/(?:dis)?allow(?:\\:)?(?:\\\s+)?//i;
$line =~ s/\/+/\//g;
$line =~ s/\\//g;
if ($line eq "") { next; }
# try to figure out file vs dir... just guess...
if (($line !~ /\./) && ($line !~ /\/$/)) { $line .= "/"; }
$line = LW2::uri_normalize($line);
# figure out dirs/files...
my $realdir = validate_and_fix_regex(LW2::uri_get_dir($line));
my $realfile = validate_and_fix_regex($line);
$realfile =~ s/^$realdir//;
nprint("- robots.txt entry dir:$realdir -- file:$realfile",
"d", ($mark->{'hostname'}, $mark->{'ip'}, $mark->{'displayname'}));
if (($realdir ne "") && ($realdir ne "/")) {
$realdir =~ s/\\//g;
$DIRS{$realdir} = 1;
}
if (($realfile ne "") && ($realfile ne "/")) {
$realfile =~ s/\\//g;
$RFILES{$realfile} = 1;
}
$DISCTR++;
if (($realdir ne "") && ($realdir ne "/")) { $has_non_root_entries = 0; }
next
if ( ($realdir eq "/" && $realfile eq "")
|| ($realfile eq "/" && $realdir eq ""));
next if ($line =~ /\*/); # Wildcards
$tocheck{$line} = 1;
} # end if $line =~ allow
} # end foreach my $line (@DOC)
# Check for allowed paths
foreach my $line (keys %tocheck) {
return if $mark->{'terminate'};
if (!defined($parameters->{'nocheck'})) {
my ($res, $content, $error, $request, $response) =
nfetch($mark, $line, "GET", "", "", "", "Robots: Check for URI");
if (!is_404($mark, $line, $response)
&& ($res !~ /^40[346]$/)
&& ($res !~ /^30[21]$/)) {
add_vulnerability(
$mark,
"/robots.txt: Entry '$line' is returned a non-forbidden or redirect HTTP code ($res)",
999997,
"https://portswigger.net/kb/issues/00600600_robots-txt-file",
"GET",
"/$line",
$request,
$response
);
}
}
}
# Use shared path matching logic
path_matcher(\%RFILES, \%DIRS, undef);
my $msg =
($DISCTR == 1) ? "contains 1 entry which should be manually viewed."
: ($DISCTR > 1) ? "contains $DISCTR entries which should be manually viewed."
: "retrieved but it does not contain any 'disallow' entries (which is odd).";
if ($has_non_root_entries eq 0) {
add_vulnerability($mark, "/robots.txt: $msg",
999996, "https://developer.mozilla.org/en-US/docs/Glossary/Robots.txt",
"GET", "/robots.txt", $request, $response);
}
}
}
1;
================================================
FILE: program/plugins/nikto_shellshock.plugin
================================================
###############################################################################
# SPDX-License-Identifier: GPL-3.0-only
# PURPOSE: Check for the bash 'shellshock' vulnerability
###############################################################################
sub nikto_shellshock_init {
my $id = { name => "shellshock",
full_name => "shellshock",
author => "sullo",
description => "Look for the bash 'shellshock' vulnerability.",
hooks => { scan => { method => \&nikto_shellshock, weight => 20 }, },
copyright => "2014 Chris Sullo",
options => { uri => "uri to assess", },
};
return $id;
}
sub nikto_shellshock {
my ($mark, $parameters) = @_;
my ($found, @names,);
# This would be better coming from live scan results and not db_variables
my @files = split(/ /, $VARIABLES{"\@SHELLSHOCK"});
push(@files, "");
my %headers;
$headers{'User-Agent'} = '() { :; }; echo 93e4r0-cve-2014-6271: true;echo;echo;';
$headers{'Referer'} = '() { _; } >_[$($())] { echo 93e4r0-cve-2014-6278: true; echo;echo; }';
my @dirs = split(/ /, $VARIABLES{'@CGIDIRS'});
push(@dirs, "/");
#check for FP... error in page
my $checkcontent = 1;
my ($res, $content, $error, $request, $response) =
nfetch($mark, "/", "GET", "", \%headers, "", "shellshock");
if ($content =~ /93e4r0-cve/) {
$checkcontent = 0;
nprint(
"Content seems to contain error headers, ignoring content match in shellshock plugin",
"v");
}
if (defined $parameters->{'uri'}) {
# request by hostname
my ($res, $content, $error, $request, $response) =
nfetch($mark, "$parameters->{'uri'}", "GET", "", \%headers, "", "shellshock");
if ( ($response->{'93e4r0-cve-2014-6271'} eq 'true')
|| ($checkcontent && ($content =~ /(?{'uri'}: Site appears vulnerable to the 'shellshock' vulnerability).",
999949,
"CVE-2014-6271",
"GET",
"$parameters->{'uri'}",
$request,
$response
);
}
if ( ($response->{'93e4r0-cve-2014-6278'} eq 'true')
|| ($checkcontent && ($content =~ /(?{'uri'}: Site appears vulnerable to the 'shellshock' vulnerability.",
999948,
"CVE-2014-6278",
"GET",
"$parameters->{'uri'}",
$request,
$response
);
}
}
else {
foreach my $cgidir (@dirs) {
foreach my $file (@files) {
return if $mark->{'terminate'};
# request by hostname
my ($res, $content, $error, $request, $response) =
nfetch($mark, "$cgidir$file", "GET", "", \%headers, "", "shellshock");
if ( ($response->{'93e4r0-cve-2014-6271'} eq 'true')
|| ($checkcontent && ($content =~ /(?{'93e4r0-cve-2014-6278'} eq 'true')
|| ($checkcontent && ($content =~ /(? "siebel",
full_name => "Siebel Checks",
author => "Tautology",
description => "Performs a set of checks against an installed Siebel application",
hooks => { scan => { method => \&nikto_siebel, }, },
copyright => "2011 Chris Sullo",
options => {
enumerate => "Flag to indicate whether we shall attempt to enumerate known apps",
applications => "List of applications",
languages => "List of Languages",
application => "Application to attack",
}
};
return $id;
}
sub nikto_siebel {
my ($mark, $parameters) = @_;
return if $mark->{'terminate'};
my $application;
# Check whether we have an application
if (defined $parameters->{'enumerate'}) {
my @apps = nikto_siebel_enumerate($mark, $parameters);
$application = $apps[0];
}
if ($application eq "" && defined $parameters->{'application'}) {
$application = $parameters->{'application'};
}
if ($application eq "") {
nprint("No Siebel Application defined", "v", "siebel");
return;
}
# Now we have an application time to perform some tests
my $path = $application . "/base.txt";
my ($res, $content, $error, $request, $response) =
nfetch($mark, $path, "GET", "", "", "", "siebel: find default pages");
if ($res eq "200") {
my ($siebelver, $appver, $hotfix);
$siebelver = $content;
$siebelver =~ s/([ \t]*)([0-9.]*)( .*\n.*)/$2/;
chomp($siebelver);
$appver = $content;
$appver =~ s/(.*\[)(.*)(\].*\n.*)/$2/;
chomp($appver);
$hotfix = $content;
$hotfix =~ s/(.*\n)(.*HOTFIX )(.*)/$3/;
add_vulnerability(
$mark,
"$path: Siebel version $siebelver found application version $appver and applied hostfixes are $hotfix",
999901,
"https://www.oracle.com/applications/siebel/",
"GET",
$path,
$request,
$response
);
}
$path = $application . "/_stats.swe";
($res, $content, $error, $request, $response) =
nfetch($mark, $path, "GET", "", "", "", "siebel: find default pages");
if ($res eq "200") {
add_vulnerability(
$mark, "/_stats.swe: Siebel stats page found",
999902,
"https://docs.oracle.com/cd/E14004_01/books/SysDiag/SysDiagSWSEstats7.html",
"GET", $path, $request, $response
);
}
foreach
my $page (split(/ /, "About_Siebel.htm files/ images/ help/ siebstarthelp.htm siebindex.htm"))
{
$path = $application . "/$page";
($res, $content, $error, $request, $response) =
nfetch($mark, $path, "GET", "", "", "", "siebel: find default pages");
if ($res eq "200") {
add_vulnerability($mark, "$path: Siebel default file found",
999903, "", "GET", $path, $request, $response);
}
}
return;
}
sub nikto_siebel_enumerate {
my ($mark, $params) = @_;
# Default apps and languages - allow parameters to over-ride them.
my $apps =
"emarketing ecustomer pmmanager sales marketing wpeserv salesce econsumerpharma emedia epublicsector eaf echannelcme epharmace siaservicece finseenenrollment ecustomercme loyalty erm etraining esales callcenter wpsales eai smc eprofessionalpharma eenergy pseservice sismarketing econsumer medicalce epharma fins finesales finscustomer htim loyaltyscw ermadmin eevents eauctionswexml cra wpserv eai_anon edealer esitesclinical eautomotive econsumersector echannelaf eEnergyOilGasChemicals cgce eclinical finsconsole finsebanking finssalespam htimpim eloyalty ememb pimportal eservice service wppm servicece edealerscw ecommunications ehospitality eretail echannelcg eCommunicationsWireless siasalesce emedical finsechannel finsebrokerage esalescme";
my $langs =
"enu euq cht dan fin deu hun kor ptb sky sve pse cat shl nld fra ell ita nor ptg slv tha psl chs csy frc heb jpn plk rus esn trk";
my @foundapps;
if ($params->{applications}) {
$apps = $params->{applications};
}
if ($params->{languages}) {
$langs = $params->{languages};
}
foreach my $language (split(/ /, $langs)) {
foreach my $application (split(/ /, $apps)) {
my $appname = $application . "_" . $language;
my $startname = $appname . "/start.swe";
($res, $content, $error, $request, $response) =
nfetch($mark, $startname, "GET", "", "", "", "Siebel: enumerate application");
if ($res eq "200") {
# We've found an app
add_vulnerability($mark, "$startname: Enumerated Siebel application: " . $appname,
999900, "", "GET", $startname, $request, $response);
push(@foundapps, $appname);
}
}
}
return @foundapps;
}
1;
================================================
FILE: program/plugins/nikto_sitefiles.plugin
================================================
###############################################################################
# SPDX-License-Identifier: GPL-3.0-only
# PURPOSE: Look for interesting files based on site name/ip
###############################################################################
sub nikto_sitefiles_init {
my $id = { name => "sitefiles",
full_name => "Site Files",
author => "sullo",
description => "Look for interesting files based on the site's IP/name",
hooks => { scan => { method => \&nikto_sitefiles, }, },
copyright => "2014 Chris Sullo"
};
return $id;
}
###############################################################################
# File type detection functions to reduce false positives
###############################################################################
sub detect_file_type {
my ($content, $uri) = @_;
my $first_bytes = substr($content, 0, 64); # Check first 64 bytes
my $content_length = length($content);
# Return early for empty content
return 'empty' if $content_length == 0;
# File signatures (Magic Numbers)
my %signatures = (
# Archive formats
'zip' => qr/^PK\x03\x04|PK\x05\x06|PK\x07\x08/,
'tar' => qr/^.{257}ustar|^.{257}ustar\x00|^.{257}ustar |^.{257}ustar\x00\x00/,
'gz' => qr/^\x1f\x8b/,
'bz2' => qr/^BZh/,
'lzma' => qr/^\x5d\x00\x00/,
# Certificate/Key formats
'pem' =>
qr/^-----BEGIN (CERTIFICATE|RSA PRIVATE KEY|DSA PRIVATE KEY|EC PRIVATE KEY|PRIVATE KEY|PUBLIC KEY)-----/,
'jks' => qr/^\xfe\xed\xfe\xed/, # Java KeyStore magic
# Database formats
'sql' => qr/^(CREATE|INSERT|UPDATE|DELETE|SELECT|DROP|ALTER|--|\/\*|select)/i,
);
# Check signatures
foreach my $type (keys %signatures) {
if ($first_bytes =~ $signatures{$type}) {
return $type;
}
}
# Special handling for ZIP-based formats (egg, war) - check BEFORE generic ZIP
if ($first_bytes =~ /^PK\x03\x04/) {
if ($uri =~ /\.egg$/i) {
return 'egg';
}
if ($uri =~ /\.war$/i) {
return 'war';
}
}
# tar detection - check for tar file structure
if ($uri =~ /\.tar$/i) {
# Tar files have 512-byte blocks, check if content length is multiple of 512
if ($content_length % 512 == 0) {
# Check for null bytes at the end (tar files end with null blocks)
my $last_block = substr($content, -512);
if ($last_block =~ /^\x00+$/) {
return 'tar';
}
}
# Check for tar header structure (first 512 bytes should have specific format)
if ($content_length >= 512) {
my $header = substr($content, 0, 512);
# Tar header: filename (100 bytes) + mode (8) + uid (8) + gid (8) + size (12) + mtime (12) + checksum (8) + typeflag (1) + linkname (100) + magic (6) + version (2) + uname (32) + gname (32) + devmajor (8) + devminor (8) + prefix (155) + padding (12)
# Check if it looks like a tar header (has printable filename, reasonable size)
my $filename = substr($header, 0, 100);
$filename =~ s/\x00.*$//; # Remove null padding
if ($filename =~ /^[[:print:]]+$/ && length($filename) > 0) {
return 'tar';
}
}
}
# Special case: Check for compressed tar variants
if ($uri =~ /\.(tar\.gz|tgz)$/i && $first_bytes =~ /^\x1f\x8b/) {
return 'tar.gz';
}
if ($uri =~ /\.(tar\.bz2)$/i && $first_bytes =~ /^BZh/) {
return 'tar.bz2';
}
if ($uri =~ /\.(tar\.lzma)$/i && $first_bytes =~ /^\x5d\x00\x00/) {
return 'tar.lzma';
}
# Check for HTML/Text content (negative cases)
if ($first_bytes =~ /^ [ 'zip', 'binary' ],
'tar' => [ 'tar', 'binary' ],
'gz' => [ 'gz', 'binary' ],
'bz2' => [ 'bz2', 'binary' ],
'lzma' => [ 'lzma', 'binary' ],
'egg' => [ 'egg', 'zip', 'binary' ], # egg files can be detected as zip
'war' => [ 'war', 'zip', 'binary' ], # war files can be detected as zip
'pem' => [ 'pem', 'text' ],
'jks' => [ 'jks', 'binary' ],
'sql' => [ 'sql', 'text' ],
'tar.gz' => [ 'gz', 'binary' ], # gz signature for tar.gz
'tar.bz2' => [ 'bz2', 'binary' ], # bz2 signature for tar.bz2
'tar.lzma' => [ 'lzma', 'binary' ], # lzma signature for tar.lzma
);
# Check if detected type matches expected
if (exists $expected_types{$expected_type}) {
my @valid_types = @{ $expected_types{$expected_type} };
foreach my $valid_type (@valid_types) {
return 1 if $detected_type eq $valid_type;
}
return 0; # Mismatch
}
# For unknown expected types, just check it's not HTML
return 0 if $detected_type eq 'html';
return 1;
}
sub is_likely_real_file {
my ($content, $uri) = @_;
# Quick size check
return 0 if length($content) < 20;
# Get expected file type from URI
my $expected_type = '';
if ($uri =~ /\.(tar\.gz|tgz)$/i) {
$expected_type = 'tar.gz';
}
elsif ($uri =~ /\.(tar\.bz2)$/i) {
$expected_type = 'tar.bz2';
}
elsif ($uri =~ /\.(tar\.lzma)$/i) {
$expected_type = 'tar.lzma';
}
elsif ($uri =~ /\.(zip|tar|gz|bz2|lzma|egg|war|pem|jks|sql)$/i) {
$expected_type = lc($1);
}
# If we can't determine expected type, do basic validation
if (!$expected_type) {
my $detected = detect_file_type($content, $uri);
return 0 if $detected eq 'html'; # HTML = likely false positive
return 1 if $detected =~ /^(binary|zip|tar|gz|bz2|lzma|egg|war|pem|jks|sql)$/;
return 0;
}
# Validate against expected type
return validate_file_content($content, $uri, $expected_type);
}
sub analyze_file_details {
my ($content, $uri) = @_;
my $detected_type = detect_file_type($content, $uri);
my $content_length = length($content);
my $entropy = calculate_entropy($content);
# Calculate confidence
my $confidence = 0;
# Base confidence on file type
$confidence += 90 if $detected_type =~ /^(zip|tar|gz|bz2|lzma|egg|war|jks)$/;
$confidence += 85 if $detected_type eq 'pem';
$confidence += 80 if $detected_type eq 'sql';
$confidence += 70 if $detected_type eq 'binary';
$confidence -= 50 if $detected_type eq 'html'; # Penalty for HTML
# Size-based adjustments
$confidence += 10 if $content_length > 1000; # Larger files more likely real
if ($content_length < 100) {
# Be more lenient with certain file types - small files are common
if ($detected_type eq 'sql') {
$confidence -= 10; # Less penalty for small SQL files
}
elsif ($detected_type eq 'lzma') {
$confidence -= 5; # Very small penalty for small LZMA files
}
else {
$confidence -= 20; # Very small files suspicious
}
}
# Entropy-based adjustments
$confidence += 15 if $entropy > 7.0; # High entropy = likely binary
if ($entropy < 3.0) {
# Be more lenient with LZMA files for low entropy
if ($detected_type eq 'lzma') {
$confidence -= 10; # Less penalty for LZMA with low entropy
}
else {
$confidence -= 20; # Low entropy = likely text/HTML
}
}
# Ensure confidence is between 0-100
$confidence = 0 if $confidence < 0;
$confidence = 100 if $confidence > 100;
# Return a simple array instead of hash to avoid construction issues
return [ $detected_type, $content_length, $entropy, $confidence ];
}
sub nikto_sitefiles {
my ($mark) = @_;
my (%flags, %files, %names);
# Minimum confidence required to report a file
my $confidence_threshold = 60;
$names{ $mark->{'hostname'} } = 1;
$names{ $mark->{'vhost'} } = 1;
foreach my $n (keys %names) {
my $nn = $n;
$nn =~ s/^www(?:\d+)?\.//;
$names{$nn} = 1;
$nn = $n;
$nn =~ s/\./_/g;
$names{$nn} = 1;
my @bits = split(/\./, $n);
my ($temp1, $temp2) = '';
for (my $i = 0 ; $i <= $#bits ; $i++) {
$names{ $bits[$i] } = 1;
$temp1 .= $bits[$i];
$temp2 .= '.' . $bits[$i];
$temp2 =~ s/^\.//;
$names{$temp1} = 1;
$names{$temp2} = 1;
}
}
$names{'backup'} = 1;
$names{'site'} = 1;
$names{'archive'} = 1;
$names{'database'} = 1;
$names{'dump'} = 1;
$names{ $mark->{'ip'} } = 1;
foreach my $item (keys %names) {
next if $item eq '';
foreach
my $ext (qw/jks cer pem zip tar tar.gz gz tgz tar.bz2 tar.lzma bz2 lzma egg war sql/) {
$files{"$item\.$ext"} = 1;
}
}
foreach my $f (keys %files) {
# trickery to test with both host header and without
foreach my $flag (0 .. 1) {
return if $mark->{'terminate'};
my $msg = "";
$flags{'nohost'} = $flag;
if ($flag) {
$msg = "(NOTE: requested by IP address).";
}
# request. flags passed will determine if hostname is used or not
my ($res, $content, $error, $request, $response) =
nfetch($mark, "/$f", "GET", "", "", \%flags, "sitefiles");
my $condition1 = defined($response->{'content-type'})
&& $response->{'content-type'} =~ /^application\//i;
my $condition2 =
($res == 200)
&& (length($content) > 0)
&& (!defined($response->{'content-type'})
|| $response->{'content-type'} !~ /^text\//i)
&& (!is_404($mark, "/$f", $response));
if ($condition1 || $condition2) {
# Enhanced content analysis to reduce false positives
if (is_likely_real_file($content, "/$f")) {
my $analysis = analyze_file_details($content, "/$f");
my $conf = $analysis->[3];
# Only report if confidence is high enough
if ($conf > $confidence_threshold) {
my $type_info = "Confidence: $conf%";
add_vulnerability(
$mark,
"/$f: Potentially interesting backup/cert file found. $msg [$type_info]",
740001,
"https://cwe.mitre.org/data/definitions/530.html",
"HEAD",
"/$f",
$request,
$response
);
}
}
last;
}
}
}
}
1;
================================================
FILE: program/plugins/nikto_springboot.plugin
================================================
###############################################################################
# SPDX-License-Identifier: GPL-3.0-only
# PURPOSE: Scan for exposed Spring Boot Actuator endpoints and basic info leaks
###############################################################################
use JSON;
sub nikto_springboot_init {
my $id = { name => "springboot",
full_name => "Spring Boot Actuator endpoint check",
author => "Sullo",
description => "Detects exposed Spring Boot Actuator endpoints and basic info leaks",
hooks => { scan => { method => \&nikto_springboot, } },
copyright => "2025 Chris Sullo"
};
return $id;
}
sub nikto_springboot {
my ($mark) = @_;
my $root = $mark->{'root'} || '';
$root =~ s/\/$//; # Remove trailing slash if present
my @endpoints = qw(
/actuator
/actuator/health
/actuator/info
/actuator/env
/actuator/mappings
/actuator/metrics
/actuator/beans
/actuator/configprops
/actuator/loggers
/actuator/threaddump
/actuator/auditevents
/actuator/httptrace
/actuator/scheduledtasks
/actuator/heapdump
/actuator/jolokia
/actuator/prometheus
);
my $host = $mark->{'hostname'};
my $port = $mark->{'port'};
my $proto = $mark->{'ssl'} ? 'https' : 'http';
my $base_url = "$proto://$host:$port";
foreach my $ep (@endpoints) {
my $path = $root . $ep;
nprint("Checking $path", "v", "springboot");
my ($res, $content, $error, $request, $response) =
nfetch($mark, $path, 'GET', '', '', undef, 'springboot');
my $ct = $response->{'content-type'} || '';
# Quick exits
if ($res == 404) {
next;
}
elsif ($res != 200 && $res != 404) {
nprint("$path: Non-200 ($res) - possibly restricted endpoint", "v", "springboot");
next;
}
# resposnes should be JSON
next unless ($ct =~ /application\/json/i || $content =~ /^\s*\{/);
my $json;
eval { $json = decode_json($content); };
if ($@ || !$json) {
nprint("$path: 200 but invalid JSON", "v", "springboot");
next;
}
# /actuator special handling
if ($ep eq '/actuator') {
if (exists $json->{'_links'} && ref($json->{'_links'}) eq 'HASH') {
my $links = $json->{'_links'};
my @found;
foreach my $k (keys %$links) {
my $href = $links->{$k}{'href'};
next unless $href;
my $report_val;
# If on same host, report only the path; otherwise, report full URL
if ($href =~ m{^$proto://$host(?::$port)?(/.*)$}) {
$report_val = $1;
}
else {
$report_val = $href;
}
push @found, $report_val;
add_vulnerability(
$mark,
"$report_val: Spring Boot Actuator endpoint discovered via /actuator _links.",
750001,
"https://docs.spring.io/spring-boot/docs/current/actuator-api/html/",
'GET',
$report_val
);
}
}
else {
nprint("/actuator: 200 but no _links", "v", "springboot");
}
next;
}
# /actuator/health
if ($ep eq '/actuator/health') {
if (exists $json->{'status'} && $json->{'status'} eq 'UP') {
add_vulnerability(
$mark,
"$path: Spring Boot Actuator health endpoint exposed",
750002,
"https://docs.spring.io/spring-boot/docs/current/actuator-api/html/#health",
'GET',
$path
);
}
next;
}
# /actuator/info
if ($ep eq '/actuator/info') {
if ( exists $json->{'build'}
&& ref($json->{'build'}) eq 'HASH'
&& exists $json->{'build'}{'version'}) {
add_vulnerability(
$mark,
"$path: Spring Boot Actuator info endpoint exposed (build version: $json->{'build'}{'version'})",
750003,
"https://docs.spring.io/spring-boot/docs/current/actuator-api/html/#info",
'GET',
$path
);
}
next;
}
# Other endpoints: report if valid, non-empty JSON
if (ref($json) eq 'HASH' && scalar(keys %$json) > 0) {
add_vulnerability(
$mark, "$path: Spring Boot Actuator endpoint exposed (valid JSON response)",
750004, "https://docs.spring.io/spring-boot/docs/current/actuator-api/html/",
'GET', $path
);
}
}
}
1;
================================================
FILE: program/plugins/nikto_ssl.plugin
================================================
###############################################################################
# SPDX-License-Identifier: GPL-3.0-only
# PURPOSE: Test certificate information
###############################################################################
sub nikto_ssl_init {
my $id = { name => "ssl",
full_name => "SSL and cert checks",
author => "Sullo",
description => "Perform checks on SSL/Certificates",
hooks => { scan => { method => \&nikto_ssl, } },
copyright => "2010 Chris Sullo"
};
return $id;
}
sub nikto_ssl {
my ($mark) = @_;
if ($mark->{ssl}) {
my @cn_names;
my @san_names;
my $match = 0;
# Extract CN from subject
if ($mark->{'ssl_cert_subject'} =~ /CN=([^$ \/]+)/) {
push(@cn_names, $1);
}
# Extract SAN names
if ($mark->{'ssl_cert_altnames'} ne '') {
foreach my $n (split(/, /, $mark->{'ssl_cert_altnames'})) {
push(@san_names, $n);
}
}
# Combine all names for validation
my @all_names = (@cn_names, @san_names);
@all_names = unique_vals(@all_names);
# Create detailed name lists for error messages
my $cn_list = @cn_names ? join(", ", @cn_names) : "none";
my $san_list = @san_names ? join(", ", @san_names) : "none";
my $allnames = join(", ", @all_names);
foreach my $cert_name (@all_names) {
next unless $cert_name; # Skip empty names
# straight up match
if (lc($mark->{'hostname'}) eq lc($cert_name)) {
$match = 1;
}
# wildcard cert
elsif ($cert_name =~ /^\*/) {
add_vulnerability($mark, "/: Server is using a wildcard certificate: $cert_name",
999992, "https://en.wikipedia.org/wiki/Wildcard_certificate");
$cert_name =~ s/^\*\.//;
$cert_name = rquote($cert_name);
# must match leading dot
# only one level of subdomain allowed
if ($mark->{'hostname'} =~ /^(.*)\.?$cert_name/i) {
my $matched = $1;
my $tldcount = ($matched =~ tr/\.//);
if ($tldcount <= 1) { $match = 1; }
}
}
last if $match;
}
if (!$match) {
my $error_msg = "/: Hostname '$mark->{'hostname'}' does not match certificate names";
$error_msg .= " (CN: $cn_list, SAN: $san_list)";
add_vulnerability($mark, $error_msg, 999993,
"https://cwe.mitre.org/data/definitions/297.html");
}
}
}
sub unique_vals {
my %seen;
grep !$seen{$_}++, @_;
}
1;
================================================
FILE: program/plugins/nikto_tests.plugin
================================================
###############################################################################
# SPDX-License-Identifier: GPL-3.0-only
# PURPOSE: Perform the full database of nikto tests against a target
###############################################################################
sub nikto_tests_init {
my $id = { name => "tests",
full_name => "Nikto Tests",
author => "Sullo, Tautology",
description => "Test host with the standard Nikto tests",
copyright => "2008 Chris Sullo",
hooks => {
scan => { method => \&nikto_tests,
weight => 99,
},
},
options => {
passfiles => "Flag to indicate whether to check for common password files",
all => "Flag to indicate whether to check all files with all directories",
report => "Report a status after the passed number of tests",
}
};
return $id;
}
sub nikto_tests {
my ($mark, $parameters) = @_;
return if $mark->{'terminate'};
my $data;
# this is the actual the looped code for all the checks
foreach my $checkid (sort keys %TESTS) {
return if $mark->{'terminate'};
# replace variables in the uri
my @urilist = change_variables($TESTS{$checkid}{'uri'}, $mark, $checkid);
# Now repeat for each uri
URI: foreach my $uri (@urilist) {
return if $mark->{'terminate'};
my (%headrs, %flags, $data);
if ($TESTS{$checkid}{'headers'} ne '') {
my $header = unslash($TESTS{$checkid}{'headers'});
# Apply variable replacement to headers
my @header_lines = split /\r\n/, $header;
foreach my $h (@header_lines) {
my ($key, $value) = split(/: /, $h);
$key = lc($key);
# Apply change_variables to the value part
my @replaced_values = change_variables($value, $mark, $checkid);
foreach my $replaced_value (@replaced_values) {
$headrs{$key} = $replaced_value;
}
}
$headrs{'host'} = $mark->{'hostname'}
unless ($headrs{'host'}); # Kludge not to override host injection vectors
$flags{'noclean'} = 1;
}
if ($TESTS{$checkid}{'data'} ne '') {
$data = unslash($TESTS{$checkid}{'data'});
$headrs{'content-length'} = length($data)
unless grep(/^(transfer-encoding|content-length)$/i, keys %headrs);
}
my ($res, $content, $error, $request, $response) =
nfetch($mark, $uri, $TESTS{$checkid}{'method'}, $data, \%headrs, \%flags, $checkid);
# DSL matcher expects response headers as a hashref
my $response_string = rebuild_response($response, 0);
my %response_headers;
foreach my $line (split(/\r?\n/, $response_string)) {
next if $line =~ /^HTTP\//; # Skip status line
last if $line =~ /^\s*$/; # Stop at blank line (end of headers)
if ($line =~ /^([^:]+):\s*(.*)$/) {
my ($name, $value) = (lc($1), $2);
if (exists $response_headers{$name}) {
$response_headers{$name} .= ', ' . $value;
}
else {
$response_headers{$name} = $value;
}
}
}
my $response_headers = \%response_headers;
# Extract cookies as a hashref for matcher
my %cookies = ();
if (ref $response->{'whisker'}->{'cookies'} eq 'ARRAY') {
foreach my $cookie (@{ $response->{'whisker'}->{'cookies'} }) {
if ($cookie =~ /^([^=]+)=([^;]*)/) {
$cookies{ lc($1) } = $2;
}
}
}
# Use the DSL matcher - now returns (match_status, captured_groups)
my ($positive, $reason, $captures) = (0, '', []);
my @matcher_result =
$TESTS{$checkid}{'matcher'}->($res, $content, $response_headers, \%cookies);
my ($match_status, $captured_groups) = @matcher_result;
if ($match_status) {
$positive = 1;
$reason = 'DSL Match';
$captures = $captured_groups || [];
}
# matched on something, check fails/ands
if ($positive) {
# if it's an index.php, check for normal /index.php to see if it's a FP
# if ($uri =~ /^\/index.php\?/i) {
# my $clean_content = rm_active_content($content, $mark->{'root'} . $uri);
# if (LW2::md5($clean_content) eq $mark->{'FoF'}{'index.php'}{'match'}) {
# next;
# }
# }
# Check user-specified error codes/strings first (highest priority, always wins)
# This must be checked before any other 404 detection logic
if (defined $VARIABLES{'ERRCODES'} && ref($VARIABLES{'ERRCODES'}) eq 'HASH') {
my $code_str = "$res";
if (exists $VARIABLES{'ERRCODES'}->{$code_str}) {
next URI; # Skip this test, it's a 404
}
}
if (defined $VARIABLES{'ERRSTRINGS'} && ref($VARIABLES{'ERRSTRINGS'}) eq 'HASH') {
foreach my $pattern (keys %{ $VARIABLES{'ERRSTRINGS'} }) {
if ($content =~ /$pattern/) {
next URI; # Skip this test, it's a 404
}
}
}
# Lastly check for a false positive based on file extension or type.
# We check is_404 when the actual response code is 200, because 404 error pages
# can return 200 status codes. However, we skip the check if:
# 1. There's a positive BODY match (specific body content indicates real content)
# 2. The DSL has an explicit non-200 CODE match (like CODE:404, CODE:403)
# without including 200, as those are intentional matches for specific status codes.
my $has_code_match = ($TESTS{$checkid}{'dsl'} =~ /CODE:/i);
# Check if DSL has an explicit non-200 CODE match that doesn't include 200
# (e.g., CODE:404, CODE:403, but not CODE:200 or CODE:200|404)
my $has_explicit_non200_code = 0;
if ($has_code_match) {
my $includes_code_200 = ($TESTS{$checkid}{'dsl'} =~ /\bCODE:\s*200(\D|$)/i);
# If there's a CODE match but it doesn't include 200, skip is_404 check
$has_explicit_non200_code = !$includes_code_200;
}
# Check if DSL has positive BODY patterns (BODY: but not !BODY:)
my $has_body_match = ($TESTS{$checkid}{'dsl'} =~ /(?:^|[^!])BODY:/i);
if ( $res == 200
&& !$has_body_match
&& !$has_explicit_non200_code
&& is_404($mark, $mark->{'root'} . $uri, $response)) {
next;
}
# Process message with captured groups (if any)
my $message = $TESTS{$checkid}{'message'};
if (@$captures && $message =~ /\$\d+/) {
$message = process_captured_groups($message, $captures);
}
# All checks passed, add vulnerability
add_vulnerability($mark, "$mark->{'root'}$uri: $message",
$checkid, $TESTS{$checkid}{'references'},
$TESTS{$checkid}{'method'}, $mark->{'root'} . $uri,
$request, $response,
$reason
);
}
}
# Percentages
if ( $OUTPUT{'progress'}
&& $parameters->{'report'}
&& ($COUNTERS{'totalrequests'} % $parameters->{'report'}) == 0) {
status_report();
}
} # end check loop
# Perform mutation tests
passchecks($mark) if $parameters->{'passfiles'};
allchecks($mark) if $parameters->{'all'};
return;
}
sub passchecks {
my ($mark) = @_;
my @DIRS = (split(/ /, $VARIABLES{"\@PASSWORDDIRS"}));
my @PFILES = (split(/ /, $VARIABLES{"\@PASSWORDFILES"}));
my @EXTS = qw(asp bak dat data dbc dbf exe htm html htx ini lst txt xml php php3);
nprint("- Performing passfiles mutation.", "v", "tests");
# Update total requests for status reports
my @CGIS = split(/ /, $VARIABLES{'@CGIDIRS'});
$COUNTERS{'total_checks'} =
$COUNTERS{'total_checks'} +
(scalar(@DIRS) * scalar(@PFILES)) +
(scalar(@DIRS) * scalar(@PFILES) * scalar(@EXTS)) +
((scalar(@DIRS) * scalar(@PFILES) * scalar(@EXTS) * scalar(@CGIS)) * 2);
foreach my $dir (@DIRS) {
return if $mark->{'terminate'};
foreach my $file (@PFILES) {
next if ($file eq "");
# dir/file
testfile($mark, "$dir$file", "passfiles", "299998");
foreach my $ext (@EXTS) {
return if $mark->{'terminate'};
# dir/file.ext
testfile($mark, "$dir$file.$ext", "passfiles", "299998");
foreach my $cgi (@CGIS) {
$cgi =~ s/\/$//;
# dir/file.ext
testfile($mark, "$cgi$dir$file.$ext", "passfiles", "299998");
# dir/file
testfile($mark, "$cgi$dir$file", "passfiles", "299998");
}
}
}
}
}
sub allchecks {
my ($mark) = @_;
# Hashes to temporarily store files/dirs in
# We're using hashes to ensure that duplicates are removed
my (%FILES, %DIRS);
# build the arrays
nprint("- Loading root level files.", "v", "tests");
foreach my $checkid (keys %TESTS) {
# Expand out vars so we get full matches
my @uris = change_variables($TESTS{$checkid}{'uri'}, $mark, $checkid);
foreach my $uri (@uris) {
my $dir = LW2::uri_get_dir($uri);
my $file = $uri;
if ($dir ne "") {
$DIRS{$dir} = "";
$dir =~ s/([^a-zA-Z0-9])/\\$1/g;
$file =~ s/$dir//;
}
if (($file ne "") && ($file !~ /^\?/)) {
$FILES{$file} = "";
}
}
}
# Update total requests for status reports
$COUNTERS{'total_checks'} = $COUNTERS{'total_checks'} + (keys(%DIRS) * keys(%FILES));
# Now do a check for each item - just check the return status, nothing else
foreach my $dir (keys %DIRS) {
foreach my $file (keys %FILES) {
return if $mark->{'terminate'};
testfile($mark, "$dir$file", "all checks", 299999);
}
}
}
sub testfile {
my ($mark, $uri, $name, $tid) = @_;
return if $mark->{'terminate'};
my ($res, $content, $error, $request, $response) =
nfetch($mark, $uri, "GET", "", "", "", "Tests: $name");
nprint("- $res for $uri (error: $error)",
"v", ($mark->{'hostname'}, $mark->{'ip'}, $mark->{'displayname'}));
if ($error) {
$mark->{'total_errors'}++;
nprint("+ ERROR: $uri returned an error: $error",
"e", ($mark->{'hostname'}, $mark->{'ip'}, $mark->{'displayname'}));
return;
}
if ($res == 200) {
add_vulnerability($mark, "$uri: file found during $name mutation",
$tid, "", "GET", $uri, $request, $response);
}
}
1;
================================================
FILE: program/templates/htm_close.tmpl
================================================
© 2008 Chris Sullo
================================================
FILE: program/templates/htm_end.tmpl
================================================
| Start Time |
#TEMPL_START# |
| End Time |
#TEMPL_END# |
| Elapsed Time |
#TEMPL_ELAPSED# seconds |
| Statistics |
#TEMPL_STATISTICS# |
================================================
FILE: program/templates/htm_host_head.tmpl
================================================
| Target IP |
#TEMPL_IP# |
| Target hostname |
#TEMPL_HOSTNAME# |
| Target Port |
#TEMPL_PORT# |
| HTTP Server |
#TEMPL_BANNER# |
| Site Link (Name) |
#TEMPL_LINK_NAME# |
| Site Link (IP) |
#TEMPL_LINK_IP# |
#TEMPL_SSL_SUBJECT_ROW#
| SSL Certificate Subject | #TEMPL_SSL_SUBJECT# |
#TEMPL_SSL_CN_ROW#
| SSL Certificate CN | #TEMPL_SSL_CN# |
#TEMPL_SSL_SAN_ROW#
| SSL Certificate SAN | #TEMPL_SSL_SAN# |
#TEMPL_SSL_CIPHERS_ROW#
| SSL Ciphers | #TEMPL_SSL_CIPHERS# |
#TEMPL_SSL_ISSUER_ROW#
| SSL Certificate Issuer | #TEMPL_SSL_ISSUER# |
================================================
FILE: program/templates/htm_host_item.tmpl
================================================
| URI |
#TEMPL_URI# |
| HTTP Method |
#TEMPL_HTTP_METHOD# |
| Description |
#TEMPL_MSG# |
| Link |
#TEMPL_ITEM_NAME_LINK#
|
#TEMPL_REFERENCES_ROW#
| Reference(s) |
#TEMPL_REFERENCES# |
================================================
FILE: program/templates/htm_start.tmpl
================================================
Nikto Report
================================================
FILE: program/templates/htm_summary.tmpl
================================================
| Software Details |
Nikto #TEMPL_NIKTO_VER# |
| CLI Options |
#TEMPL_NIKTO_CLI# |
| Hosts Tested |
#TEMPL_NIKTO_HOSTS_TESTED# |
| Start Time |
#TEMPL_SCAN_START# |
| End Time |
#TEMPL_SCAN_END# |
| Elapsed Time |
#TEMPL_SCAN_ELAPSED# |
================================================
FILE: program/templates/nikto.dtd
================================================
================================================
FILE: program/utils/nikto-bulk.sh
================================================
#!/usr/bin/env bash
set -euo pipefail
# Don't run as root
if [[ "$(id -u)" -eq 0 ]]; then
echo "ERROR: Do not run this script as root." >&2
exit 1
fi
############################
# SPDX-License-Identifier: GPL-3.0-only
# PURPOSE: Run multiple copies of Nikto against a list of targets
# on a *nix system with the screen utility.
############################
# Paths / config
############################
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
: "${NIKTO_BIN:="${SCRIPT_DIR}/../nikto.pl"}"
# Nikto flags (customize here)
# "-S ."
NIKTO_FLAGS=(
"-ask" "no"
"-F" "html"
)
# Where Nikto writes scan results (must be "." per your requirement)
NIKTO_OUT_DIR="."
# Where this script writes logs (also "." per your requirement)
LOG_DIR="."
############################
# Runner configuration
############################
INPUT="${1:-}"
MAX_CONCURRENT="${2:-5}"
############################
# Safety checks
############################
if [[ -z "${INPUT}" || ! -f "${INPUT}" ]]; then
echo "Usage: $0 [max_concurrent]" >&2
exit 1
fi
command -v screen >/dev/null 2>&1 || { echo "Error: 'screen' not found." >&2; exit 1; }
[[ -f "$NIKTO_BIN" ]] || { echo "Error: nikto.pl not found at: $NIKTO_BIN" >&2; exit 1; }
############################
# Helpers
############################
count_running() {
local count
count=$(screen -ls 2>/dev/null | grep -c 'nikbulk_' 2>/dev/null)
# Strip all whitespace and ensure it's just a number
count="${count//[[:space:]]/}"
# Default to 0 if empty or invalid
if [[ -z "$count" ]] || ! [[ "$count" =~ ^[0-9]+$ ]]; then
count=0
fi
printf "%d" "$count"
}
sanitize() {
echo "$1" | tr -c 'A-Za-z0-9._-`' '_' | tr -s '_' | sed 's/^_//;s/_$//'
}
############################
# Main loop
############################
# First pass: count total valid targets
total_targets=0
while IFS= read -r raw || [[ -n "$raw" ]]; do
line="$(echo "$raw" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
[[ -z "$line" || "$line" =~ ^# ]] && continue
total_targets=$((total_targets + 1))
done < "$INPUT"
# Second pass: launch scans with progress indicator
linenum=0
while IFS= read -r raw || [[ -n "$raw" ]]; do
line="$(echo "$raw" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
[[ -z "$line" || "$line" =~ ^# ]] && continue
linenum=$((linenum+1))
while [[ "$(count_running)" -ge "$MAX_CONCURRENT" ]]; do
sleep 2
done
url="$line"
safe_name="$(sanitize "$url")"
screen_name="nikbulk_${safe_name}_${linenum}"
log_file="${LOG_DIR}/${screen_name}.log"
echo "[${linenum} of ${total_targets}] Scanning: $url"
echo " screen: $screen_name"
echo " log: $log_file"
# Build a safely-quoted command string (no login shell)
cmd="$(printf '%q ' "$NIKTO_BIN" -h "$url" -o "$NIKTO_OUT_DIR" "${NIKTO_FLAGS[@]}")"
# Redirect *inside* screen so logs actually capture Nikto stdout/stderr
screen -S "$screen_name" -d -m bash -c "$cmd >> $(printf '%q' "$log_file") 2>&1"
done < "$INPUT"
echo "******************************************"
echo "All targets queued."
echo ""
# Monitor screen sessions with live status updates
monitor_scans() {
# Temporarily disable exit on error within monitor to prevent premature exits
set +e
local total_scans=$linenum
local running=0
local last_count=-1
local consecutive_zeros=0
if [[ "$total_scans" -eq 0 ]]; then
echo "No scans to monitor."
set -e
return 0
fi
# Give sessions a moment to initialize and start launching
sleep 2
# Show initial message before clearing
echo "Monitoring ${total_scans} scan session(s)..."
echo "Press Ctrl+C to stop monitoring (scans will continue)"
sleep 0.5
# Trap to ensure we can exit cleanly
trap 'echo ""; echo "Monitor stopped. Scans continue in background."; set -e; exit 0' INT TERM
while true; do
# Get running count with error handling
running=$(count_running 2>/dev/null || echo "0")
# Strip any whitespace/newlines
running="${running//[[:space:]]/}"
# Ensure it's a valid number
if ! [[ "$running" =~ ^[0-9]+$ ]]; then
running=0
fi
# Always update display for live feedback - try multiple clear methods
if command -v tput >/dev/null 2>&1; then
tput clear 2>/dev/null || true
else
clear 2>/dev/null || printf '\033[2J\033[H' 2>/dev/null || true
fi
echo "╔════════════════════════════════════════════════════════╗"
echo "║ Nikto Bulk Scan Monitor ║"
echo "╚════════════════════════════════════════════════════════╝"
echo ""
echo " Total sessions: ${total_scans}"
echo " Running: ${running}"
completed=$((total_scans - running)) || completed=0
echo " Completed: ${completed}"
echo ""
if [[ "$running" -gt 0 ]]; then
echo " Active sessions:"
# Get screen sessions and process them without subshell
IFS=$'\n'
sessions=($(screen -ls 2>/dev/null | grep 'nikbulk_' 2>/dev/null || true))
unset IFS
for line in "${sessions[@]}"; do
[[ -z "$line" ]] && continue
session_name=$(echo "$line" | awk '{print $1}' | sed 's/^[0-9]*\.//' 2>/dev/null || echo "unknown")
status=$(echo "$line" | awk '{for(i=2;i/dev/null || echo "unknown")
echo " • ${session_name} - ${status}"
done
else
if [[ "$consecutive_zeros" -lt 2 ]]; then
echo " Waiting for sessions to start..."
else
echo " ✓ All scans completed!"
fi
fi
echo ""
echo " (Press Ctrl+C to exit monitor)"
last_count=$running
# Exit if all scans are done (wait a few checks to be sure)
if [[ "$running" -eq 0 ]] && [[ "$total_scans" -gt 0 ]]; then
consecutive_zeros=$((consecutive_zeros + 1))
if [[ "$consecutive_zeros" -ge 3 ]]; then
sleep 1
clear
echo "╔════════════════════════════════════════════════════════╗"
echo "║ All Scans Completed! ║"
echo "╚════════════════════════════════════════════════════════╝"
echo ""
echo " Total sessions: ${total_scans}"
echo " Completed: ${total_scans}"
echo ""
set -e
break
fi
else
consecutive_zeros=0
fi
# Always sleep to prevent tight loop - this is critical for the loop to continue
sleep 2 || sleep 1 || sleep 0.5 || true
done
# Clear trap on exit and re-enable error handling
trap - INT TERM
set -e
}
# Start monitoring (with error handling to prevent script exit)
monitor_scans || {
echo ""
echo "Monitor exited. Check screen sessions manually: screen -ls | grep nikbulk_"
exit 0
}
================================================
FILE: program/utils/replay.pl
================================================
#!/usr/bin/perl
use strict;
use warnings;
###############################################################################
# SPDX-License-Identifier: GPL-3.0-only
# PURPOSE:Replay a saved request
###############################################################################
use Getopt::Long;
use JSON::PP;
use FindBin;
use File::Spec;
# Determine the program directory (parent of utils/)
# Use RealBin to get absolute path even if script was invoked via symlink
my $program_dir = File::Spec->catdir($FindBin::RealBin || $FindBin::Bin, '..');
$program_dir = File::Spec->rel2abs($program_dir);
# Define replay_usage() to avoid function name collision with nikto_core.plugin's usage()
sub replay_usage {
print "replay.pl -- Replay a saved scan result\n";
print " -file Parse request from this file\n";
print " -proxy Send request through this proxy (format: host:port)\n";
print " -help Help output\n";
exit;
}
# Save @ARGV before requiring nikto_core.plugin (it may process @ARGV)
my @saved_argv = @ARGV;
require File::Spec->catfile($program_dir, 'plugins', 'LW2.pm');
require File::Spec->catfile($program_dir, 'plugins', 'nikto_core.plugin');
# Restore @ARGV for our own GetOptions processing
@ARGV = @saved_argv;
# Initialize variables
my $infile = '';
my $proxy = '';
my $header = '';
my $s_request;
my %request;
my %result;
LW2::http_init_request(\%request);
# options
GetOptions("help" => \&replay_usage,
"file=s" => \$infile,
"proxy=s" => \$proxy
)
or replay_usage();
# Check for file argument if not provided via -file
if ($infile eq '' && @ARGV > 0 && -r $ARGV[0]) {
$infile = $ARGV[0];
}
if ($infile eq '') {
replay_usage();
}
# load save file
if (!-r $infile) {
print "ERROR: Argument 1 should be '-help' or a Nikto save file\n\n";
exit 1;
}
open(my $INFILE, "<$infile") || die "Unable to open file: $!\n\n";
while (<$INFILE>) {
if ($_ =~ /^(Test ID|Message|References):/) { $header .= $_; next; }
next unless $_ =~ /^REQUEST:/;
chomp;
$_ =~ s/^REQUEST://;
$s_request = JSON::PP->new->utf8(1)->allow_nonref(1)->decode($_);
if (ref($s_request) ne 'HASH') {
print "ERROR: Unable to read JSON into request structure\n";
exit 1;
}
}
close($INFILE);
# set into request hash
foreach my $key (keys %{$s_request}) {
$request{$key} = $s_request->{$key};
}
# proxy
if ($proxy ne '') {
my @p = split(/:/, $proxy);
if (($p[0] eq '') || ($p[1] eq '') || ($p[1] =~ /[^\d]/)) {
print "ERROR: Invalid proxy -- use 'host:port' format\n";
exit 1;
}
$request{'whisker'}->{'proxy_host'} = $p[0];
$request{'whisker'}->{'proxy_port'} = $p[1];
}
# output for the user
print "-" x 44, " Info\n";
print "Request to: http";
print "s" if $request{'whisker'}->{'ssl'};
print "://"
. $request{'whisker'}->{'host'} . ":"
. $request{'whisker'}->{'port'}
. $request{'whisker'}->{'uri'} . "\n";
print $header;
# make request
LW2::http_fixup_request(\%request);
LW2::http_do_request_timeout(\%request, \%result);
# output for the user
print "-" x 44, " Response\n";
# Use rebuild_response to properly format the response
print rebuild_response(\%result, 1);
|