diff --git a/scripts/dns-zone-transfer.nse b/scripts/dns-zone-transfer.nse index 7119ac9b4..dfab1f91d 100644 --- a/scripts/dns-zone-transfer.nse +++ b/scripts/dns-zone-transfer.nse @@ -32,6 +32,12 @@ Useful resources -- @args dns-zone-transfer.port DNS server port, this argument concerns -- the "Script Pre-scanning phase" and it's optional, the default -- value is 53. +-- @args newtargets If specified, adds returned DNS records onto Nmap +-- scanning queue. +-- @args dns-zone-transfer.addall If specified, adds all IP addresses +-- including private ones onto Nmap scanning queue when the +-- script argument newtargets is given. The default +-- behavior is to skip private IPs (non-routable). -- @output -- 53/tcp open domain -- | dns-zone-transfer: @@ -69,6 +75,8 @@ require('listop') require('bit') require('tab') require('dns') +require('target') +require('ipOps') author = "Eddie Bell" license = "Same as Nmap--See http://nmap.org/book/man-legal.html" @@ -77,6 +85,9 @@ categories = {'default', 'intrusive', 'discovery'} prerule = function() return true end portrule = shortport.portnumber(53, 'tcp') +-- DNS arguments which are used to filter results. +local dns_filter = {} + --- DNS query and response types. --@class table --@name typetab @@ -209,8 +220,8 @@ function get_rdata(data, offset, ttype) string.byte(data, offset+3) offset = offset + 4 - elseif typetab[ttype] == 'PTR' or - typetab[ttype] == 'NS' then + elseif typetab[ttype] == 'PTR' or typetab[ttype] == 'NS' or + typetab[ttype] == 'CNAME' then -- domain/domain server name offset, field = parse_domain(data, offset) info = info .. field; @@ -225,12 +236,12 @@ function get_answer_record(table, data, offset) -- answer domain offset, line = parse_domain(data, offset) - tab.add(table, 1, line) + table.domain = line -- answer record type ttype = bto16(data, offset) if not(typetab[ttype] == nil) then - tab.add(table, 2, typetab[ttype]) + table.ttype = typetab[ttype] end -- length of type specific data @@ -240,17 +251,63 @@ function get_answer_record(table, data, offset) offset, line = get_rdata(data, offset+10, ttype) if(line == '') then offset = offset + rdlen + return false, offset else - tab.add(table, 3, line) + table.rdata = line end - return offset, tab + return true, offset end -function parse_records(number, data, table, offset) +-- parse and save uniq records in the results table +function parse_uniq_records(results, record) + if record.domain and not results['Node Names'][record.domain] then + local str = string.gsub(record.domain, "^%s*(.-)%s*$", "%1") + if not results['Node Names'][str] then + results['Node Names'][str] = 1 + end + end + if record.ttype and record.rdata then + if not results[record.ttype] then + results[record.ttype] = {} + end + local str = string.gsub(record.rdata, "^%s*(.-)%s*$", "%1") + if not results[record.ttype][str] then + results[record.ttype][str] = 1 + end + end +end + +-- parse and save only valid records +function parse_records(number, data, results, offset) while number > 0 do - tab.nextrow(table) - offset = get_answer_record(table, data, offset) + local answer, st = {} + st, offset = get_answer_record(answer, data, offset) + if st then + parse_uniq_records(results, answer) + end + number = number - 1 + end + return offset +end + +-- parse and save all records in order to dump them to output +function parse_records_table(number, data, table, offset) + while number > 0 do + local answer, st = {} + st, offset = get_answer_record(answer, data, offset) + if st then + tab.nextrow(table) + if answer.domain then + tab.add(table, 1, answer.domain) + end + if answer.ttype then + tab.add(table, 2, answer.ttype) + end + if answer.rdata then + tab.add(table, 3, answer.rdata) + end + end number = number - 1 end return offset @@ -280,26 +337,130 @@ function responses_iter(data) end end -function dump_zone_info(table, data) - local answers, line, offset - local questions, auth_answers, add_answers +-- add axfr results to Nmap scan queue +function add_zone_info(response) + local RR = {} + for data in responses_iter(response) do - offset = 1 - -- number of available records - questions = bto16(data, offset+4) - answers = bto16(data, offset+6) - auth_answers = bto16(data, offset+8) - add_answers = bto16(data, offset+10) + local offset, line = 1 + local questions = bto16(data, offset+4) + local answers = bto16(data, offset+6) + local auth_answers = bto16(data, offset+8) + local add_answers = bto16(data, offset+10) -- move to beginning of first section offset = offset + 12 if questions > 1 then - return 'More then 1 question record, something has gone wrong' + return false, 'More then 1 question record, something has gone wrong' end if answers == 0 then - return 'transfer successful but no records' + return false, 'transfer successful but no records' + end + + -- skip over the question section, we don't need it + if questions == 1 then + offset, line = parse_domain(data, offset) + offset = offset + 4 + end + + -- parse all available resource records + stdnse.print_debug(3, + "Script %s: parsing ANCOUNT == %d, NSCOUNT == %d, ARCOUNT == %d", + SCRIPT_NAME, answers, auth_answers, add_answers) + RR['Node Names'] = {} + offset = parse_records(answers, data, RR, offset) + offset = parse_records(auth_answers, data, RR, offset) + offset = parse_records(add_answers, data, RR, offset) + end + + local outtab, nhosts = tab.new(2), 0 + local newhosts_count, status, ret = 0, false + + tab.nextrow(outtab) + tab.addrow(outtab, " Domains", "Added Targets") + for rdata in pairs(RR['Node Names']) do + status, ret = target.add(rdata) + if not status then + stdnse.print_debug(3, "Error: failed to add all Node Names.") + break + end + newhosts_count = newhosts_count + ret + end + if newhosts_count == 0 then + return false, ret and ret or "Error: failed to add DNS records." + end + tab.addrow(outtab, " Node Names", newhosts_count) + nhosts = newhosts_count + + tab.nextrow(outtab) + tab.addrow(outtab, " DNS Records", "Added Targets") + for rectype in pairs(RR) do + newhosts_count = 0 + -- filter Private IPs + if rectype == 'A' then + for rdata in pairs(RR[rectype]) do + if dns_filter.addall or not ipOps.isPrivate(rdata) then + status, ret = target.add(rdata) + if not status then + stdnse.print_debug(3, + "Error: failed to add all 'A' records.") + break + end + newhosts_count = newhosts_count + ret + end + end + elseif rectype ~= 'Node Names' then + for rdata in pairs(RR[rectype]) do + status, ret = target.add(rdata) + if not status then + stdnse.print_debug(3, + "Error: failed to add all '%s' records.", rectype) + break + end + newhosts_count = newhosts_count + ret + end + end + + if newhosts_count ~= 0 then + tab.addrow(outtab, " "..rectype, newhosts_count) + nhosts = nhosts + newhosts_count + elseif nhosts == 0 then + -- error: we can't add new targets + return false, ret and ret or "Error: failed to add DNS records." + end + end + + -- error: no *valid records* or we can't add new targets + if nhosts == 0 then + return false, "Error: failed to add valid DNS records." + end + + return true, tab.dump(outtab) .. + string.format("Total new targets added to Nmap scan queue: %d.", + nhosts) +end + +function dump_zone_info(table, response) + for data in responses_iter(response) do + local offset, line = 1 + + -- number of available records + local questions = bto16(data, offset+4) + local answers = bto16(data, offset+6) + local auth_answers = bto16(data, offset+8) + local add_answers = bto16(data, offset+10) + + -- move to beginning of first section + offset = offset + 12 + + if questions > 1 then + return false, 'More then 1 question record, something has gone wrong' + end + + if answers == 0 then + return false, 'transfer successful but no records' end -- skip over the question section, we don't need it @@ -309,22 +470,27 @@ function dump_zone_info(table, data) end -- parse all available resource records - offset = parse_records(answers, data, table, offset) - offset = parse_records(auth_answers, data, table, offset) - offset = parse_records(add_answers, data, table, offset) - return offset + stdnse.print_debug(3, + "Script %s: parsing ANCOUNT == %d, NSCOUNT == %d, ARCOUNT == %d", + SCRIPT_NAME, answers, auth_answers, add_answers) + offset = parse_records_table(answers, data, table, offset) + offset = parse_records_table(auth_answers, data, table, offset) + offset = parse_records_table(add_answers, data, table, offset) + end + + return true end action = function(host, port) - local soc, status, data - local catch = function() soc:close() end - local try = nmap.new_try(catch) + local domain, dns_server, dns_port - local domain, dns_server, dns_port = stdnse.get_script_args( + domain, dns_server, dns_port, dns_filter.addall = stdnse.get_script_args( {"dns-zone-transfer.domain", "dnszonetransfer.domain"}, {"dns-zone-transfer.server", "dnszonetransfer.server"}, - {"dns-zone-transfer.port", "dnszonetransfer.port"} + {"dns-zone-transfer.port", "dnszonetransfer.port"}, + {"dns-zone-transfer.addall", "dnszonetransfer.addall"} ) + if not dns_port then dns_port = 53 end @@ -362,12 +528,13 @@ action = function(host, port) assert(domain) - soc = nmap.new_socket() + local soc = nmap.new_socket() + local catch = function() soc:close() end + local try = nmap.new_try(catch) soc:set_timeout(4000) try(soc:connect(dns_server, dns_port)) local req_id = '\222\173' - local table = tab.new(3) local offset = 1 local name = build_domain(string.lower(domain)) local pkt_len = string.len(name) + 16 @@ -383,10 +550,11 @@ action = function(host, port) -- multiple packets from a single request local response = strbuf.new() while true do - status, data = soc:receive_bytes(1) + local status, data = soc:receive_bytes(1) if not status then break end response = response .. data end + soc:close() local response_str = strbuf.dump(response) local length = string.len(response_str) @@ -397,11 +565,20 @@ action = function(host, port) return nil end - -- parse zone information from all returned packets - for r in responses_iter(response_str) do - dump_zone_info(table, r) + -- add axfr results to Nmap scanning queue + if target.ALLOW_NEW_TARGETS then + local status, ret = add_zone_info(response_str) + if not status then + return stdnse.format_output(false, ret) + end + return stdnse.format_output(true, ret) + -- dump axfr results + else + local table = tab.new(3) + local status, ret = dump_zone_info(table, response_str) + if not status then + return stdnse.format_output(false, ret) + end + return '\n' .. tab.dump(table) end - - soc:close() - return '\n' .. tab.dump(table) end