Land #6611, add native DNS to Rex, MSF mixin, sample modules

4.x
Brent Cook 2018-01-22 23:54:11 -06:00 committed by Jeffrey Martin
parent 5ec3da843e
commit d6beb94c59
No known key found for this signature in database
GPG Key ID: 0CD9BBC2AF15F171
16 changed files with 2061 additions and 123 deletions

View File

@ -0,0 +1,20 @@
# -*- coding: binary -*-
require 'msf/core'
require 'rex/proto/dns'
module Msf
###
#
# This namespace exposes methods for interacting with and providing services
#
###
module Exploit::Remote::DNS
end
end
require 'msf/core/exploit/dns/common'
require 'msf/core/exploit/dns/client'
require 'msf/core/exploit/dns/server'

View File

@ -0,0 +1,217 @@
# -*- coding: binary -*-
require 'msf/core'
require 'rex/proto/dns'
module Msf
###
#
# This module exposes methods for querying a remote DNS service
#
###
module Exploit::Remote::DNS
module Client
include Common
include Exploit::Remote::Udp
include Exploit::Remote::Tcp
#
# Initializes an exploit module that interacts with a DNS server.
#
def initialize(info = {})
super
deregister_options('RHOST')
register_options(
[
Opt::RPORT(53),
Opt::Proxies,
OptString.new('DOMAIN', [ false, "The target domain name"]),
OptString.new('NS', [ false, "Specify the nameservers to use for queries, space separated" ]),
OptString.new('SEARCHLIST', [ false, "DNS domain search list, comma separated"]),
OptInt.new('THREADS', [true, "Number of threads to use in threaded queries", 1])
], Exploit::Remote::DNS::Client
)
register_advanced_options(
[
OptString.new('DnsClientDefaultNS', [ false, "Specify the default to use for queries, space separated", '8.8.8.8 8.8.4.4' ]),
OptInt.new('DnsClientRetry', [ false, "Number of times to try to resolve a record if no response is received", 2]),
OptInt.new('DnsClientRetryInterval', [ false, "Number of seconds to wait before doing a retry", 2]),
OptBool.new('DnsClientReportARecords', [false, "Add hosts found via BRT and RVL to DB", true]),
OptBool.new('DnsClientRVLExistingOnly', [false, "Only perform lookups on hosts in DB", true]),
OptBool.new('DnsClientTcpDns', [false, "Run queries over TCP", false]),
OptPath.new('DnsClientResolvconf', [true, "Resolvconf formatted configuration file to use for Resolver", "/dev/null"])
], Exploit::Remote::DNS::Client
)
register_autofilter_ports([ 53 ]) if respond_to?(:register_autofilter_ports)
register_autofilter_services(%W{ dns }) if respond_to?(:register_autofilter_services)
end
#
# Convenience wrapper around Resolver's query method - send DNS request
#
# @param domain [String] Domain for which to request a record
# @param type [String] Type of record to request for domain
#
# @return [Dnsruby::RR] DNS response
def query(domain = datastore['DOMAIN'], type = 'A')
client.query(domain, type)
end
#
# Performs a set of asynchronous lookups for an array of domain,type pairs
#
# @param queries [Array] Set of domain,type pairs to pass into #query
# @param threadmax [Fixnum] Max number of running threads at a time
# @param block [Proc] Code block to execute with the query result
#
# @return [Array] Resulting set of responses or responses processed by passed blocks
def query_async(queries = [], threadmax = datastore['THREADS'], &block)
running = []
while !queries.empty?
domain, type = queries.shift
running << framework.threads.spawn("Module(#{self.refname})-#{domain} #{type}", false) do |qat|
if block
block.call(query(domain,type))
else
query(domain,type)
end
end
while running.select(&:alive?).count >= threadmax
Rex::ThreadSafe.sleep(1)
end
end
return running.join
end
#
# Switch DNS forwarders in resolver with thread safety
#
# @param ns [Array, String] List of (or single) nameservers to use
def set_nameserver(ns = [])
if ns.respond_to?(:split)
ns = [ns]
end
@lock.synchronize do
@dns_resolver.nameserver = ns.flatten
end
end
#
# Switch nameservers to use explicit NS or SOA for target
#
# @param domain [String] Domain for which to find SOA
def switchdns(domain)
if datastore['NS'].blank?
resp_soa = client.query(target, "SOA")
if (resp_soa)
(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))
print_status("Set DNS Server to #{target} NS: #{client.nameserver.join(', ')}")
break
end
end
end
else
vprint_status("Using DNS Server: #{client.nameserver.join(', ')}")
client.nameserver = process_nameservers
end
end
#
# Detect if target has wildcards enabled for a record type
#
# @param target [String] Domain to test
# @param type [String] Record type to test
#
# @return [String] Address which is returned for wildcard requests
def wildcard(domain, type = "A")
addr = false
rendsub = rand(10000).to_s
response = query("#{rendsub}.#{target}", type)
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 != Dnsruby::RR::CNAME
addr = rr.address.to_s
end
end
return addr
end
#
# Create and configure Resolver object
#
def setup_resolver
options.validate(datastore) # This is a hack, DS values should not be Strings prior to this
config = {
:config_file => datastore['DnsClientResolvconf'],
:nameservers => process_nameservers,
:port => datastore['RPORT'],
:retry_number => datastore['DnsClientRetry'].to_i,
:retry_interval => datastore['DnsClientRetryInterval'].to_i,
:use_tcp => datastore['DnsClientTcpDns'],
:context => {'Msf' => framework, 'MsfExploit' => self}
}
if datastore['SEARCHLIST']
if datastore['SEARCHLIST'].split(',').all? do |search|
search.match(MATCH_HOSTNAME)
end
config[:search_list] = datastore['SEARCHLIST'].split(',')
else
raise 'Domain search list must consist of valid domains'
end
end
if datastore['CHOST']
config[:source_address] = IPAddr.new(datastore['CHOST'].to_s)
end
if datastore['CPORT']
config[:source_port] = datastore['CPORT'] unless datastore['CPORT'] == 0
end
if datastore['Proxies']
vprint_status("Using DNS/TCP resolution for proxy config")
config[:use_tcp] = true
config[:proxies] = datastore['Proxies']
end
@dns_resolver_lock = Mutex.new unless @dns_resolver_lock
@dns_resolver = Rex::Proto::DNS::Resolver.new(config)
end
#
# Convenience method for DNS resolver as client
# Executes setup_resolver if none exists
#
def client
setup_resolver unless @dns_resolver
@dns_resolver
end
#
# Sets the resolver's nameservers
# Uses explicitly defined NS option if set
# Uses RHOSTS if not explicitly defined
def process_nameservers
if datastore['NS'].blank?
nameservers = datastore['DnsClientDefaultNS'].split(/\s|,/)
else
nameservers = datastore['NS'].split(/\s|,/)
end
invalid = nameservers.select { |ns| !Rex::Socket.dotted_ip?(ns) }
if !invalid.empty?
raise "Nameservers must be IP addresses. The following were invalid: #{invalid.join(", ")}"
end
nameservers
end
end
end
end

