mirror of
https://github.com/nmap/nmap.git
synced 2025-12-15 20:29:03 +00:00
Merged dns-nsec3-enum to trunk
This commit is contained in:
200
nselib/base32.lua
Normal file
200
nselib/base32.lua
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
---
|
||||||
|
-- Base32 encoding and decoding. Follows RFC 4648.
|
||||||
|
--
|
||||||
|
-- @author Philip Pickering <pgpickering@gmail.com>
|
||||||
|
-- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html
|
||||||
|
-- @ported base64 to base32 <john.r.bond@gmail.com>
|
||||||
|
|
||||||
|
-- thanks to Patrick Donnelly for some optimizations
|
||||||
|
|
||||||
|
--module(... or "base32",package.seeall)
|
||||||
|
-- local bin = require 'bin'
|
||||||
|
-- local stdnse = require 'stdnse'
|
||||||
|
-- _ENV = stdnse.module("base32", stdnse.seeall)
|
||||||
|
|
||||||
|
local bin = require "bin"
|
||||||
|
local stdnse = require "stdnse"
|
||||||
|
local string = require "string"
|
||||||
|
local table = require "table"
|
||||||
|
_ENV = stdnse.module("base32", stdnse.seeall)
|
||||||
|
|
||||||
|
-- todo: make metatable/index --> '' for b32dctable
|
||||||
|
|
||||||
|
|
||||||
|
local b32standard = {
|
||||||
|
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
|
||||||
|
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
|
||||||
|
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
|
||||||
|
'Y', 'Z', '2', '3', '4', '5', '6', '7',
|
||||||
|
}
|
||||||
|
|
||||||
|
local b32dcstandard = {} -- efficency
|
||||||
|
b32dcstandard['A'] = '00000'
|
||||||
|
b32dcstandard['B'] = '00001'
|
||||||
|
b32dcstandard['C'] = '00010'
|
||||||
|
b32dcstandard['D'] = '00011'
|
||||||
|
b32dcstandard['E'] = '00100'
|
||||||
|
b32dcstandard['F'] = '00101'
|
||||||
|
b32dcstandard['G'] = '00110'
|
||||||
|
b32dcstandard['H'] = '00111'
|
||||||
|
b32dcstandard['I'] = '01000'
|
||||||
|
b32dcstandard['J'] = '01001'
|
||||||
|
b32dcstandard['K'] = '01010'
|
||||||
|
b32dcstandard['L'] = '01011'
|
||||||
|
b32dcstandard['M'] = '01100'
|
||||||
|
b32dcstandard['N'] = '01101'
|
||||||
|
b32dcstandard['O'] = '01110'
|
||||||
|
b32dcstandard['P'] = '01111'
|
||||||
|
b32dcstandard['Q'] = '10000'
|
||||||
|
b32dcstandard['R'] = '10001'
|
||||||
|
b32dcstandard['S'] = '10010'
|
||||||
|
b32dcstandard['T'] = '10011'
|
||||||
|
b32dcstandard['U'] = '10100'
|
||||||
|
b32dcstandard['V'] = '10101'
|
||||||
|
b32dcstandard['W'] = '10110'
|
||||||
|
b32dcstandard['X'] = '10111'
|
||||||
|
b32dcstandard['Y'] = '11000'
|
||||||
|
b32dcstandard['Z'] = '11001'
|
||||||
|
b32dcstandard['2'] = '11010'
|
||||||
|
b32dcstandard['3'] = '11011'
|
||||||
|
b32dcstandard['4'] = '11100'
|
||||||
|
b32dcstandard['5'] = '11101'
|
||||||
|
b32dcstandard['6'] = '11110'
|
||||||
|
b32dcstandard['7'] = '11111'
|
||||||
|
|
||||||
|
local b32hexExtend = {
|
||||||
|
'0', '1', '2', '3', '4', '5', '6', '7',
|
||||||
|
'8', '9', 'A', 'B', 'C', 'D', 'E', 'F',
|
||||||
|
'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
|
||||||
|
'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',
|
||||||
|
}
|
||||||
|
|
||||||
|
local b32dchexExtend = {} -- efficency
|
||||||
|
b32dchexExtend['0'] = '00000'
|
||||||
|
b32dchexExtend['1'] = '00001'
|
||||||
|
b32dchexExtend['2'] = '00010'
|
||||||
|
b32dchexExtend['3'] = '00011'
|
||||||
|
b32dchexExtend['4'] = '00100'
|
||||||
|
b32dchexExtend['5'] = '00101'
|
||||||
|
b32dchexExtend['6'] = '00110'
|
||||||
|
b32dchexExtend['7'] = '00111'
|
||||||
|
b32dchexExtend['8'] = '01000'
|
||||||
|
b32dchexExtend['9'] = '01001'
|
||||||
|
b32dchexExtend['A'] = '01010'
|
||||||
|
b32dchexExtend['B'] = '01011'
|
||||||
|
b32dchexExtend['C'] = '01100'
|
||||||
|
b32dchexExtend['D'] = '01101'
|
||||||
|
b32dchexExtend['E'] = '01110'
|
||||||
|
b32dchexExtend['F'] = '01111'
|
||||||
|
b32dchexExtend['G'] = '10000'
|
||||||
|
b32dchexExtend['H'] = '10001'
|
||||||
|
b32dchexExtend['I'] = '10010'
|
||||||
|
b32dchexExtend['J'] = '10011'
|
||||||
|
b32dchexExtend['K'] = '10100'
|
||||||
|
b32dchexExtend['L'] = '10101'
|
||||||
|
b32dchexExtend['M'] = '10110'
|
||||||
|
b32dchexExtend['N'] = '10111'
|
||||||
|
b32dchexExtend['O'] = '11000'
|
||||||
|
b32dchexExtend['P'] = '11001'
|
||||||
|
b32dchexExtend['Q'] = '11010'
|
||||||
|
b32dchexExtend['R'] = '11011'
|
||||||
|
b32dchexExtend['S'] = '11100'
|
||||||
|
b32dchexExtend['T'] = '11101'
|
||||||
|
b32dchexExtend['U'] = '11110'
|
||||||
|
b32dchexExtend['V'] = '11111'
|
||||||
|
|
||||||
|
local b32table = b32standard
|
||||||
|
local b32dctable = b32dcstandard
|
||||||
|
|
||||||
|
local append = table.insert
|
||||||
|
local substr = string.sub
|
||||||
|
local bpack = bin.pack
|
||||||
|
local bunpack = bin.unpack
|
||||||
|
local concat = table.concat
|
||||||
|
|
||||||
|
---
|
||||||
|
-- Encode bits to a Base32-encoded character.
|
||||||
|
-- @param bits String of five bits to be encoded.
|
||||||
|
-- @return Encoded character.
|
||||||
|
local function b32enc5bit(bits)
|
||||||
|
local byte = tonumber(bits, 2) + 1
|
||||||
|
return b32table[byte]
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
-- Decodes a Base32-encoded character into a string of binary digits.
|
||||||
|
-- @param b32byte A single base32-encoded character.
|
||||||
|
-- @return String of five decoded bits.
|
||||||
|
local function b32dec5bit(b32byte)
|
||||||
|
local bits = b32dctable[b32byte]
|
||||||
|
if bits then return bits end
|
||||||
|
return ''
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
-- Encodes a string to Base32.
|
||||||
|
-- @param bdata Data to be encoded.
|
||||||
|
-- @param hexExtend pass true to use the hex extended char set
|
||||||
|
-- @return Base32-encoded string.
|
||||||
|
function enc(bdata, hexExtend)
|
||||||
|
local _, bitstring = bunpack(">B".. #bdata,bdata)
|
||||||
|
local b32dataBuf = {}
|
||||||
|
|
||||||
|
if hexExtend then
|
||||||
|
b32table = b32hexExtend
|
||||||
|
b32dctable = b32dchexExtend
|
||||||
|
end
|
||||||
|
|
||||||
|
while #bitstring > 4 do
|
||||||
|
append(b32dataBuf,b32enc5bit(substr(bitstring,1,5)))
|
||||||
|
bitstring = substr(bitstring,6)
|
||||||
|
end
|
||||||
|
if #bitstring == 1 then
|
||||||
|
append(b32dataBuf, b32enc5bit(bitstring .. "0000"))
|
||||||
|
append(b32dataBuf, '====')
|
||||||
|
elseif #bitstring == 2 then
|
||||||
|
append(b32dataBuf, b32enc5bit(bitstring .. "000") )
|
||||||
|
append(b32dataBuf, '=')
|
||||||
|
elseif #bitstring == 3 then
|
||||||
|
append(b32dataBuf, b32enc5bit(bitstring .. "00") )
|
||||||
|
append(b32dataBuf, "======")
|
||||||
|
elseif #bitstring == 4 then
|
||||||
|
append(b32dataBuf, b32enc5bit(bitstring .. "0") )
|
||||||
|
append(b32dataBuf, '===')
|
||||||
|
end
|
||||||
|
return concat(b32dataBuf)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
-- Decodes Base32-encoded data.
|
||||||
|
-- @param b32data Base32 encoded data.
|
||||||
|
-- @param hexExtend pass true to use the hex extended char set
|
||||||
|
-- @return Decoded data.
|
||||||
|
function dec(b32data, hexExtend)
|
||||||
|
local bdataBuf = {}
|
||||||
|
local pos = 1
|
||||||
|
local byte
|
||||||
|
local nbyte = ''
|
||||||
|
|
||||||
|
if hexExtend then
|
||||||
|
b32table = b32hexExtend
|
||||||
|
b32dctable = b32dchexExtend
|
||||||
|
end
|
||||||
|
|
||||||
|
for pos = 1, #b32data do -- while pos <= string.len(b32data) do
|
||||||
|
byte = b32dec5bit(substr(b32data, pos, pos))
|
||||||
|
if not byte then return end
|
||||||
|
nbyte = nbyte .. byte
|
||||||
|
if #nbyte >= 8 then
|
||||||
|
append(bdataBuf, bpack("B", substr(nbyte, 1, 8)))
|
||||||
|
nbyte = substr(nbyte, 9)
|
||||||
|
end
|
||||||
|
-- pos = pos + 1
|
||||||
|
end
|
||||||
|
return concat(bdataBuf)
|
||||||
|
end
|
||||||
|
|
||||||
|
return _ENV;
|
||||||
@@ -38,6 +38,7 @@ local nmap = require "nmap"
|
|||||||
local stdnse = require "stdnse"
|
local stdnse = require "stdnse"
|
||||||
local string = require "string"
|
local string = require "string"
|
||||||
local table = require "table"
|
local table = require "table"
|
||||||
|
local base32 = require "base32"
|
||||||
_ENV = stdnse.module("dns", stdnse.seeall)
|
_ENV = stdnse.module("dns", stdnse.seeall)
|
||||||
|
|
||||||
get_servers = nmap.get_dns_servers
|
get_servers = nmap.get_dns_servers
|
||||||
@@ -60,6 +61,7 @@ types = {
|
|||||||
OPT = 41,
|
OPT = 41,
|
||||||
SSHFP = 44,
|
SSHFP = 44,
|
||||||
NSEC = 47,
|
NSEC = 47,
|
||||||
|
NSEC3 = 50,
|
||||||
AXFR = 252,
|
AXFR = 252,
|
||||||
ANY = 255
|
ANY = 255
|
||||||
}
|
}
|
||||||
@@ -1034,6 +1036,49 @@ decoder[types.NSEC] = function (entry, data, pos)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
-- Decodes NSEC3 records, puts result in <code>entry.NSEC3</code>. See RFC 5155.
|
||||||
|
--
|
||||||
|
-- <code>entry.NSEC3</code> has the fields <code>dname</code>,
|
||||||
|
-- <code>hash.alg</code>, and <code>hash.base32</code>.
|
||||||
|
-- <code>hash.bin</code>, and <code>hash.hex</code>.
|
||||||
|
-- <code>salt.bin</code>, and <code>salt.hex</code>.
|
||||||
|
-- <code>iterations</code>, and <code>types</code>.
|
||||||
|
-- @param entry RR in packet.
|
||||||
|
-- @param data Complete encoded DNS packet.
|
||||||
|
-- @param pos Position in packet after RR.
|
||||||
|
decoder[types.NSEC3] = function (entry, data, pos)
|
||||||
|
local np = pos - #entry.data
|
||||||
|
local _
|
||||||
|
local flags
|
||||||
|
|
||||||
|
entry.NSEC3 = {}
|
||||||
|
entry.NSEC3.dname = entry.dname
|
||||||
|
entry.NSEC3.salt, entry.NSEC3.hash = {}, {}
|
||||||
|
|
||||||
|
np, entry.NSEC3.hash.alg,flags,entry.NSEC3.iterations = bin.unpack(">CBS", data, np)
|
||||||
|
-- do we even need to decode these do we care about opt out?
|
||||||
|
-- entry.NSEC3.flags = decodeFlagsNSEC3(flags)
|
||||||
|
|
||||||
|
np, entry.NSEC3.salt.bin = bin.unpack(">p", data, np)
|
||||||
|
_, entry.NSEC3.salt.hex = bin.unpack("H" .. #entry.NSEC3.salt.bin, entry.NSEC3.salt.bin)
|
||||||
|
|
||||||
|
np, entry.NSEC3.hash.bin = bin.unpack(">p" , data, np)
|
||||||
|
_, entry.NSEC3.hash.hex = bin.unpack(">H" .. #entry.NSEC3.hash.bin , entry.NSEC3.hash.bin)
|
||||||
|
entry.NSEC3.hash.base32 = base32.enc(entry.NSEC3.hash.bin, true)
|
||||||
|
|
||||||
|
np, entry.NSEC3.WinBlockNo, entry.NSEC3.bmplength = bin.unpack(">CC", data, np)
|
||||||
|
np, entry.NSEC3.bin = bin.unpack(">B".. entry.NSEC3.bmplength, data, np)
|
||||||
|
entry.NSEC3.types = {}
|
||||||
|
if entry.NSEC3.bin == nil then
|
||||||
|
entry.NSEC3.bin = ""
|
||||||
|
end
|
||||||
|
for i=1, string.len(entry.NSEC3.bin) do
|
||||||
|
local bit = string.sub(entry.NSEC3.bin,i,i)
|
||||||
|
if bit == "1" then
|
||||||
|
table.insert(entry.NSEC3.types, (entry.NSEC3.WinBlockNo*256+i-1))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- Decodes records that consist only of one domain, for example CNAME, NS, PTR.
|
-- Decodes records that consist only of one domain, for example CNAME, NS, PTR.
|
||||||
-- Puts result in <code>entry.domain</code>.
|
-- Puts result in <code>entry.domain</code>.
|
||||||
|
|||||||
424
scripts/dns-nsec3-enum.nse
Normal file
424
scripts/dns-nsec3-enum.nse
Normal file
@@ -0,0 +1,424 @@
|
|||||||
|
local stdnse = require "stdnse"
|
||||||
|
local shortport = require "shortport"
|
||||||
|
local dns = require "dns"
|
||||||
|
local base32 = require "base32"
|
||||||
|
local openssl = require "openssl"
|
||||||
|
local msrpc = require "msrpc" -- just for random string generation
|
||||||
|
local math = require "math"
|
||||||
|
|
||||||
|
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 http://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>
|
||||||
|
|
||||||
|
References:
|
||||||
|
* http://dnscurve.org/nsec3walker.html
|
||||||
|
]]
|
||||||
|
---
|
||||||
|
-- @usage
|
||||||
|
--nmap.exe -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.
|
||||||
|
--
|
||||||
|
-- @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 Bond"
|
||||||
|
license = "Simplified (2-clause) BSD license--See http://nmap.org/svn/docs/licenses/BSD-simplified"
|
||||||
|
categories = {"discovery", "intrusive"}
|
||||||
|
|
||||||
|
portrule = shortport.port_or_service(53, "domain", {"tcp", "udp"})
|
||||||
|
|
||||||
|
all_results = {}
|
||||||
|
|
||||||
|
-- get time (in miliseconds) 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 stdnse.strsplit("%.", domain)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function join(components)
|
||||||
|
return stdnse.strjoin(".", 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
|
||||||
|
|
||||||
|
local function random_string()
|
||||||
|
return msrpc.random_crap(8,"etaoinshrdlucmfw")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- generate a random hash with domains suffix
|
||||||
|
-- return both domain and it's hash
|
||||||
|
local function generate_hash(domain, iter, salt)
|
||||||
|
local rand_str = random_string()
|
||||||
|
local random_domain = rand_str .. "." .. domain
|
||||||
|
local packed_domain = ""
|
||||||
|
for word in string.gmatch(domain,"[^%.]+") do
|
||||||
|
packed_domain = packed_domain .. bin.pack("c",string.len(word)) .. word
|
||||||
|
end
|
||||||
|
local to_hash = bin.pack("c",string.len(rand_str)) .. rand_str .. packed_domain .. bin.pack("c",0) .. bin.pack("H",salt)
|
||||||
|
iter = iter - 1
|
||||||
|
local hash = openssl.sha1(to_hash)
|
||||||
|
for i=0,iter do
|
||||||
|
hash = hash .. bin.pack("H",salt)
|
||||||
|
hash = openssl.sha1(hash)
|
||||||
|
end
|
||||||
|
return string.lower(base32.enc(hash,true)), random_domain
|
||||||
|
end
|
||||||
|
|
||||||
|
-- convenience function , returns size of a table
|
||||||
|
local function table_size(tbl)
|
||||||
|
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
|
||||||
|
|
||||||
|
-- convenience function , check if table cointains an element
|
||||||
|
local function table_contains(tbl,element)
|
||||||
|
for _, value in pairs(tbl) do
|
||||||
|
if value == element then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
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})
|
||||||
|
for _, nsec3 in ipairs(auth_filter(result, "NSEC3")) do
|
||||||
|
h1 = string.lower(remove_suffix(nsec3.dname,domain))
|
||||||
|
h2 = string.lower(nsec3.hash.base32)
|
||||||
|
if not table_contains(all_results,"nexthash " .. h1 .. " " .. h2) then
|
||||||
|
table.insert(all_results, "nexthash " .. h1 .. " " .. h2)
|
||||||
|
stdnse.print_debug("nexthash " .. h1 .. " " .. h2)
|
||||||
|
end
|
||||||
|
ranges[h1] = h2
|
||||||
|
end
|
||||||
|
|
||||||
|
return ranges
|
||||||
|
end
|
||||||
|
|
||||||
|
-- does the actuall 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 = msrpc.random_crap(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
|
||||||
|
h1 = string.lower(remove_suffix(nsec3.dname,domain))
|
||||||
|
h2 = string.lower(nsec3.hash.base32)
|
||||||
|
if table_size(todo) == 0 then
|
||||||
|
table.insert(all_results, "domain " .. domain)
|
||||||
|
stdnse.print_debug("domain " .. domain)
|
||||||
|
table.insert(all_results, "salt " .. salt)
|
||||||
|
stdnse.print_debug("salt " .. salt)
|
||||||
|
table.insert(all_results, "iterations " .. iter)
|
||||||
|
stdnse.print_debug("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.print_debug("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
|
||||||
|
hash, subdomain = generate_hash(domain,iter,salt)
|
||||||
|
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
|
||||||
|
hash_pairs = query_for_hashes(host,subdomain,domain)
|
||||||
|
queried = true
|
||||||
|
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.print_debug("break[]")
|
||||||
|
--break
|
||||||
|
-- end
|
||||||
|
end
|
||||||
|
elseif a > b then -- ][ range
|
||||||
|
if hash > a or hash < b then
|
||||||
|
hash_pairs = query_for_hashes(host,subdomain,domain)
|
||||||
|
queried = true
|
||||||
|
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
|
||||||
@@ -75,6 +75,7 @@ Entry { filename = "dns-client-subnet-scan.nse", categories = { "discovery", "sa
|
|||||||
Entry { filename = "dns-fuzz.nse", categories = { "fuzzer", "intrusive", } }
|
Entry { filename = "dns-fuzz.nse", categories = { "fuzzer", "intrusive", } }
|
||||||
Entry { filename = "dns-ip6-arpa-scan.nse", categories = { "discovery", "intrusive", } }
|
Entry { filename = "dns-ip6-arpa-scan.nse", categories = { "discovery", "intrusive", } }
|
||||||
Entry { filename = "dns-nsec-enum.nse", categories = { "discovery", "intrusive", } }
|
Entry { filename = "dns-nsec-enum.nse", categories = { "discovery", "intrusive", } }
|
||||||
|
Entry { filename = "dns-nsec3-enum.nse", categories = { "discovery", "intrusive", } }
|
||||||
Entry { filename = "dns-nsid.nse", categories = { "default", "discovery", } }
|
Entry { filename = "dns-nsid.nse", categories = { "default", "discovery", } }
|
||||||
Entry { filename = "dns-random-srcport.nse", categories = { "external", "intrusive", } }
|
Entry { filename = "dns-random-srcport.nse", categories = { "external", "intrusive", } }
|
||||||
Entry { filename = "dns-random-txid.nse", categories = { "external", "intrusive", } }
|
Entry { filename = "dns-random-txid.nse", categories = { "external", "intrusive", } }
|
||||||
|
|||||||
Reference in New Issue
Block a user