1557 lines
51 KiB
Lua
1557 lines
51 KiB
Lua
|
---
|
||
|
-- Simple DNS library supporting packet creation, encoding, decoding,
|
||
|
-- and querying.
|
||
|
--
|
||
|
-- The most common interface to this module are the <code>query</code> and
|
||
|
-- <code>reverse</code> functions. <code>query</code> performs a DNS query,
|
||
|
-- and <code>reverse</code> prepares an ip address to have a reverse query
|
||
|
-- performed.
|
||
|
--
|
||
|
-- <code>query</code> takes two options - a domain name to look up and an
|
||
|
-- optional table of options. For more information on the options table,
|
||
|
-- see the documentation for <code>query</code>.
|
||
|
--
|
||
|
-- Example usage:
|
||
|
-- <code>
|
||
|
-- -- After this call, <code>status</code> is <code>true</code> and <code>result</code> is <code>"72.14.204.104"</code>
|
||
|
-- local status, result = dns.query('www.google.ca')
|
||
|
--
|
||
|
-- -- After this call, <code>status</code> is <code>false</code> and <code>result</code> is <code>"No such name"</code>
|
||
|
-- local status, result = dns.query('www.google.abc')
|
||
|
--
|
||
|
-- -- After this call, <code>status</code> is <code>true</code> and <code>result</code> is the table <code>{"72.14.204.103", "72.14.204.104", "72.14.204.147", "72.14.204.99"}</code>
|
||
|
-- local status, result = dns.query('www.google.ca', {retAll=true})
|
||
|
--
|
||
|
-- -- After this call, <code>status</code> is <code>true</code> and <code>result</code> is the <code>"2001:19f0:0:0:0:dead:beef:cafe"</code>
|
||
|
-- local status, result = dns.query('irc.ipv6.efnet.org', {dtype='AAAA'})
|
||
|
--</code>
|
||
|
--
|
||
|
--
|
||
|
-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
|
||
|
|
||
|
|
||
|
local coroutine = require "coroutine"
|
||
|
local ipOps = require "ipOps"
|
||
|
local nmap = require "nmap"
|
||
|
local stdnse = require "stdnse"
|
||
|
local string = require "string"
|
||
|
local stringaux = require "stringaux"
|
||
|
local table = require "table"
|
||
|
local base32 = require "base32"
|
||
|
local unittest = require "unittest"
|
||
|
_ENV = stdnse.module("dns", stdnse.seeall)
|
||
|
|
||
|
get_servers = nmap.get_dns_servers
|
||
|
|
||
|
---
|
||
|
-- Table of DNS resource types.
|
||
|
-- @name types
|
||
|
-- @class table
|
||
|
types = {
|
||
|
A = 1,
|
||
|
NS = 2,
|
||
|
SOA = 6,
|
||
|
CNAME = 5,
|
||
|
PTR = 12,
|
||
|
HINFO = 13,
|
||
|
MX = 15,
|
||
|
TXT = 16,
|
||
|
AAAA = 28,
|
||
|
SRV = 33,
|
||
|
OPT = 41,
|
||
|
SSHFP = 44,
|
||
|
NSEC = 47,
|
||
|
NSEC3 = 50,
|
||
|
AXFR = 252,
|
||
|
ANY = 255
|
||
|
}
|
||
|
|
||
|
CLASS = {
|
||
|
IN = 1,
|
||
|
CH = 3,
|
||
|
ANY = 255
|
||
|
}
|
||
|
|
||
|
|
||
|
---
|
||
|
-- Repeatedly sends UDP packets to host, waiting for an answer.
|
||
|
-- @param data Data to be sent.
|
||
|
-- @param host Host to connect to.
|
||
|
-- @param port Port to connect to.
|
||
|
-- @param timeout Number of ms to wait for a response.
|
||
|
-- @param cnt Number of tries.
|
||
|
-- @param multiple If true, keep reading multiple responses until timeout.
|
||
|
-- @return Status (true or false).
|
||
|
-- @return Response (if status is true).
|
||
|
local function sendPacketsUDP(data, host, port, timeout, cnt, multiple)
|
||
|
local socket = nmap.new_socket("udp")
|
||
|
local responses = {}
|
||
|
|
||
|
socket:set_timeout(timeout)
|
||
|
|
||
|
if ( not(multiple) ) then
|
||
|
socket:connect( host, port, "udp" )
|
||
|
end
|
||
|
|
||
|
for i = 1, cnt do
|
||
|
local status, err
|
||
|
|
||
|
if ( multiple ) then
|
||
|
status, err = socket:sendto(host, port, data)
|
||
|
else
|
||
|
status, err = socket:send(data)
|
||
|
end
|
||
|
|
||
|
if (not(status)) then return false, err end
|
||
|
|
||
|
local response
|
||
|
|
||
|
if ( multiple ) then
|
||
|
while(true) do
|
||
|
status, response = socket:receive()
|
||
|
if( not(status) ) then break end
|
||
|
|
||
|
local status, _, _, ip, _ = socket:get_info()
|
||
|
table.insert(responses, { data = response, peer = ip } )
|
||
|
end
|
||
|
else
|
||
|
status, response = socket:receive()
|
||
|
if ( status ) then
|
||
|
local status, _, _, ip, _ = socket:get_info()
|
||
|
table.insert(responses, { data = response, peer = ip } )
|
||
|
end
|
||
|
end
|
||
|
|
||
|
if (#responses>0) then
|
||
|
socket:close()
|
||
|
return true, responses
|
||
|
end
|
||
|
end
|
||
|
socket:close()
|
||
|
return false
|
||
|
end
|
||
|
|
||
|
---
|
||
|
-- Send TCP DNS query
|
||
|
-- @param data Data to be sent.
|
||
|
-- @param host Host to connect to.
|
||
|
-- @param port Port to connect to.
|
||
|
-- @param timeout Number of ms to wait for a response.
|
||
|
-- @return Status (true or false).
|
||
|
-- @return Response (if status is true).
|
||
|
local function sendPacketsTCP(data, host, port, timeout)
|
||
|
local socket = nmap.new_socket()
|
||
|
local response
|
||
|
local responses = {}
|
||
|
socket:set_timeout(timeout)
|
||
|
socket:connect(host, port)
|
||
|
-- add payload size we are assuming a minimum size here of 256?
|
||
|
local send_data = '\000' .. string.char(#data) .. data
|
||
|
socket:send(send_data)
|
||
|
local response = ''
|
||
|
local got_response = false
|
||
|
while true do
|
||
|
local status, recv_data = socket:receive_bytes(1)
|
||
|
if not status then break end
|
||
|
got_response = true
|
||
|
response = response .. recv_data
|
||
|
end
|
||
|
local status, _, _, ip, _ = socket:get_info()
|
||
|
socket:close()
|
||
|
if not got_response then
|
||
|
return false
|
||
|
end
|
||
|
-- remove payload size
|
||
|
table.insert(responses, { data = string.sub(response,3), peer = ip } )
|
||
|
return true, responses
|
||
|
end
|
||
|
|
||
|
---
|
||
|
-- Call appropriate protocol handler
|
||
|
-- @param data Data to be sent.
|
||
|
-- @param host Host to connect to.
|
||
|
-- @param port Port to connect to.
|
||
|
-- @param timeout Number of ms to wait for a response.
|
||
|
-- @param cnt Number of tries.
|
||
|
-- @param multiple If true, keep reading multiple responses until timeout.
|
||
|
-- @return Status (true or false).
|
||
|
local function sendPackets(data, host, port, timeout, cnt, multiple, proto)
|
||
|
if proto == nil or proto == 'udp' then
|
||
|
return sendPacketsUDP(data, host, port, timeout, cnt, multiple)
|
||
|
else
|
||
|
return sendPacketsTCP(data, host, port, timeout)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
---
|
||
|
-- Checks if a DNS response packet contains a useful answer.
|
||
|
-- @param rPkt Decoded DNS response packet.
|
||
|
-- @return True if useful, false if not.
|
||
|
local function gotAnswer(rPkt)
|
||
|
-- have we even got answers?
|
||
|
if #rPkt.answers > 0 then
|
||
|
|
||
|
-- some MDNS implementation incorrectly return an empty question section
|
||
|
-- if this is the case return true
|
||
|
if rPkt.questions[1] == nil then
|
||
|
return true
|
||
|
end
|
||
|
|
||
|
-- are those answers not just cnames?
|
||
|
if rPkt.questions[1].dtype == types.A then
|
||
|
for _, v in ipairs(rPkt.answers) do
|
||
|
-- if at least one answer is an A record, it's an answer
|
||
|
if v.dtype == types.A then
|
||
|
return true
|
||
|
end
|
||
|
end
|
||
|
-- if none was an A record, it's not really an answer
|
||
|
return false
|
||
|
else -- there was no A request, CNAMEs are not of interest
|
||
|
return true
|
||
|
end
|
||
|
-- no such name is the answer
|
||
|
elseif rPkt.flags.RC3 and rPkt.flags.RC4 then
|
||
|
return true
|
||
|
-- really no answer
|
||
|
else
|
||
|
return false
|
||
|
end
|
||
|
end
|
||
|
|
||
|
|
||
|
---
|
||
|
-- Tries to find the next nameserver with authority to get a result for
|
||
|
-- query.
|
||
|
-- @param rPkt Decoded DNS response packet
|
||
|
-- @return String or table of next server(s) to query, or false.
|
||
|
local function getAuthDns(rPkt)
|
||
|
if #rPkt.auth == 0 then
|
||
|
if #rPkt.answers == 0 then
|
||
|
return false
|
||
|
else
|
||
|
if rPkt.answers[1].dtype == types.CNAME then
|
||
|
return {cname = rPkt.answers[1].domain}
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
if rPkt.auth[1].dtype == types.NS then
|
||
|
if #rPkt.add > 0 then
|
||
|
local hosts = {}
|
||
|
for _, v in ipairs(rPkt.add) do
|
||
|
if v.dtype == types.A then
|
||
|
table.insert(hosts, v.ip)
|
||
|
end
|
||
|
end
|
||
|
if #hosts > 0 then return hosts end
|
||
|
end
|
||
|
local status, next = query(rPkt.auth[1].domain, {dtype = "A" })
|
||
|
return next
|
||
|
end
|
||
|
return false
|
||
|
end
|
||
|
|
||
|
local function processResponse( response, dname, dtype, options )
|
||
|
|
||
|
local rPkt = decode(response)
|
||
|
-- is it a real answer?
|
||
|
if gotAnswer(rPkt) then
|
||
|
if (options.retPkt) then
|
||
|
return true, rPkt
|
||
|
else
|
||
|
return findNiceAnswer(dtype, rPkt, options.retAll)
|
||
|
end
|
||
|
elseif ( not(options.noauth) ) then -- if not, ask the next server in authority
|
||
|
|
||
|
local next_server = getAuthDns(rPkt)
|
||
|
|
||
|
-- if we got a CNAME, ask for the CNAME
|
||
|
if type(next_server) == 'table' and next_server.cname then
|
||
|
options.tries = options.tries - 1
|
||
|
return query(next_server.cname, options)
|
||
|
end
|
||
|
|
||
|
-- only ask next server in authority, if
|
||
|
-- we got an auth dns and
|
||
|
-- it isn't the one we just asked
|
||
|
if next_server and next_server ~= options.host and options.tries > 1 then
|
||
|
options.host = next_server
|
||
|
options.tries = options.tries - 1
|
||
|
return query(dname, options)
|
||
|
end
|
||
|
elseif ( options.retPkt ) then
|
||
|
return true, rPkt
|
||
|
end
|
||
|
|
||
|
-- nothing worked
|
||
|
stdnse.debug1("dns.query() failed to resolve the requested query%s%s", dname and ": " or ".", dname or "")
|
||
|
return false, "No Answers"
|
||
|
|
||
|
end
|
||
|
|
||
|
---
|
||
|
-- Query DNS servers for a DNS record.
|
||
|
-- @param dname Desired domain name entry.
|
||
|
-- @param options A table containing any of the following fields:
|
||
|
-- * <code>dtype</code>: Desired DNS record type (default: <code>"A"</code>).
|
||
|
-- * <code>host</code>: DNS server to be queried (default: DNS servers known to Nmap).
|
||
|
-- * <code>port</code>: Port of DNS server to connect to (default: <code>53</code>).
|
||
|
-- * <code>tries</code>: How often should <code>query</code> try to contact another server (for non-recursive queries).
|
||
|
-- * <code>retAll</code>: Return all answers, not just the first.
|
||
|
-- * <code>retPkt</code>: Return the packet instead of using the answer-fetching mechanism.
|
||
|
-- * <code>norecurse</code>: If true, do not set the recursion (RD) flag.
|
||
|
-- * <code>noauth</code>: If true, do not try to find authoritative server
|
||
|
-- * <code>multiple</code>: If true, expects multiple hosts to respond to multicast request
|
||
|
-- * <code>flags</code>: numeric value to set flags in the DNS query to a specific value
|
||
|
-- * <code>id</code>: numeric value to use for the DNS transaction id
|
||
|
-- * <code>nsid</code>: If true, queries the server for the nameserver identifier (RFC 5001)
|
||
|
-- * <code>subnet</code>: table, if set perform a edns-client-subnet lookup. The table should contain the fields:
|
||
|
-- <code>family</code> - IPv4: "inet" or 1 (default), IPv6: "inet6" or 2
|
||
|
-- <code>address</code> - string containing the originating subnet IP address
|
||
|
-- <code>mask</code> - number containing the number of subnet bits
|
||
|
-- @return <code>true</code> if a dns response was received and contained an answer of the requested type,
|
||
|
-- or the decoded dns response was requested (retPkt) and is being returned - or <code>false</code> otherwise.
|
||
|
-- @return String answer of the requested type, table of answers or a String error message of one of the following:
|
||
|
-- "No Such Name", "No Servers", "No Answers", "Unable to handle response"
|
||
|
function query(dname, options)
|
||
|
if not options then options = {} end
|
||
|
|
||
|
local dtype, host, port, proto = options.dtype, options.host, options.port, options.proto
|
||
|
if proto == nil then proto = 'udp' end
|
||
|
if port == nil then port = '53' end
|
||
|
|
||
|
local class = options.class or CLASS.IN
|
||
|
if not options.tries then options.tries = 10 end -- don't get into an infinite loop
|
||
|
|
||
|
if not options.sendCount then options.sendCount = 2 end
|
||
|
|
||
|
if type( options.timeout ) ~= "number" then options.timeout = get_default_timeout() end
|
||
|
|
||
|
if type(dtype) == "string" then
|
||
|
dtype = types[dtype]
|
||
|
end
|
||
|
if not dtype then dtype = types.A end
|
||
|
|
||
|
local srv
|
||
|
local srvI = 1
|
||
|
if not port then port = 53 end
|
||
|
if not host then
|
||
|
srv = get_servers()
|
||
|
if srv and srv[1] then
|
||
|
host = srv[1]
|
||
|
else
|
||
|
return false, "No Servers"
|
||
|
end
|
||
|
elseif type(host) == "table" then
|
||
|
srv = host
|
||
|
host = srv[1]
|
||
|
end
|
||
|
|
||
|
local pkt = newPacket()
|
||
|
addQuestion(pkt, dname, dtype, class)
|
||
|
if options.norecurse then pkt.flags.RD = false end
|
||
|
|
||
|
local dnssec = {}
|
||
|
if ( options.dnssec ) then
|
||
|
dnssec = { DO = true }
|
||
|
end
|
||
|
|
||
|
if ( options.nsid ) then
|
||
|
addNSID(pkt, dnssec)
|
||
|
elseif ( options.subnet ) then
|
||
|
addClientSubnet(pkt, dnssec, options.subnet )
|
||
|
elseif ( dnssec.DO ) then
|
||
|
addOPT(pkt, {DO = true})
|
||
|
end
|
||
|
|
||
|
if ( options.flags ) then pkt.flags.raw = options.flags end
|
||
|
if ( options.id ) then pkt.id = options.id end
|
||
|
|
||
|
local data = encode(pkt)
|
||
|
|
||
|
local status, response = sendPackets(data, host, port, options.timeout, options.sendCount, options.multiple, proto)
|
||
|
|
||
|
|
||
|
-- if working with know nameservers, try the others
|
||
|
while((not status) and srv and srvI < #srv) do
|
||
|
srvI = srvI + 1
|
||
|
host = srv[srvI]
|
||
|
status, response = sendPackets(data, host, port, options.timeout, options.sendCount)
|
||
|
end
|
||
|
|
||
|
-- if we got any response:
|
||
|
if status then
|
||
|
if ( options.multiple ) then
|
||
|
local multiresponse = {}
|
||
|
for _, r in ipairs( response ) do
|
||
|
local status, presponse = processResponse( r.data, dname, dtype, options )
|
||
|
if( status ) then
|
||
|
table.insert( multiresponse, { ['output']=presponse, ['peer']=r.peer } )
|
||
|
end
|
||
|
end
|
||
|
return true, multiresponse
|
||
|
else
|
||
|
return processResponse( response[1].data, dname, dtype, options)
|
||
|
end
|
||
|
else
|
||
|
stdnse.debug1("dns.query() got zero responses attempting to resolve query%s%s", dname and ": " or ".", dname or "")
|
||
|
return false, "No Answers"
|
||
|
end
|
||
|
end
|
||
|
|
||
|
|
||
|
---
|
||
|
-- Formats an IP address for reverse lookup.
|
||
|
-- @param ip IP address string.
|
||
|
-- @return "Domain"-style representation of IP as subdomain of in-addr.arpa or
|
||
|
-- ip6.arpa.
|
||
|
function reverse(ip)
|
||
|
ip = ipOps.expand_ip(ip)
|
||
|
if type(ip) ~= "string" then return nil end
|
||
|
local delim = "%."
|
||
|
local arpa = ".in-addr.arpa"
|
||
|
if ip:match(":") then
|
||
|
delim = ":"
|
||
|
arpa = ".ip6.arpa"
|
||
|
end
|
||
|
local ipParts = stringaux.strsplit(delim, ip)
|
||
|
if #ipParts == 8 then
|
||
|
-- padding
|
||
|
local mask = "0000"
|
||
|
for i, part in ipairs(ipParts) do
|
||
|
ipParts[i] = mask:sub(1, #mask - #part) .. part
|
||
|
end
|
||
|
-- 32 parts from 8
|
||
|
local temp = {}
|
||
|
for i, hdt in ipairs(ipParts) do
|
||
|
for part in hdt:gmatch("%x") do
|
||
|
temp[#temp+1] = part
|
||
|
end
|
||
|
end
|
||
|
ipParts = temp
|
||
|
end
|
||
|
local ipReverse = {}
|
||
|
for i = #ipParts, 1, -1 do
|
||
|
table.insert(ipReverse, ipParts[i])
|
||
|
end
|
||
|
return table.concat(ipReverse, ".") .. arpa
|
||
|
end
|
||
|
|
||
|
-- Table for answer fetching functions.
|
||
|
local answerFetcher = {}
|
||
|
|
||
|
-- Answer fetcher for TXT 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 TXT record or Table of TXT records or String Error message.
|
||
|
answerFetcher[types.TXT] = function(dec, retAll)
|
||
|
local answers = {}
|
||
|
if not retAll and dec.answers[1].data then
|
||
|
return true, string.sub(dec.answers[1].data, 2)
|
||
|
elseif not retAll then
|
||
|
stdnse.debug1("dns.answerFetcher found no records of the required type: TXT")
|
||
|
return false, "No Answers"
|
||
|
else
|
||
|
for _, v in ipairs(dec.answers) do
|
||
|
if v.TXT and v.TXT.text then
|
||
|
for _, v in ipairs( v.TXT.text ) do
|
||
|
table.insert(answers, v)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
if #answers == 0 then
|
||
|
stdnse.debug1("dns.answerFetcher found no records of the required type: TXT")
|
||
|
return false, "No Answers"
|
||
|
end
|
||
|
return true, answers
|
||
|
end
|
||
|
|
||
|
-- Answer fetcher for A 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 A record or Table of A records or String Error message.
|
||
|
answerFetcher[types.A] = function(dec, retAll)
|
||
|
local answers = {}
|
||
|
for _, ans in ipairs(dec.answers) do
|
||
|
if ans.dtype == types.A then
|
||
|
if not retAll then
|
||
|
return true, ans.ip
|
||
|
end
|
||
|
table.insert(answers, ans.ip)
|
||
|
end
|
||
|
end
|
||
|
if not retAll or #answers == 0 then
|
||
|
stdnse.debug1("dns.answerFetcher found no records of the required type: A")
|
||
|
return false, "No Answers"
|
||
|
end
|
||
|
return true, answers
|
||
|
end
|
||
|
|
||
|
|
||
|
-- Answer fetcher for CNAME 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 Domain entry or Table of domain entries or String Error message.
|
||
|
answerFetcher[types.CNAME] = function(dec, retAll)
|
||
|
local answers = {}
|
||
|
if not retAll and dec.answers[1].domain then
|
||
|
return true, dec.answers[1].domain
|
||
|
elseif not retAll then
|
||
|
stdnse.debug1("dns.answerFetcher found no records of the required type: NS, PTR or CNAME")
|
||
|
return false, "No Answers"
|
||
|
else
|
||
|
for _, v in ipairs(dec.answers) do
|
||
|
if v.domain then table.insert(answers, v.domain) end
|
||
|
end
|
||
|
end
|
||
|
if #answers == 0 then
|
||
|
stdnse.debug1("dns.answerFetcher found no records of the required type: NS, PTR or CNAME")
|
||
|
return false, "No Answers"
|
||
|
end
|
||
|
return true, answers
|
||
|
end
|
||
|
|
||
|
-- Answer fetcher for MX 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 MX record or Table of MX records or String Error message.
|
||
|
-- Note that the format of a returned MX answer is "preference:hostname:IPaddress" where zero
|
||
|
-- or more IP addresses may be present.
|
||
|
answerFetcher[types.MX] = function(dec, retAll)
|
||
|
local mx, ip, answers = {}, {}, {}
|
||
|
for _, ans in ipairs(dec.answers) do
|
||
|
if ans.MX then mx[#mx+1] = ans.MX end
|
||
|
if not retAll then break end
|
||
|
end
|
||
|
if #mx == 0 then
|
||
|
stdnse.debug1("dns.answerFetcher found no records of the required type: MX")
|
||
|
return false, "No Answers"
|
||
|
end
|
||
|
for _, add in ipairs(dec.add) do
|
||
|
if ip[add.dname] then table.insert(ip[add.dname], add.ip)
|
||
|
else ip[add.dname] = {add.ip} end
|
||
|
end
|
||
|
for _, mxrec in ipairs(mx) do
|
||
|
if ip[mxrec.server] then
|
||
|
table.insert( answers, ("%s:%s:%s"):format(mxrec.pref or "-", mxrec.server or "-", table.concat(ip[mxrec.server], ":")) )
|
||
|
if not retAll then return true, answers[1] end
|
||
|
else
|
||
|
-- no IP ?
|
||
|
table.insert( answers, ("%s:%s"):format(mxrec.pref or "-", mxrec.server or "-") )
|
||
|
if not retAll then return true, answers[1] end
|
||
|
end
|
||
|
end
|
||
|
return true, answers
|
||
|
end
|
||
|
|
||
|
-- Answer fetcher for SRV 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 SRV record or Table of SRV records or String Error message.
|
||
|
-- Note that the format of a returned SRV answer is "priority:weight:port:target" where zero
|
||
|
-- or more IP addresses may be present.
|
||
|
answerFetcher[types.SRV] = function(dec, retAll)
|
||
|
local srv, ip, answers = {}, {}, {}
|
||
|
for _, ans in ipairs(dec.answers) do
|
||
|
if ans.dtype == types.SRV then
|
||
|
if not retAll then
|
||
|
return true, ("%s:%s:%s:%s"):format( ans.SRV.prio, ans.SRV.weight, ans.SRV.port, ans.SRV.target )
|
||
|
end
|
||
|
table.insert( answers, ("%s:%s:%s:%s"):format( ans.SRV.prio, ans.SRV.weight, ans.SRV.port, ans.SRV.target ) )
|
||
|
end
|
||
|
end
|
||
|
if #answers == 0 then
|
||
|
stdnse.debug1("dns.answerFetcher found no records of the required type: SRV")
|
||
|
return false, "No Answers"
|
||
|
end
|
||
|
|
||
|
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.debug1("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 "-", table.concat(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
|
||
|
-- @param dec Decoded DNS response.
|
||
|
-- @return True if one or more answers of the required type were found - otherwise false.
|
||
|
-- @return String first Domain entry or Table of domain entries or String Error message.
|
||
|
answerFetcher[types.NS] = answerFetcher[types.CNAME]
|
||
|
|
||
|
-- Answer fetcher for PTR records.
|
||
|
-- @name answerFetcher[types.PTR]
|
||
|
-- @class function
|
||
|
-- @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 Domain entry or Table of domain entries or String Error message.
|
||
|
answerFetcher[types.PTR] = answerFetcher[types.CNAME]
|
||
|
|
||
|
-- Answer fetcher for AAAA 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 AAAA record or Table of AAAA records or String Error message.
|
||
|
answerFetcher[types.AAAA] = function(dec, retAll)
|
||
|
local answers = {}
|
||
|
for _, ans in ipairs(dec.answers) do
|
||
|
if ans.dtype == types.AAAA then
|
||
|
if not retAll then
|
||
|
return true, ans.ipv6
|
||
|
end
|
||
|
table.insert(answers, ans.ipv6)
|
||
|
end
|
||
|
end
|
||
|
if not retAll or #answers == 0 then
|
||
|
stdnse.debug1("dns.answerFetcher found no records of the required type: AAAA")
|
||
|
return false, "No Answers"
|
||
|
end
|
||
|
return true, answers
|
||
|
end
|
||
|
|
||
|
|
||
|
---Calls the answer fetcher for <code>dtype</code> or returns an error code in
|
||
|
-- case of a "no such name" error.
|
||
|
--
|
||
|
-- @param dtype DNS resource record type.
|
||
|
-- @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 Answer according to the answer fetcher for <code>dtype</code> or an Error message.
|
||
|
function findNiceAnswer(dtype, dec, retAll)
|
||
|
if (#dec.answers > 0) then
|
||
|
if answerFetcher[dtype] then
|
||
|
return answerFetcher[dtype](dec, retAll)
|
||
|
else
|
||
|
stdnse.debug1("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
|
||
|
return false, "No Such Name"
|
||
|
else
|
||
|
stdnse.debug1("dns.findNiceAnswer() found zero answers in a response, but got an unexpected flags.replycode")
|
||
|
return false, "No Answers"
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- Table for additional fetching functions.
|
||
|
-- Some servers return their answers in the additional section. The
|
||
|
-- findNiceAdditional function with its relevant additionalFetcher functions
|
||
|
-- addresses this. This unfortunately involved some code duplication (because
|
||
|
-- of current design of the dns library) from the answerFetchers to the
|
||
|
-- additionalFetchers.
|
||
|
local additionalFetcher = {}
|
||
|
|
||
|
-- Additional fetcher for TXT 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 TXT record or Table of TXT records or String Error message.
|
||
|
additionalFetcher[types.TXT] = function(dec, retAll)
|
||
|
local answers = {}
|
||
|
if not retAll and dec.add[1].data then
|
||
|
return true, string.sub(dec.add[1].data, 2)
|
||
|
elseif not retAll then
|
||
|
stdnse.debug1("dns.additionalFetcher found no records of the required type: TXT")
|
||
|
return false, "No Answers"
|
||
|
else
|
||
|
for _, v in ipairs(dec.add) do
|
||
|
if v.TXT and v.TXT.text then
|
||
|
for _, v in ipairs( v.TXT.text ) do
|
||
|
table.insert(answers, v)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
if #answers == 0 then
|
||
|
stdnse.debug1("dns.answerFetcher found no records of the required type: TXT")
|
||
|
return false, "No Answers"
|
||
|
end
|
||
|
return true, answers
|
||
|
end
|
||
|
|
||
|
-- Additional fetcher for A 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 A record or Table of A records or String Error message.
|
||
|
additionalFetcher[types.A] = function(dec, retAll)
|
||
|
local answers = {}
|
||
|
for _, ans in ipairs(dec.add) do
|
||
|
if ans.dtype == types.A then
|
||
|
if not retAll then
|
||
|
return true, ans.ip
|
||
|
end
|
||
|
table.insert(answers, ans.ip)
|
||
|
end
|
||
|
end
|
||
|
if not retAll or #answers == 0 then
|
||
|
stdnse.debug1("dns.answerFetcher found no records of the required type: A")
|
||
|
return false, "No Answers"
|
||
|
end
|
||
|
return true, answers
|
||
|
end
|
||
|
|
||
|
|
||
|
-- Additional fetcher for SRV 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 SRV record or Table of SRV records or String Error message.
|
||
|
-- Note that the format of a returned SRV answer is "priority:weight:port:target" where zero
|
||
|
-- or more IP addresses may be present.
|
||
|
additionalFetcher[types.SRV] = function(dec, retAll)
|
||
|
local srv, ip, answers = {}, {}, {}
|
||
|
for _, ans in ipairs(dec.add) do
|
||
|
if ans.dtype == types.SRV then
|
||
|
if not retAll then
|
||
|
return true, ("%s:%s:%s:%s"):format( ans.SRV.prio, ans.SRV.weight, ans.SRV.port, ans.SRV.target )
|
||
|
end
|
||
|
table.insert( answers, ("%s:%s:%s:%s"):format( ans.SRV.prio, ans.SRV.weight, ans.SRV.port, ans.SRV.target ) )
|
||
|
end
|
||
|
end
|
||
|
if #answers == 0 then
|
||
|
stdnse.debug1("dns.answerFetcher found no records of the required type: SRV")
|
||
|
return false, "No Answers"
|
||
|
end
|
||
|
|
||
|
return true, answers
|
||
|
end
|
||
|
|
||
|
|
||
|
-- Additional fetcher for AAAA 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 AAAA record or Table of AAAA records or String Error message.
|
||
|
additionalFetcher[types.AAAA] = function(dec, retAll)
|
||
|
local answers = {}
|
||
|
for _, ans in ipairs(dec.add) do
|
||
|
if ans.dtype == types.AAAA then
|
||
|
if not retAll then
|
||
|
return true, ans.ipv6
|
||
|
end
|
||
|
table.insert(answers, ans.ipv6)
|
||
|
end
|
||
|
end
|
||
|
if not retAll or #answers == 0 then
|
||
|
stdnse.debug1("dns.answerFetcher found no records of the required type: AAAA")
|
||
|
return false, "No Answers"
|
||
|
end
|
||
|
return true, answers
|
||
|
end
|
||
|
|
||
|
---
|
||
|
-- Calls the answer fetcher for <code>dtype</code> or returns an error code in
|
||
|
-- case of a "no such name" error.
|
||
|
-- @param dtype DNS resource record type.
|
||
|
-- @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 Answer according to the answer fetcher for <code>dtype</code> or an Error message.
|
||
|
function findNiceAdditional(dtype, dec, retAll)
|
||
|
if (#dec.add > 0) then
|
||
|
if additionalFetcher[dtype] then
|
||
|
return additionalFetcher[dtype](dec, retAll)
|
||
|
else
|
||
|
stdnse.debug1("dns.findNiceAdditional() does not have an additionalFetcher for dtype %s",
|
||
|
(type(dtype) == 'string' and dtype) or type(dtype) or "nil")
|
||
|
return false, "Unable to handle response"
|
||
|
end
|
||
|
elseif (dec.flags.RC3 and dec.flags.RC4) then
|
||
|
return false, "No Such Name"
|
||
|
else
|
||
|
stdnse.debug1("dns.findNiceAdditional() found zero answers in a response, but got an unexpected flags.replycode")
|
||
|
return false, "No Answers"
|
||
|
end
|
||
|
end
|
||
|
|
||
|
--
|
||
|
-- Encodes a FQDN
|
||
|
-- @param fqdn containing the fully qualified domain name
|
||
|
-- @return encQ containing the encoded value
|
||
|
local function encodeFQDN(fqdn)
|
||
|
if ( not(fqdn) or #fqdn == 0 ) then return "\0" end
|
||
|
|
||
|
local encQ = {}
|
||
|
for part in string.gmatch(fqdn, "[^%.]+") do
|
||
|
encQ[#encQ+1] = string.pack("s1", part)
|
||
|
end
|
||
|
encQ[#encQ+1] = "\0"
|
||
|
return table.concat(encQ)
|
||
|
end
|
||
|
|
||
|
---
|
||
|
-- Encodes the question part of a DNS request.
|
||
|
-- @param questions Table of questions.
|
||
|
-- @return Encoded question string.
|
||
|
local function encodeQuestions(questions)
|
||
|
if type(questions) ~= "table" then return nil end
|
||
|
local encQ = {}
|
||
|
for _, v in ipairs(questions) do
|
||
|
encQ[#encQ+1] = encodeFQDN(v.dname)
|
||
|
encQ[#encQ+1] = string.pack(">I2I2", v.dtype, v.class)
|
||
|
end
|
||
|
return table.concat(encQ)
|
||
|
end
|
||
|
|
||
|
---
|
||
|
-- Encodes the zone part of a DNS request.
|
||
|
-- @param questions Table of questions.
|
||
|
-- @return Encoded question string.
|
||
|
local function encodeZones(zones)
|
||
|
return encodeQuestions(zones)
|
||
|
end
|
||
|
|
||
|
local function encodeUpdates(updates)
|
||
|
if type(updates) ~= "table" then return nil end
|
||
|
local encQ = {}
|
||
|
for _, v in ipairs(updates) do
|
||
|
encQ[#encQ+1] = encodeFQDN(v.dname)
|
||
|
encQ[#encQ+1] = string.pack(">I2I2I4s2", v.dtype, v.class, v.ttl, v.data)
|
||
|
end
|
||
|
return table.concat(encQ)
|
||
|
end
|
||
|
|
||
|
---
|
||
|
-- Encodes the additional part of a DNS request.
|
||
|
-- @param additional Table of additional records. Each must have the keys
|
||
|
-- <code>type</code>, <code>class</code>, <code>ttl</code>,
|
||
|
-- and <code>rdata</code>.
|
||
|
-- @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+1] = string.pack(">xI2I2I4s2", v.type, v.class, v.ttl, v.rdata)
|
||
|
end
|
||
|
return table.concat(encA)
|
||
|
end
|
||
|
|
||
|
---
|
||
|
-- Encodes DNS flags to a binary digit string.
|
||
|
-- @param flags Flag table, each entry representing a flag (QR, OCx, AA, TC, RD,
|
||
|
-- RA, RCx).
|
||
|
-- @return Binary digit string representing flags.
|
||
|
local function encodeFlags(flags)
|
||
|
if type(flags) == "number" then return flags end
|
||
|
if type(flags) ~= "table" then return nil end
|
||
|
local fb = 0
|
||
|
if flags.QR then fb = fb|0x8000 end
|
||
|
if flags.OC1 then fb = fb|0x4000 end
|
||
|
if flags.OC2 then fb = fb|0x2000 end
|
||
|
if flags.OC3 then fb = fb|0x1000 end
|
||
|
if flags.OC4 then fb = fb|0x0800 end
|
||
|
if flags.AA then fb = fb|0x0400 end
|
||
|
if flags.TC then fb = fb|0x0200 end
|
||
|
if flags.RD then fb = fb|0x0100 end
|
||
|
if flags.RA then fb = fb|0x0080 end
|
||
|
if flags.RC1 then fb = fb|0x0008 end
|
||
|
if flags.RC2 then fb = fb|0x0004 end
|
||
|
if flags.RC3 then fb = fb|0x0002 end
|
||
|
if flags.RC4 then fb = fb|0x0001 end
|
||
|
return fb
|
||
|
end
|
||
|
|
||
|
---
|
||
|
-- Encode a DNS packet.
|
||
|
--
|
||
|
-- Caution: doesn't encode answer and authority part.
|
||
|
-- @param pkt Table representing DNS packet, initialized by
|
||
|
-- <code>newPacket</code>.
|
||
|
-- @return Encoded DNS packet.
|
||
|
function encode(pkt)
|
||
|
if type(pkt) ~= "table" then return nil end
|
||
|
local encFlags = encodeFlags(pkt.flags)
|
||
|
local additional = encodeAdditional(pkt.additional)
|
||
|
local aorplen = #pkt.answers
|
||
|
local data, qorzlen, aorulen
|
||
|
|
||
|
if ( #pkt.questions > 0 ) then
|
||
|
data = encodeQuestions( pkt.questions )
|
||
|
qorzlen = #pkt.questions
|
||
|
aorulen = 0
|
||
|
else
|
||
|
-- The packet has no questions, assume we're dealing with an update
|
||
|
data = encodeZones( pkt.zones ) .. encodeUpdates( pkt.updates )
|
||
|
qorzlen = #pkt.zones
|
||
|
aorulen = #pkt.updates
|
||
|
end
|
||
|
|
||
|
local encStr
|
||
|
if ( pkt.flags.raw ) then
|
||
|
encStr = string.pack(">I2I2I2I2I2I2", pkt.id, pkt.flags.raw, qorzlen, aorplen, aorulen, #pkt.additional) .. data .. additional
|
||
|
else
|
||
|
encStr = string.pack(">I2I2I2I2I2I2", pkt.id, encFlags, qorzlen, aorplen, aorulen, #pkt.additional) .. data .. additional
|
||
|
end
|
||
|
return encStr
|
||
|
end
|
||
|
|
||
|
|
||
|
---
|
||
|
-- Decodes a domain in a DNS packet. Handles "compressed" data too.
|
||
|
-- @param data Complete DNS packet.
|
||
|
-- @param pos Starting position in packet.
|
||
|
-- @return Position after decoding.
|
||
|
-- @return Decoded domain, or <code>nil</code> on error.
|
||
|
function decStr(data, pos)
|
||
|
local function dec(data, pos, limit)
|
||
|
local partlen
|
||
|
local parts = {}
|
||
|
local part
|
||
|
|
||
|
-- Avoid infinite recursion on malformed compressed messages.
|
||
|
limit = limit or 10
|
||
|
if limit < 0 then
|
||
|
return pos, nil
|
||
|
end
|
||
|
|
||
|
partlen, pos = string.unpack(">B", data, pos)
|
||
|
while (partlen ~= 0) do
|
||
|
if (partlen < 64) then
|
||
|
if (#data - pos + 1) < partlen then
|
||
|
return pos
|
||
|
end
|
||
|
part, pos = string.unpack("c" .. partlen, data, pos)
|
||
|
table.insert(parts, part)
|
||
|
partlen, pos = string.unpack(">B", data, pos)
|
||
|
else
|
||
|
partlen, pos = string.unpack(">I2", data, pos - 1)
|
||
|
local _, part = dec(data, partlen - 0xC000 + 1, limit - 1)
|
||
|
if part == nil then
|
||
|
return pos
|
||
|
end
|
||
|
table.insert(parts, part)
|
||
|
partlen = 0
|
||
|
end
|
||
|
end
|
||
|
return pos, table.concat(parts, ".")
|
||
|
end
|
||
|
|
||
|
return dec(data, pos)
|
||
|
end
|
||
|
|
||
|
|
||
|
---
|
||
|
-- Decodes questions in a DNS packet.
|
||
|
-- @param data Complete DNS packet.
|
||
|
-- @param count Value of question counter in header.
|
||
|
-- @param pos Starting position in packet.
|
||
|
-- @return Position after decoding.
|
||
|
-- @return Table of decoded questions.
|
||
|
local function decodeQuestions(data, count, pos)
|
||
|
local q = {}
|
||
|
for i = 1, count do
|
||
|
local currQ = {}
|
||
|
pos, currQ.dname = decStr(data, pos)
|
||
|
currQ.dtype, currQ.class, pos = string.unpack(">I2I2", data, pos)
|
||
|
table.insert(q, currQ)
|
||
|
end
|
||
|
return pos, q
|
||
|
end
|
||
|
|
||
|
|
||
|
---
|
||
|
-- Table of functions to decode resource records
|
||
|
local decoder = {}
|
||
|
|
||
|
-- Decodes IP of A record, puts it in <code>entry.ip</code>.
|
||
|
-- @param entry RR in packet.
|
||
|
decoder[types.A] = function(entry)
|
||
|
entry.ip = ipOps.str_to_ip(entry.data:sub(1,4))
|
||
|
end
|
||
|
|
||
|
-- Decodes IP of AAAA record, puts it in <code>entry.ipv6</code>.
|
||
|
-- @param entry RR in packet.
|
||
|
decoder[types.AAAA] = function(entry)
|
||
|
entry.ipv6 = ipOps.str_to_ip(entry.data:sub(1,16))
|
||
|
end
|
||
|
|
||
|
-- Decodes SSH fingerprint record, puts it in <code>entry.SSHFP</code> as
|
||
|
-- defined in RFC 4255.
|
||
|
--
|
||
|
-- <code>entry.SSHFP</code> has the fields <code>algorithm</code>,
|
||
|
-- <code>fptype</code>, and <code>fingerprint</code>.
|
||
|
-- @param entry RR in packet.
|
||
|
decoder[types.SSHFP] = function(entry)
|
||
|
local pos
|
||
|
entry.SSHFP = {}
|
||
|
entry.SSHFP.algorithm, entry.SSHFP.fptype, pos = string.unpack(">BB", entry.data)
|
||
|
entry.SSHFP.fingerprint = stdnse.tohex(entry.data:sub(pos))
|
||
|
end
|
||
|
|
||
|
|
||
|
-- Decodes SOA record, puts it in <code>entry.SOA</code>.
|
||
|
--
|
||
|
-- <code>entry.SOA</code> has the fields <code>mname</code>, <code>rname</code>,
|
||
|
-- <code>serial</code>, <code>refresh</code>, <code>retry</code>,
|
||
|
-- <code>expire</code>, and <code>minimum</code>.
|
||
|
-- @param entry RR in packet.
|
||
|
-- @param data Complete encoded DNS packet.
|
||
|
-- @param pos Position in packet after RR.
|
||
|
decoder[types.SOA] = function(entry, data, pos)
|
||
|
|
||
|
local np = pos - #entry.data
|
||
|
|
||
|
entry.SOA = {}
|
||
|
|
||
|
np, entry.SOA.mname = decStr(data, np)
|
||
|
np, entry.SOA.rname = decStr(data, np)
|
||
|
entry.SOA.serial,
|
||
|
entry.SOA.refresh,
|
||
|
entry.SOA.retry,
|
||
|
entry.SOA.expire,
|
||
|
entry.SOA.minimum,
|
||
|
np = string.unpack(">I4I4I4I4I4", data, np)
|
||
|
end
|
||
|
|
||
|
-- An iterator that returns the positions of nonzero bits in the given binary
|
||
|
-- string.
|
||
|
local function bit_iter(bits)
|
||
|
return coroutine.wrap(function()
|
||
|
for i = 1, #bits do
|
||
|
local n = string.byte(bits, i)
|
||
|
local j = 0
|
||
|
local mask = 0x80
|
||
|
|
||
|
while mask > 0 do
|
||
|
if (n & mask) ~= 0 then
|
||
|
coroutine.yield((i - 1) * 8 + j)
|
||
|
end
|
||
|
j = j + 1
|
||
|
mask = (mask >> 1)
|
||
|
end
|
||
|
end
|
||
|
end)
|
||
|
end
|
||
|
|
||
|
-- Decodes NSEC records, puts result in <code>entry.NSEC</code>. See RFC 4034,
|
||
|
-- section 4.
|
||
|
--
|
||
|
-- <code>entry.NSEC</code> has the fields <code>dname</code>,
|
||
|
-- <code>next_dname</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.NSEC] = function (entry, data, pos)
|
||
|
local np = pos - #entry.data
|
||
|
entry.NSEC = {}
|
||
|
entry.NSEC.dname = entry.dname
|
||
|
np, entry.NSEC.next_dname = decStr(data, np)
|
||
|
while np < pos do
|
||
|
local block_num, type_bitmap
|
||
|
block_num, type_bitmap, np = string.unpack(">Bs1", data, np)
|
||
|
entry.NSEC.types = {}
|
||
|
for i in bit_iter(type_bitmap) do
|
||
|
entry.NSEC.types[(block_num - 1) * 256 + i] = true
|
||
|
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 = {}, {}
|
||
|
|
||
|
entry.NSEC3.hash.alg, entry.NSEC3.flags, entry.NSEC3.iterations, np = string.unpack(">BBI2", data, np)
|
||
|
-- do we even need to decode these do we care about opt out?
|
||
|
-- entry.NSEC3.flags = decodeFlagsNSEC3(flags)
|
||
|
|
||
|
entry.NSEC3.salt.bin, np = string.unpack(">s1", data, np)
|
||
|
entry.NSEC3.salt.hex = stdnse.tohex(entry.NSEC3.salt.bin)
|
||
|
|
||
|
entry.NSEC3.hash.bin, np = string.unpack(">s1" , data, np)
|
||
|
entry.NSEC3.hash.hex = stdnse.tohex(entry.NSEC3.hash.bin)
|
||
|
entry.NSEC3.hash.base32 = base32.enc(entry.NSEC3.hash.bin, true)
|
||
|
|
||
|
entry.NSEC3.WinBlockNo, entry.NSEC3.bin, np = string.unpack(">Bs1", data, np)
|
||
|
entry.NSEC3.types = {}
|
||
|
for i in bit_iter(entry.NSEC3.bin) do
|
||
|
entry.NSEC3.types[(entry.NSEC3.WinBlockNo - 1) * 256 + i] = true
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- Decodes records that consist only of one domain, for example CNAME, NS, PTR.
|
||
|
-- Puts result in <code>entry.domain</code>.
|
||
|
-- @param entry RR in packet.
|
||
|
-- @param data Complete encoded DNS packet.
|
||
|
-- @param pos Position in packet after RR.
|
||
|
local function decDomain(entry, data, pos)
|
||
|
local np = pos - #entry.data
|
||
|
local _
|
||
|
_, entry.domain = decStr(data, np)
|
||
|
end
|
||
|
|
||
|
-- Decodes CNAME records.
|
||
|
-- Puts result in <code>entry.domain</code>.
|
||
|
-- @name decoder[types.CNAME]
|
||
|
-- @class function
|
||
|
-- @param entry RR in packet.
|
||
|
-- @param data Complete encoded DNS packet.
|
||
|
-- @param pos Position in packet after RR.
|
||
|
decoder[types.CNAME] = decDomain
|
||
|
|
||
|
-- Decodes NS records.
|
||
|
-- Puts result in <code>entry.domain</code>.
|
||
|
-- @name decoder[types.NS]
|
||
|
-- @class function
|
||
|
-- @param entry RR in packet.
|
||
|
-- @param data Complete encoded DNS packet.
|
||
|
-- @param pos Position in packet after RR.
|
||
|
decoder[types.NS] = decDomain
|
||
|
|
||
|
-- Decodes PTR records.
|
||
|
-- Puts result in <code>entry.domain</code>.
|
||
|
-- @name decoder[types.PTR]
|
||
|
-- @class function
|
||
|
-- @param entry RR in packet.
|
||
|
-- @param data Complete encoded DNS packet.
|
||
|
-- @param pos Position in packet after RR.
|
||
|
decoder[types.PTR] = decDomain
|
||
|
|
||
|
-- Decodes TXT records.
|
||
|
-- Puts result in <code>entry.domain</code>.
|
||
|
-- @name decoder[types.TXT]
|
||
|
-- @class function
|
||
|
-- @param entry RR in packet.
|
||
|
-- @param data Complete encoded DNS packet.
|
||
|
-- @param pos Position in packet after RR.
|
||
|
decoder[types.TXT] =
|
||
|
function (entry, data, pos)
|
||
|
|
||
|
local len = entry.data:len()
|
||
|
local np = pos - #entry.data
|
||
|
local txt_len
|
||
|
local txt
|
||
|
|
||
|
if len > 0 then
|
||
|
entry.TXT = {}
|
||
|
entry.TXT.text = {}
|
||
|
end
|
||
|
|
||
|
while np < len do
|
||
|
txt, np = string.unpack("s1", data, np)
|
||
|
table.insert( entry.TXT.text, txt )
|
||
|
end
|
||
|
|
||
|
end
|
||
|
|
||
|
---
|
||
|
-- Decodes OPT record, puts it in <code>entry.OPT</code>.
|
||
|
--
|
||
|
-- <code>entry.OPT</code> has the fields <code>mname</code>, <code>rname</code>,
|
||
|
-- <code>serial</code>, <code>refresh</code>, <code>retry</code>,
|
||
|
-- <code>expire</code>, and <code>minimum</code>.
|
||
|
-- @param entry RR in packet.
|
||
|
-- @param data Complete encoded DNS packet.
|
||
|
-- @param pos Position in packet after RR.
|
||
|
decoder[types.OPT] =
|
||
|
function(entry, data, pos)
|
||
|
local np = pos - #entry.data - 6
|
||
|
local opt = { bufsize = entry.class }
|
||
|
opt.rcode, opt.version, opt.zflags, opt.data, np = string.unpack(">BBI2s2", data, np)
|
||
|
entry.OPT = opt
|
||
|
end
|
||
|
|
||
|
|
||
|
-- Decodes MX record, puts it in <code>entry.MX</code>.
|
||
|
--
|
||
|
-- <code>entry.MX</code> has the fields <code>pref</code> and
|
||
|
-- <code>server</code>.
|
||
|
-- @param entry RR in packet.
|
||
|
-- @param data Complete encoded DNS packet.
|
||
|
-- @param pos Position in packet after RR.
|
||
|
decoder[types.MX] =
|
||
|
function(entry, data, pos)
|
||
|
local np = pos - #entry.data + 2
|
||
|
local _
|
||
|
entry.MX = {}
|
||
|
entry.MX.pref = string.unpack(">I2", entry.data)
|
||
|
_, entry.MX.server = decStr(data, np)
|
||
|
end
|
||
|
|
||
|
-- Decodes SRV record, puts it in <code>entry.SRV</code>.
|
||
|
--
|
||
|
-- <code>entry.SRV</code> has the fields <code>prio</code>,
|
||
|
-- <code>weight</code>, <code>port</code> and
|
||
|
-- <code>target</code>.
|
||
|
-- @param entry RR in packet.
|
||
|
-- @param data Complete encoded DNS packet.
|
||
|
-- @param pos Position in packet after RR.
|
||
|
decoder[types.SRV] =
|
||
|
function(entry, data, pos)
|
||
|
local np = pos - #entry.data
|
||
|
local _
|
||
|
entry.SRV = {}
|
||
|
entry.SRV.prio, entry.SRV.weight, entry.SRV.port, np = string.unpack(">I2I2I2", data, np)
|
||
|
np, entry.SRV.target = decStr(data, np)
|
||
|
end
|
||
|
|
||
|
-- Decodes returned resource records (answer, authority, or additional part).
|
||
|
-- @param data Complete encoded DNS packet.
|
||
|
-- @param count Value of according counter in header.
|
||
|
-- @param pos Starting position in packet.
|
||
|
-- @return Table of RRs.
|
||
|
local function decodeRR(data, count, pos)
|
||
|
local ans = {}
|
||
|
for i = 1, count do
|
||
|
local currRR = {}
|
||
|
pos, currRR.dname = decStr(data, pos)
|
||
|
currRR.dtype, currRR.class, currRR.ttl, pos = string.unpack(">I2I2I4", data, pos)
|
||
|
|
||
|
currRR.data, pos = string.unpack(">s2", data, pos)
|
||
|
|
||
|
-- try to be smart: decode per type
|
||
|
if decoder[currRR.dtype] then
|
||
|
decoder[currRR.dtype](currRR, data, pos)
|
||
|
end
|
||
|
|
||
|
table.insert(ans, currRR)
|
||
|
end
|
||
|
return pos, ans
|
||
|
end
|
||
|
|
||
|
---
|
||
|
-- Decodes DNS flags.
|
||
|
-- @param flgStr Flags as a binary digit string.
|
||
|
-- @return Table representing flags.
|
||
|
local function decodeFlags(flags)
|
||
|
local tflags = {}
|
||
|
if (flags & 0x8000) ~= 0 then tflags.QR = true end
|
||
|
if (flags & 0x4000) ~= 0 then tflags.OC1 = true end
|
||
|
if (flags & 0x2000) ~= 0 then tflags.OC2 = true end
|
||
|
if (flags & 0x1000) ~= 0 then tflags.OC3 = true end
|
||
|
if (flags & 0x0800) ~= 0 then tflags.OC4 = true end
|
||
|
if (flags & 0x0400) ~= 0 then tflags.AA = true end
|
||
|
if (flags & 0x0200) ~= 0 then tflags.TC = true end
|
||
|
if (flags & 0x0100) ~= 0 then tflags.RD = true end
|
||
|
if (flags & 0x0080) ~= 0 then tflags.RA = true end
|
||
|
if (flags & 0x0008) ~= 0 then tflags.RC1 = true end
|
||
|
if (flags & 0x0004) ~= 0 then tflags.RC2 = true end
|
||
|
if (flags & 0x0002) ~= 0 then tflags.RC3 = true end
|
||
|
if (flags & 0x0001) ~= 0 then tflags.RC4 = true end
|
||
|
return tflags
|
||
|
end
|
||
|
|
||
|
---
|
||
|
-- Decodes a DNS packet.
|
||
|
-- @param data Encoded DNS packet.
|
||
|
-- @return Table representing DNS packet.
|
||
|
function decode(data)
|
||
|
local pos
|
||
|
local pkt = {}
|
||
|
local encFlags
|
||
|
local cnt = {}
|
||
|
pkt.id, encFlags, cnt.q, cnt.a, cnt.auth, cnt.add, pos = string.unpack(">I2I2I2I2I2I2", data)
|
||
|
-- for now, don't decode the flags
|
||
|
pkt.flags = decodeFlags(encFlags)
|
||
|
|
||
|
--
|
||
|
-- check whether this is an update response or not
|
||
|
-- a quick fix to allow decoding of non updates and not break for updates
|
||
|
-- the flags are enough for the current code to determine whether an update was successful or not
|
||
|
--
|
||
|
local flags = encodeFlags(pkt.flags)
|
||
|
-- QR, OC2
|
||
|
if (flags & 0xF000) == 0xA000 then
|
||
|
return pkt
|
||
|
else
|
||
|
pos, pkt.questions = decodeQuestions(data, cnt.q, pos)
|
||
|
pos, pkt.answers = decodeRR(data, cnt.a, pos)
|
||
|
pos, pkt.auth = decodeRR(data, cnt.auth, pos)
|
||
|
pos, pkt.add = decodeRR(data, cnt.add, pos)
|
||
|
end
|
||
|
return pkt
|
||
|
end
|
||
|
|
||
|
|
||
|
---
|
||
|
-- Creates a new table representing a DNS packet.
|
||
|
-- @return Table representing a DNS packet.
|
||
|
function newPacket()
|
||
|
local pkt = {}
|
||
|
pkt.id = 1
|
||
|
pkt.flags = {}
|
||
|
pkt.flags.RD = true
|
||
|
pkt.questions = {}
|
||
|
pkt.zones = {}
|
||
|
pkt.updates = {}
|
||
|
pkt.answers = {}
|
||
|
pkt.auth = {}
|
||
|
pkt.additional = {}
|
||
|
return pkt
|
||
|
end
|
||
|
|
||
|
|
||
|
---
|
||
|
-- Adds a question to a DNS packet table.
|
||
|
-- @param pkt Table representing DNS packet.
|
||
|
-- @param dname Domain name to be asked.
|
||
|
-- @param dtype RR to be asked.
|
||
|
function addQuestion(pkt, dname, dtype, class)
|
||
|
if type(pkt) ~= "table" then return nil end
|
||
|
if type(pkt.questions) ~= "table" then return nil end
|
||
|
local class = class or CLASS.IN
|
||
|
local q = {}
|
||
|
q.dname = dname
|
||
|
q.dtype = dtype
|
||
|
q.class = class
|
||
|
table.insert(pkt.questions, q)
|
||
|
return pkt
|
||
|
end
|
||
|
|
||
|
|
||
|
get_default_timeout = function()
|
||
|
local timeout = {[0] = 10000, 7000, 5000, 4000, 4000, 4000}
|
||
|
return timeout[nmap.timing_level()] or 4000
|
||
|
end
|
||
|
|
||
|
---
|
||
|
-- Adds a zone to a DNS packet table
|
||
|
-- @param pkt Table representing DNS packet.
|
||
|
-- @param dname Domain name to be asked.
|
||
|
function addZone(pkt, dname)
|
||
|
if ( type(pkt) ~= "table" ) or (type(pkt.updates) ~= "table") then return nil end
|
||
|
table.insert(pkt.zones, { dname=dname, dtype=types.SOA, class=CLASS.IN })
|
||
|
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) == "number" then return flags end
|
||
|
assert(type(flags) == "table")
|
||
|
local bits = 0
|
||
|
if flags.DO then bits = bits|0x8000 end
|
||
|
return bits
|
||
|
end
|
||
|
|
||
|
---
|
||
|
-- Adds an client-subnet payload to the OPT packet
|
||
|
--
|
||
|
-- implementing https://tools.ietf.org/html/rfc7871
|
||
|
-- @param pkt Table representing DNS packet.
|
||
|
-- @param Z Table of Z flags. Only DO is supported.
|
||
|
-- @param client_subnet table containing the following fields
|
||
|
-- <code>family</code> - IPv4: "inet" or 1 (default), IPv6: "inet6" or 2
|
||
|
-- <code>mask</code> - byte containing the length of the subnet mask
|
||
|
-- <code>address</code> - string containing the IP address
|
||
|
function addClientSubnet(pkt,Z,subnet)
|
||
|
local family = subnet.family or 1
|
||
|
if type(family) == "string" then
|
||
|
family = ({inet=1,inet6=2})[family]
|
||
|
end
|
||
|
assert(family == 1 or family == 2, "Unsupported subnet family")
|
||
|
local code = 8 -- https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-11
|
||
|
local mask = subnet.mask
|
||
|
local scope_mask = 0 -- In requests, it MUST be set to 0
|
||
|
-- Per RFC 7871, section 6:
|
||
|
-- Address must have all insignificant bits zeroed out and insignificant bytes
|
||
|
-- must be trimmed off. (/24 IPv4 address is submitted as 3 octets, not 4.)
|
||
|
local addr = ipOps.get_first_ip(subnet.address, mask)
|
||
|
addr = ipOps.ip_to_str(addr):sub(1, (mask + 7) // 8)
|
||
|
local data = string.pack(">I2BB", family, mask, scope_mask) .. addr
|
||
|
local opt = string.pack(">I2s2", code, data)
|
||
|
addOPT(pkt,Z,opt)
|
||
|
end
|
||
|
|
||
|
---
|
||
|
-- Adds an NSID payload to the OPT packet
|
||
|
-- @param pkt Table representing DNS packet.
|
||
|
-- @param Z Table of Z flags. Only DO is supported.
|
||
|
function addNSID (pkt,Z)
|
||
|
local opt = string.pack(">I2I2", 3, 0) -- nsid data
|
||
|
addOPT(pkt,Z,opt)
|
||
|
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, opt)
|
||
|
local rdata = opt or ""
|
||
|
if type(pkt) ~= "table" then return nil end
|
||
|
if type(pkt.additional) ~= "table" then return nil end
|
||
|
local Z_int = encodeOPT_Z(Z)
|
||
|
local opt = {
|
||
|
type = types.OPT,
|
||
|
class = 4096, -- Actually the sender UDP payload size.
|
||
|
ttl = 0 * (0x01000000) + 0 * (0x00010000) + Z_int,
|
||
|
rdata = rdata,
|
||
|
}
|
||
|
table.insert(pkt.additional, opt)
|
||
|
return pkt
|
||
|
end
|
||
|
|
||
|
---
|
||
|
-- Adds a update to a DNS packet table
|
||
|
-- @param pkt Table representing DNS packet.
|
||
|
-- @param dname Domain name to be asked.
|
||
|
-- @param dtype to be updated
|
||
|
-- @param ttl the time-to-live of the record
|
||
|
-- @param data type specific data
|
||
|
function addUpdate(pkt, dname, dtype, ttl, data, class)
|
||
|
if ( type(pkt) ~= "table" ) or (type(pkt.updates) ~= "table") then return nil end
|
||
|
table.insert(pkt.updates, { dname=dname, dtype=dtype, class=class, ttl=ttl, data=(data or "") } )
|
||
|
return pkt
|
||
|
end
|
||
|
|
||
|
|
||
|
--- Adds a record to the Zone
|
||
|
-- @param dname containing the hostname to add
|
||
|
-- @param options A table containing any of the following fields:
|
||
|
-- * <code>dtype</code>: Desired DNS record type (default: <code>"A"</code>).
|
||
|
-- * <code>host</code>: DNS server to be queried (default: DNS servers known to Nmap).
|
||
|
-- * <code>timeout</code>: The time to wait for a response
|
||
|
-- * <code>sendCount</code>: The number of send attempts to perform
|
||
|
-- * <code>zone</code>: If not supplied deduced from hostname
|
||
|
-- * <code>data</code>: Table or string containing update data (depending on record type):
|
||
|
-- A - String containing the IP address
|
||
|
-- CNAME - String containing the FQDN
|
||
|
-- MX - Table containing <code>pref</code>, <code>mx</code>
|
||
|
-- SRV - Table containing <code>prio</code>, <code>weight</code>, <code>port</code>, <code>target</code>
|
||
|
--
|
||
|
-- @return status true on success false on failure
|
||
|
-- @return msg containing the error message
|
||
|
--
|
||
|
-- Examples
|
||
|
--
|
||
|
-- Adding different types of records to a server
|
||
|
-- * update( "www.cqure.net", { host=host, port=port, dtype="A", data="10.10.10.10" } )
|
||
|
-- * update( "alias.cqure.net", { host=host, port=port, dtype="CNAME", data="www.cqure.net" } )
|
||
|
-- * update( "cqure.net", { host=host, port=port, dtype="MX", data={ pref=10, mx="mail.cqure.net"} })
|
||
|
-- * update( "_ldap._tcp.cqure.net", { host=host, port=port, dtype="SRV", data={ prio=0, weight=100, port=389, target="ldap.cqure.net" } } )
|
||
|
--
|
||
|
-- Removing the above records by setting an empty data and a ttl of zero
|
||
|
-- * update( "www.cqure.net", { host=host, port=port, dtype="A", data="", ttl=0 } )
|
||
|
-- * update( "alias.cqure.net", { host=host, port=port, dtype="CNAME", data="", ttl=0 } )
|
||
|
-- * update( "cqure.net", { host=host, port=port, dtype="MX", data="", ttl=0 } )
|
||
|
-- * update( "_ldap._tcp.cqure.net", { host=host, port=port, dtype="SRV", data="", ttl=0 } )
|
||
|
--
|
||
|
function update(dname, options)
|
||
|
local options = options or {}
|
||
|
local pkt = newPacket()
|
||
|
local flags = pkt.flags
|
||
|
local host, port = options.host, options.port
|
||
|
local timeout = ( type(options.timeout) == "number" ) and options.timeout or get_default_timeout()
|
||
|
local sendcount = options.sendCount or 2
|
||
|
local dtype = ( type(options.dtype) == "string" ) and types[options.dtype] or types.A
|
||
|
local updata = options.data
|
||
|
local ttl = options.ttl or 86400
|
||
|
local zone = options.zone or dname:match("^.-%.(.+)$")
|
||
|
local class = CLASS.IN
|
||
|
|
||
|
assert(host, "dns.update needs a valid host in options")
|
||
|
assert(port, "dns.update needs a valid port in options")
|
||
|
|
||
|
if ( options.zone ) then dname = dname .. "." .. options.zone end
|
||
|
|
||
|
if ( not(zone) and not( dname:match("^.-%..+") ) ) then
|
||
|
return false, "hostname needs to be supplied as FQDN"
|
||
|
end
|
||
|
|
||
|
flags.RD = false
|
||
|
flags.OC1, flags.OC2, flags.OC3, flags.OC4 = false, true, false, true
|
||
|
|
||
|
-- If ttl is zero and updata is nil or a string of zero length, assume delete record
|
||
|
if ttl == 0 and (not updata or (type(updata) == "string" and #updata == 0)) then
|
||
|
class = CLASS.ANY
|
||
|
updata = ""
|
||
|
if ( types.MX == dtype and not(options.zone) ) then zone=dname end
|
||
|
if ( types.SRV == dtype and not(options.zone) ) then
|
||
|
zone=dname:match("^_.-%._.-%.(.+)$")
|
||
|
end
|
||
|
-- if not, let's try to update the zone
|
||
|
else
|
||
|
if ( dtype == types.A or dtype == types.AAAA ) then
|
||
|
updata = updata and ipOps.ip_to_str(updata) or ""
|
||
|
elseif( dtype == types.CNAME ) then
|
||
|
updata = encodeFQDN(updata)
|
||
|
elseif( dtype == types.MX ) then
|
||
|
assert( not( type(updata) ~= "table" ), "dns.update expected options.data to be a table")
|
||
|
if ( not(options.zone) ) then zone = dname end
|
||
|
local data = string.pack(">I2", updata.pref)
|
||
|
data = data .. encodeFQDN(updata.mx)
|
||
|
updata = data
|
||
|
elseif ( dtype == types.SRV ) then
|
||
|
assert( not( type(updata) ~= "table" ), "dns.update expected options.data to be a table")
|
||
|
local data = string.pack(">I2I2I2", updata.prio, updata.weight, updata.port )
|
||
|
data = data .. encodeFQDN(updata.target)
|
||
|
updata = data
|
||
|
zone = options.zone or dname:match("^_.-%._.-%.(.+)$")
|
||
|
else
|
||
|
return false, "Unsupported record type"
|
||
|
end
|
||
|
end
|
||
|
|
||
|
pkt = addZone(pkt, zone)
|
||
|
pkt = addUpdate(pkt, dname, dtype, ttl, updata, class)
|
||
|
|
||
|
local data = encode(pkt)
|
||
|
local status, response = sendPackets(data, host, port, timeout, sendcount, false)
|
||
|
|
||
|
if ( status ) then
|
||
|
local decoded = decode(response[1].data)
|
||
|
local flags = encodeFlags(decoded.flags)
|
||
|
if (flags & 0xF) == 0 then
|
||
|
return true
|
||
|
end
|
||
|
end
|
||
|
return false
|
||
|
end
|
||
|
|
||
|
if not unittest.testing() then
|
||
|
return _ENV
|
||
|
end
|
||
|
|
||
|
-- Self test
|
||
|
test_suite = unittest.TestSuite:new()
|
||
|
|
||
|
test_suite:add_test(unittest.equal(encodeFQDN("test.me.com"), "\x04test\x02me\x03com\0"), "encodeFQDN")
|
||
|
return _ENV;
|