From 0d47952611198ef6b1163f366dc03922d20b1475 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 17 Apr 2024 09:42:04 +0200 Subject: Adding upstream version 7.94+git20230807.3be01efb1+dfsg. Signed-off-by: Daniel Baumann --- nselib/dns.lua | 1837 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1837 insertions(+) create mode 100644 nselib/dns.lua (limited to 'nselib/dns.lua') diff --git a/nselib/dns.lua b/nselib/dns.lua new file mode 100644 index 0000000..7fb8a76 --- /dev/null +++ b/nselib/dns.lua @@ -0,0 +1,1837 @@ +--- +-- 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 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: +-- * 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 - IPv4: "inet" or 1 (default), IPv6: "inet6" or 2 +-- 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 + 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 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] = 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 +-- type, class, ttl, +-- 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] = 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 +-- 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 = 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 nil on error. +function decStr(data, pos) + local function dec(data, pos, limit) + assert(pos > 0) + 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 entry.ip. +-- @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 entry.ipv6. +-- @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 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 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 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) + 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 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 + 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 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 = {}, {} + + 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 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 + local np = pos - len + local txt + + if len > 0 then + entry.TXT = {} + entry.TXT.text = {} + end + + while np < pos do + txt, np = string.unpack("s1", data, np) + 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 } + opt.rcode, opt.version, opt.zflags, opt.data, np = string.unpack(">BBI2s2", 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 = string.unpack(">I2", 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 = {} + 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 +-- family - IPv4: "inet" or 1 (default), IPv6: "inet6" or 2 +-- mask - byte containing the length of the subnet mask +-- address - 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: +-- * 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 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") +local tests = { { + pkt = string.char(0x92, 0xdc, -- Trsnsaction ID + 0x81, 0x80, -- Flags + 0x00, 0x01, -- Questions count + 0x00, 0x01, -- Answers RRs count + 0x00, 0x00, -- Authorities RRs count + 0x00, 0x00, -- Additionals RRs count + 0x06, -- Label length <-- [12] + 0x73, 0x63, 0x61, 0x6e, 0x6d, 0x65, -- "scanme" + 0x04, -- Label length + 0x6e, 0x6d, 0x61, 0x70, -- "nmap" + 0x03, -- Label length + 0x6f, 0x72, 0x67, -- "org" + 0x00, -- Name terminator + 0x00, 0x01, -- A + 0x00, 0x01, -- CLASS_IN + 0xc0, 0x0c, -- Compressed name pointer to offset 12 + 0x00, 0x01, -- A + 0x00, 0x01, -- CLASS_IN + 0x00, 0x00, 0x0e, 0x0f, -- TTL 3599 + 0x00, 0x04, -- Record Length + 0x2d, 0x21, 0x20, 0x9c ), -- 45.33.32.156 + qname = "scanme.nmap.org", + qtype = "A", + ans = {"ip", "45.33.32.156"} + }, + { + pkt = string.char( + 0x08, 0xf2, -- ID + 0x81, 0x80, -- Flags + 0x00, 0x01, -- Questions count + 0x00, 0x01, -- Answers RRs count + 0x00, 0x00, -- Authorities RRs count + 0x00, 0x00, -- Additionals RRs count + 0x03, -- Label length + 0x31, 0x35, 0x36, -- "156" + 0x02, -- Label length + 0x33, 0x32, -- "32" + 0x02, -- Label length + 0x33, 0x33, -- "33" + 0x02, -- Label length + 0x34, 0x35, -- "45" + 0x07, -- Label length + 0x69, 0x6e, 0x2d, 0x61, 0x64, 0x64, 0x72, -- "in-addr" + 0x04, -- Label length + 0x61, 0x72, 0x70, 0x61, -- "arpa" + 0x00, -- Name terminator + 0x00, 0x0c, -- PTR + 0x00, 0x01, -- CLASS_IN + 0xc0, 0x0c, -- Compressed name pointer to offset 12 + 0x00, 0x0c, -- PTR + 0x00, 0x01, -- CLASS_IN + 0x00, 0x01, 0x51, 0x78, -- TTL 86392 + 0x00, 0x11, -- Record Length + 0x06, -- Label length + 0x73, 0x63, 0x61, 0x6e, 0x6d, 0x65, -- "scanme" + 0x04, -- Label length + 0x6e, 0x6d, 0x61, 0x70, -- "nmap" + 0x03, -- Label length + 0x6f, 0x72, 0x67, -- "org" + 0x00), -- Name terminator + qname = "156.32.33.45.in-addr.arpa", + qtype = "PTR", + ans = {"domain", "scanme.nmap.org"} + }, +} +for _, t in ipairs(tests) do + local decoded = decode(t.pkt) + local q = decoded.questions[1] + local a = decoded.answers[1] + test_suite:add_test(unittest.equal(q.dname, t.qname), "decode question name") + test_suite:add_test(unittest.equal(q.dtype, types[t.qtype]), "decode question type") + test_suite:add_test(unittest.equal(a[t.ans[1]], t.ans[2]), "decode answer") +end +local axfr = stdnse.fromhex( +"dead840000010032000000000c7a\z +6f6e657472616e73666572026d650000\z +fc0001c00c0006000100001c20002f06\z +6e737a746d310464696769056e696e6a\z +610005726f62696ec034785908810002\z +a300000003840012750000000e10c00c\z +000d00010000012c00190d436173696f\z +2066782d373030470a57696e646f7773\z +205850c00c001000010000012d004544\z +676f6f676c652d736974652d76657269\z +6669636174696f6e3d74795032384a37\z +4a41554841396677327348584d676343\z +4330493658426d6d6f56693034566c4d\z +65777841c00c000f000100001c200016\z +0000054153504d58014c06474f4f474c\z +4503434f4d00c00c000f000100001c20\z +0009000a04414c5431c0e0c00c000f00\z +0100001c200009000a04414c5432c0e0\z +c00c000f000100001c20001600140641\z +53504d58320a474f4f474c454d41494c\z +c0efc00c000f000100001c20000b0014\z +064153504d5833c133c00c000f000100\z +001c20000b0014064153504d5834c133\z +c00c000f000100001c20000b00140641\z +53504d5835c133c00c0001000100001c\z +20000405c4690ec00c0002000100001c\z +200002c02dc00c0002000100001c2000\z +09066e737a746d32c0340f5f61636d65\z +2d6368616c6c656e6765c00c00100001\z +0000012d002c2b364f6130356862554a\z +39785373765979377041705176774355\z +535347677876726264697a6a65504573\z +5a49045f736970045f746370c00c0021\z +0001000036b0001b0000000013c40377\z +77770c7a6f6e657472616e7366657202\z +6d650002313403313035033139360135\z +07494e2d414444520441525041c22000\z +0c000100001c200002c21c0c61736664\z +6261757468646e73c220001200010000\z +1edc001c0001086173666462626f780c\z +7a6f6e657472616e73666572026d6500\z +c2740001000100001c2000047f000001\z +0b6173666462766f6c756d65c27d0012\z +000100001e78001c0001086173666462\z +626f780c7a6f6e657472616e73666572\z +026d65000f63616e62657272612d6f66\z +66696365c2c10001000100001c200004\z +ca0e51e607636d6465786563c2c10010\z +00010000012c0005043b206c7307636f\z +6e74616374c2c10010000100278d0000\z +646352656d656d62657220746f206361\z +6c6c206f7220656d61696c2050697070\z +61206f6e202b34342031323320343536\z +37383930206f72207069707061407a6f\z +6e657472616e736665722e6d65207768\z +656e206d616b696e6720444e53206368\z +616e6765730964632d6f6666696365c2\z +c10001000100001c2000048fe4b58408\z +6465616462656566c2c1001c00010000\z +1c210010deadbeaf0000000000000000\z +00000000026472c2c1001d0001000001\z +2c0010001216138b728cee7fa5c44a00\z +98968003445a43c2c10010000100001c\z +200008074162436445664705656d6169\z +6cc2c100230001000008ae0038000100\z +010150094532552b656d61696c000565\z +6d61696c0c7a6f6e657472616e736665\z +72026d650c7a6f6e657472616e736665\z +72026d6500c3f90001000100001c2000\z +044a7dce1a0548656c6c6fc432001000\z +0100001c20001d1c486920746f204a6f\z +736820616e6420616c6c206869732063\z +6c61737304686f6d65c4320001000100\z +001c2000047f00000104496e666fc432\z +0010000100001c20008b8a5a6f6e6554\z +72616e736665722e6d65207365727669\z +63652070726f76696465642062792052\z +6f62696e20576f6f64202d20726f6269\z +6e40646967692e6e696e6a612e205365\z +6520687474703a2f2f646967692e6e69\z +6e6a612f70726f6a656374732f7a6f6e\z +657472616e736665726d652e70687020\z +666f72206d6f726520696e666f726d61\z +74696f6e2e08696e7465726e616cc432\z +000200010000012c000906696e746e73\z +31c432c533000200010000012c000906\z +696e746e7332c432c548000100010000\z +012c000451046c29c55d000100010000\z +012c0004a7582a5e066f6666696365c4\z +320001000100001c200004041727fe0a\z +697076366163746e6f77036f7267c432\z +001c000100001c2000102001067c02e8\z +001100000000c1001332036f7761c432\z +0001000100001c200004cf2ec5200972\z +6f62696e776f6f64c432001000010000\z +012e000b0a526f62696e20576f6f6402\z +7270c432001100010000014100320572\z +6f62696e0c7a6f6e657472616e736665\z +72026d650009726f62696e776f6f640c\z +7a6f6e657472616e73666572026d6500\z +03736970c62d0023000100000d05003b\z +000200030150074532552b7369702b21\z +5e2e2a24217369703a637573746f6d65\z +722d73657276696365407a6f6e657472\z +616e736665722e6d6521000473716c69\z +c62d001000010000012c000c0b27206f\z +7220313d31202d2d067373686f636bc6\z +2d0010000100001c20001c1b2829207b\z +203a5d7d3b206563686f205368656c6c\z +53686f636b65640773746167696e67c6\z +2d0005000100001c20001a0377777710\z +7379646e65796f70657261686f757365\z +03636f6d000f616c6c746370706f7274\z +736f70656e086669726577616c6c0474\z +657374c62d000100010000012d00047f\z +0000010774657374696e67c62d000500\z +010000012d0002c21c0376706ec62d00\z +01000100000fa00004ae243b9ac21c00\z +01000100001c20000405c4690e037873\z +73c62d001000010000012c00201f273e\z +3c7363726970743e616c657274282742\z +6f6f27293c2f7363726970743ec62d00\z +06000100001c200018c02dc040785908\z +810002a300000003840012750000000e\z +10") +local axfr_decoded = decode(axfr) +local answers = { + {dname="zonetransfer.me", dtype=types["SOA"], mname="nsztm1.digi.ninja", rname="robin.digi.ninja"}, + {dname="zonetransfer.me", dtype=types["HINFO"]}, -- "Casio fx-700G" "Windows XP" + {dname="zonetransfer.me", dtype=types["TXT"], text="google-site-verification=tyP28J7JAUHA9fw2sHXMgcCC0I6XBmmoVi04VlMewxA"}, + {dname="zonetransfer.me", dtype=types["MX"], pref=0, server="ASPMX.L.GOOGLE.COM"}, + {dname="zonetransfer.me", dtype=types["MX"], pref=10, server="ALT1.ASPMX.L.GOOGLE.COM"}, + {dname="zonetransfer.me", dtype=types["MX"], pref=10, server="ALT2.ASPMX.L.GOOGLE.COM"}, + {dname="zonetransfer.me", dtype=types["MX"], pref=20, server="ASPMX2.GOOGLEMAIL.COM"}, + {dname="zonetransfer.me", dtype=types["MX"], pref=20, server="ASPMX3.GOOGLEMAIL.COM"}, + {dname="zonetransfer.me", dtype=types["MX"], pref=20, server="ASPMX4.GOOGLEMAIL.COM"}, + {dname="zonetransfer.me", dtype=types["MX"], pref=20, server="ASPMX5.GOOGLEMAIL.COM"}, + {dname="zonetransfer.me", dtype=types["A"], ip="5.196.105.14"}, + {dname="zonetransfer.me", dtype=types["NS"], domain="nsztm1.digi.ninja"}, + {dname="zonetransfer.me", dtype=types["NS"], domain="nsztm2.digi.ninja"}, + {dname="_acme-challenge.zonetransfer.me", dtype=types["TXT"], text="6Oa05hbUJ9xSsvYy7pApQvwCUSSGgxvrbdizjePEsZI"}, + {dname="_sip._tcp.zonetransfer.me", dtype=types["SRV"], prio=0, weight=0, port=5060, target="www.zonetransfer.me"}, + {dname="14.105.196.5.IN-ADDR.ARPA.zonetransfer.me", dtype=types["PTR"], domain="www.zonetransfer.me"}, + {dname="asfdbauthdns.zonetransfer.me", dtype=types["AFSDB"]}, -- 1 asfdbbox.zonetransfer.me. + {dname="asfdbbox.zonetransfer.me", dtype=types["A"], ip="127.0.0.1"}, + {dname="asfdbvolume.zonetransfer.me", dtype=types["AFSDB"]}, -- 1 asfdbbox.zonetransfer.me. + {dname="canberra-office.zonetransfer.me", dtype=types["A"], ip="202.14.81.230"}, + {dname="cmdexec.zonetransfer.me", dtype=types["TXT"], text="; ls"}, + {dname="contact.zonetransfer.me", dtype=types["TXT"], text="Remember to call or email Pippa on +44 123 4567890 or pippa@zonetransfer.me when making DNS changes"}, + {dname="dc-office.zonetransfer.me", dtype=types["A"], ip="143.228.181.132"}, + {dname="deadbeef.zonetransfer.me", dtype=types["AAAA"], ipv6="dead:beaf::"}, + {dname="dr.zonetransfer.me", dtype=types["LOC"]}, -- 53.349044 N 1.642646 W 0m 1.0m 10000.0m 10.0m + {dname="DZC.zonetransfer.me", dtype=types["TXT"], text="AbCdEfG"}, + {dname="email.zonetransfer.me", dtype=types["NAPTR"]}, -- 1 1 "P" "E2U+email" "" email.zonetransfer.me.zonetransfer.me. + {dname="email.zonetransfer.me", dtype=types["A"], ip="74.125.206.26"}, + {dname="Hello.zonetransfer.me", dtype=types["TXT"], text="Hi to Josh and all his class"}, + {dname="home.zonetransfer.me", dtype=types["A"], ip="127.0.0.1"}, + {dname="Info.zonetransfer.me", dtype=types["TXT"], text="ZoneTransfer.me service provided by Robin Wood - robin@digi.ninja. See http://digi.ninja/projects/zonetransferme.php for more information."}, + {dname="internal.zonetransfer.me", dtype=types["NS"], domain="intns1.zonetransfer.me"}, + {dname="internal.zonetransfer.me", dtype=types["NS"], domain="intns2.zonetransfer.me"}, + {dname="intns1.zonetransfer.me", dtype=types["A"], ip="81.4.108.41"}, + {dname="intns2.zonetransfer.me", dtype=types["A"], ip="167.88.42.94"}, + {dname="office.zonetransfer.me", dtype=types["A"], ip="4.23.39.254"}, + {dname="ipv6actnow.org.zonetransfer.me", dtype=types["AAAA"], ipv6="2001:67c:2e8:11::c100:1332"}, + {dname="owa.zonetransfer.me", dtype=types["A"], ip="207.46.197.32"}, + {dname="robinwood.zonetransfer.me", dtype=types["TXT"], text="Robin Wood"}, + {dname="rp.zonetransfer.me", dtype=types["RP"]}, -- robin.zonetransfer.me. robinwood.zonetransfer.me. + {dname="sip.zonetransfer.me", dtype=types["NAPTR"]}, -- 2 3 "P" "E2U+sip" "!^.*$!sip:customer-service@zonetransfer.me!" . + {dname="sqli.zonetransfer.me", dtype=types["TXT"], text="' or 1=1 --"}, + {dname="sshock.zonetransfer.me", dtype=types["TXT"],text="() { :]}; echo ShellShocked"}, + {dname="staging.zonetransfer.me", dtype=types["CNAME"], domain="www.sydneyoperahouse.com"}, + {dname="alltcpportsopen.firewall.test.zonetransfer.me", dtype=types["A"], ip="127.0.0.1"}, + {dname="testing.zonetransfer.me", dtype=types["CNAME"], domain="www.zonetransfer.me"}, + {dname="vpn.zonetransfer.me", dtype=types["A"], ip="174.36.59.154"}, + {dname="www.zonetransfer.me", dtype=types["A"], ip="5.196.105.14"}, + {dname="xss.zonetransfer.me", dtype=types["TXT"], text="'>"}, + {dname="zonetransfer.me", dtype=types["SOA"], mname="nsztm1.digi.ninja", rname="robin.digi.ninja"}, +} +for i, a in ipairs(axfr_decoded.answers) do + ta = answers[i] + if ta.dtype == types.TXT then + test_suite:add_test(unittest.equal(a.dname, ta.dname), i .. ".dname") + test_suite:add_test(unittest.equal(a.dtype, ta.dtype), i .. ".dtype") + test_suite:add_test(unittest.equal(a.TXT.text[1], ta.text), i .. ".text") + elseif ta.dtype == types.SOA then + test_suite:add_test(unittest.equal(a.dname, ta.dname), i .. ".dname") + test_suite:add_test(unittest.equal(a.dtype, ta.dtype), i .. ".dtype") + test_suite:add_test(unittest.equal(a.SOA.mname, ta.mname), i .. ".mname") + test_suite:add_test(unittest.equal(a.SOA.rname, ta.rname), i .. ".rname") + elseif ta.dtype == types.MX then + test_suite:add_test(unittest.equal(a.dname, ta.dname), i .. ".dname") + test_suite:add_test(unittest.equal(a.dtype, ta.dtype), i .. ".dtype") + test_suite:add_test(unittest.equal(a.MX.pref, ta.pref), i .. ".pref") + test_suite:add_test(unittest.equal(a.MX.server, ta.server), i .. ".server") + elseif ta.dtype == types.SRV then + test_suite:add_test(unittest.equal(a.dname, ta.dname), i .. ".dname") + test_suite:add_test(unittest.equal(a.dtype, ta.dtype), i .. ".dtype") + test_suite:add_test(unittest.equal(a.SRV.prio, ta.prio), i .. ".prio") + test_suite:add_test(unittest.equal(a.SRV.weight, ta.weight), i .. ".weight") + test_suite:add_test(unittest.equal(a.SRV.port, ta.port), i .. ".port") + else + for k, v in pairs(ta) do + test_suite:add_test(unittest.equal(a[k], v), ("%d.%s"):format(i, k)) + end + end +end +return _ENV; -- cgit v1.2.3