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][^>]*>([^<]*)") + + 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