Migrate native DNS services to Dnsruby data format

Dnsruby provides advanced options like DNSSEC in its data format
and is a current and well supported library.
The infrastructure services - resolver, server, etc, were designed
for a standalone configuration, and carry entirely too much weight
and redundancy to implement for this context. Instead of porting
over their native resolver, update the Net::DNS subclassed Rex
Resolver to use Dnsruby data formats and method calls.
Update the Msf namespace infrastructure mixins and native server
module with new method calls and workarounds for some instance
variables having only readers without writers. Implement the Rex
ServerManager to start and stop the DNS service adding relevant
alias methods to the Rex::Proto::DNS::Server class.

Rex services are designed to be modular and lightweight, as well
as implement the sockets, threads, and other low-level interfaces.
Dnsruby's operations classes implement their own threading and
socket semantics, and do not fit with the modular mixin workflow
used throughout Framework. So while the updated resolver can be
seen as adding rubber to the tire fire, converting to dnsruby's
native classes for resolvers, servers, and caches, would be more
like adding oxy acetylene and heavy metals.

Testing:
  Internal tests for resolution of different record types locally
and over pivot sessions.
MS-2855/keylogger-mettle-extension
RageLtMan 2018-01-12 05:00:00 -05:00
parent f76adf6a62
commit c65c03722c
6 changed files with 176 additions and 76 deletions

View File

@ -47,8 +47,8 @@ module Client
], Exploit::Remote::DNS::Client
)
register_autofilter_ports([ 53 ])
register_autofilter_services(%W{ dns })
register_autofilter_ports([ 53 ]) if respond_to?(:register_autofilter_ports)
register_autofilter_services(%W{ dns }) if respond_to?(:register_autofilter_services)
end
@ -58,7 +58,7 @@ module Client
# @param domain [String] Domain for which to request a record
# @param type [String] Type of record to request for domain
#
# @return [Net::DNS::RR] DNS response
# @return [Dnsruby::RR] DNS response
def query(domain = datastore['DOMAIN'], type = 'A')
client.query(domain, type)
end
@ -110,7 +110,7 @@ module Client
if datastore['NS'].blank?
resp_soa = client.query(target, "SOA")
if (resp_soa)
(resp_soa.answer.select { |i| i.class == Net::DNS::RR::SOA}).each do |rr|
(resp_soa.answer.select { |i| i.is_a?(Dnsruby::RR::SOA)}).each do |rr|
resp_1_soa = client.search(rr.mname)
if (resp_1_soa and resp_1_soa.answer[0])
set_nameserver(resp_1_soa.answer.map(&:address).compact.map(&:to_s))
@ -139,7 +139,7 @@ module Client
if response.answer.length != 0
vprint_status("This domain has wildcards enabled!!")
response.answer.each do |rr|
print_status("Wildcard IP for #{rendsub}.#{target} is: #{rr.address.to_s}") if rr.class != Net::DNS::RR::CNAME
print_status("Wildcard IP for #{rendsub}.#{target} is: #{rr.address.to_s}") if rr.class != Dnsruby::RR::CNAME
addr = rr.address.to_s
end
end

View File

