local http = require "http"
local shortport = require "shortport"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"
local datafiles = require "datafiles"
description = [[
Searches for web virtual hostnames by making a large number of HEAD requests against http servers using common hostnames.
Each HEAD request provides a different
Host
header. The hostnames come from a built-in default
list. Shows the names that return a document. Also shows the location of
redirections.
The domain can be given as the http-vhosts.domain
argument or
deduced from the target's name. For example when scanning www.example.com,
various names of the form .example.com are tried.
]]
---
-- @usage
-- nmap --script http-vhosts -p 80,8080,443
-- @arg http-vhosts.domain The domain that hostnames will be prepended to, for
-- example example.com
yields www.example.com, www2.example.com,
-- etc. If not provided, a guess is made based on the hostname.
-- @arg http-vhosts.path The path to try to retrieve. Default /
.
-- @arg http-vhosts.collapse The limit to start collapsing results by status code. Default 20
-- @arg http-vhosts.filelist file with the vhosts to try. Default nselib/data/vhosts-default.lst
-- @output
-- PORT STATE SERVICE REASON
-- 80/tcp open http syn-ack
-- | http-vhosts:
-- | example.com: 301 -> http://www.example.com/
-- | www.example.com: 200
-- | docs.example.com: 302 -> https://www.example.com/docs/
-- |_images.example.com: 200
--
-- @todo feature: move hostnames to an external file and allow the user to use another one
-- @internal: see http://seclists.org/nmap-dev/2010/q4/401 and http://seclists.org/nmap-dev/2010/q4/445
--
--
-- @todo feature: add option report and implement it
-- @internal after stripping sensitive info like ip, domain names, hostnames
-- and redirection targets from the result, append it to a file
-- that can then be uploaded. If enough info is gathered, the names
-- will be weighted. It can be shared with metasploit
--
-- @todo feature: fill nsedoc
--
-- @todo feature: register results for other scripts (external help needed)
--
-- @todo feature: grow names list (external help needed)
--
author = "Carlos Pantelides"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = { "discovery", "intrusive" }
-- Defines domain to use, first from user and then from host
defineDomain = function(host)
if stdnse.get_script_args(SCRIPT_NAME..".domain") then return stdnse.get_script_args(SCRIPT_NAME..".domain") end
local name = stdnse.get_hostname(host)
if name and name ~= host.ip then
local pos = string.find (name, ".",1,true)
if not pos then return name end
return string.sub (name, pos + 1)
end
end
---
-- Makes a target name with a name and a domain
-- @param name string
-- @param domain string
-- @return string
local makeTargetName = function(name,domain)
if name and name ~= "" then
if domain and domain ~= "" then
return name .. "." .. domain
else
return name
end
elseif domain and domain ~= "" then
return domain
else
return nil
end
end
---
-- Collapses a result
-- key -> table
-- @param result table
-- @return string
local collapse = function(result)
local collapsed = {""}
local limit = tonumber(stdnse.get_script_args(SCRIPT_NAME..".collapse")) or 10
for code, group in next, result do
if #group > limit then
collapsed[#collapsed + 1] = #group .. " names had status " .. code
else
for _,name in ipairs(group) do
collapsed[#collapsed + 1] = name
end
end
end
return table.concat(collapsed,"\n")
end
portrule = shortport.http
---
-- Script action
-- @param host table
-- @param port table
action = function(host, port)
local service = "http"
local domain = defineDomain(host)
local path = stdnse.get_script_args(SCRIPT_NAME..".path") or "/"
local result = {}
local filelist = stdnse.get_script_args(SCRIPT_NAME..'filelist')
local status, HOSTNAMES = datafiles.parse_file(filelist or "nselib/data/vhosts-default.lst" , {})
if not status then
stdnse.print_debug(1, "Can not open file with vhosts file names list")
return {}
end
for _,name in ipairs(HOSTNAMES) do
local http_response
local targetname
targetname = makeTargetName(name , domain)
if targetname ~= nil then
http_response = http.head(host, port, path, {header={Host=targetname}, bypass_cache=true, redirect_ok = false})
if not http_response.status then
if not http_response["ERROR"] then
result["ERROR"]={}
end
result["ERROR"][#result["ERROR"] + 1] = targetname
else
local status = tostring(http_response.status)
if not result[status] then
result[status]={}
end
if 300 <= http_response.status and http_response.status < 400 then
result[status][#result[status] + 1] = targetname .. " : " .. status .. " -> " .. (http_response.header.location or "(no Location provided)")
else
result[status][#result[status] + 1] = targetname .. " : " .. status
end
end
end
end
return collapse(result)
end