View File

@ -0,0 +1,22 @@
# -*- coding: binary -*-
require 'msf/core'
require 'rex/proto/dns'
module Msf
###
#
# This module exposes methods for querying a remote DNS service
#
###
module Exploit::Remote::DNS
module Common
MATCH_HOSTNAME = Rex::Proto::DNS::Constants::MATCH_HOSTNAME
Packet = Rex::Proto::DNS::Packet
end
end
end

View File

@ -0,0 +1,163 @@
# -*- coding: binary -*-
require 'msf/core'
require 'rex/proto/dns'
require 'msf/core/exploit/dns/common'
module Msf
###
#
# This module exposes methods for querying a remote DNS service
#
###
module Exploit::Remote::DNS
module Server
include Exploit::Remote::DNS::Common
include Exploit::Remote::SocketServer
#
# Initializes an exploit module that serves DNS requests
#
def initialize(info = {})
super
register_options(
[
OptPort.new('SRVPORT', [true, 'The local port to listen on.', 53]),
OptString.new('STATIC_ENTRIES', [ false, "DNS domain search list (hosts file or space/semicolon separate entries)"]),
OptBool.new('DISABLE_RESOLVER', [ false, "Disable DNS request forwarding", false]),
OptBool.new('DISABLE_NS_CACHE', [ false, "Disable DNS response caching", false])
], Exploit::Remote::DNS::Server
)
register_advanced_options(
[
OptBool.new('DnsServerUdp', [true, "Serve UDP DNS requests", true]),
OptBool.new('DnsServerTcp', [true, "Serve TCP DNS requests", false])
], Exploit::Remote::DNS::Server
)
end
attr_accessor :service # :nodoc:
#
# Process static entries
#
# @param entries [String] Filename or String containing static entries
# @param type [String] Type of record for which to add static entries
#
# @return [Array] List of static entries in the cache
def add_static_hosts(entries = datastore['STATIC_ENTRIES'], type = 'A')
return if entries.nil? or entries.empty?
if File.file?(File.expand_path(entries))
data = File.read(File.expand_path(entries)).split("\n")
else
data = entries.split(';')
end
data.each do |entry|
next if entry.gsub(/\s/,'').empty?
addr, names = entry.split(' ', 2)
names.split.each do |name|
name << '.' unless name[-1] == '.' or name == '*'
service.cache.add_static(name, addr, type)
end
end
service.cache.records.select {|r,e| e == 0}
end
#
# Flush all static entries
#
def flush_static_hosts
data.cache.records.select {|r,e| e == 0}.each do |flush|
data.cache.delete(flush)
end
end
#
# Flush cache entries
# @param static [TrueClass, FalseClass] flush static hosts
def flush_cache(static = false)
self.service.cache.stop(true)
flush_static_hosts if static
self.service.cache.start
end
#
# Handle incoming requests
# Override this method in modules to take flow control
#
def on_dispatch_request(cli, data)
service.default_dispatch_request(cli,data)
end
#
# Handle incoming requests
# Override this method in modules to take flow control
#
def on_send_response(cli, data)
cli.write(data)
end
#
# Starts the server
#
def start_service
begin
comm = _determine_server_comm
self.service = Rex::ServiceManager.start(
Rex::Proto::DNS::Server,
datastore['SRVHOST'],
datastore['SRVPORT'],
datastore['DnsServerUdp'],
datastore['DnsServerTcp'],
(use_resolver? ? setup_resolver : false),
comm,
{'Msf' => framework, 'MsfExploit' => self}
)
self.service.dispatch_request_proc = Proc.new do |cli, data|
on_dispatch_request(cli,data)
end
self.service.send_response_proc = Proc.new do |cli, data|
on_send_response(cli,data)
end
add_static_hosts
self.service.start(!datastore['DISABLE_NS_CACHE'])
rescue ::Errno::EACCES => e
raise Rex::BindFailed.new(e.message)
end
end
#
# Stops the server
# @param destroy [TrueClass,FalseClass] Dereference the server object
def stop_service(destroy = false)
Rex::ServiceManager.stop_service(self.service) if self.service
if destroy
@dns_resolver = nil if @dns_resolver
self.service = nil if self.service
end
end
#
# Resets the DNS server
#
def reset_service
stop_service(true)
start_service
end
#
# Determines if resolver is available and configured for use
#
def use_resolver?
!datastore['DISABLE_RESOLVER'] and self.respond_to?(:setup_resolver)
end
end
end
end