@ -1,7 +1,7 @@
# -*- coding: binary -*-
require 'msf/core'
require 'rex/proto/dns'
require 'msf/core/exploit/dns/common'
module Msf
@ -12,7 +12,7 @@ module Msf
###
module Exploit::Remote::DNS
module Server
include Common
include Exploit::Remote::DNS::Common
include Exploit::Remote::SocketServer
#
@ -110,7 +110,8 @@ module Server
begin
comm = _determine_server_comm
self.service = Rex::Proto::DNS::Server.new(
self.service = Rex::ServiceManager.start(
Rex::Proto::DNS::Server,
datastore['SRVHOST'],
datastore['SRVPORT'],
datastore['DnsServerUdp'],
@ -154,7 +155,7 @@ module Server
# Stops the server
# @param destroy [TrueClass,FalseClass] Dereference the server object
def stop_service(destroy = false)
self.service.stop unless self.service.nil?
Rex::ServiceManager.stop_service(self.service) if self.service
if destroy
@dns_resolver = nil
self.service = nil

View File

@ -1,5 +1,8 @@
# -*- coding: binary -*-
require 'net/dns'
require 'resolv'
require 'dnsruby'
module Rex
module Proto
@ -21,26 +24,26 @@ module Packet
# Reconstructs a packet with both standard DNS libraries
# Ensures that headers match the payload
#
# @param packet [String, Net::DNS::Packet] Data to be validated
# @param packet [String, Net::DNS::Packet, Dnsruby::Message] Data to be validated
#
# @return [Net::DNS::Packet]
# @return [Dnsruby::Message]
def self.validate(packet)
self.encode_net(self.encode_res(self.encode_raw(packet)))
self.encode_drb(self.encode_net(self.encode_res(packet)))
end
#
# Sets header values to match packet content
#
# @param packet [String] Net::DNS::Packet, Resolv::DNS::Message]
# @param packet [String] Net::DNS::Packet, Resolv::DNS::Message, Dnsruby::Message]
#
# @return [Net::DNS::Packet]
# @return [Dnsruby::Message]
def self.recalc_headers(packet)
packet = self.encode_net(packet)
packet = self.encode_drb(packet)
{
:qdCount= => :question,
:anCount= => :answer,
:nsCount= => :authority,
:arCount= => :additional
:qdcount= => :question,
:ancount= => :answer,
:nscount= => :authority,
:arcount= => :additional
}.each do |header,body|
packet.header.send(header,packet.send(body).count)
end
@ -51,36 +54,48 @@ module Packet
#
# Reads a packet into the Net::DNS::Packet format
#
# @param data [String, Net::DNS::Packet, Resolv::DNS::Message] Input data
# @param data [String, Net::DNS::Packet, Resolv::DNS::Message, Dnsruby::Message] Input data
#
# @return [Net::DNS::Packet]
def self.encode_net(packet)
return packet if packet.respond_to?(:data)
return packet if packet.is_a?(Net::DNS::Packet)
Net::DNS::Packet.parse(
packet.respond_to?(:encode) ? packet.encode : packet
self.encode_raw(packet)
)
end
# Reads a packet into the Resolv::DNS::Message format
#
# @param data [String, Net::DNS::Packet, Resolv::DNS::Message] Input data
# @param data [String, Net::DNS::Packet, Resolv::DNS::Message, Dnsruby::Message] Input data
#
# @return [Resolv::DNS::Message]
def self.encode_res(packet)
return packet if packet.respond_to?(:encode)
return packet if packet.is_a?(Resolv::DNS::Message)
Resolv::DNS::Message.decode(
packet.respond_to?(:data) ? packet.data : packet
self.encode_raw(packet)
)
end
# Reads a packet into the Dnsruby::Message format
#
# @param data [String, Net::DNS::Packet, Resolv::DNS::Message, Dnsruby::Message] Input data
#
# @return [Dnsruby::Message]
def self.encode_drb(packet)
return packet if packet.is_a?(Dnsruby::Message)
Dnsruby::Message.decode(
self.encode_raw(packet)
)
end
# Reads a packet into the raw String format
#
# @param data [String, Net::DNS::Packet, Resolv::DNS::Message] Input data
# @param data [String, Net::DNS::Packet, Resolv::DNS::Message, Dnsruby::Message] Input data
#
# @return [Resolv::DNS::Message]
# @return [String]
def self.encode_raw(packet)
return packet unless packet.respond_to?(:encode) or packet.respond_to?(:data)
packet.respond_to?(:data) ? packet.data : packet.encode
(packet.respond_to?(:data) ? packet.data : packet.encode).force_encoding('binary')
end
#
@ -91,16 +106,16 @@ module Packet
# @param cls [Fixnum] Class of dns record to query
# @param recurse [Fixnum] Recursive query or not
#
# @return [Net::DNS::Packet] request packet
def self.generate_request(subject, type = Net::DNS::A, cls = Net::DNS::IN, recurse = 1)
# @return [Dnsruby::Message] request packet
def self.generate_request(subject, type = Dnsruby::Types::A, cls = Dnsruby::Classes::IN, recurse = 1)
case subject
when IPAddr
name = subject.reverse
type = Net::DNS::PTR
type = Dnsruby::Types::PTR
when /\d/ # Contains a number, try to see if it's an IP or IPv6 address
begin
name = IPAddr.new(subject).reverse
type = Net::DNS::PTR
type = Dnsruby::Types::PTR
rescue ArgumentError
name = subject if self.valid_hostname?(subject)
end
@ -109,9 +124,9 @@ module Packet
end
# Create the packet
packet = Net::DNS::Packet.new(name,type,cls)
packet = Dnsruby::Message.new(name, type, cls)
if packet.query?
if packet.header.opcode == Dnsruby::OpCode::Query
packet.header.recursive = recurse
end
@ -128,26 +143,26 @@ module Packet
# @param authority [Array] Set of authority records to provide in the response
# @param additional [Array] Set of additional records to provide in the response
#
# @return [Net::DNS::Packet] Response packet
# @return [Dnsruby::Message] Response packet
def self.generate_response(request, answer = nil, authority = nil, additional = nil)
packet = self.encode_net(request)
packet = self.encode_drb(request)
packet.answer = answer if answer
packet.authority = authority if authority
packet.additional = additional if additional
packet = self.recalc_headers(packet)
# Set error code for NXDomain or unset it if reprocessing a response
if packet.header.anCount < 1
packet.header.rCode = 3
if packet.header.ancount < 1
packet.header.rcode = Dnsruby::RCode::NXDOMAIN
else
if packet.header.response? and packet.header.rCode.code == 3
packet.header.rCode = 0
if packet.header.qr and packet.header.get_header_rcode.to_i == 3
packet.header.rcode = Dnsruby::RCode::NOERROR
end
end
# Set response bit last to allow reprocessing of responses
packet.header.qr = 1
packet.header.qr = true
# Set recursion available bit if recursion desired
packet.header.ra = 1 if packet.header.recursive?
packet.header.ra = true if packet.header.rd
return packet
end

View File

@ -1,3 +1,5 @@
# -*- coding: binary -*-
require 'net/dns/resolver'
module Rex
@ -6,6 +8,7 @@ module DNS
##
# Provides Rex::Sockets compatible version of Net::DNS::Resolver
# Modified to work with Dnsruby::Messages, their resolvers are too heavy
##
class Resolver < Net::DNS::Resolver
@ -112,24 +115,25 @@ module DNS
# @param argument
# @param type [Fixnum] Type of record to look up
# @param cls [Fixnum] Class of question to look up
def send(argument,type=Net::DNS::A,cls=Net::DNS::IN)
def send(argument, type = Dnsruby::Types::A, cls = Dnsruby::Classes::IN)
if @config[:nameservers].size == 0
raise ResolverError, "No nameservers specified!"
end
method = self.use_tcp? ? :send_tcp : :send_udp
if argument.kind_of? Net::DNS::Packet
case argument
when Dnsruby::Message
packet = argument
elsif argument.kind_of? Resolv::DNS::Message
packet = Net::DNS::Packet.parse(argument.encode)
when Net::DNS::Packet, Resolv::DNS::Message
packet = Rex::Proto::DNS::Packet.encode_drb(argument)
else
packet = make_query_packet(argument,type,cls)
end
# Store packet_data for performance improvements,
# so methods don't keep on calling Packet#data
packet_data = packet.data
# so methods don't keep on calling Packet#encode
packet_data = packet.encode
packet_size = packet_data.size
# Choose whether use TCP, UDP
@ -146,7 +150,7 @@ module DNS
end
end
if type == Net::DNS::AXFR
if type == Dnsruby::Types::AXFR
@logger.warn "AXFR query, switching to TCP" unless method == :send_tcp
method = :send_tcp
end
@ -160,9 +164,10 @@ module DNS
end
@logger.info "Received #{ans[0].size} bytes from #{ans[1][2]+":"+ans[1][1].to_s}"
response = Net::DNS::Packet.parse(ans[0],ans[1])
# response = Net::DNS::Packet.parse(ans[0],ans[1])
response = Dnsruby::Message.decode(ans[0])
if response.header.truncated? and not ignore_truncated?
if response.header.tc and not ignore_truncated?
@logger.warn "Packet truncated, retrying using TCP"
self.use_tcp = true
begin
@ -215,7 +220,7 @@ module DNS
got_something = false
loop do
buffer = ""
ans = socket.recv(Net::DNS::INT16SZ)
ans = socket.recv(2)
if ans.size == 0
if got_something
break #Proper exit from loop
@ -305,8 +310,69 @@ module DNS
end
return ans
end
#
# Perform search using the configured searchlist and resolvers
#
# @param name
# @param type [Fixnum] Type of record to look up
# @param cls [Fixnum] Class of question to look up
#
# @return ans [Dnsruby::Message] DNS Response
def search(name, type = Dnsruby::Types::A, cls = Dnsruby::Classes::IN)
return query(name,type,cls) if name.class == IPAddr
# If the name contains at least one dot then try it as is first.
if name.include? "."
@logger.debug "Search(#{name},#{Dnsruby::Types.new(type)},#{Dnsruby::Classes.new(cls)})"
ans = query(name,type,cls)
return ans if ans.header.ancount > 0
end
# If the name doesn't end in a dot then apply the search list.
if name !~ /\.$/ and @config[:dns_search]
@config[:searchlist].each do |domain|
newname = name + "." + domain
@logger.debug "Search(#{newname},#{Dnsruby::Types.new(type)},#{Dnsruby::Classes.new(cls)})"
ans = query(newname,type,cls)
return ans if ans.header.ancount > 0
end
end
# Finally, if the name has no dots then try it as is.
@logger.debug "Search(#{name},#{Dnsruby::Types.new(type)},#{Dnsruby::Classes.new(cls)})"
return query(name+".",type,cls)
end
end
#
# Perform query with default domain validation
#
# @param name
# @param type [Fixnum] Type of record to look up
# @param cls [Fixnum] Class of question to look up
#
# @return ans [Dnsruby::Message] DNS Response
def query(name, type = Dnsruby::Types::A, cls = Dnsruby::Classes::IN)
return send(name,type,cls) if name.class == IPAddr
# If the name doesn't contain any dots then append the default domain.
if name !~ /\./ and name !~ /:/ and @config[:defname]
name += "." + @config[:domain]
end
@logger.debug "Query(#{name},#{Dnsruby::Types.new(type)},#{Dnsruby::Classes.new(cls)})"
return send(name,type,cls)
end
end
end
end

View File

@ -39,7 +39,7 @@ class Server
)
end.keys.map do |record|
if search.to_s.match(MATCH_HOSTNAME) and record.name == '*'
record = Net::DNS::RR.new(:name => search, :address => record.address)
record = Dnsruby::RR.create(name: name, type: type, address: address)
else
record
end
@ -49,10 +49,10 @@ class Server
#
# Add record to cache, only when "running"
#
# @param record [Net::DNS::RR] Record to cache
# @param record [Dnsruby::RR] Record to cache
def cache_record(record)
return unless @monitor_thread
if record.class.ancestors.include?(Net::DNS::RR) and
if record.is_a?(Dnsruby::RR) and
(!record.respond_to?(:address) or Rex::Socket.is_ip_addr?(record.address.to_s)) and
record.name.to_s.match(MATCH_HOSTNAME)
add(record, Time.now.to_i + record.ttl)
@ -73,7 +73,7 @@ class Server
find(name, type).each do |found|
delete(found)
end if replace
add(Net::DNS::RR.new(:name => name, :address => address),0)
add(Dnsruby::RR.create(name: name, type: type, address: address),0)
else
raise "Invalid parameters for static entry - #{name}, #{address}, #{type}"
end
@ -120,7 +120,7 @@ class Server
#
# Add a record to the cache with thread safety
#
# @param record [Net::DNS::RR] Record to add
# @param record [Dnsruby::RR] Record to add
# @param expire [Fixnum] Time in seconds when record becomes stale
def add(record, expire = 0)
self.lock.synchronize do
@ -131,7 +131,7 @@ class Server
#
# Delete a record from the cache with thread safety
#
# @param record [Net::DNS::RR] Record to delete
# @param record [Dnsruby::RR] Record to delete
def delete(record)
self.lock.synchronize do
self.records.delete(record)
@ -285,11 +285,12 @@ class Server
# @param cli [Rex::Socket::Tcp, Rex::Socket::Udp] Client sending the request
# @param data [String] raw DNS request data
def default_dispatch_request(cli,data)
req = Packet.encode_net(data)
return if data.strip.empty?
req = Packet.encode_drb(data)
forward = req.dup
# Find cached items, remove request from forwarded packet
req.question.each do |ques|
cached = self.cache.find(ques.qName, ques.qType.to_s)
cached = self.cache.find(ques.qname, ques.qtype.to_s)
if cached.empty?
next
else
@ -304,17 +305,32 @@ class Server
forwarded.answer.each do |ans|
self.cache.cache_record(ans)
end
req.header.ra = 1 # Set recursion bit
req.header.ra = true # Set recursion bit
end
# Finalize answers in response
# Check for empty response prior to sending
if req.answer.size < 1
req.header.rCode = 3
req.header.rCode = Dnsruby::RCode::NOERROR
end
req.header.qr = 1 # Set response bit
req.header.qr = true # Set response bit
send_response(cli, validate_packet(req).data)
end
#
# Returns the hardcore alias for the DNS service
#
def self.hardcore_alias(*args)
"#{(args[0] || '')}#{(args[1] || '')}"
end
#
# DNS server.
#
def alias
"DNS Server"
end
protected
#
# This method monitors the listener socket for new connections and calls

View File

@ -45,35 +45,37 @@ class MetasploitModule < Msf::Auxiliary
# Creates Proc to handle incoming requests
#
def on_dispatch_request(cli,data)
req = Packet.encode_net(data)
return if data.strip.empty?
req = Packet.encode_drb(data)
peer = "#{cli.peerhost}:#{cli.peerport}"
asked = req.question.map(&:qName).join(', ')
asked = req.question.map(&:qname).map(&:to_s).join(', ')
vprint_status("Received request for #{asked} from #{peer}")
answered = []
# Find cached items, remove request from forwarded packet
req.question.each do |ques|
cached = service.cache.find(ques.qName, ques.qType.to_s)
cached = service.cache.find(ques.qname, ques.qtype.to_s)
if cached.empty?
next
else
req.answer = (req.answer + cached).uniq
req.instance_variable_set(:@answer, (req.answer + cached).uniq)
answered << ques
cached.map do |hit|
if hit.respond_to?(:address)
hit.name + ':' + hit.address.to_s + ' ' + hit.type
hit.name.to_s + ':' + hit.address.to_s + ' ' + hit.type.to_s
else
hit.name + ' ' + hit.type
hit.name.to_s + ' ' + hit.type.to_s
end
end.each {|h| vprint_status("Cache hit for #{h}")}
end
end unless service.cache.nil?
# Forward remaining requests, cache responses
if answered.count < req.question.count and service.fwd_res
if !req.header.recursive?
if !req.header.rd
vprint_status("Recursion forbidden in query for #{req.question.first.name} from #{peer}")
else
forward = req.dup
forward.question = req.question - answered
# forward.question = req.question - answered
forward.instance_variable_set(:@question, req.question - answered)
forwarded = service.fwd_res.send(Packet.validate(forward))
forwarded.answer.each do |ans|
rstring = ans.respond_to?(:address) ? "#{ans.name}:#{ans.address}" : ans.name
@ -81,20 +83,20 @@ class MetasploitModule < Msf::Auxiliary
service.cache.cache_record(ans)
end unless service.cache.nil?
# Merge the answers and use the upstream response
forwarded.answer = (req.answer + forwarded.answer).uniq
forward.instance_variable_set(:@question, (req.answer + forwarded.answer).uniq)
req = forwarded
end
end
service.send_response(cli, Packet.validate(Packet.generate_response(req)).data)
service.send_response(cli, Packet.validate(Packet.generate_response(req)).encode)
end
#
# Creates Proc to handle outbound responses
#
def on_send_response(cli,data)
res = Packet.encode_net(data)
res = Packet.encode_drb(data)
peer = "#{cli.peerhost}:#{cli.peerport}"
asked = res.question.map(&:qName).join(', ')
asked = res.question.map(&:qname).map(&:to_s).join(', ')
vprint_status("Sending response for #{asked} to #{peer}")
cli.write(data)
end