From 5579ac94d366929274b9310e649f5512e3f5386c Mon Sep 17 00:00:00 2001 From: fyodor Date: Sat, 11 Aug 2007 06:07:31 +0000 Subject: [PATCH] merge soc07 r5317:5322 - DNS zone transfer script. --- nselib/stdnse.lua | 8 +- scripts/zoneTrans.nse | 315 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 319 insertions(+), 4 deletions(-) create mode 100644 scripts/zoneTrans.nse diff --git a/nselib/stdnse.lua b/nselib/stdnse.lua index 58b69cc12..85ea5acb0 100644 --- a/nselib/stdnse.lua +++ b/nselib/stdnse.lua @@ -24,17 +24,17 @@ function strsplit(delimiter, text) local list = {} local pos = 1 - if strfind("", delimiter, 1) then -- this would result in endless loops + if string.find("", delimiter, 1) then -- this would result in endless loops error("delimiter matches empty string!") end while 1 do - local first, last = strfind(text, delimiter, pos) + local first, last = string.find(text, delimiter, pos) if first then -- found? - tinsert(list, strsub(text, pos, first-1)) + table.insert(list, string.sub(text, pos, first-1)) pos = last+1 else - tinsert(list, strsub(text, pos)) + table.insert(list, string.sub(text, pos)) break end end diff --git a/scripts/zoneTrans.nse b/scripts/zoneTrans.nse new file mode 100644 index 000000000..57ed8ea39 --- /dev/null +++ b/scripts/zoneTrans.nse @@ -0,0 +1,315 @@ +--[[ + +Send axfr queries to DNS servers. The domain to query is determined +by examining the domain servers hostname. If the query is successful +all domains and domain types are returned along with common type +specific data (SOA/MX/NS/PTR/A) + +constraints +----------- +If we don't have the 'true' hostname for the dns server we cannot +determine a likely zone to perform the transfer on + +useful resources +---------------- +DNS for rocket scientists - http://www.zytrax.com/books/dns/ +How the AXFR protocol works - http://cr.yp.to/djbdns/axfr-notes.html + +--]] + +require('shortport') +require('strbuf') +require('stdnse') +require('listop') +require('bit') + +id = 'zone-transfer' +author = 'Eddie Bell ' +description = 'Request a zone transfer (AXFR) from a DNS server' +license = 'See nmaps COPYING for licence' +categories = {'intrusive', 'discovery'} +runlevel = 1.0 + +portrule = shortport.portnumber(53, 'tcp') + +-- DNS query and response types. +local typetab = { 'A', 'NS', 'MD', 'MF', 'CNAME', 'SOA', 'MB', 'MG', 'MR', + 'NULL', 'WKS', 'PTR', 'HINFO', 'MINFO', 'MX', 'TXT', 'RP', 'AFSDB', 'X25', + 'ISDN', 'RT', 'NSAP', 'NSAP-PTR', 'SIG', 'KEY', 'PX', 'GPOS', 'AAAAA', 'LOC', + 'NXT', 'EID', 'NIMLOC', 'SRV', 'ATMA', 'NAPTR', 'KX', 'CERT', 'A6', 'DNAME', + 'SINK', 'OPT', [250]='TSIG', [251]='IXFR', [252]='AXFR', [253]='MAILB', + [254]='MAILA', [255]='ANY', [256]='ZXFR' +} + +-- Whitelist of TLDs. Only way to reliably determine the root of a domain +local tld = { + 'aero', 'asia', 'biz', 'cat', 'com', 'coop', 'info', 'jobs', 'mobi', 'museum', + 'name', 'net', 'org', 'pro', 'tel', 'travel', 'gov', 'edu', 'mil', 'int', + 'ac','ad','ae','af','ag','ai','al','am','an','ao','aq','ar','as','at','au','aw', + 'ax','az','ba','bb','bd','be','bf','bg','bh','bi','bj','bm','bn','bo','br','bs', + 'bt','bv','bw','by','bz','ca','cc','cd','cf','cg','ch','ci','ck','cl','cm','cn', + 'co','cr','cu','cv','cx','cy','cz','de','dj','dk','dm','do','dz','ec','ee','eg', + 'eh','er','es','et','eu','fi','fj','fk','fm','fo','fr','ga','gb','gd','ge','gf', + 'gg','gh','gi','gl','gm','gn','gp','gq','gr','gs','gt','gu','gw','gy','hk','hm', + 'hn','hr','ht','hu','id','ie','il','im','in','io','iq','ir','is','it','je','jm', + 'jo','jp','ke','kg','kh','ki','km','kn','kp','kr','kw','ky','kz','la','lb','lc', + 'li','lk','lr','ls','lt','lu','lv','ly','ma','mc','md','me','mg','mh','mk','ml', + 'mm','mn','mo','mp','mq','mr','ms','mt','mu','mv','mw','mx','my','mz','na','nc', + 'ne','nf','ng','ni','nl','no','np','nr','nu','nz','om','pa','pe','pf','pg','ph', + 'pk','pl','pm','pn','pr','ps','pt','pw','py','qa','re','ro','rs','ru','rw','sa', + 'sb','sc','sd','se','sg','sh','si','sj','sk','sl','sm','sn','so','sr','st','su', + 'sv','sy','sz','tc','td','tf','tg','th','tj','tk','tl','tm','tn','to','tp','tr', + 'tt','tv','tw','tz','ua','ug','uk','um','us','uy','uz','va','vc','ve','vg','vi', + 'vn','vu','wf','ws','ye','yt','yu','za','zm','zw' +} + +-- Convert two bytes into a 16bit number. +function bto16(data, idx) + local b1 = string.byte(data, idx) + local b2 = string.byte(data, idx+1) + -- (b2 & 0xff) | ((b1 & 0xff) << 8) + return bit.bor(bit.band(b2, 255), bit.lshift(bit.band(b1, 255), 8)) +end + +-- Check if domain name element is a tld +function valid_tld(elm) + for i,v in ipairs(tld) do + if elm == v then return true end + end + return false +end + +-- parse RFC 1035 domain name +function parse_domain(data, offset) + local i, x, record, line, ptr + + record = strbuf.new() + x = string.byte(data, offset) + ptr = bto16(data, offset) + + while not(x == 0) do + -- if the first two bits are '11' then the next 14 + -- point to another location in the packet + if(bit.band(ptr, 49152) == 49152) then + ptr, line = parse_domain(data, bit.band(ptr, 16383) + 3) + record = record .. line + offset = offset + 1 + break + end + + -- RFC 1035 format name + for i=0, x do + offset = offset + 1 + record = record .. string.char(string.byte(data, offset)) + end + + x = string.byte(data, offset) + ptr = bto16(data, offset) + end + + return offset+1, string.gsub(strbuf.dump(record), 0, '.') +end + +-- build RFC 1035 root domain name from the name of the +-- DNS server (e.g ns1.website.com.ar -> \007website\003com\002ar\000) +function build_domain(host) + local names, buf, x + local abs_name, i, tmp + + buf = strbuf.new() + abs_name = {} + + names = stdnse.strsplit('%.', host) + if names == nil then names = {host} end + + -- try to determine root of domain name + for i, x in ipairs(listop.reverse(names)) do + table.insert(abs_name, x) + if not valid_tld(x) then break end + end + + i = 1 + abs_name = listop.reverse(abs_name) + + -- prepend each element with its length + while i <= table.getn(abs_name) do + buf = buf .. string.char(string.len(abs_name[i])) .. abs_name[i] + i = i + 1 + end + + buf = buf .. '\000' + return strbuf.dump(buf) +end + +-- retrieve type specific data (rdata) from dns packets +function get_rdata(data, offset, ttype) + local field, info, i + + info = strbuf.new() + info = info .. '' + + if typetab[ttype] == nil then + return offset, '' + + elseif typetab[ttype] == 'SOA' then + -- name server + offset, field = parse_domain(data, offset) + info = info .. field; + -- mail box + offset, field = parse_domain(data, offset) + info = info .. field; + -- ignore other values + offset = offset + 20 + + elseif typetab[ttype] == 'MX' then + -- mail server + offset = offset + 2 + offset, field = parse_domain(data, offset) + info = info .. field + + elseif typetab[ttype] == 'A' then + -- ip address + info = info .. + string.byte(data, offset) .. '.' .. + string.byte(data, offset+1) .. '.' .. + string.byte(data, offset+2) .. '.' .. + string.byte(data, offset+3) + offset = offset + 4 + + elseif typetab[ttype] == 'PTR' or + typetab[ttype] == 'NS' then + -- domain/domain server name + offset, field = parse_domain(data, offset) + info = info .. field; + end + + return offset, strbuf.dump(info, ' ') +end + +-- get a single answer record from the current offset +function get_answer_record(data, offset) + local line, record, rdlen, ttype + record = strbuf.new() + + -- answer domain + offset, line = parse_domain(data, offset) + record = record .. line + + -- answer record type + ttype = bto16(data, offset) + if not(typetab[ttype] == nil) then + record = record .. typetab[ttype] + end + + -- length of type specific data + rdlen = bto16(data, offset+8) + + -- extra data, ignore ttl and class + offset, line = get_rdata(data, offset+10, ttype) + if(line == '') then + offset = offset + rdlen + else + record = record .. line + end + + return offset, strbuf.dump(record, '\t') +end + +function parse_records(number, data, results, offset) + local record + while number > 0 do + offset, record = get_answer_record(data, offset) + results = results .. record + number = number - 1 + end + return offset +end + +function dump_zone_info(data, offset) + local results, answers, line + local questions, auth_answers, add_answers + results = strbuf.new() + + -- number of available records + questions = bto16(data, offset+6) + answers = bto16(data, offset+8) + auth_answers = bto16(data, offset+10) + add_answers = bto16(data, offset+12) + + -- move to beginning of first section + offset = offset + 14 + + if questions > 1 then + return 'More then 1 question record, something has gone wrong' + end + + if answers == 0 then + return '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 + offset = parse_records(answers, data, results, offset) + offset = parse_records(auth_answers, data, results, offset) + offset = parse_records(add_answers, data, results, offset) + + return offset, strbuf.dump(results, '\n') +end + +action = function(host, port) + local soc, status, data + local catch = function() soc.close() end + local try = nmap.new_try(catch) + + -- can't do anything without a hostname + if host.name == "" then return nil end + + soc = nmap.new_socket() + soc:set_timeout(4000) + try(soc:connect(host.ip, port.number)) + + local req_id = '\222\173' + local results = strbuf.new() + local name = build_domain(string.lower(host.name)) + local pkt_len = string.len(name) + 16 + + -- build axfr request + local buf = strbuf.new() + buf = buf .. '\000' .. string.char(pkt_len) .. req_id + buf = buf .. '\000\000\000\001\000\000\000\000\000\000' + buf = buf .. name .. '\000\252\000\001' + try(soc:send(strbuf.dump(buf))) + + -- read all data returned. Common to have + -- multiple packets from a single request + local response = strbuf.new() + while true do + status, data = soc:receive_bytes(1) + if not status then break end + response = response .. data + end + + local response_str = strbuf.dump(response) + local length = string.len(response_str) + + -- check server response code + if length < 6 or + not (bit.band(string.byte(response_str, 6), 15) == 0) then + return nil + end + + -- parse zone information from all returned packets + local offset = 1 + while(offset < length) do + offset, data = dump_zone_info(response_str, offset) + results = results .. data + end + + soc:close() + return '\r\n' .. strbuf.dump(results, '\n') +end