diff --git a/CHANGELOG b/CHANGELOG
index 3f2463ba8..eb75789ad 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,9 @@
# Nmap Changelog ($Id$); -*-text-*-
+o showHTMLTitle.nse can now follow (non-standard) relative redirects,
+ and may do a DNS lookup to find if the redirected-to host has the
+ same IP address as the scanned host. [Jah]
+
o Enhanced the tohex() function in the NSE stdnse library to support strings
and added options to control the formatting. [Sven]
diff --git a/scripts/showHTMLTitle.nse b/scripts/showHTMLTitle.nse
index 97740b511..502af9c4a 100644
--- a/scripts/showHTMLTitle.nse
+++ b/scripts/showHTMLTitle.nse
@@ -14,8 +14,11 @@ license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"default", "demo", "safe"}
-require 'http'
-require 'url'
+local url = require 'url'
+local dns = require 'dns'
+local http = require 'http'
+local ipOps = require 'ipOps'
+local stdnse = require 'stdnse'
portrule = function(host, port)
if not (port.service == 'http' or port.service == 'https') then
@@ -30,34 +33,138 @@ portrule = function(host, port)
end
action = function(host, port)
- local data, result, title, protocol
- data = http.get( host, port, '/' )
- -- follow ONE redirect if host is not some other host
- if data.status == 301 or data.status == 302 then
- local url = url.parse( data.header.location )
- if url.host == host.targetname or url.host == ( host.name ~= '' and host.name ) or url.host == host.ip then
- stdnse.print_debug("showHTMLTitle.nse: Default page is located at %s://%s%s", url.scheme, url.authority, url.path)
- data = http.get( host, port, url.path )
- end
- end
- result = data.body
+ local data, result, redir, title
- -- watch out, this doesn't really work for all html tags
- result = string.gsub(result, "<(/?%a+)>", function(c) return "<" .. string.lower(c) .. ">" end)
+ data = http.get( host, port, '/' )
- title = string.match(result, "
(.+)")
+ -- check for a redirect
+ if data and data.status and tostring( data.status ):match( "30%d" ) and data.header and data.header.location then
+ redir = ("Did not follow redirect to %s"):format( data.header.location )
+ local url = url.parse( data.header.location )
+ local loc = {}
+ -- test the redirect to see if we're allowed to go there
+ for i, rule in ipairs( rules ) do
+ if not rule( loc, url, host, port ) then break end
+ end
+ -- follow redirect
+ if loc.host and loc.port and loc.path then
+ redir = ("Requested resource was %s://%s%s"):format( url.scheme or port.service, loc.host, loc.path )
+ data = http.get( loc.host, loc.port, loc.path )
+ else
+ loc = nil -- killed so we know we didn't follow a redirect
+ end
+ end
- if title ~= nil then
- result = string.gsub(title , "[\n\r\t]", "")
- if string.len(title) > 65 then
- stdnse.print_debug("showHTMLTitle.nse: Title got truncated!");
- result = string.sub(result, 1, 62) .. "..."
- end
- else
- result = "Site doesn't have a title."
- end
+ -- check that body was received
+ if data.body and data.body ~= "" then
+ result = data.body
+ else
+ -- debug msg and no output; or no debug msg and some output if we were redirected.
+ if not redir then stdnse.print_debug( "showHTMLTitle.nse: %s did not respond with any data.", host.targetname or host.ip ) end
+ return (redir and ("%s %s no page was returned."):format( redir, (loc and ", but") or "and" )) or nil
+ end
+
+ -- try and match title tags
+ title = string.match(result, "<[Tt][Ii][Tt][Ll][Ee][^>]*>([^<]*)[Tt][Ii][Tt][Ll][Ee]>")
+
+ if title and title ~= "" then
+ result = string.gsub(title , "[\n\r\t]", "")
+ if string.len(title) > 65 then
+ stdnse.print_debug("showHTMLTitle.nse: (%s) Title got truncated!", host.targetname or host.ip );
+ result = string.sub(result, 1, 62) .. "..."
+ end
+ else
+ result = ("Site doesn't have a title%s"):format( ( data.header and data.header["content-type"] and (" (%s)."):format( data.header["content-type"] ) ) or ".")
+ end
+
+ return (redir and ("%s\n%s"):format( result, redir )) or result
- return result
end
+
+
+rules = {
+ function (loc, url, host, port)
+ -- if url.scheme is present then it must match the scanned port
+ if url.scheme and url.scheme ~= port.service then return false end
+ return true
+ end,
+
+ function (loc, url, host, port)
+ -- bail if userinfo is present
+ return ( url.userinfo and false ) or true
+ end,
+
+ function (loc, url, host, port)
+ -- if present, url.host must be the same scanned target
+ -- loc.host must be set if returning true
+ if not url.host then
+ loc.host = ( host.targetname or host.ip )
+ return true
+ end
+ if url.host and
+ url.host == host.ip or
+ url.host == host.targetname or
+ url.host == ( host.name ~= '' and host.name ) or
+ is_vhost( url.host, host ) then -- dns lookup as last resort
+ loc.host = url.host
+ return true
+ end
+ return false
+ end,
+
+ function (loc, url, host, port)
+ -- if present, url.port must be the same as the scanned port
+ -- loc.port must be set if returning true
+ if (not url.port) or url.port == port.number then
+ loc.port = port
+ return true
+ end
+ return false
+ end,
+
+ function (loc, url, host, port)
+ -- path cannot be unchanged unless host has changed
+ -- loc.path must be set if returning true
+ if ( not url.path or url.path == "/" ) and url.host == ( host.targetname or host.ip) then return false end
+ if not url.path then loc.path = "/"; return true end
+ loc.path = ( ( url.path:sub(1,1) == "/" and "" ) or "/" ) .. url.path -- ensuring leading slash
+ return true
+ end,
+
+ function (loc, url, host, port)
+ -- always true - jut add the query to loc.path
+ if url.query then loc.path = ("%s?%s"):format( loc.path, url.query ) end
+ return true
+ end
+ }
+
+
+function is_vhost( rhost, host )
+
+ -- query is sane?
+ if rhost:match( ":" ) or rhost:match( "^[%d%.]+$" ) then
+ return false
+ end
+
+ local opts = {}
+ opts.dtype = "A"
+ opts.retAll = true
+ if host.ip:match( ":" ) then opts.dtype = "AAAA" end
+
+ local answer, msg = dns.query( rhost, opts )
+
+ if not answer then
+ stdnse.print_debug( "showHTMLTitle: DNS query failed for target %s. Query was: %s. Error Code: %s", host.targetname or host.ip, rhost, msg or "nil" )
+ return false
+ end
+
+ for i, ip_rec in ipairs( answer ) do
+ if ipOps.compare_ip( ip_rec, "eq", host.ip ) then
+ return true
+ end
+ end
+
+ return false
+end