############################################################################### # SPDX-License-Identifier: GPL-3.0-only # PURPOSE: General HTTP headers checks ############################################################################### use vars qw/$HEADERSDB/; sub nikto_headers_init { my $id = { name => "headers", full_name => "HTTP Headers", author => "Sullo", description => "Performs various checks against the headers returned from an HTTP request.", hooks => { scan => { method => \&nikto_headers, }, postfetch => { method => \&nikto_headers_postfetch, }, start => { method => \&nikto_headers_load, }, }, copyright => "2008 Chris Sullo" }; # some global variables use vars qw/%ETAGS %BREACH %HEADERS_FOUND/; return $id; } sub nikto_headers_load { $HEADERSDB = init_db("db_headers_common"); } sub nikto_headers_postfetch { my ($mark, $parameters, $request, $response) = @_; return if $mark->{'terminate'}; # Skip OPTIONS return if $request->{'whisker'}{'method'} eq "OPTIONS"; # These headers are very common but unlikely to be useful, so we can quickly skip them my %skip_headers = map { $_ => 1 } qw( whisker date content-type content-length connection x-mod-pagespeed x-page-speed set-cookie ); # look for internal IPs foreach my $header (keys %$response) { next if exists $skip_headers{$header} || exists $HEADERS_FOUND{ $mark->{hostname} }{ $mark->{port} }{$header}; # Check for known headers nikto_headers_check($mark, $response, $header, 999986, $request); my $header_value = $response->{$header}; # Do not check the Location header's path target for internal IPs if ($header eq 'location') { if ($header_value =~ /^\//) { next; } else { my @uri_parts = LW2::uri_split($header_value); # reconstruct without path/query/fragment $header_value = $uri_parts[2]; } # Is it what we scanned? if (defined $header_value && ( $header_value eq $mark->{'ip'} || (defined $mark->{'vhost'} && $header_value eq $mark->{'vhost'}) || (defined $mark->{'hostname'} && $header_value eq $mark->{'hostname'}) || (defined $mark->{'displayname'} && $header_value eq $mark->{'displayname'})) ) { next; } } foreach my $ip (get_ips($header_value)) { my ($valid, $internal, $loopback) = is_ip($ip); if ($valid && !$loopback) { if ($ip ne $mark->{'ip'}) { # is it an internal, or just different? my $msg; $msg .= $request->{'whisker'}{'uri'} . ": "; if ($internal) { $msg .= "RFC-1918 "; } $msg .= "IP address found in the '$header' header. The IP is \"$ip\"."; add_vulnerability( $mark, $msg, 999979, "https://portswigger.net/kb/issues/00600300_private-ip-addresses-disclosed", $request->{'whisker'}->{'method'}, $request->{'whisker'}->{'uri'}, $request, $response ); $HEADERS_FOUND{ $mark->{hostname} }{ $mark->{port} }{$header} = 1; } } } } # look for inode in etag header if ( !defined $ETAGS{ $mark->{hostname} }{ $mark->{port} } && defined $response->{'etag'} && $response->{'etag'} =~ m/-/) { my $etag = $response->{'etag'}; $etag =~ s/"//g; my @fields = split("-", $etag); # Only report ETags which actually leak inodes... if (scalar(@fields) == 3) { my $message = $request->{'whisker'}{'uri'} . ": Server may leak inodes via ETags, header found with file " . $request->{'whisker'}->{'uri'}; # check for numbers that are too large my $ishex = 1; for (my $i = 0 ; $i < 3 ; $i++) { if ((length($fields[$i]) > 14) || ($fields[$i] !~ /^[0-9A-F]+$/i)) { $ishex = 0; } } use bignum; my ($inode, $size, $mtime, $ltime); if ($ishex) { $inode = "0x$fields[0]"; $size = "0x$fields[1]"; $mtime = "0x$fields[2]"; $ltime = substr(sprintf("%s", hex($mtime)), 0, 10); } else { $inode = $fields[0]; $size = $fields[1]; $mtime = $ltime = $fields[2]; } $message .= ($ishex == 1) ? sprintf(", inode: %d, size: %d, mtime: %s", hex($inode), hex($size), scalar(localtime($ltime))) : sprintf(", inode: %s, size: %s, mtime: %s", $inode, $size, $mtime); add_vulnerability($mark, $message, 999984, "CVE-2003-1418", $request->{'whisker'}->{'method'}, $request->{'whisker'}->{'uri'}, $request, $response); $ETAGS{ $mark->{hostname} }{ $mark->{port} } = 1; } } if (defined $response->{'link'} && !$HEADERS_FOUND{ $mark->{hostname} }{ $mark->{port} }{'link'}) { my $values = report_multiheaders($response->{'link'}); add_vulnerability( $mark, $request->{'whisker'}{'uri'} . ": Link header(s) found with value(s): $values.", "000427", "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link", $request->{'whisker'}->{'method'}, $request->{'whisker'}->{'uri'}, $request, $response ); $HEADERS_FOUND{ $mark->{hostname} }{ $mark->{port} }{'link'} = 1; } # Look for any uncommon headers foreach my $header (keys %$response) { next if defined $HEADERS_FOUND{ $mark->{'hostname'} }{ $mark->{'port'} }{"uncommon-$header"}; my $found = 0; foreach my $st_header (@$HEADERSDB) { if ($header eq $st_header->{'header'}) { $found = 1; last; } } if ($found == 0) { my $x = $response->{$header}; $x =~ s/\s+.*$//; $mark->{'components'}->{$x} = 1; my $values = report_multiheaders($response->{$header}); $vuln = $request->{'whisker'}{'uri'} . ": Uncommon header(s) '$header' found, with contents: $values."; add_vulnerability($mark, $vuln, 999100, "", $request->{'whisker'}->{'method'}, $request->{'whisker'}->{'uri'}, $request, $response); $HEADERS_FOUND{ $mark->{'hostname'} }{ $mark->{'port'} }{"uncommon-$header"} = 1; } } # Strict-Transport-Security if ( $mark->{'ssl'} && !$HEADERS_FOUND{ $mark->{hostname} }{ $mark->{port} }{'strict-transport-security'} && defined $response) { if ($response->{'strict-transport-security'} =~ /max-age="?0"?/i) { add_vulnerability( $mark, $request->{'whisker'}{'uri'} . ": The site uses TLS and the Strict-Transport-Security HTTP header is set with max-age=0, which effectively disables it.", 999971, "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security", $request->{'whisker'}->{'method'}, $request->{'whisker'}->{'uri'}, $request, $response ); } $HEADERS_FOUND{ $mark->{hostname} }{ $mark->{port} }{'strict-transport-security'} = 1; } # Alt-Svc (HTTP/3) if (!$HEADERS_FOUND{ $mark->{hostname} }{ $mark->{port} }{'alt-svc'} && defined $response->{'whisker'}->{'code'}) { if (defined $response->{'alt-svc'}) { my ($protocol, $msg); $response->{'alt-svc'} =~ /.*(h[23])="([^"]+)"/; my $endpoint = $2; if ($1 eq "h2") { $protocol = "HTTP/2 over TLS"; } elsif ($1 eq "h2c") { $protocol = "HTTP/2 over TCP"; $msg = " Unencrypted."; } elsif ($1 eq "h3") { $protocol = "HTTP/3"; $msg = " Nikto cannot test HTTP/3 over QUIC."; } else { $protocol = $1; } add_vulnerability( $mark, $request->{'whisker'}{'uri'} . ": An alt-svc header was found which is advertising $protocol. The endpoint is: '$endpoint'.$msg", "011799", "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/alt-svc", $request->{'whisker'}->{'method'}, $request->{'whisker'}->{'uri'}, $request, $response ); $HEADERS_FOUND{ $mark->{hostname} }{ $mark->{port} }{'alt-svc'} = 1; } } # x-clacks-overhead if (!$HEADERS_FOUND{ $mark->{hostname} }{ $mark->{port} }{'x-clacks-overhead'} && defined $response->{'whisker'}->{'code'}) { if (defined $response->{'x-clacks-overhead'}) { add_vulnerability( $mark, $request->{'whisker'}{'uri'} . ": There appears to be Clacks Overhead on the server and the message is: $response->{'x-clacks-overhead'}", 999104, "https://xclacksoverhead.org/home/about", $request->{'whisker'}->{'method'}, $request->{'whisker'}->{'uri'}, $request, $response ); $HEADERS_FOUND{ $mark->{hostname} }{ $mark->{port} }{'x-clacks-overhead'} = 1; } } # CSP Report URLs if (!$HEADERS_FOUND{ $mark->{hostname} }{ $mark->{port} }{'report-uri'} && defined $response->{'whisker'}->{'code'}) { if (defined $response->{'report-uri'}) { add_vulnerability( $mark, $request->{'whisker'}{'uri'} . ": A CSP report-uri header was found. The URL is: $response->{'report-uri'}", 000433, "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-uri", $request->{'whisker'}->{'method'}, $request->{'whisker'}->{'uri'}, $request, $response ); $HEADERS_FOUND{ $mark->{hostname} }{ $mark->{port} }{'report-uri'} = 1; } } # Cloudflare if ( !$HEADERS_FOUND{ $mark->{hostname} }{ $mark->{port} }{'cf-ray'} && defined $response->{'cf-ray'} && !$CLI{'useproxy'}) { add_vulnerability( $mark, $request->{'whisker'}{'uri'} . ": Cloudflare detected via cf-ray header. Recommend proxying via Burp or mitmproxy to avoid TLS fingerprint blocks.", 999106, "https://github.com/sullo/nikto/wiki/Using-a-Proxy", $request->{'whisker'}->{'method'}, $request->{'whisker'}->{'uri'}, $request, $response ); $HEADERS_FOUND{ $mark->{hostname} }{ $mark->{port} }{'cf-ray'} = 1; } return $request, $response; } # If multiple headers were sent with the same name, turn the array into a string for reporting sub report_multiheaders { my $header = $_[0] || return; if (ref($header) eq 'ARRAY') { return join(',', @{$header}); } else { return $header; } } sub nikto_headers_check { my ($mark, $response, $header, $tid, $request) = @_; return if $mark->{'terminate'}; my @interesting_headers = split(" ", $VARIABLES{'@INTERESTING_HEADERS'}); foreach my $header (@interesting_headers) { if (exists $response->{$header}) { if (!exists $HEADERS_FOUND{ $mark->{'hostname'} }{ $mark->{'port'} } {"interesting-$header"}) { my $value = ""; if (ref($response->{$header}) eq ARRAY) { $value = join(', ', @{ $response->{$header} }); } else { $value = $response->{$header}; } $HEADERS_FOUND{ $mark->{'hostname'} }{ $mark->{'port'} }{"interesting-$header"} = 1; add_vulnerability( $mark, $request->{'whisker'}{'uri'} . ": Retrieved $header header: $value", $tid, "", $request->{'whisker'}->{'method'}, $request->{'whisker'}->{'uri'}, $request, $response ); } } } } sub nikto_headers { my ($mark) = @_; return if $mark->{'terminate'}; ####################################################################### # look for to see whether its vulnerable to the Translate: f my %transheaders; $transheaders{'Translate'} = "f"; foreach my $f (qw/\/index.asp \/njkk999.asp \/index.aspx \/jkl988.aspx \/login.asp \/login.aspx/) { return if $mark->{'terminate'}; my ($res, $content, $error, $request, $response) = nfetch($mark, $f, "GET", "", "", "", "Headers"); if ($res eq "200") { ($res, $content, $error, $request, $response) = nfetch($mark, $f . "\\", "GET", "", \%transheaders, "", "Headers"); if ($res eq "200") { if ($content =~ /{'whisker'}{'uri'} . ": Host may be vulnerable to a source disclosure using the Translate: header", 999983, "CVE-2000-0778", "GET", $f, $request, $response ); last; } } } } ####################################################################### # Content-Location header in IIS my (%locheaders, %locflags, $cl, $l, $wa, $rt); $locheaders{'User-Agent'} = get_ua(); $locflags{'noclean'} = 1; $locflags{'nohost'} = 1; $cl = $l = $wa = $rt = 0; foreach my $uri ('/', '/images', '/Autodiscover/Autodiscover.xml', '/Autodiscover/', '/Microsoft-Server-ActiveSync', '/Microsoft-Server-ActiveSync/default.css', '/ECP', '/EWS', '/EWS/Exchange.asmx', '/Exchange', '/OWA', '/Microsoft-Server-ActiveSync/default.eas', '/Rpc', '/EWS/Services.wsdl', '/ecp', '/OAB', '/aspnet_client', '/PowerShell' ) { return if $mark->{'terminate'}; my ($res, $content, $errors, $request, $response) = nfetch($mark, $uri, "GET", "", \%locheaders, \%locflags, "Headers", "1.0"); if (!$cl && ($response->{'content-location'} ne "")) { my @ips = get_ips($response->{'content-location'}); my ($valid, $internal, $loopback) = is_ip($ips[0]); if (($ips[0] ne '') && ($ips[0] ne $mark->{'ip'})) { $cl = 1; add_vulnerability( $mark, $uri . ": IIS may reveal its internal or real IP in the Content-Location header via a request with HTTP/1.0. The value is \"$ips[0]\".", 999989, "CVE-2000-0649", "GET", $uri, $request, $response ); } } if (!$l && ($response->{'location'} ne "")) { my @ips = get_ips($response->{'location'}); my ($valid, $internal, $loopback) = is_ip($ips[0]); if (($ips[0] ne '') && ($ips[0] ne $mark->{'ip'})) { $l = 1; add_vulnerability( $mark, $uri . ": The web server may reveal its internal or real IP in the Location header via a request to with HTTP/1.0. The value is \"$ips[0]\".", 999988, "CVE-2000-0649", "GET", $uri, $request, $response ); } } if (!$wa && ($response->{'www-authenticate'} ne "")) { my @ips = get_ips($response->{'www-authenticate'}); my ($valid, $internal, $loopback) = is_ip($ips[0]); if (($ips[0] ne '') && ($ips[0] ne $mark->{'ip'})) { $wa = 1; add_vulnerability( $mark, $request->{'whisker'}{'uri'} . ": Microsoft Exchange Systems (CAS and OWA) may reveal the internal or real IP in the WWW-Authenticate header via a request to $uri over HTTP/1.0. The value is \"$ips[0]\".", 999986, "CVE-2000-0649", "GET", $uri, $request, $response ); } } if (!$rt && ($response->{'report-to'} ne "")) { my @bits = split("url\": \"", $response->{'report-to'}); my $have = 0; my @urls; foreach my $b (@bits) { next if $b !~ /^http/; $b =~ s/\".*$//; if ($b ne "") { push(@urls, $b); $have = $rt = 1; } } if ($have) { add_vulnerability( $mark, $request->{'whisker'}{'uri'} . ": A CSP report-to header was found with the following URLs: " . join(", ", @urls), 999945, "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-to", "GET", $uri, $request, $response ); } } if ($cl && $l && $wa && $rt) { last; } } ####################################################################### # Location header in WebLogic # Clear out locheaders for (keys %locheaders) { delete $locheaders{$_}; } $locheaders{'User-Agent'} = get_ua(); ($res, $content, $errors, $request, $response) = nfetch($mark, '.', "GET", "", \%locheaders, \%flags, "Headers", "1.0"); my @ips = get_ips($response->{'location'}); if ($ips[0] ne '') { my ($valid, $internal, $loopback) = is_ip($ips[0]); if ($valid && !$loopback && ($ips[0] ne $mark->{'ip'})) { my $msg = $request->{'whisker'}{'uri'} . ": WebLogic may reveal "; if ($internal) { $msg .= "an RFC-1918 IP address. "; } else { $msg .= "a different IP address in the Location header."; } $msg .= "The IP is \"$ips[0]\"."; add_vulnerability( $mark, $msg, 999987, "https://web.archive.org/web/20080821223545/http://archives.neohapsis.com/archives/bugtraq/2003-04/0034.html", "GET", ".", $request, $response ); } } ####################################################################### # BREACH Checks for (keys %locheaders) { delete $locheaders{$_}; } $locheaders{'User-Agent'} = get_ua(); $locheaders{'Accept-Encoding'} = "deflate, gzip"; ($res, $content, $errors, $request, $response) = nfetch($mark, '/', "GET", "", \%locheaders, \%flags, "Headers", "1.1"); if (!$BREACH && defined $response && $mark->{'ssl'}) { if (defined $response->{'content-encoding'}) { if ($response->{'content-encoding'} =~ "(deflate|gzip)") { $BREACH = 1; add_vulnerability( $mark, $request->{'whisker'}{'uri'} . ": The Content-Encoding header is set to \"deflate\" which may mean that the server is vulnerable to the BREACH attack.", 999966, "http://breachattack.com/", $request->{'whisker'}->{'method'}, $request->{'whisker'}->{'uri'}, $request, $response ); } } } ####################################################################### # Jetty CVE-2015-2080 for (keys %locheaders) { delete $locheaders{$_}; } $locheaders{'User-Agent'} = get_ua(); $locheaders{'Nikto'} = "\\x1f"; ($res, $content, $errors, $request, $response) = nfetch($mark, '/', "GET", "", \%locheaders, \%flags, "Headers", "1.0"); if (($res eq "400") && ($response->{'whisker'}->{'message'} =~ /x1f<<{'whisker'}{'uri'} . ": Jetty > 9.2.3 and < 9.2.9 may reveal the contents of internal memory if illegal characters are passed.", "000477", "CVE-2015-2080", "GET", ".", $request, $response ); } ####################################################################### # Missing suggested headers ($res, $content, $errors, $request, $response) = nfetch($mark, '/', "GET", "", {}, {}, "Headers", "1.1"); while (my ($header, $ref) = each %{ $VARIABLES->{'SUGGESTED_HEADERS'} }) { if (!exists $response->{$header}) { add_vulnerability($mark, $request->{'whisker'}{'uri'} . ": Suggested security header missing: " . $header, "013587", $ref, "GET", "/", $request, $response); } } } 1;