diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 07:42:04 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 07:42:04 +0000 |
commit | 0d47952611198ef6b1163f366dc03922d20b1475 (patch) | |
tree | 3d840a3b8c0daef0754707bfb9f5e873b6b1ac13 /scripts/dns-nsec3-enum.nse | |
parent | Initial commit. (diff) | |
download | nmap-0d47952611198ef6b1163f366dc03922d20b1475.tar.xz nmap-0d47952611198ef6b1163f366dc03922d20b1475.zip |
Adding upstream version 7.94+git20230807.3be01efb1+dfsg.upstream/7.94+git20230807.3be01efb1+dfsgupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'scripts/dns-nsec3-enum.nse')
-rw-r--r-- | scripts/dns-nsec3-enum.nse | 427 |
1 files changed, 427 insertions, 0 deletions
diff --git a/scripts/dns-nsec3-enum.nse b/scripts/dns-nsec3-enum.nse new file mode 100644 index 0000000..1932ce5 --- /dev/null +++ b/scripts/dns-nsec3-enum.nse @@ -0,0 +1,427 @@ +local stdnse = require "stdnse" +local shortport = require "shortport" +local dns = require "dns" +local base32 = require "base32" +local nmap = require "nmap" +local string = require "string" +local stringaux = require "stringaux" +local table = require "table" +local tableaux = require "tableaux" +local rand = require "rand" + +local openssl = stdnse.silent_require "openssl" + +description = [[ +Tries to enumerate domain names from the DNS server that supports DNSSEC +NSEC3 records. + +The script queries for nonexistant domains until it exhausts all domain +ranges keeping track of hashes. At the end, all hashes are printed along +with salt and number of iterations used. This technique is known as +"NSEC3 walking". + +That info should then be fed into an offline cracker, like +<code>unhash</code> from https://dnscurve.org/nsec3walker.html, to +bruteforce the actual names from the hashes. Assuming that the script +output was written into a text file <code>hashes.txt</code> like: +<code> +domain example.com +salt 123456 +iterations 10 +nexthash d1427bj0ahqnpi4t0t0aaun18oqpgcda vhnelm23s1m3japt7gohc82hgr9un2at +nexthash k7i4ekvi22ebrim5b6celtaniknd6ilj prv54a3cr1tbcvqslrb7bftf5ji5l0p8 +nexthash 9ool6bk7r2diaiu81ctiemmb6n961mph nm7v0ig7h9c0agaedc901kojfj9bgabj +nexthash 430456af8svfvl98l66shhrgucoip7mi mges520acstgaviekurg3oksh9u31bmb +</code> + +Run this command to recover the domain names: +<code> +# ./unhash < hashes.txt > domains.txt +names: 8 +d1427bj0ahqnpi4t0t0aaun18oqpgcda ns.example.com. +found 1 private NSEC3 names (12%) using 235451 hash computations +k7i4ekvi22ebrim5b6celtaniknd6ilj vulpix.example.com. +found 2 private NSEC3 names (25%) using 35017190 hash computations +</code> + +Use the <code>dns-nsec-enum</code> script to handle servers that use NSEC +rather than NSEC3. + +References: +* https://dnscurve.org/nsec3walker.html +]] +--- +-- @usage +-- nmap -sU -p 53 <target> --script=dns-nsec3-enum --script-args dns-nsec3-enum.domains=example.com +--- +-- @args dns-nsec3-enum.domains The domain or list of domains to +-- enumerate. If not provided, the script will make a guess based on the +-- name of the target. +-- @args dns-nsec3-enum.timelimit Sets a script run time limit. Default 30 minutes. +-- +-- @see dns-nsec-enum.nse +-- @see dns-ip6-arpa-scan.nse +-- @see dns-brute.nse +-- @see dns-zone-transfer.nse +-- +-- @output +-- PORT STATE SERVICE +-- 53/udp open domain +-- | dns-nsec3-enum: +-- | domain example.com +-- | salt 123456 +-- | iterations 10 +-- | nexthash d1427bj0ahqnpi4t0t0aaun18oqpgcda vhnelm23s1m3japt7gohc82hgr9un2at +-- | nexthash k7i4ekvi22ebrim5b6celtaniknd6ilj prv54a3cr1tbcvqslrb7bftf5ji5l0p8 +-- | nexthash 9ool6bk7r2diaiu81ctiemmb6n961mph nm7v0ig7h9c0agaedc901kojfj9bgabj +-- | nexthash 430456af8svfvl98l66shhrgucoip7mi mges520acstgaviekurg3oksh9u31bmb +-- |_ Total hashes found: 8 + +author = {"Aleksandar Nikolic", "John R. Bond"} +license = "Simplified (2-clause) BSD license--See https://nmap.org/svn/docs/licenses/BSD-simplified" +categories = {"discovery", "intrusive"} + +portrule = shortport.port_or_service(53, "domain", {"tcp", "udp"}) + +all_results = {} + +-- get time (in milliseconds) when the script should finish +local function get_end_time() + local t = nmap.timing_level() + local limit = stdnse.parse_timespec(stdnse.get_script_args('dns-nsec3-enum.timelimit') or "30m") + local end_time = 1000 * limit + nmap.clock_ms() + return end_time +end + +local function remove_empty(t) + local result = {} + for _, v in ipairs(t) do + if v ~= "" then + result[#result + 1] = v + end + end + return result +end + +local function split(domain) + return stringaux.strsplit("%.", domain) +end + +local function join(components) + return table.concat(remove_empty(components), ".") +end + +-- Remove the first component of a domain name. Return nil if the number of +-- components drops below min_length (default 0). +local function remove_component(domain, min_length) + local components + + min_length = min_length or 0 + components = split(domain) + if #components < min_length then + return nil + end + table.remove(components, 1) + + return join(components) +end + +-- Guess the domain given a host. Return nil on failure. This function removes +-- a domain name component unless the name would become shorter than 2 +-- components. +local function guess_domain(host) + local name + local components + + name = stdnse.get_hostname(host) + if name and name ~= host.ip then + return remove_component(name, 2) or name + else + return nil + end +end + +-- Remove a suffix from a domain (to isolate a subdomain from its parent). +local function remove_suffix(domain, suffix) + local dc, sc + + dc = split(domain) + sc = split(suffix) + while #dc > 0 and #sc > 0 and dc[#dc] == sc[#sc] do + dc[#dc] = nil + sc[#sc] = nil + end + + return join(dc), join(sc) +end + +-- Return the subset of authoritative records with the given label. +local function auth_filter(retPkt, label) + local result = {} + + for _, rec in ipairs(retPkt.auth) do + if rec[label] then + result[#result + 1] = rec[label] + end + end + + return result +end + + +local function empty(t) + return not next(t) +end + +-- generate a random hash with domains suffix +-- return both domain and its hash +local function generate_hash(domain, iter, salt) + local rand_str = rand.random_string(8, "etaoinshrdlucmfw") + local random_domain = rand_str .. "." .. domain + local packed_domain = {} + for word in string.gmatch(random_domain, "[^%.]+") do + packed_domain[#packed_domain+1] = string.pack("s1", word) + end + salt = stdnse.fromhex( salt) + local to_hash = ("%s\0%s"):format(table.concat(packed_domain), salt) + iter = iter - 1 + local hash = openssl.sha1(to_hash) + for i=0,iter do + hash = openssl.sha1(hash .. salt) + end + return string.lower(base32.enc(hash,true)), random_domain +end + +-- convenience function , returns size of a table +local function table_size(tbl) + local numItems = 0 + for k,v in pairs(tbl) do + numItems = numItems + 1 + end + return numItems +end + +-- convenience function , return first item in a table +local function get_first(tbl) + for k,v in pairs(tbl) do + return k,v + end +end + +-- queries the domain and parses the results +-- returns the list of new ranges +local function query_for_hashes(host,subdomain,domain) + local status + local result + local ranges = {} + status, result = dns.query(subdomain, {host = host.ip, dtype='NSEC3', retAll=true, retPkt=true, dnssec=true}) + if status then + for _, nsec3 in ipairs(auth_filter(result, "NSEC3")) do + local h1 = string.lower(remove_suffix(nsec3.dname,domain)) + local h2 = string.lower(nsec3.hash.base32) + if not tableaux.contains(all_results,"nexthash " .. h1 .. " " .. h2) then + table.insert(all_results, "nexthash " .. h1 .. " " .. h2) + stdnse.debug1("nexthash " .. h1 .. " " .. h2) + end + ranges[h1] = h2 + end + else + stdnse.debug1("DNS error: %s", result) + end + return ranges +end + +-- does the actual enumeration +local function enum(host, port, domain) + + local seen, seen_subdomain = {}, {} + local ALG ={} + ALG[1] = "SHA-1" + local todo = {} + local dnssec, status, result = false, false, "No Answer" + local result = {} + local subdomain = rand.random_string(8, "etaoinshrdlucmfw") + local full_domain = join({subdomain, domain}) + local iter + local salt + local end_time = get_end_time() + + -- do one query to determine the hash and if DNSSEC is actually used + status, result = dns.query(full_domain, {host = host.ip, dtype='NSEC3', retAll=true, retPkt=true, dnssec=true}) + if status then + local is_nsec3 = false + for _, nsec3 in ipairs(auth_filter(result, "NSEC3")) do -- parse the results and add initial ranges + is_nsec3 = true + dnssec = true + iter = nsec3.iterations + salt = nsec3.salt.hex + local h1 = string.lower(remove_suffix(nsec3.dname,domain)) + local h2 = string.lower(nsec3.hash.base32) + if table_size(todo) == 0 then + table.insert(all_results, "domain " .. domain) + stdnse.debug1("domain " .. domain) + table.insert(all_results, "salt " .. salt) + stdnse.debug1("salt " .. salt) + table.insert(all_results, "iterations " .. iter) + stdnse.debug1("iterations " .. iter) + if h1 < h2 then + todo[h2] = h1 + else + todo[h1] = h2 + end + else + for b,a in pairs(todo) do + if h1 == b and h2 == a then -- h2:a b:h1 case + todo[b] = nil + break + end + if h1 == b and h2 > h1 then -- a b:h1 h2 case + todo[b] = nil + todo[h2] = a + break + end + if h1 == b and h2 < a then -- h2 a b:h1 + todo[b] = nil + todo[b] = h2 + break + end + if h1 > b then -- a b h1 h2 + todo[b] = nil + todo[b] = h1 + todo[h2] = a + break + end + if h1 < a then -- h1 h2 a b + todo[b] = nil + todo[b] = h1 + todo[h2] = a + break + end + end -- for + end -- else + table.insert(all_results, "nexthash " .. h1 .. " " .. h2) + stdnse.debug1("nexthash " .. h1 .. " " .. h2) + end + end + + -- find hash that falls into one of the ranges and query for it + while table_size(todo) > 0 and nmap.clock_ms() < end_time do + local hash + hash, subdomain = generate_hash(domain,iter,salt) + local queried = false + for a,b in pairs(todo) do + if a == b then + todo[a] = nil + break + end + if a < b then -- [] range + if hash > a and hash < b then + -- do the query + local hash_pairs = query_for_hashes(host,subdomain,domain) + queried = true + local changed = false + for h1,h2 in pairs(hash_pairs) do + if h1 == a and h2 == b then -- h1:a h2:b case + todo[a] = nil + changed = true + end + if h1 == a then -- h1:a h2 b case + todo[a] = nil + todo[h2] = b + changed = true + end + if h2 == b then -- a h1 bh:2 case + todo[a] = nil + todo[a] = h1 + changed = true + end + if h1 > a and h2 < b then -- a h1 h2 b case + todo[a] = nil + todo[a] = h1 + todo[h2] = b + changed = true + end + end + --if changed then + -- stdnse.debug1("break[]") + --break + -- end + end + elseif a > b then -- ][ range + if hash > a or hash < b then + local hash_pairs = query_for_hashes(host,subdomain,domain) + queried = true + local changed = false + for h1,h2 in pairs(hash_pairs) do + if h1 == a and h2 == b then -- h2:b a:h1 case + todo[a] = nil + changed = true + end + if h1 == a and h2 > h1 then -- b a:h1 h2 case + todo[a] = nil + todo[h1] = b + changed = true + end + if h1 == a and h2 < b then -- h2 b a:h1 case + todo[a] = nil + todo[h2] = b + changed = true + end + if h1 > a and h2 > h1 then -- b a h1 h2 case + todo[a] = nil + todo[a] = h1 + todo[h2] = b + changed = true + end + if h1 > a and h2 < b then -- h2 b a h1 case + todo[a] = nil + todo[a] = h1 + todo[h2] = b + changed = true + end + if h1 < b then -- h1 h2 b a case + todo[a] = nil + todo[a] = h1 + todo[h2] = b + changed = true + end + end + if changed then + --break + end + end + end + if queried then + break + end + end + end + return dnssec, status, all_results +end + +action = function(host, port) + local output = {} + local domains + domains = stdnse.get_script_args('dns-nsec3-enum.domains') + if not domains then + domains = guess_domain(host) + end + if not domains then + return string.format("Can't determine domain for host %s; use %s.domains script arg.", host.ip, SCRIPT_NAME) + end + if type(domains) == 'string' then + domains = { domains } + end + + for _, domain in ipairs(domains) do + local dnssec, status, result = enum(host, port, domain) + if dnssec and type(result) == "table" then + output[#output + 1] = result + output[#output + 1] = "Total hashes found: " .. #result + + else + output[#output + 1] = "DNSSEC NSEC3 not supported" + end + end + return stdnse.format_output(true, output) +end |