View File

@ -0,0 +1,172 @@
# -*- coding: binary -*-
module Msf
###
#
# This mixin provides a generic interface for running a socket server of some
# sort that is designed to exploit clients. Exploits that include this mixin
# automatically take a passive stance.
#
###
module Exploit::Remote::SocketServer
def initialize(info = {})
super(update_info(info,
'Stance' => Msf::Exploit::Stance::Passive))
register_options(
[
OptAddress.new('SRVHOST', [ true, "The local host to listen on. This must be an address on the local machine or 0.0.0.0", '0.0.0.0' ]),
OptPort.new('SRVPORT', [ true, "The local port to listen on.", 8080 ]),
], Msf::Exploit::Remote::SocketServer
)
register_advanced_options(
[
OptString.new('ListenerComm', [ false, 'The specific communication channel to use for this service'])
], Msf::Exploit::Remote::SocketServer
)
end
#
# This mixin overrides the exploit method so that it can initiate the
# service that corresponds with what the client has requested.
#
def exploit
start_service()
print_status("Server started.")
# Call the exploit primer
primer
# Wait on the service to stop
self.service.wait
end
#
# Primer method to call after starting service but before handling connections
#
def primer
end
#
# Stops the service, if one was created.
#
def cleanup
super
if(service)
stop_service()
print_status("Server stopped.")
end
end
#
# Called when a client has data available for reading.
#
def on_client_data(client)
end
#
# Starts the service. Override this method in consumers
#
def start_service(*args)
end
#
# Stops the service.
#
def stop_service
if (service)
begin
self.service.deref if self.service.kind_of?(Rex::Service)
if self.service.kind_of?(Rex::Socket)
self.service.close
self.service.stop
end
self.service = nil
rescue ::Exception
end
end
end
#
# Returns the local host that is being listened on.
#
def srvhost
datastore['SRVHOST']
end
#
# Returns the local port that is being listened on.
#
def srvport
datastore['SRVPORT']
end
#
# Re-generates the payload, substituting the current RHOST and RPORT with
# the supplied client host and port from the socket.
#
def regenerate_payload(cli, arch = nil, platform = nil, target = nil)
ohost = datastore['RHOST']
oport = datastore['RPORT']
p = nil
begin
# Update the datastore with the supplied client peerhost/peerport
datastore['RHOST'] = cli.peerhost
datastore['RPORT'] = cli.peerport
if ((p = super(arch, platform, target)) == nil)
print_error("Failed to generate payload")
return nil
end
# Allow the payload to start a new handler
add_handler({
'RHOST' => datastore['RHOST'],
'RPORT' => datastore['RPORT']
})
ensure
datastore['RHOST'] = ohost
datastore['RPORT'] = oport
end
p
end
protected
#
# Determines appropriate listener comm
#
def _determine_server_comm(srv_comm = datastore['ListenerComm'].to_s)
case srv_comm
when 'local'
comm = ::Rex::Socket::Comm::Local
when /\A[0-9]+\Z/
comm = framework.sessions[srv_comm.to_i]
raise(RuntimeError, "Socket Server Comm (Session #{srv_comm}) does not exist") unless comm
raise(RuntimeError, "Socket Server Comm (Session #{srv_comm}) does not implement Rex::Socket::Comm") unless comm.is_a? ::Rex::Socket::Comm
when nil, ''
comm = nil
else
raise(RuntimeError, "SocketServer Comm '#{srv_comm}' is invalid")
end
comm
end
attr_accessor :service # :nodoc:
end
end

View File

