1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185
|
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
<code>Host</code> 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 <code>http-vhosts.domain</code> argument or
deduced from the target's name. For example when scanning www.example.com,
various names of the form <name>.example.com are tried.
]]
---
-- @usage
-- nmap --script http-vhosts -p 80,8080,443 <target>
--
-- @arg http-vhosts.domain The domain that hostnames will be prepended to, for
-- example <code>example.com</code> 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 <code>/</code>.
-- @arg http-vhosts.collapse The limit to start collapsing results by status code. Default <code>20</code>
-- @arg http-vhosts.filelist file with the vhosts to try. Default <code>nselib/data/vhosts-default.lst</code>
--
-- @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
|