--[[

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 <ejlbell@gmail.com>'
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
