diff --git a/docs/3rd-party-licenses.txt b/docs/3rd-party-licenses.txt
index f7dcf7ecc..24f2346c7 100644
--- a/docs/3rd-party-licenses.txt
+++ b/docs/3rd-party-licenses.txt
@@ -38,6 +38,9 @@ On Windows only, Nmap uses:
distributed with Nmap in the subdirectory mswin32/winpcap.
http://www.winpcap.org/
+Certain Nmap Scripting Engine scripts use the simplified BSD license in
+licenses/BSD-simplified.
+
Zenmap and Ndiff require:
o Python. The binary distributions of Nmap include a Python interpreter
and various libraries, built using either py2exe or py2app.
diff --git a/docs/licenses/BSD-simplified b/docs/licenses/BSD-simplified
new file mode 100644
index 000000000..dd0e8e759
--- /dev/null
+++ b/docs/licenses/BSD-simplified
@@ -0,0 +1,26 @@
+Copyright. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR IMPLIED
+WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
+SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+The views and conclusions contained in the software and documentation are those
+of the authors and should not be interpreted as representing official policies,
+either expressed or implied, of the authors.
diff --git a/nselib/dns.lua b/nselib/dns.lua
index 952c1a424..05bae112c 100644
--- a/nselib/dns.lua
+++ b/nselib/dns.lua
@@ -43,7 +43,6 @@ get_servers = nmap.get_dns_servers
-- @class table
types = {
A = 1,
- AAAA = 28,
NS = 2,
SOA = 6,
CNAME = 5,
@@ -51,8 +50,11 @@ types = {
HINFO = 13,
MX = 15,
TXT = 16,
+ AAAA = 28,
SRV = 33,
+ OPT = 41,
SSHFP = 44,
+ NSEC = 47,
AXFR = 252,
ANY = 255
}
@@ -277,6 +279,10 @@ function query(dname, options)
addQuestion(pkt, dname, dtype)
if options.norecurse then pkt.flags.RD = false end
+ if options.dnssec then
+ addOPT(pkt, {DO = true})
+ end
+
local data = encode(pkt)
local status, response = sendPackets(data, host, port, options.timeout, options.sendCount, options.multiple)
@@ -484,6 +490,29 @@ answerFetcher[types.SRV] = function(dec, retAll)
return true, answers
end
+-- Answer fetcher for NSEC records.
+-- @param dec Decoded DNS response.
+-- @param retAll If true, return all entries, not just the first.
+-- @return True if one or more answers of the required type were found - otherwise false.
+-- @return String first dns NSEC record or Table of NSEC records or String Error message.
+-- Note that the format of a returned NSEC answer is "name:dname:types".
+answerFetcher[types.NSEC] = function(dec, retAll)
+ local nsec, answers = {}, {}
+ for _, auth in ipairs(dec.auth) do
+ if auth.NSEC then nsec[#nsec+1] = auth.NSEC end
+ if not retAll then break end
+ end
+ if #nsec == 0 then
+ stdnse.print_debug(1, "dns.answerFetcher found no records of the required type: NSEC")
+ return false, "No Answers"
+ end
+ for _, nsecrec in ipairs(nsec) do
+ table.insert( answers, ("%s:%s:%s"):format(nsecrec.name or "-", nsecrec.dname or "-", stdnse.strjoin(":", nsecrec.types) or "-"))
+ end
+ if not retAll then return true, answers[1] end
+ return true, answers
+end
+
-- Answer fetcher for NS records.
-- @name answerFetcher[types.NS]
-- @class function
@@ -537,8 +566,7 @@ function findNiceAnswer(dtype, dec, retAll)
if answerFetcher[dtype] then
return answerFetcher[dtype](dec, retAll)
else
- stdnse.print_debug(1, "dns.findNiceAnswer() does not have an answerFetcher for dtype %s",
- (type(dtype) == 'string' and dtype) or type(dtype) or "nil")
+ stdnse.print_debug(1, "dns.findNiceAnswer() does not have an answerFetcher for dtype %s", tostring(dtype))
return false, "Unable to handle response"
end
elseif (dec.flags.RC3 and dec.flags.RC4) then
@@ -729,6 +757,21 @@ local function encodeUpdates(updates)
return encQ
end
+---
+-- Encodes the additional part of a DNS request.
+-- @param additional Table of additional records. Each must have the keys
+-- type, class, ttl, rdlen,
+-- and rdata.
+-- @return Encoded additional string.
+local function encodeAdditional(additional)
+ if type(additional) ~= "table" then return nil end
+ local encA = ""
+ for _, v in ipairs(additional) do
+ encA = encA .. bin.pack(">xSSISA", v.type, v.class, v.ttl, v.rdlen, v.rdata)
+ end
+ return encA
+end
+
---
-- Encodes DNS flags to a binary digit string.
-- @param flags Flag table, each entry representing a flag (QR, OCx, AA, TC, RD,
@@ -758,14 +801,15 @@ end
---
-- Encode a DNS packet.
--
--- Caution: doesn't encode answer, authority and additional part.
+-- Caution: doesn't encode answer and authority part.
-- @param pkt Table representing DNS packet, initialized by
-- newPacket.
-- @return Encoded DNS packet.
function encode(pkt)
if type(pkt) ~= "table" then return nil end
local encFlags = encodeFlags(pkt.flags)
- local data = encodeQuestions(pkt.questions)
+ local questions = encodeQuestions(pkt.questions)
+ local additional = encodeAdditional(pkt.additional)
local qorzlen = #pkt.questions
local aorplen = #pkt.answers
local aorulen = #pkt.auth
@@ -779,7 +823,7 @@ function encode(pkt)
data = data .. encodeUpdates( pkt.updates )
end
- local encStr = bin.pack(">SBS4", pkt.id, encFlags, qorzlen, aorplen, aorulen, #pkt.additional) .. data
+ local encStr = bin.pack(">SBS4", pkt.id, encFlags, qorzlen, aorplen, aorulen, #pkt.additional) .. questions .. additional
return encStr
end
@@ -911,6 +955,32 @@ decoder[types.SOA] = function(entry, data, pos)
= bin.unpack(">I5", data, np)
end
+-- Decodes NSEC records, puts result in entry.NSEC.
+--
+-- entry.NSEC has the fields dname,
+-- NSEC, name, WinBlockNo,
+-- bmplength, bin, and types.
+-- @param entry RR in packet.
+-- @param data Complete encoded DNS packet.
+-- @param pos Position in packet after RR.
+decoder[types.NSEC] = function (entry, data, pos)
+ local np = pos - #entry.data
+ entry.NSEC = {}
+ entry.NSEC.dname = entry.dname
+ entry.NSEC.NSEC = true
+ np, entry.NSEC.name = decStr(data, np)
+ np, entry.NSEC.WinBlockNo, entry.NSEC.bmplength = bin.unpack(">CC", data, np)
+ np, entry.NSEC.bin = bin.unpack("B".. entry.NSEC.bmplength, data, np)
+ entry.NSEC.types = {}
+ for i=1, string.len(entry.NSEC.bin) do
+ local bit = string.sub(entry.NSEC.bin,i,i)
+ if bit == "1" then
+ --the first bit represents window block 0 hence -1
+ table.insert(entry.NSEC.types, (entry.NSEC.WinBlockNo*256+i-1))
+ end
+ end
+end
+
-- Decodes records that consist only of one domain, for example CNAME, NS, PTR.
-- Puts result in entry.domain.
-- @param entry RR in packet.
@@ -1030,7 +1100,9 @@ local function decodeRR(data, count, pos)
pos, currRR.data = bin.unpack("A" .. reslen, data, pos)
-- try to be smart: decode per type
- decoder[currRR.dtype](currRR, data, pos)
+ if decoder[currRR.dtype] then
+ decoder[currRR.dtype](currRR, data, pos)
+ end
table.insert(ans, currRR)
end
@@ -1153,6 +1225,42 @@ function addZone(pkt, dname)
return pkt
end
+---
+-- Encodes the Z bitfield of an OPT record.
+-- @param flags Flag table, each entry representing a flag (only DO flag implmented).
+-- @return Binary digit string representing flags.
+local function encodeOPT_Z(flags)
+ if type(flags) == "string" then return flags end
+ if type(flags) ~= "table" then return nil end
+ local bits = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
+ for n, k in pairs({[1] = "DO"}) do
+ if flags[k] then
+ bits[n] = 1
+ end
+ end
+ return table.concat(bits)
+end
+
+---
+-- Adds an OPT RR to a DNS packet's additional section. Only the table of Z
+-- flags is supported (i.e., not RDATA). See RFC 2671 section 4.3.
+-- @param pkt Table representing DNS packet.
+-- @param Z Table of Z flags. Only DO is supported.
+function addOPT(pkt, Z)
+ if type(pkt) ~= "table" then return nil end
+ if type(pkt.additional) ~= "table" then return nil end
+ local _, Z_int = bin.unpack(">S", bin.pack("B", encodeOPT_Z(Z)))
+ local opt = {
+ type = types.OPT,
+ class = 4096, -- Actually the sender UDP payload size.
+ ttl = 0 * (0x01000000) + 0 * (0x00010000) + Z_int,
+ rdlen = 0,
+ rdata = "",
+ }
+ table.insert(pkt.additional, opt)
+ return pkt
+end
+
---
-- Adds a update to a DNS packet table
-- @param pkt Table representing DNS packet.
diff --git a/scripts/dns-nsec-enum.nse b/scripts/dns-nsec-enum.nse
new file mode 100644
index 000000000..dc63d280e
--- /dev/null
+++ b/scripts/dns-nsec-enum.nse
@@ -0,0 +1,379 @@
+description = [[
+Enumerates DNS names using the DNSSEC NSEC-walking technique.
+
+Output is arranged by domain. Within a domain, subzones are shown with
+increased indentation.
+
+The NSEC response record in DNSSEC is used to give negative answers to
+queries, but it has the side effect of allowing enumeration of all
+names, much like a zone transfer. This script doesn't work against
+servers that use NSEC3 rather than NSEC.
+]]
+
+---
+-- @args dns-nsec-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.
+--
+-- @usage
+-- nmap -sSU -p 53 --script dns-nsec-enum --script-args dns-nsec-enum.domains=example.com
+--
+-- @output
+-- 53/udp open domain udp-response
+-- | dns-nsec-enum:
+-- | example.com
+-- | bulbasaur.example.com
+-- | charmander.example.com
+-- | dugtrio.example.com
+-- | www.dugtrio.example.com
+-- | gyarados.example.com
+-- | johto.example.com
+-- | blue.johto.example.com
+-- | green.johto.example.com
+-- | ns.johto.example.com
+-- | red.johto.example.com
+-- | ns.example.com
+-- | snorlax.example.com
+-- |_ vulpix.example.com
+
+author = "John Bond"
+license = "Simplified (2-clause) BSD license--See http://nmap.org/svn/docs/licenses/BSD-simplified"
+
+categories = {"discovery", "intrusive"}
+
+require "stdnse"
+require "shortport"
+require "dns"
+
+portrule = shortport.port_or_service(53, "domain", {"tcp", "udp"})
+
+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
+
+local function invert(t)
+ local result = {}
+
+ for k, v in pairs(t) do
+ result[v] = k
+ end
+
+ return result
+end
+
+-- RFC 952: "A 'name' is a text string up to 24 characters drawn from the
+-- alphabet (A-Z), digits (0-9), minus sign (-), and period (.). ... The first
+-- character must be an alpha character."
+-- RFC 1123, section 2.1: "One aspect of host name syntax is hereby changed:
+-- the restriction on the first character is relaxed to allow either a letter
+-- or a digit."
+-- RFC 2782: An underscore (_) is prepended to the service identifier to avoid
+-- collisions with DNS labels that occur in nature.
+local DNS_CHARS = { string.byte("-0123456789_abcdefghijklmnopqrstuvwxyz", 1, -1) }
+local DNS_CHARS_INV = invert(DNS_CHARS)
+
+-- Return the lexicographically next component, or nil if component is the
+-- lexicographically last.
+local function increment_component(name)
+ local i, bytes, indexes
+
+ -- Easy cases first.
+ if #name == 0 then
+ return "0"
+ elseif #name < 63 then
+ return name .. "-"
+ elseif #name > 64 then
+ -- Shouldn't happen.
+ return nil
+ end
+
+ -- Convert the string into an array of indexes into DNS_CHARS.
+ indexes = {}
+ for i, b in ipairs({ string.byte(name, 1, -1) }) do
+ indexes[i] = DNS_CHARS_INV[b]
+ end
+ -- Increment.
+ i = #name
+ while i >= 1 do
+ repeat
+ indexes[i] = indexes[i] + 1
+ -- No "-" in first position.
+ until not (i == 1 and string.char(DNS_CHARS[indexes[i]]) == "-")
+ if indexes[i] > #DNS_CHARS then
+ -- Wrap around, next digit.
+ indexes[i] = 1
+ else
+ break
+ end
+ i = i - 1
+ end
+ -- Overflow.
+ if i == 0 then
+ return nil
+ end
+ -- Convert array of indexes back into string.
+ bytes = {}
+ for i, index in ipairs(indexes) do
+ bytes[i] = DNS_CHARS[index]
+ end
+
+ return string.char(unpack(bytes))
+end
+
+-- Return the lexicographically next domain name that does not add a new
+-- subdomain. This is used after enumerating a whole subzone to jump out of the
+-- subzone and on to more names.
+local function bump_domain(domain)
+ local components
+
+ components = split(domain)
+ while #components > 0 do
+ components[1] = increment_component(components[1])
+ if components[1] then
+ break
+ else
+ table.remove(components[1])
+ end
+ end
+
+ if #components == 0 then
+ return nil
+ else
+ return join(components)
+ end
+end
+
+-- Return the lexicographically next domain name. This adds a new subdomain
+-- consisting of the smallest character. This function never returns a domain
+-- outside the current subzone.
+local function next_domain(domain)
+ if #domain == 0 then
+ return "0"
+ else
+ return "0" .. "." .. domain
+ end
+end
+
+-- Cut out a portion of an array and return it as a new array, setting the
+-- elements in the original array to nil.
+local function excise(t, i, j)
+ local result
+
+ result = {}
+ if j < 0 then
+ j = #t + j + 1
+ end
+ for i = i, j do
+ result[#result + 1] = t[i]
+ t[i] = nil
+ end
+
+ return result
+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
+
+-- "Less than" function for two domain names. Compares starting with the last
+-- component.
+local function domain_lt(a, b)
+ local a_parts, b_parts
+
+ a_parts = split(a)
+ b_parts = split(b)
+ while #a_parts > 0 and #b_parts > 0 do
+ if a_parts[#a_parts] < b_parts[#b_parts] then
+ return true
+ elseif a_parts[#a_parts] > b_parts[#b_parts] then
+ return false
+ end
+ a_parts[#a_parts] = nil
+ b_parts[#b_parts] = nil
+ end
+
+ return #a_parts < #b_parts
+end
+
+-- Find the NSEC record that brackets the given domain.
+local function get_next_nsec(retPkt, domain)
+ for _, nsec in ipairs(auth_filter(retPkt, "NSEC")) do
+ -- The last NSEC record points backwards to the start of the subzone.
+ if domain_lt(nsec.dname, domain) and not domain_lt(nsec.dname, nsec.name) then
+ return nsec
+ end
+ if domain_lt(nsec.dname, domain) and domain_lt(domain, nsec.name) then
+ return nsec
+ end
+ end
+end
+
+local function empty(t)
+ return not next(t)
+end
+
+-- Enumerate a single domain.
+local function enum(host, port, domain)
+ local all_results = {}
+ local seen = {}
+ local subdomain = next_domain("")
+
+ while subdomain do
+ local result = {}
+ local status, result, nsec
+ stdnse.print_debug("Trying %q.%q", subdomain, domain)
+ status, result = dns.query(join({subdomain, domain}), {host = host.ip, dtype='A', retAll=true, retPkt=true, dnssec=true})
+ nsec = status and get_next_nsec(result, join({subdomain, domain})) or nil
+ if nsec then
+ local first, last, remainder
+ local index
+
+ first, remainder = remove_suffix(nsec.dname, domain)
+ if #remainder > 0 then
+ stdnse.print_debug("Result name %q doesn't end in %q.", nsec.dname, domain)
+ subdomain = nil
+ break
+ end
+ last, remainder = remove_suffix(nsec.name, domain)
+ if #remainder > 0 then
+ stdnse.print_debug("Result name %q doesn't end in %q.", nsec.name, domain)
+ subdomain = nil
+ break
+ end
+ if #last == 0 then
+ stdnse.print_debug("Wrapped")
+ subdomain = nil
+ break
+ end
+
+ if not seen[first] then
+ table.insert(all_results, join({first, domain}))
+ seen[first] = #all_results
+ end
+ index = seen[last]
+ if index then
+ -- Ignore if first is the original domain.
+ if #first > 0 then
+ subdomain = bump_domain(last)
+ -- Replace a chunk of the output with a sub-table for the zone.
+ all_results[index] = excise(all_results, index, -1)
+ end
+ else
+ stdnse.print_debug("adding %s", last)
+ subdomain = next_domain(last)
+ table.insert(all_results, join({last, domain}))
+ seen[last] = #all_results
+ end
+ else
+ local parent = remove_component(subdomain, 1)
+
+ -- This branch is entered if name resolution failed or
+ -- there were no NSEC records. If at the top, quit.
+ -- Otherwise continue to the next subdomain.
+ if parent then
+ subdomain = bump_domain(parent)
+ else
+ return nil
+ end
+ end
+ end
+
+ return all_results
+end
+
+action = function(host, port)
+ local output = {}
+ local domains
+
+ domains = stdnse.get_script_args('dns-nsec-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 result = enum(host, port, domain)
+ if type(result) == "table" then
+ result["name"] = domain
+ output[#output + 1] = result
+ else
+ output[#output + 1] = "No NSEC records found"
+ end
+ end
+
+ return stdnse.format_output(true, output)
+end
diff --git a/scripts/script.db b/scripts/script.db
index cb70351ae..86c74ede8 100644
--- a/scripts/script.db
+++ b/scripts/script.db
@@ -26,6 +26,7 @@ Entry { filename = "dhcp-discover.nse", categories = { "default", "discovery", "
Entry { filename = "dns-brute.nse", categories = { "discovery", "intrusive", } }
Entry { filename = "dns-cache-snoop.nse", categories = { "discovery", "intrusive", } }
Entry { filename = "dns-fuzz.nse", categories = { "fuzzer", "intrusive", } }
+Entry { filename = "dns-nsec-enum.nse", categories = { "discovery", "intrusive", } }
Entry { filename = "dns-random-srcport.nse", categories = { "external", "intrusive", } }
Entry { filename = "dns-random-txid.nse", categories = { "external", "intrusive", } }
Entry { filename = "dns-recursion.nse", categories = { "default", "intrusive", } }