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