@ -1,5 +1,7 @@
# -*- coding: binary -*-
require 'msf/core/exploit/socket_server'
module Msf
###
@ -10,20 +12,18 @@ module Msf
#
###
module Exploit::Remote::TcpServer
include Exploit::Remote::SocketServer
def initialize(info = {})
super(update_info(info,
'Stance' => Msf::Exploit::Stance::Passive))
super
register_options(
[
OptBool.new('SSL', [ false, 'Negotiate SSL for incoming connections', false]),
# SSLVersion is currently unsupported for TCP servers (only supported by clients at the moment)
OptPath.new('SSLCert', [ false, 'Path to a custom SSL certificate (default is randomly generated)']),
OptAddress.new('SRVHOST', [ true, "The local host to listen on. This must be an address on the local machine or 0.0.0.0", '0.0.0.0' ]),
OptPort.new('SRVPORT', [ true, "The local port to listen on.", 8080 ]),
], Msf::Exploit::Remote::TcpServer)
OptPath.new('SSLCert', [ false, 'Path to a custom SSL certificate (default is randomly generated)'])
], Msf::Exploit::Remote::TcpServer
)
register_advanced_options(
[
@ -40,51 +40,11 @@ module Exploit::Remote::TcpServer
)
end
#
# This mixin overrides the exploit method so that it can initiate the
# service that corresponds with what the client has requested.
#
def exploit
start_service()
print_status("Server started.")
# Call the exploit primer
primer
# Wait on the service to stop
self.service.wait
end
#
# Primer method to call after starting service but before handling connections
#
def primer
end
#
# Stops the service, if one was created.
#
def cleanup
super
if(service)
stop_service()
print_status("Server stopped.")
end
end
#
# Called when a client connects.
#
def on_client_connect(client)
end
#
# Called when a client has data available for reading.
#
def on_client_data(client)
end
#
# Called when a client has disconnected.
#
@ -97,12 +57,7 @@ module Exploit::Remote::TcpServer
def start_service(*args)
begin
comm = datastore['ListenerComm']
if comm == "local"
comm = ::Rex::Socket::Comm::Local
else
comm = nil
end
comm = _determine_server_comm
self.service = Rex::Socket::TcpServer.create(
'LocalHost' => srvhost,
@ -151,38 +106,6 @@ module Exploit::Remote::TcpServer
end
end
#
# Stops the service.
#
def stop_service
if (service)
begin
self.service.deref if self.service.kind_of?(Rex::Service)
if self.service.kind_of?(Rex::Socket)
self.service.close
self.service.stop
end
self.service = nil
rescue ::Exception
end
end
end
#
# Returns the local host that is being listened on.
#
def srvhost
datastore['SRVHOST']
end
#
# Returns the local port that is being listened on.
#
def srvport
datastore['SRVPORT']
end
#
# Returns the SSL option
#
@ -209,44 +132,6 @@ module Exploit::Remote::TcpServer
datastore['SSLCompression']
end
#
# Re-generates the payload, substituting the current RHOST and RPORT with
# the supplied client host and port from the socket.
#
def regenerate_payload(cli, arch = nil, platform = nil, target = nil)
ohost = datastore['RHOST']
oport = datastore['RPORT']
p = nil
begin
# Update the datastore with the supplied client peerhost/peerport
datastore['RHOST'] = cli.peerhost
datastore['RPORT'] = cli.peerport
if ((p = super(arch, platform, target)) == nil)
print_error("Failed to generate payload")
return nil
end
# Allow the payload to start a new handler
add_handler({
'RHOST' => datastore['RHOST'],
'RPORT' => datastore['RPORT']
})
ensure
datastore['RHOST'] = ohost
datastore['RPORT'] = oport
end
p
end
protected
attr_accessor :service # :nodoc:
end
end

View File

@ -184,6 +184,7 @@ module Net # :nodoc:
nscount += 1
end
@additional.each do |rr|
next if rr.nil?
data += rr.data#(data.length)
arcount += 1
end

94
lib/rex/io/gram_server.rb Normal file
View File

@ -0,0 +1,94 @@
# -*- coding: binary -*-
require 'thread'
module Rex
module IO
###
#
# This mixin provides the framework and interface for implementing a datagram
# server that can handle incoming datagrams. Datagram servers include this mixin
#
###
module GramServer
##
#
# Abstract methods
#
##
##
#
# Default server monitoring and client management implementation follows
# below.
#
##
#
# This callback is notified when a client connection has data that needs to
# be processed.
#
def dispatch_request(client, data)
if (dispatch_request_proc)
dispatch_request_proc.call(client, data)
end
end
#
# This callback is notified when data must be returned to the client
# @param client [Socket] Client/Socket to receive data
# @param data [String] Data to be sent to client/socket
def send_response(client, data)
if (send_response_proc)
send_response_proc.call(client, data)
else
client.write(data)
end
end
#
# Start monitoring the listener socket for connections and keep track of
# all client connections.
#
def start
self.listener_thread = Rex::ThreadFactory.spawn("GramServerListener", false) {
monitor_listener
}
end
#
# Terminates the listener monitoring threads and closes all active clients.
#
def stop
self.listener_thread.kill
end
#
# This method waits on the server listener thread
#
def wait
self.listener_thread.join if self.listener_thread
end
##
#
# Callback procedures.
#
##
#
# This callback procedure can be set and will be called when clients
# have data to be processed.
#
attr_accessor :dispatch_request_proc, :send_response_proc
attr_accessor :listener_thread# :nodoc:
end
end
end

View File

@ -250,6 +250,13 @@ class Channel
written.nil? ? 0 : written.value
end
#
# Wrapper around check for self.cid
#
def closed?
self.cid.nil?
end
#
# Wrapper around the low-level close.
#

View File

@ -9,6 +9,7 @@ require 'rex/proto/kerberos'
require 'rex/proto/rmi'
require 'rex/proto/sms'
require 'rex/proto/mms'
require 'rex/proto/dns'
module Rex
module Proto

17
lib/rex/proto/dns.rb Normal file
View File

@ -0,0 +1,17 @@
# -*- coding: binary -*-
module Rex
module Proto
module DNS
module Constants
MATCH_HOSTNAME=/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)+([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9]\.*)$/
end
end
end
end
require 'rex/proto/dns/packet'
require 'rex/proto/dns/resolver'
require 'rex/proto/dns/server'

315
lib/rex/proto/dns/packet.rb Normal file
View File

@ -0,0 +1,315 @@
# -*- coding: binary -*-
require 'net/dns'
require 'resolv'
require 'dnsruby'
module Rex
module Proto
module DNS
module Packet
#
# Checks string to ensure it can be used as a valid hostname
#
# @param subject [String] Subject name to check
#
# @return [TrueClass,FalseClass] Disposition on name match
def self.valid_hostname?(subject = '')
!subject.match(Rex::Proto::DNS::Constants::MATCH_HOSTNAME).nil?
end
#
# Reconstructs a packet with both standard DNS libraries
# Ensures that headers match the payload
#
# @param packet [String, Net::DNS::Packet, Dnsruby::Message] Data to be validated
#
# @return [Dnsruby::Message]
def self.validate(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, Dnsruby::Message]
#
# @return [Dnsruby::Message]
def self.recalc_headers(packet)
packet = self.encode_drb(packet)
{
:qdcount= => :question,
:ancount= => :answer,
:nscount= => :authority,
:arcount= => :additional
}.each do |header,body|
packet.header.send(header,packet.send(body).count)
end
return packet
end
#
# Reads a packet into the Net::DNS::Packet format
#
# @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.is_a?(Net::DNS::Packet)
Net::DNS::Packet.parse(
self.encode_raw(packet)
)
end
# Reads a packet into the Resolv::DNS::Message format
#
# @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.is_a?(Resolv::DNS::Message)
Resolv::DNS::Message.decode(
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, Dnsruby::Message] Input data
#
# @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).force_encoding('binary')
end
#
# Generates a request packet, taken from Net::DNS::Resolver
#
# @param subject [String] Subject name of question section
# @param type [Fixnum] Type of DNS record to query
# @param cls [Fixnum] Class of dns record to query
# @param recurse [Fixnum] Recursive query or not
#
# @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 = 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 = Dnsruby::Types::PTR
rescue ArgumentError
name = subject if self.valid_hostname?(subject)
end
else
name = subject if self.valid_hostname?(subject)
end
# Create the packet
packet = Dnsruby::Message.new(name, type, cls)
if packet.header.opcode == Dnsruby::OpCode::Query
packet.header.recursive = recurse
end
# DNSSEC and TSIG stuff to be inserted here
return packet
end
#
# Generates a response packet for an existing request
#
# @param request [String] Net::DNS::Packet, Resolv::DNS::Message] Original request
# @param answer [Array] Set of answers to provide in the response
# @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 [Dnsruby::Message] Response packet
def self.generate_response(request, answer = nil, authority = nil, additional = nil)
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 = Dnsruby::RCode::NXDOMAIN
else
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 = true
# Set recursion available bit if recursion desired
packet.header.ra = true if packet.header.rd
return packet
end
module Raw
#
# Convert data to little endian unsigned short
#
# @param data [Fixnum, Float, Array] Input for conversion
#
# @return [String] Raw output
def self.to_short_le(data)
[data].flatten.pack('S*')
end
#
# Convert data from little endian unsigned short
#
# @param data [String] Input for conversion
#
# @return [Array] Integer array output
def self.from_short_le(data)
data.unpack('S*')
end
#
# Convert data to little endian unsigned int
#
# @param data [Fixnum, Float, Array] Input for conversion
#
# @return [String] Raw output
def self.to_int_le(data)
[data].flatten.pack('I*')
end
#
# Convert data from little endian unsigned int
#
# @param data [String] Input for conversion
#
# @return [Array] Integer array output
def self.from_int_le(data)
data.unpack('I*')
end
#
# Convert data to little endian unsigned long
#
# @param data [Fixnum, Float, Array] Input for conversion
#
# @return [String] Raw output
def self.to_long_le(data)
[data].flatten.pack('L*')
end
#
# Convert data from little endian unsigned long
#
# @param data [String] Input for conversion
#
# @return [Array] Integer array output
def self.from_long_le(data)
data.unpack('L*')
end
#
# Convert data to big endian unsigned short
#
# @param data [Fixnum, Float, Array] Input for conversion
#
# @return [String] Raw output
def self.to_short_be(data)
[data].flatten.pack('S>*')
end
#
# Convert data from big endian unsigned short
#
# @param data [String] Input for conversion
#
# @return [Array] Integer array output
def self.from_short_be(data)
data.unpack('S>*')
end
#
# Convert data to big endian unsigned int
#
# @param data [Fixnum, Float, Array] Input for conversion
#
# @return [String] Raw output
def self.to_int_be(data)
[data].flatten.pack('I>*')
end
#
# Convert data from big endian unsigned int
#
# @param data [String] Input for conversion
#
# @return [Array] Integer array output
def self.from_int_be(data)
data.unpack('I>*')
end
#
# Convert data to big endian unsigned long
#
# @param data [Fixnum, Float, Array] Input for conversion
#
# @return [String] Raw output
def self.to_long_be(data)
[data].flatten.pack('L>*')
end
#
# Convert data from big endian unsigned long
#
# @param data [String] Input for conversion
#
# @return [Array] Integer array output
def self.from_long_be(data)
data.unpack('L>*')
end
#
# Returns request ID from raw packet skipping parsing
#
# @param data [String] Request data
#
# @return [Fixnum] Request ID
def self.request_id(data)
self.from_short_be(data[0..1])[0]
end
#
# Returns request length from raw packet skipping parsing
#
# @param data [String] Request data
#
# @return [Fixnum] Request Length
def self.request_length(data)
self.from_short_le(data[0..2])[0]
end
end
end
end
end
end

View File

@ -0,0 +1,378 @@
# -*- 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

377
lib/rex/proto/dns/server.rb Normal file
View File

@ -0,0 +1,377 @@
# -*- coding: binary -*-
require 'rex/io/gram_server'
require 'rex/socket'
require 'rex/proto/dns'
module Rex
module Proto
module DNS
class Server
class Cache
attr_reader :records, :lock, :monitor_thread
include Rex::Proto::DNS::Constants
# class DNSRecordError < ::Exception
#
# Create DNS Server cache
#
def initialize
@records = {}
@lock = Mutex.new
end
#
# Find entries in cache, substituting names for '*' in return
#
# @param search [String] Name or address to search for
# @param type [String] Record type to search for
#
# @return [Array] Records found
def find(search, type = 'A')
self.records.select do |record,expire|
record.type == type and (expire < 1 or expire > Time.now.to_i) and
(
record.name == '*' or
record.name == search or record.name[0..-2] == search or
( record.respond_to?(:address) and record.address.to_s == search )
)
end.keys.map do |record|
if search.to_s.match(MATCH_HOSTNAME) and record.name == '*'
record = Dnsruby::RR.create(name: name, type: type, address: address)
else
record
end
end
end
#
# Add record to cache, only when "running"
#
# @param record [Dnsruby::RR] Record to cache
def cache_record(record)
return unless @monitor_thread
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)
else
raise "Invalid record for cache entry - #{record.inspect}"
end
end
#
# Add static record to cache
#
# @param name [String] Name of record
# @param address [String] Address of record
# @param type [String] Record type to add
def add_static(name, address, type = 'A', replace = false)
if Rex::Socket.is_ip_addr?(address.to_s) and
( name.to_s.match(MATCH_HOSTNAME) or name == '*')
find(name, type).each do |found|
delete(found)
end if replace
add(Dnsruby::RR.create(name: name, type: type, address: address),0)
else
raise "Invalid parameters for static entry - #{name}, #{address}, #{type}"
end
end
#
# Prune cache entries
#
# @param before [Fixnum] Time in seconds before which records are evicted
def prune(before = Time.now.to_i)
self.records.select do |rec, expire|
expire > 0 and expire < before
end.each {|rec, exp| delete(rec)}
end
#
# Start the cache monitor
#
def start
@monitor_thread = Rex::ThreadFactory.spawn("DNSServerCacheMonitor", false) {
while true
prune
Rex::ThreadSafe.sleep(0.5)
end
} unless @monitor_thread
end
#
# Stop the cache monitor
#
# @param flush [TrueClass,FalseClass] Remove non-static entries
def stop(flush = false)
self.monitor_thread.kill unless @monitor_thread.nil?
@monitor_thread = nil
if flush
self.records.select do |rec, expire|
rec.ttl > 0
end.each {|rec| delete(rec)}
end
end
protected
#
# Add a record to the cache with thread safety
#
# @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
self.records[record] = expire
end
end
#
# Delete a record from the cache with thread safety
#
# @param record [Dnsruby::RR] Record to delete
def delete(record)
self.lock.synchronize do
self.records.delete(record)
end
end
end # Cache
class MockDnsClient
attr_reader :peerhost, :peerport, :srvsock
#
# Create mock DNS client
#
# @param host [String] PeerHost IP address
# @param port [Fixnum] PeerPort integer
def initialize(host, port, sock)
@peerhost = host
@peerport = port
@srvsock = sock
end
#
# Test method to prevent GC/ObjectSpace abuse via class lookups
#
def mock_dns_client?
true
end
def write(data)
srvsock.sendto(data, peerhost, peerport)
end
end
include Rex::IO::GramServer
Packet = Rex::Proto::DNS::Packet
#
# Create DNS Server
#
# @param lhost [String] Listener address
# @param lport [Fixnum] Listener port
# @param udp [TrueClass, FalseClass] Listen on UDP socket
# @param tcp [TrueClass, FalseClass] Listen on TCP socket
# @param res [Rex::Proto::DNS::Resolver] Resolver to use, nil to create a fresh one
# @param ctx [Hash] Framework context for sockets
# @param dblock [Proc] Handler for :dispatch_request flow control interception
# @param sblock [Proc] Handler for :send_response flow control interception
#
# @return [Rex::Proto::DNS::Server] DNS Server object
attr_accessor :serve_tcp, :serve_udp, :fwd_res, :cache
attr_reader :serve_udp, :serve_tcp, :sock_options, :lock, :udp_sock, :tcp_sock
def initialize(lhost = '0.0.0.0', lport = 53, udp = true, tcp = false, res = nil, comm = nil, ctx = {}, dblock = nil, sblock = nil)
@serve_udp = udp
@serve_tcp = tcp
@sock_options = {
'LocalHost' => lhost,
'LocalPort' => lport,
'Context' => ctx,
'Comm' => comm
}
self.fwd_res = res.nil? ? Rex::Proto::DNS::Resolver.new(:comm => comm, :context => ctx) : res
self.listener_thread = nil
self.dispatch_request_proc = dblock
self.send_response_proc = sblock
self.cache = Cache.new
@lock = Mutex.new
end
#
# Switch DNS forwarders in resolver with thread safety
#
# @param ns [Array, String] List of (or single) nameservers to use
def switchns(ns = [])
if ns.respond_to?(:split)
ns = [ns]
end
self.lock.synchronize do
self.fwd_res.nameserver = ns
end
end
#
# Check if server is running
#
def running?
self.listener_thread and self.listener_thread.alive?
end
#
# Start the DNS server and cache
# @param start_cache [TrueClass, FalseClass] stop the cache
def start(start_cache = true)
if self.serve_udp
@udp_sock = Rex::Socket::Udp.create(self.sock_options)
self.listener_thread = Rex::ThreadFactory.spawn("UDPDNSServerListener", false) {
monitor_listener
}
end
if self.serve_tcp
@tcp_sock = Rex::Socket::TcpServer.create(self.sock_options)
self.tcp_sock.on_client_data_proc = Proc.new { |cli|
on_client_data(cli)
}
self.tcp_sock.start
if !self.serve_udp
self.listener_thread = tcp_sock.listener_thread
end
end
self.cache.start if start_cache
end
#
# Stop the DNS server and cache
#
# @param flush_cache [TrueClass,FalseClass] Flush eDNS cache on stop
def stop(flush_cache = false)
ensure_close = [self.udp_sock, self.tcp_sock].compact
begin
self.listener_thread.kill if self.listener_thread.respond_to?(:kill)
self.listener_thread = nil
ensure
while csock = ensure_close.shift
csock.stop if csock.respond_to?(:stop)
csock.close unless csock.respond_to?(:close) and csock.closed?
end
end
self.cache.stop(flush_cache)
end
#
# Process client request, handled with dispatch_request_proc if set
#
# @param cli [Rex::Socket::Tcp, Rex::Socket::Udp] Client sending the request
# @param data [String] raw DNS request data
def dispatch_request(cli, data)
if self.dispatch_request_proc
self.dispatch_request_proc.call(cli,data)
else
default_dispatch_request(cli,data)
end
end
#
# Default DNS request dispatcher, attempts to find
# response records in cache or forwards request upstream
#
# @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)
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)
if cached.empty?
next
else
req.answer = req.answer + cached
forward.question.delete(ques)
end
end
# Forward remaining requests, cache responses
if forward.question.count > 0 and @fwd_res
forwarded = self.fwd_res.send(validate_packet(forward))
req.answer = req.answer + forwarded.answer
forwarded.answer.each do |ans|
self.cache.cache_record(ans)
end
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 = Dnsruby::RCode::NOERROR
end
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
# the +on_client_connect+ callback routine.
#
def monitor_listener
while true
rds = [self.udp_sock]
wds = []
eds = [self.udp_sock]
r,_,_ = ::IO.select(rds,wds,eds,1)
if (r != nil and r[0] == self.udp_sock)
buf,host,port = self.udp_sock.recvfrom(65535)
# Mock up a client object for sending back data
cli = MockDnsClient.new(host, port, r[0])
dispatch_request(cli, buf)
end
end
end
#
# Processes request coming from client
#
# @param cli [Rex::Socket::Tcp] Client sending request
def on_client_data(cli)
begin
data = cli.read(65535)
raise ::EOFError if not data
raise ::EOFError if data.empty?
dispatch_request(cli, data)
rescue EOFError => e
self.tcp_socket.close_client(cli) if cli
raise e
end
end
end
end
end
end

