--- -- Simple DNS library supporting packet creation, encoding, decoding, -- and querying. -- -- The most common interface to this module are the query and -- reverse functions. query performs a DNS query, -- and reverse prepares an ip address to have a reverse query -- performed. -- -- query 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 query. -- -- Example usage: -- -- -- After this call, status is true and result is "72.14.204.104" -- local status, result = dns.query('www.google.ca') -- -- -- After this call, status is false and result is "No such name" -- local status, result = dns.query('www.google.abc') -- -- -- After this call, status is true and result is the table {"72.14.204.103", "72.14.204.104", "72.14.204.147", "72.14.204.99"} -- local status, result = dns.query('www.google.ca', {retAll=true}) -- -- -- After this call, status is true and result is the "2001:19f0:0:0:0:dead:beef:cafe" -- local status, result = dns.query('irc.ipv6.efnet.org', {dtype='AAAA'}) -- -- -- -- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html local bin = require "bin" local bit = require "bit" local coroutine = require "coroutine" local ipOps = require "ipOps" local nmap = require "nmap" local stdnse = require "stdnse" local string = require "string" 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: -- * dtype: Desired DNS record type (default: "A"). -- * host: DNS server to be queried (default: DNS servers known to Nmap). -- * port: Port of DNS server to connect to (default: 53). -- * tries: How often should query try to contact another server (for non-recursive queries). -- * retAll: Return all answers, not just the first. -- * retPkt: Return the packet instead of using the answer-fetching mechanism. -- * norecurse: If true, do not set the recursion (RD) flag. -- * noauth: If true, do not try to find authoritative server -- * multiple: If true, expects multiple hosts to respond to multicast request -- * flags: numeric value to set flags in the DNS query to a specific value -- * id: numeric value to use for the DNS transaction id -- * nsid: If true, queries the server for the nameserver identifier (RFC 5001) -- * subnet: table, if set perform a edns-client-subnet lookup. The table should contain the fields: -- family - string can be either inet or inet6 -- address - string containing the originating subnet IP address -- mask - number containing the number of subnet bits -- @return true 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 false 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 local family = { ["inet"] = 1, ["inet6"] = 2 } assert( family[options.subnet.family], "Unsupported subnet family") options.subnet.family = family[options.subnet.family] 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 = stdnse.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 "-", 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 -- @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 dtype 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 dtype 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 dtype 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 dtype 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] = bin.pack("p", 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] = bin.pack(">SS", 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] = bin.pack(">SSISA", v.dtype, v.class, v.ttl, #v.data, 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 -- 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+1] = bin.pack(">xSSISA", v.type, v.class, v.ttl, v.rdlen, 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 -- newPacket. -- @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 = bin.pack(">SSS4", pkt.id, pkt.flags.raw, qorzlen, aorplen, aorulen, #pkt.additional) .. data .. additional else encStr = bin.pack(">SSS4", 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 nil 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 pos, partlen = bin.unpack(">C", data, pos) while (partlen ~= 0) do if (partlen < 64) then pos, part = bin.unpack("A" .. partlen, data, pos) if part == nil then return pos end table.insert(parts, part) pos, partlen = bin.unpack(">C", data, pos) else pos, partlen = bin.unpack(">S", 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) pos, currQ.dtype, currQ.class = bin.unpack(">SS", 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 entry.ip. -- @param entry RR in packet. decoder[types.A] = function(entry) local ip = {} local _ _, ip[1], ip[2], ip[3], ip[4] = bin.unpack(">C4", entry.data) entry.ip = table.concat(ip, ".") end -- Decodes IP of AAAA record, puts it in entry.ipv6. -- @param entry RR in packet. decoder[types.AAAA] = function(entry) local ip = {} local pos = 1 local num for i = 1, 8 do pos, num = bin.unpack(">S", entry.data, pos) table.insert(ip, string.format('%x', num)) end entry.ipv6 = table.concat(ip, ":") end -- Decodes SSH fingerprint record, puts it in entry.SSHFP as -- defined in RFC 4255. -- -- entry.SSHFP has the fields algorithm, -- fptype, and fingerprint. -- @param entry RR in packet. decoder[types.SSHFP] = function(entry) local _ entry.SSHFP = {} _, entry.SSHFP.algorithm, entry.SSHFP.fptype, entry.SSHFP.fingerprint = bin.unpack(">C2H" .. (#entry.data - 2), entry.data) end -- Decodes SOA record, puts it in entry.SOA. -- -- entry.SOA has the fields mname, rname, -- serial, refresh, retry, -- expire, and minimum. -- @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) np, entry.SOA.serial, entry.SOA.refresh, entry.SOA.retry, entry.SOA.expire, entry.SOA.minimum = bin.unpack(">I5", 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 bit.band(n, mask) ~= 0 then coroutine.yield((i - 1) * 8 + j) end j = j + 1 mask = bit.rshift(mask, 1) end end end) end -- Decodes NSEC records, puts result in entry.NSEC. See RFC 4034, -- section 4. -- -- entry.NSEC has the fields dname, -- next_dname, 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 np, entry.NSEC.next_dname = decStr(data, np) while np < pos do local block_num, type_bitmap np, block_num, type_bitmap = bin.unpack(">Cp", 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 entry.NSEC3. See RFC 5155. -- -- entry.NSEC3 has the fields dname, -- hash.alg, and hash.base32. -- hash.bin, and hash.hex. -- salt.bin, and salt.hex. -- iterations, and types. -- @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. -- Puts result in entry.domain. -- @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 entry.domain. -- @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 entry.domain. -- @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 entry.domain. -- @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 entry.domain. -- @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 len > 0 do np, txt_len = bin.unpack("C", data, np) np, txt = bin.unpack("A" .. txt_len, data, np ) len = len - txt_len - 1 table.insert( entry.TXT.text, txt ) end end --- -- Decodes OPT record, puts it in entry.OPT. -- -- entry.OPT has the fields mname, rname, -- serial, refresh, retry, -- expire, and minimum. -- @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 } np, opt.rcode, opt.version, opt.zflags, opt.rdlen = bin.unpack(">CCSS", data, np) np, opt.data = bin.unpack("A" .. opt.rdlen, data, np) entry.OPT = opt end -- Decodes MX record, puts it in entry.MX. -- -- entry.MX has the fields pref and -- server. -- @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 = bin.unpack(">S", entry.data) _, entry.MX.server = decStr(data, np) end -- Decodes SRV record, puts it in entry.SRV. -- -- entry.SRV has the fields prio, -- weight, port and -- target. -- @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 = {} np, entry.SRV.prio, entry.SRV.weight, entry.SRV.port = bin.unpack(">S>S>S", 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) pos, currRR.dtype, currRR.class, currRR.ttl = bin.unpack(">SSI", data, pos) local reslen pos, reslen = bin.unpack(">S", data, pos) pos, currRR.data = bin.unpack("A" .. reslen, 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 = {} pos, pkt.id, encFlags, cnt.q, cnt.a, cnt.auth, cnt.add = bin.unpack(">SSS4", 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 http://tools.ietf.org/html/draft-vandergaast-edns-client-subnet-00 -- @param pkt Table representing DNS packet. -- @param Z Table of Z flags. Only DO is supported. -- @param client_subnet table containing the following fields -- family - 1 IPv4, 2 - IPv6 -- mask - byte containing the length of the subnet mask -- address - string containing the IP address function addClientSubnet(pkt,Z,subnet) local udp_payload_size = 4096 local code = 20730 -- temporary option-code http://comments.gmane.org/gmane.ietf.dnsext/19776 local scope_mask = 0 -- In requests, it MUST be set to 0 see draft local data = bin.pack(">SCCA",subnet.family or 1,subnet.mask,scope_mask,ipOps.ip_to_str(subnet.address)) local opt = bin.pack(">SS",code, #data) .. 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 udp_payload_size = 4096 local opt = bin.pack(">SS",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, rdlen = #rdata, 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: -- * dtype: Desired DNS record type (default: "A"). -- * host: DNS server to be queried (default: DNS servers known to Nmap). -- * timeout: The time to wait for a response -- * sendCount: The number of send attempts to perform -- * zone: If not supplied deduced from hostname -- * data: Table or string containing update data (depending on record type): -- A - String containing the IP address -- CNAME - String containing the FQDN -- MX - Table containing pref, mx -- SRV - Table containing prio, weight, port, target -- -- @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 string and zero length or nil, assume delete record if ( ttl == 0 and ( ( type(updata) == "string" and #updata == 0 ) or not(updata) ) ) 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 ) then updata = updata and bin.pack(">I", ipOps.todword(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 = bin.pack(">S", 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 = bin.pack(">SSS", 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;