c65c03722c
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.
379 lines
12 KiB
Ruby
379 lines
12 KiB
Ruby
# -*- coding: binary -*-
|
|
|
|
require 'net/dns/resolver'
|
|
|
|
module Rex
|
|
module Proto
|
|
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
|
|
|
|
Defaults = {
|
|
:config_file => "/dev/null", # default can lead to info leaks
|
|
:log_file => "/dev/null", # formerly $stdout, should be tied in with our loggers
|
|
:port => 53,
|
|
:searchlist => [],
|
|
:nameservers => [IPAddr.new("127.0.0.1")],
|
|
:domain => "",
|
|
:source_port => 0,
|
|
:source_address => IPAddr.new("0.0.0.0"),
|
|
:retry_interval => 5,
|
|
:retry_number => 4,
|
|
:recursive => true,
|
|
:defname => true,
|
|
:dns_search => true,
|
|
:use_tcp => false,
|
|
:ignore_truncated => false,
|
|
:packet_size => 512,
|
|
:tcp_timeout => TcpTimeout.new(30),
|
|
:udp_timeout => UdpTimeout.new(30),
|
|
:context => {},
|
|
:comm => nil
|
|
}
|
|
|
|
attr_accessor :context, :comm
|
|
#
|
|
# Provide override for initializer to use local Defaults constant
|
|
#
|
|
# @param config [Hash] Configuration options as conusumed by parent class
|
|
def initialize(config = {})
|
|
raise ResolverArgumentError, "Argument has to be Hash" unless config.kind_of? Hash
|
|
# config.key_downcase!
|
|
@config = Defaults.merge config
|
|
@raw = false
|
|
|
|
# New logger facility
|
|
@logger = Logger.new(@config[:log_file])
|
|
@logger.level = $DEBUG ? Logger::DEBUG : Logger::WARN
|
|
|
|
#------------------------------------------------------------
|
|
# Resolver configuration will be set in order from:
|
|
# 1) initialize arguments
|
|
# 2) ENV variables
|
|
# 3) config file
|
|
# 4) defaults (and /etc/resolv.conf for config)
|
|
#------------------------------------------------------------
|
|
|
|
|
|
|
|
#------------------------------------------------------------
|
|
# Parsing config file
|
|
#------------------------------------------------------------
|
|
parse_config_file
|
|
|
|
#------------------------------------------------------------
|
|
# Parsing ENV variables
|
|
#------------------------------------------------------------
|
|
parse_environment_variables
|
|
|
|
#------------------------------------------------------------
|
|
# Parsing arguments
|
|
#------------------------------------------------------------
|
|
comm = config.delete(:comm)
|
|
context = context = config.delete(:context)
|
|
config.each do |key,val|
|
|
next if key == :log_file or key == :config_file
|
|
begin
|
|
eval "self.#{key.to_s} = val"
|
|
rescue NoMethodError
|
|
raise ResolverArgumentError, "Option #{key} not valid"
|
|
end
|
|
end
|
|
end
|
|
#
|
|
# Provides current proxy setting if configured
|
|
#
|
|
# @return [String] Current proxy configuration
|
|
def proxies
|
|
@config[:proxies].inspect if @config[:proxies]
|
|
end
|
|
|
|
#
|
|
# Configure proxy setting and additional timeout
|
|
#
|
|
# @param prox [String] SOCKS proxy connection string
|
|
# @param timeout_added [Fixnum] Added TCP timeout to account for proxy
|
|
def proxies=(prox, timeout_added = 250)
|
|
return if prox.nil?
|
|
if prox.is_a?(String) and prox.strip =~ /^socks/i
|
|
@config[:proxies] = prox.strip
|
|
@config[:use_tcp] = true
|
|
self.tcp_timeout = self.tcp_timeout.to_s.to_i + timeout_added
|
|
@logger.info "SOCKS proxy set, using TCP, increasing timeout"
|
|
else
|
|
raise ResolverError, "Only socks proxies supported"
|
|
end
|
|
end
|
|
|
|
#
|
|
# Send DNS request over appropriate transport and process response
|
|
#
|
|
# @param argument
|
|
# @param type [Fixnum] Type of record to look up
|
|
# @param cls [Fixnum] Class of question to look up
|
|
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
|
|
|
|
case argument
|
|
when Dnsruby::Message
|
|
packet = argument
|
|
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#encode
|
|
packet_data = packet.encode
|
|
packet_size = packet_data.size
|
|
|
|
# Choose whether use TCP, UDP
|
|
if packet_size > @config[:packet_size] # Must use TCP
|
|
@logger.info "Sending #{packet_size} bytes using TCP due to size"
|
|
method = :send_tcp
|
|
else # Packet size is inside the boundaries
|
|
if use_tcp? or !(proxies.nil? or proxies.empty?) # User requested TCP
|
|
@logger.info "Sending #{packet_size} bytes using TCP due to tcp flag"
|
|
method = :send_tcp
|
|
else # Finally use UDP
|
|
@logger.info "Sending #{packet_size} bytes using UDP"
|
|
method = :send_udp unless method == :send_tcp
|
|
end
|
|
end
|
|
|
|
if type == Dnsruby::Types::AXFR
|
|
@logger.warn "AXFR query, switching to TCP" unless method == :send_tcp
|
|
method = :send_tcp
|
|
end
|
|
|
|
ans = self.__send__(method,packet_data)
|
|
|
|
unless (ans and ans[0].length > 0)
|
|
@logger.fatal "No response from nameservers list: aborting"
|
|
raise NoResponseError
|
|
return nil
|
|
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 = Dnsruby::Message.decode(ans[0])
|
|
|
|
if response.header.tc and not ignore_truncated?
|
|
@logger.warn "Packet truncated, retrying using TCP"
|
|
self.use_tcp = true
|
|
begin
|
|
return send(argument,type,cls)
|
|
ensure
|
|
self.use_tcp = false
|
|
end
|
|
end
|
|
|
|
return response
|
|
end
|
|
|
|
#
|
|
# Send request over TCP
|
|
#
|
|
# @param packet_data [String] Data segment of DNS request packet
|
|
# @param prox [String] Proxy configuration for TCP socket
|
|
#
|
|
# @return ans [String] Raw DNS reply
|
|
def send_tcp(packet_data,prox = @config[:proxies])
|
|
ans = nil
|
|
length = [packet_data.size].pack("n")
|
|
@config[:nameservers].each do |ns|
|
|
begin
|
|
socket = nil
|
|
@config[:tcp_timeout].timeout do
|
|
catch(:next_ns) do
|
|
begin
|
|
config = {
|
|
'PeerHost' => ns.to_s,
|
|
'PeerPort' => @config[:port].to_i,
|
|
'Proxies' => prox,
|
|
'Context' => @config[:context],
|
|
'Comm' => @config[:comm]
|
|
}
|
|
if @config[:source_port] > 0
|
|
config['LocalPort'] = @config[:source_port]
|
|
end
|
|
if @config[:source_host].to_s != '0.0.0.0'
|
|
config['LocalHost'] = @config[:source_host] unless @config[:source_host].nil?
|
|
end
|
|
socket = Rex::Socket::Tcp.create(config)
|
|
rescue
|
|
@logger.warn "TCP Socket could not be established to #{ns}:#{@config[:port]} #{@config[:proxies]}"
|
|
throw :next_ns
|
|
end
|
|
next unless socket #
|
|
@logger.info "Contacting nameserver #{ns} port #{@config[:port]}"
|
|
socket.write(length+packet_data)
|
|
got_something = false
|
|
loop do
|
|
buffer = ""
|
|
ans = socket.recv(2)
|
|
if ans.size == 0
|
|
if got_something
|
|
break #Proper exit from loop
|
|
else
|
|
@logger.warn "Connection reset to nameserver #{ns}, trying next."
|
|
throw :next_ns
|
|
end
|
|
end
|
|
got_something = true
|
|
len = ans.unpack("n")[0]
|
|
|
|
@logger.info "Receiving #{len} bytes..."
|
|
|
|
if len.nil? or len == 0
|
|
@logger.warn "Receiving 0 length packet from nameserver #{ns}, trying next."
|
|
throw :next_ns
|
|
end
|
|
|
|
while (buffer.size < len)
|
|
left = len - buffer.size
|
|
temp,from = socket.recvfrom(left)
|
|
buffer += temp
|
|
end
|
|
|
|
unless buffer.size == len
|
|
@logger.warn "Malformed packet from nameserver #{ns}, trying next."
|
|
throw :next_ns
|
|
end
|
|
if block_given?
|
|
yield [buffer,["",@config[:port],ns.to_s,ns.to_s]]
|
|
else
|
|
return [buffer,["",@config[:port],ns.to_s,ns.to_s]]
|
|
end
|
|
end
|
|
end
|
|
end
|
|
rescue Timeout::Error
|
|
@logger.warn "Nameserver #{ns} not responding within TCP timeout, trying next one"
|
|
next
|
|
ensure
|
|
socket.close if socket
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
#
|
|
# Send request over UDP
|
|
#
|
|
# @param packet_data [String] Data segment of DNS request packet
|
|
#
|
|
# @return ans [String] Raw DNS reply
|
|
def send_udp(packet_data)
|
|
ans = nil
|
|
response = ""
|
|
@config[:nameservers].each do |ns|
|
|
begin
|
|
@config[:udp_timeout].timeout do
|
|
begin
|
|
config = {
|
|
'PeerHost' => ns.to_s,
|
|
'PeerPort' => @config[:port].to_i,
|
|
'Context' => @config[:context],
|
|
'Comm' => @config[:comm]
|
|
}
|
|
if @config[:source_port] > 0
|
|
config['LocalPort'] = @config[:source_port]
|
|
end
|
|
if @config[:source_host] != IPAddr.new('0.0.0.0')
|
|
config['LocalHost'] = @config[:source_host] unless @config[:source_host].nil?
|
|
end
|
|
socket = Rex::Socket::Udp.create(config)
|
|
rescue
|
|
@logger.warn "UDP Socket could not be established to #{ns}:#{@config[:port]}"
|
|
return nil
|
|
end
|
|
@logger.info "Contacting nameserver #{ns} port #{@config[:port]}"
|
|
#socket.sendto(packet_data, ns.to_s, @config[:port].to_i, 0)
|
|
socket.write(packet_data)
|
|
ans = socket.recvfrom(@config[:packet_size])
|
|
end
|
|
break if ans
|
|
rescue Timeout::Error
|
|
@logger.warn "Nameserver #{ns} not responding within UDP timeout, trying next one"
|
|
next
|
|
end
|
|
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
|