View File

@ -0,0 +1,107 @@
##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'msf/core/exploit/dns'
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::DNS::Client
include Msf::Exploit::Remote::DNS::Server
def initialize(info = {})
super(update_info(info,
'Name' => 'Native DNS Server (Example)',
'Description' => %q{
This module provides a Rex based DNS service which can store static entries,
resolve names over pivots, and serve DNS requests across routed session comms.
DNS tunnels can operate across the the Rex switchboard, and DNS other modules
can use this as a template. Setting static records via hostfile allows for DNS
spoofing attacks without direct traffic manipulation at the handlers. handlers
for requests and responses provided here mimic the internal Rex functionality,
but utilize methods within this module's namespace to output content processed
in the Proc contexts via vprint_status.
},
'Author' => 'RageLtMan <rageltman[at]sempervictus>',
'License' => MSF_LICENSE,
'References' => []
))
end
#
# Wrapper for service execution and cleanup
#
def run
begin
start_service
service.wait
rescue Rex::BindFailed => e
print_error "Failed to bind to port #{datastore['RPORT']}: #{e.message}"
ensure
stop_service(true)
end
end
#
# Creates Proc to handle incoming requests
#
def on_dispatch_request(cli,data)
return if data.strip.empty?
req = Packet.encode_drb(data)
peer = "#{cli.peerhost}:#{cli.peerport}"
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)
if cached.empty?
next
else
req.instance_variable_set(:@answer, (req.answer + cached).uniq)
answered << ques
cached.map do |hit|
if hit.respond_to?(:address)
hit.name.to_s + ':' + hit.address.to_s + ' ' + hit.type.to_s
else
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.rd
vprint_status("Recursion forbidden in query for #{req.question.first.name} from #{peer}")
else
forward = req.dup
# 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
vprint_status("Caching response #{rstring} #{ans.type}")
service.cache.cache_record(ans)
end unless service.cache.nil?
# Merge the answers and use the upstream response
forward.instance_variable_set(:@question, (req.answer + forwarded.answer).uniq)
req = forwarded
end
end
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_drb(data)
peer = "#{cli.peerhost}:#{cli.peerport}"
asked = res.question.map(&:qname).map(&:to_s).join(', ')
vprint_status("Sending response for #{asked} to #{peer}")
cli.write(data)
end
end

