#!/usr/bin/ruby # # ns-digger.rb - Name Server digger v0.5 - 2007-12-18 # # # Short: ns-digger.rb looks up name servers for given domains # and performs a number of checks on each name server returned # # # Copyright (C) 2007 By Pedro Venda < pjvenda (at) pjvenda org > # # Distributed under the terms of the GNU Public License Agreement (GPLv2) # http://www.fsf.org/licensing/licenses/gpl.html or # http://www.fsf.org/licensing/licenses/gpl.txt # # # Long: This tool was written to assist me in some routine tasks. None # of those tasks are particularly complicated or time consuming but # when they are done about once a day, it becomes... boring. # So ns-digger.rb will take one (or more) domain name(s), look up # their authoritative name servers and does some simple security # checks in each of them. # First it checks if the name servers accept anonymous zone # transfers of the originally specified domain name for which they # are authoritative. # Next it checks if any found name server supports recursive # queries - queries for names belonging to domains for which they # are not authoritative. # # Sound configurations must refuse zone transfers and should not # accept recursive queries. Allowing anonymous zone transfers is # by itself a security vulnerability and allowing recursive queries # can make the service vulnerable to cache poisoning or snooping # attacks. # # # Features # ======== # # - Fetches name servers of one or more given domains in the command line # - Obtains IP addresses of each name server # - Checks if name servers accept anonymous zone transfer requests # - Checks if name servers accept recursive queries # # # Usage # ===== # # This script is designed to run from the command line. # Run it with -h or --help for help. # # # TODO # ==== # # TODO: sort name servers by default within single domains but don't sort # domains # # Changelog # ========= # # v0.5 - 2007-12-18 # # - initial version. # # # References / Credits # ==================== # # - Dnsresolv library does everything, really. # # wrapper that does 'require' calls with error handling def load_module(module_name) begin require module_name return true rescue LoadError => e return false end end # defines and parses command line options def parse_cmdline_options(argv) options={} opts=OptionParser.new do |opts| opts.banner = "Usage: ns-digger.rb [options] domain [domain] [domain] ..." opts.on('-f','--format FORMAT',[:text,:csv],"Output format. 'text' (default) or 'csv' for further processing.") do |f| options[:format]=f end opts.on('-s','--sort-all','Sort output domains and name servers') do |s| options[:sortall]=s end opts.on('-n','--nameserver NAMESERVERS','Name servers to query. Separate multiple entries with a single comma, no spaces') do |n| n=n.split(',') options[:nameservers]=n end opts.on('-z','--no-zone-transfer','Disable anonymous zone transfer requests.') do |z| options[:zonetransfer]=z # because of the 'no-' prefix of the long option # the variable comes inverted end opts.on('-r','--no-recursive-dns','Disable recursive DNS requests.') do |r| options[:recdns]=r # because of the 'no-' prefix of the long option # the variable comes inverted end opts.on('-q','--recursive-query HOSTNAME',"Host name to use for recursive query test.") do |q| options[:recdns_query]=q end opts.on('-v','--verbose',"Verbose mode") do |v| options[:verbose]=v end opts.on_tail('-h','--help',"Show this help text") do |h| puts opts.help exit end end begin opts.parse!(ARGV) rescue OptionParser::ParseError => e puts e.message exit end [options,argv] end # verbose puts # wrapper function for puts conditional to options[:verbose] def vputs(str,options,nlf=false) if options[:verbose] if nlf options[:output].print str.to_s else options[:output].puts str.to_s end end end # produces human readable output def output_nsdig_text(result,options) ret="" if result.length == 0 return ret end result.each do |domain| ret+=domain[:domain]+"\n" if domain[:nameservers].length == 0 ret+=" -- none --" else domain[:nameservers].each do |ns| ret+=" "+ns[:name].ljust(32)+" "+("("+ns[:addr]+")").ljust(17)+" [" if options[:recdns] && ns.has_key?(:recdns) if ns[:recdns] == true ret+="REC" elsif ns[:recdns] == false ret+=" " end else ret+="---" end ret+="|" if options[:zonetransfer] && ns.has_key?(:zone) if ns[:zone] == true ret+="ZTR" elsif ns[:zone] == false ret+=" " end else ret+="---" end ret+="]\n" end end end ret end # produces a comma separated field file. good for further parsing def output_nsdig_csv(result,options) ret="" if result.length == 0 return ret end ret+="domain,name server,name server address,recursive queries,zone transfer\n" result.each do |domain| domain[:nameservers].each do |ns| ret+=domain[:domain]+","+ns[:name]+","+ns[:addr]+"," if options[:recdns] && ns.has_key?(:recdns) if ns[:recdns] == true ret+="YES" elsif ns[:recdns] == false ret+="NO" end end ret+="," if options[:zonetransfer] && ns.has_key?(:zone) if ns[:zone] == true ret+="YES" elsif ns[:zone] == false ret+="NO" end end ret+="\n" end end ret end def print_configuration(options) vputs("# * Configuration variables:",options) # check for output format if not options[:format] options[:format]=:text end vputs("# - output format: "+options[:format].to_s,options) vputs("# - zone transfer test: ",options,true) if not options.has_key?(:zonetransfer) options[:zonetransfer]=true vputs("YES",options) else # not necessary but just in case... options[:zonetransfer]=false vputs("NO",options) end vputs("# - recursive query test: ",options,true) if not options.has_key?(:recdns) options[:recdns]=true vputs("YES",options) else # not necessary but just in case... options[:recdns]=false vputs("NO",options) end if options[:recdns] if not options.has_key?(:recdns_query) options[:recdns_query]='www.google.com' vputs("# hostname to query: "+options[:recdns_query]+" (default)",options) else vputs("# hostname to query: "+options[:recdns_query],options) end end vputs("# - primary name servers to be used",options,true) if options[:nameservers] req=Dnsruby::DNS.new(:nameserver => options[:nameservers]) vputs(" (user defined):",options) else req=Dnsruby::DNS.new vputs(" (system defined):",options) end req.config.nameserver.each do |config_ns| vputs("# "+config_ns.to_s,options) end options[:output].flush req end # see if we were called directly if $0 == __FILE__ # load some modules we'll need if not load_module('optparse') puts "unable to load module 'optparse'." exit end if not load_module('rubygems') puts "unable to load module 'rubygems'." puts "ruby gems is the ruby package/library/module manager." exit end if not load_module('dnsruby') puts "unable to load module 'dnsruby'." puts "dnsruby can be obtained through the gems interface." exit end # parse command line options parsed_options = parse_cmdline_options(ARGV) options = parsed_options[0] new_argv=parsed_options[1] if new_argv.length == 0 puts "not enough arguments." exit 1 end # add extra configuration options: static options[:output]=STDOUT # print configuration. requires -v (verbose) flag # outputting req here is utterly ugly, but I can't be arsed to # make this look prettier. req=print_configuration(options) result=[] # iterate through given list of domains new_argv.each do |domain| nameservers = [] vputs("# * Obtaining name server info for: "+domain,options) options[:output].flush begin rr=req.getresources(domain,"NS") rescue Dnsruby::ResolvTimeout => rt # Resolv timeout vputs("# resolver timeout: "+rt.message,options) rescue Dnsruby::NXDomain => rd vputs("# domain not found",options) rescue Dnsruby::ResolvError => re # Resolv error vputs("# unknown resolver error: "+re.message,options) end options[:output].flush if rr != nil && rr.length > 0 rr.each do |r| # process NS records only if r.type == 'NS' ns={} ns[:name]=r.rdata.to_s begin rr=req.getaddress(ns[:name]) rescue Dnsruby::ResolvTimeout => rt # Resolv timeout # rescue Dnsruby::ResolvError => re # Resolv error # else ns[:addr]=rr.to_s end # test zone transfers if options[:zonetransfer] zt = Dnsruby::ZoneTransfer.new zt.transfer_type = Dnsruby::Types.AXFR zt.server = ns[:name] begin zone = zt.transfer(domain) rescue Dnsruby::ResolvError => re # resolv error. transfer was probably refused vputs("# - zone transfer of "+domain+" from ns "+ns[:name]+" refused.",options) options[:output].flush ns[:zone]=false rescue # resolv error. transfer was probably refused vputs("# - zone transfer of "+domain+" from ns "+ns[:name]+" failed.",options) options[:output].flush ns[:zone]=false else vputs("# - zone transfer of "+domain+" from ns "+ns[:name]+" succeeded.",options) options[:output].flush # no exception? zone transfer succeeded ns[:zone]=true end end if options[:recdns] rd = Dnsruby::DNS.new(:nameserver => [ns[:addr]], :ndots => 1, :search => '') begin rr=rd.getaddress(options[:recdns_query]) rescue Dnsruby::ResolvError # Resolv error ns[:recdns]=false vputs("# - recursive query of "+options[:recdns_query]+" to "+ns[:name]+" failed",options) rescue ns[:recdns]=false vputs("# - recursive query of "+options[:recdns_query]+" to "+ns[:name]+" failed",options) else ns[:recdns]=true vputs("# - recursive query of "+options[:recdns_query]+" to "+ns[:name]+" succeeded",options) options[:output].flush end end end nameservers << ns end end if options[:sortall] && nameservers.length > 0 # sort name server list nameservers.sort! { |x,y| x[:name] <=> y[:name] } end if nameservers.length > 0 result << {:domain => domain, :nameservers => nameservers} end end # sort result data structure vputs("# * Sorting output data structure",options) if options[:sortall] result.sort! { |x,y| x[:domain] <=> y[:domain] } end options[:output].flush # generate output vputs("# * Generating output (",options,true) if options[:format] == :csv vputs("csv):",options) options[:output].flush puts output_nsdig_csv(result,options) elsif options[:format] == :text vputs("text):",options) options[:output].flush puts output_nsdig_text(result,options) end options[:output].flush # that's it! end