local coroutine = require "coroutine"
local dns = require "dns"
local io = require "io"
local math = require "math"
local nmap = require "nmap"
local stdnse = require "stdnse"
local string = require "string"
local stringaux = require "stringaux"
local table = require "table"
local target = require "target"
local rand = require "rand"
description = [[
Attempts to enumerate DNS hostnames by brute force guessing of common
subdomains. With the dns-brute.srv
argument, dns-brute will also
try to enumerate common DNS SRV records.
Wildcard records are listed as "*A" and "*AAAA" for IPv4 and IPv6 respectively.
]]
-- 2011-01-26
---
-- @usage
-- nmap --script dns-brute --script-args dns-brute.domain=foo.com,dns-brute.threads=6,dns-brute.hostlist=./hostfile.txt,newtargets -sS -p 80
-- nmap --script dns-brute www.foo.com
-- @args dns-brute.hostlist The filename of a list of host strings to try.
-- Defaults to "nselib/data/vhosts-default.lst"
-- @args dns-brute.threads Thread to use (default 5).
-- @args dns-brute.srv Perform lookup for SRV records
-- @args dns-brute.srvlist The filename of a list of SRV records to try.
-- Defaults to "nselib/data/dns-srv-names"
-- @args dns-brute.domain Domain name to brute force if no host is specified
--
-- @see dns-nsec3-enum.nse
-- @see dns-ip6-arpa-scan.nse
-- @see dns-nsec-enum.nse
-- @see dns-zone-transfer.nse
--
-- @output
-- Pre-scan script results:
-- | dns-brute:
-- | DNS Brute-force hostnames
-- | www.foo.com - 127.0.0.1
-- | mail.foo.com - 127.0.0.2
-- | blog.foo.com - 127.0.1.3
-- | ns1.foo.com - 127.0.0.4
-- | admin.foo.com - 127.0.0.5
-- |_ *A: 127.0.0.123
--
-- @xmloutput
--
--
-- 127.0.0.1
-- www.foo.com
--
--
-- 127.0.0.2
-- mail.foo.com
--
--
-- 127.0.1.3
-- blog.foo.com
--
--
-- 127.0.0.4
-- ns1.foo.com
--
--
-- 127.0.0.5
-- admin.foo.com
--
-- 127.0.0.123
--
--
author = "Cirrus"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"intrusive", "discovery"}
prerule = function()
if not stdnse.get_script_args("dns-brute.domain") then
stdnse.debug1("Skipping '%s' %s, 'dns-brute.domain' argument is missing.", SCRIPT_NAME, SCRIPT_TYPE)
return false
end
return true
end
hostrule = function(host)
return true
end
local function guess_domain(host)
local name
name = stdnse.get_hostname(host)
if name and name ~= host.ip then
return string.match(name, "%.([^.]+%..+)%.?$") or string.match(name, "^([^.]+%.[^.]+)%.?$")
else
return nil
end
end
-- Single DNS lookup, returning all results. dtype should be e.g. "A", "AAAA".
local function resolve(host, dtype)
local status, result = dns.query(host, {dtype=dtype,retAll=true})
return status and result or false
end
local function array_iter(array, i, j)
return coroutine.wrap(function ()
while i <= j do
coroutine.yield(array[i])
i = i + 1
end
end)
end
local record_mt = {
__tostring = function(t)
return ("%s - %s"):format(t.hostname, t.address)
end
}
local function make_record(hostn, addr)
local record = { hostname=hostn, address=addr }
setmetatable(record, record_mt)
return record
end
local function thread_main(domainname, results, name_iter)
local condvar = nmap.condvar( results )
for name in name_iter do
for _, dtype in ipairs({"A", "AAAA"}) do
local res = resolve(name..'.'..domainname, dtype)
if(res) then
table.sort(res)
if results["*" .. dtype] ~= res[1] then
for _,addr in ipairs(res) do
local hostn = name..'.'..domainname
if target.ALLOW_NEW_TARGETS then
stdnse.debug1("Added target: "..hostn)
local status,err = target.add(hostn)
end
stdnse.debug2("Hostname: "..hostn.." IP: "..addr)
results[#results+1] = make_record(hostn, addr)
end
end
end
end
end
condvar("signal")
end
local function srv_main(domainname, srvresults, srv_iter)
local condvar = nmap.condvar( srvresults )
for name in srv_iter do
local res = resolve(name..'.'..domainname, "SRV")
if(res) then
for _,addr in ipairs(res) do
local hostn = name..'.'..domainname
addr = stringaux.strsplit(":",addr)
for _, dtype in ipairs({"A", "AAAA"}) do
local srvres = resolve(addr[4], dtype)
if(srvres) then
for srvhost,srvip in ipairs(srvres) do
if target.ALLOW_NEW_TARGETS then
stdnse.debug1("Added target: "..srvip)
local status,err = target.add(srvip)
end
stdnse.debug1("Hostname: "..hostn.." IP: "..srvip)
srvresults[#srvresults+1] = make_record(hostn, srvip)
end
end
end
end
end
end
condvar("signal")
end
local function detect_wildcard(domainname, record)
local rand_host1 = rand.random_alpha(24).."."..domainname
local rand_host2 = rand.random_alpha(24).."."..domainname
local res1 = resolve(rand_host1, record)
stdnse.debug1("Detecting wildcard for \"%s\" records using random hostname \"%s\".", record, rand_host1)
if res1 then
stdnse.debug1("Random hostname resolved. Comparing to second random hostname \"%s\".", rand_host2)
local res2 = resolve(rand_host2, record)
table.sort(res1)
table.sort(res2)
if (res1[1] == res2[1]) then
stdnse.debug1("Both random hostnames resolved to the same IP. Wildcard detected.")
return res1[1]
end
end
return nil
end
action = function(host)
local domainname = stdnse.get_script_args('dns-brute.domain')
if not domainname then
domainname = guess_domain(host)
end
if not domainname then
return string.format("Can't guess domain of \"%s\"; use %s.domain script argument.", stdnse.get_hostname(host), SCRIPT_NAME)
end
if not nmap.registry.bruteddomains then
nmap.registry.bruteddomains = {}
end
if nmap.registry.bruteddomains[domainname] then
stdnse.debug1("Skipping already-bruted domain %s", domainname)
return nil
end
nmap.registry.bruteddomains[domainname] = true
stdnse.debug1("Starting dns-brute at: "..domainname)
local max_threads = tonumber( stdnse.get_script_args('dns-brute.threads') ) or 5
local dosrv = stdnse.get_script_args("dns-brute.srv") or false
stdnse.debug1("THREADS: "..max_threads)
-- First look for dns-brute.hostlist
local fileName = stdnse.get_script_args('dns-brute.hostlist')
-- Check fetchfile locations, then relative paths
local commFile = (fileName and nmap.fetchfile(fileName)) or fileName
-- Finally, fall back to vhosts-default.lst
commFile = commFile or nmap.fetchfile("nselib/data/vhosts-default.lst")
local hostlist = {}
if commFile then
for l in io.lines(commFile) do
if not l:match("#!comment:") then
table.insert(hostlist, l)
end
end
else
stdnse.debug1("Cannot find hostlist file, quitting")
return
end
local threads, results, srvresults = {}, {}, {}
for _, dtype in ipairs({"A", "AAAA"}) do
results["*" .. dtype] = detect_wildcard(domainname, dtype)
end
local condvar = nmap.condvar( results )
local i = 1
local howmany = math.floor(#hostlist/max_threads)+1
stdnse.debug1("Hosts per thread: "..howmany)
repeat
local j = math.min(i+howmany, #hostlist)
local name_iter = array_iter(hostlist, i, j)
threads[stdnse.new_thread(thread_main, domainname, results, name_iter)] = true
i = j+1
until i > #hostlist
local done
-- wait for all threads to finish
while( not(done) ) do
done = true
for thread in pairs(threads) do
if (coroutine.status(thread) ~= "dead") then done = false end
end
if ( not(done) ) then
condvar("wait")
end
end
if(dosrv) then
-- First look for dns-brute.srvlist
fileName = stdnse.get_script_args('dns-brute.srvlist')
-- Check fetchfile locations, then relative paths
commFile = (fileName and nmap.fetchfile(fileName)) or fileName
-- Finally, fall back to dns-srv-names
commFile = commFile or nmap.fetchfile("nselib/data/dns-srv-names")
local srvlist = {}
if commFile then
for l in io.lines(commFile) do
if not l:match("#!comment:") then
table.insert(srvlist, l)
end
end
i = 1
threads = {}
howmany = math.floor(#srvlist/max_threads)+1
condvar = nmap.condvar( srvresults )
stdnse.debug1("SRV's per thread: "..howmany)
repeat
local j = math.min(i+howmany, #srvlist)
local name_iter = array_iter(srvlist, i, j)
threads[stdnse.new_thread(srv_main, domainname, srvresults, name_iter)] = true
i = j+1
until i > #srvlist
local done
-- wait for all threads to finish
while( not(done) ) do
done = true
for thread in pairs(threads) do
if (coroutine.status(thread) ~= "dead") then done = false end
end
if ( not(done) ) then
condvar("wait")
end
end
else
stdnse.debug1("Cannot find srvlist file, skipping")
end
end
local response = stdnse.output_table()
if(#results==0) then
setmetatable(results, { __tostring = function(t) return "No results." end })
end
response["DNS Brute-force hostnames"] = results
if(dosrv) then
if(#srvresults==0) then
setmetatable(srvresults, { __tostring = function(t) return "No results." end })
end
response["SRV results"] = srvresults
end
return response
end