View File

@ -0,0 +1,162 @@
##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'msf/core/exploit/dns'
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Capture
include Msf::Exploit::Remote::DNS::Client
include Msf::Exploit::Remote::DNS::Server
def initialize(info = {})
super(update_info(info,
'Name' => 'Native DNS Spoofer (Example)',
'Description' => %q{
This module provides a Rex based DNS service to resolve queries intercepted
via the capture mixin. Configure STATIC_ENTRIES to contain host-name mappings
desired for spoofing using a hostsfile or space/semicolon separated entries.
In default configuration, the service operates as a normal native DNS server
with the exception of consuming from and writing to the wire as opposed to a
listening socket. Best when compromising routers or spoofing L2 in order to
prevent return of the real reply which causes a race condition. The method
by which replies are filtered is up to the user (though iptables works fine).
},
'Author' => 'RageLtMan <rageltman[at]sempervictus>',
'License' => MSF_LICENSE,
'References' => []
))
register_options(
[
OptString.new('FILTER', [false, 'The filter string for capturing traffic', 'dst port 53']),
OptAddress.new('SRVHOST', [true, 'The local host to listen on for DNS services.', '127.0.2.2'])
])
deregister_options('PCAPFILE')
end
#
# Wrapper for service execution and cleanup
#
def run
begin
start_service
capture_traffic
service.wait
rescue Rex::BindFailed => e
print_error "Failed to bind to port #{datastore['RPORT']}: #{e.message}"
ensure
@capture_thread.kill if @capture_thread
close_pcap
stop_service(true)
end
end
#
# Generates reply with src and dst reversed
# Maintains original packet structure, proto, etc, changes ip_id
#
def reply_packet(pack)
rep = pack.dup
rep.eth_dst, rep.eth_src = rep.eth_src, rep.eth_dst
rep.ip_dst, rep.ip_src = rep.ip_src, rep.ip_dst
if pack.is_udp?
rep.udp_dst, rep.udp_src = rep.udp_src, rep.udp_dst
else
rep.tcp_dst, rep.tcp_src = rep.tcp_src, rep.tcp_dst
end
rep.ip_id = StructFu::Int16.new(rand(2**16))
return rep
end
#
# Configures capture and handoff
#
def capture_traffic
check_pcaprub_loaded()
::Socket.do_not_reverse_lookup = true # Mac OS X workaround
open_pcap({'FILTER' => datastore['FILTER']})
@capture_thread = Rex::ThreadFactory.spawn("DNSSpoofer", false) do
each_packet do |pack|
begin
parsed = PacketFu::Packet.parse(pack)
reply = reply_packet(parsed)
service.dispatch_request(reply, parsed.payload)
rescue => e
vprint_status("PacketFu could not parse captured packet")
dlog(e.backtrace)
end
end
end
end
#
# Creates Proc to handle incoming requests
#
def on_dispatch_request(cli,data)
peer = "#{cli.ip_daddr}:" << (cli.is_udp? ? "#{cli.udp_dst}" : "#{cli.tcp_dst}")
# Deal with non DNS traffic
begin
req = Packet.encode_net(data)
rescue => e
print_error("Could not decode payload segment of packet from #{peer}, check log")
dlog e.backtrace
return
end
answered = []
# Find cached items, remove request from forwarded packet
req.question.each do |ques|
cached = service.cache.find(ques.qName, ques.qType.to_s)
if cached.empty?
next
else
req.answer = (req.answer + cached).uniq
answered << ques
end
end
if answered.count < req.question.count and service.fwd_res
if !req.header.recursive?
vprint_status("Recursion forbidden in query for #{req.question.first.name} from #{peer}")
else
forward = req.dup
forward.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
vprint_status("Caching response #{rstring} #{ans.type}")
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
req = forwarded
end
end
service.send_response(cli, Packet.validate(Packet.generate_response(req)).data)
end
#
# Creates Proc to handle outbound responses
#
def on_send_response(cli,data)
cli.payload = data
cli.recalc
inject cli.to_s
sent_info(cli,data) if datastore['VERBOSE']
end
#
# Prints information about spoofed packet after injection to reduce latency of operation
# Shown to improve response time by >50% from ~1ms -> 0.3-0.4ms
#
def sent_info(cli,data)
net = Packet.encode_net(data)
peer = "#{cli.ip_daddr}:" << (cli.is_udp? ? "#{cli.udp_dst}" : "#{cli.tcp_dst}")
asked = net.question.map(&:qName).join(', ')
vprint_good("Sent packet with header:\n#{cli.inspect}")
vprint_good("Spoofed records for #{asked} to #{peer}")
end
end