local coroutine = require "coroutine"
local http = require "http"
local io = require "io"
local nmap = require "nmap"
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
--
-- @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 https://nmap.org/book/man-legal.html"
categories = { "discovery", "intrusive" }
local arg_domain = stdnse.get_script_args(SCRIPT_NAME..".domain")
local arg_path = stdnse.get_script_args(SCRIPT_NAME..".path") or "/"
local arg_filelist = stdnse.get_script_args(SCRIPT_NAME..'.filelist')
local arg_collapse = tonumber(stdnse.get_script_args(SCRIPT_NAME..".collapse")) or 10
-- Defines domain to use, first from user and then from host
local defineDomain = function(host)
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
end
end
---
-- Collapses a result
-- key -> table
-- @param result table
-- @return string
local collapse = function(result)
local collapsed = {""}
for code, group in next, result do
if #group > arg_collapse then
table.insert(collapsed, ("%d names had status %s"):format(#group, code))
else
for _,name in ipairs(group) do
table.insert(collapsed, name)
end
end
end
return table.concat(collapsed,"\n")
end
local testThread = function(result, host, port, name)
local condvar = nmap.condvar(result)
local targetname = makeTargetName(name , arg_domain)
if targetname ~= nil then
local http_response = http.generic_request(host, port, "HEAD", arg_path, {header={Host=targetname}})
if not http_response.status then
result["ERROR"] = result["ERROR"] or {}
table.insert(result["ERROR"], targetname)
else
local status = tostring(http_response.status)
result[status] = result[status] or {}
if ( 300 <= http_response.status and http_response.status < 400 ) then
table.insert(result[status], ("%s : %s -> %s"):format(targetname, status, (http_response.header.location or "(no Location provided)")))
else
table.insert(result[status], ("%s : %s"):format(targetname, status))
end
end
end
condvar "signal"
end
local readFromFile = function(filename)
local database = {}
for l in io.lines(filename) do
table.insert(database, l)
end
return database
end
portrule = shortport.http
---
-- Script action
-- @param host table
-- @param port table
action = function(host, port)
local result, threads, hostnames = {}, {}, {}
local condvar = nmap.condvar(result)
local status
if arg_filelist then
hostnames = readFromFile(arg_filelist)
else
status, hostnames = datafiles.parse_file("nselib/data/vhosts-default.lst" , {})
if not status then
stdnse.debug1("Can not open file with vhosts file names list")
return
end
end
arg_domain = arg_domain or defineDomain(host)
for _,name in ipairs(hostnames) do
local co = stdnse.new_thread(testThread, result, host, port, name)
threads[co] = true
end
while(next(threads)) do
for t in pairs(threads) do
threads[t] = ( coroutine.status(t) ~= "dead" ) and true or nil
end
if ( next(threads) ) then
condvar "wait"
end
end
return collapse(result)
end