diff --git a/Gemfile b/Gemfile index 22462cbb7..39885aa8b 100644 --- a/Gemfile +++ b/Gemfile @@ -38,8 +38,7 @@ gem "dm-migrations" gem "msfrpc-client" # Metasploit Integration extension #gem "twitter", ">= 5.0.0" # Twitter Notifications extension gem "rubyzip", ">= 1.0.0" -gem "rubydns" # DNS extension -gem "sourcify" +gem "rubydns", "0.7.0" # DNS extension gem "geoip" # geolocation support # For running unit tests diff --git a/config.yaml b/config.yaml index ea678242d..05dbb3982 100644 --- a/config.yaml +++ b/config.yaml @@ -146,4 +146,4 @@ beef: enable: true # this is still experimental, we're working on it.. dns: - enable: false + enable: true diff --git a/core/filters/base.rb b/core/filters/base.rb index 0c96fde20..33c5a5b73 100644 --- a/core/filters/base.rb +++ b/core/filters/base.rb @@ -5,7 +5,7 @@ # module BeEF module Filters - + # Check if the string is not empty and not nil # @param [String] str String for testing # @return [Boolean] Whether the string is not empty @@ -24,7 +24,7 @@ module Filters regex = Regexp.new('[^' + chars + ']') regex.match(str).nil? end - + # Check if one or more characters in 'chars' are in 'str' # @param [String] chars List of characters to match # @param [String] str String for testing @@ -33,7 +33,7 @@ module Filters regex = Regexp.new(chars) not regex.match(str).nil? end - + # Check for null char # @param [String] str String for testing # @return [Boolean] If the string has a null character @@ -98,14 +98,58 @@ module Filters return false if not is_non_empty_string?(str) only?("a-zA-Z0-9", str) end - - # Check if valid ip address string - # @param [String] ip String for testing - # @return [Boolean] If the string is a valid IP address - # @note only IPv4 compliant - def self.is_valid_ip?(ip) - return false if not is_non_empty_string?(ip) - return true if ip =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/ + + # @overload self.is_valid_ip?(version, ip) + # Checks if the given string is a valid IP address + # @param [Symbol] version IP version (either :ipv4 or :ipv6) + # @param [String] ip string to be tested + # @return [Boolean] true if the string is a valid IP address, otherwise false + # + # @overload self.is_valid_ip?(ip) + # Checks if the given string is either a valid IPv4 or IPv6 address + # @param [String] ip string to be tested + # @return [Boolean] true if the string is a valid IPv4 or IPV6 address, otherwise false + def self.is_valid_ip?(version = :both, ip) + valid = false + + if is_non_empty_string?(ip) + valid = case version.inspect.downcase + when /^:ipv4$/ + ip =~ /^((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3} + (25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])$/x + when /^:ipv6$/ + ip =~ /^(([0-9a-f]{1,4}:){7,7}[0-9a-f]{1,4}| + ([0-9a-f]{1,4}:){1,7}:| + ([0-9a-f]{1,4}:){1,6}:[0-9a-f]{1,4}| + ([0-9a-f]{1,4}:){1,5}(:[0-9a-f]{1,4}){1,2}| + ([0-9a-f]{1,4}:){1,4}(:[0-9a-f]{1,4}){1,3}| + ([0-9a-f]{1,4}:){1,3}(:[0-9a-f]{1,4}){1,4}| + ([0-9a-f]{1,4}:){1,2}(:[0-9a-f]{1,4}){1,5}| + [0-9a-f]{1,4}:((:[0-9a-f]{1,4}){1,6})| + :((:[0-9a-f]{1,4}){1,7}|:)| + fe80:(:[0-9a-f]{0,4}){0,4}%[0-9a-z]{1,}| + ::(ffff(:0{1,4}){0,1}:){0,1} + ((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3} + (25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])| + ([0-9a-f]{1,4}:){1,4}: + ((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3} + (25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/ix + when /^:both$/ + is_valid_ip?(:ipv4, ip) || is_valid_ip?(:ipv6, ip) + end ? true : false + end + + valid + end + + # Checks if string is a valid domain name + # @param [String] domain string for testing + # @return [Boolean] If the string is a valid domain name + # @note Only validates the string format. It does not check for a valid TLD since ICANN's list of + # TLD's is not static. + def self.is_valid_domain?(domain) + return false unless is_non_empty_string?(domain) + return true if domain =~ /^[0-9a-z-]+(\.[0-9a-z-]+)*(\.[a-z]{2,}).?$/i false end @@ -138,6 +182,6 @@ module Filters return false if str.length > 200 true end - + end end diff --git a/core/loader.rb b/core/loader.rb index c3929bc59..298494444 100644 --- a/core/loader.rb +++ b/core/loader.rb @@ -16,7 +16,6 @@ require 'base64' require 'xmlrpc/client' require 'openssl' require 'rubydns' -require 'sourcify' # @note Include the filters require 'core/filters' diff --git a/core/main/crypto.rb b/core/main/crypto.rb index eda268f48..3700fe84e 100644 --- a/core/main/crypto.rb +++ b/core/main/crypto.rb @@ -39,6 +39,23 @@ module Core config.set('beef.api_token', token) token end + + # Generates a unique identifier for DNS rules. + # + # @return [String] 8-character hex identifier + def self.dns_rule_id + id = nil + length = 4 + + begin + id = OpenSSL::Random.random_bytes(length).unpack('H*')[0] + BeEF::Core::Models::Dns::Rule.each { |rule| throw StandardError if id == rule.id } + rescue StandardError + retry + end + + id.to_s + end end end diff --git a/extensions/dns/api.rb b/extensions/dns/api.rb index 5070a3ac2..974b30488 100644 --- a/extensions/dns/api.rb +++ b/extensions/dns/api.rb @@ -1,5 +1,5 @@ # -# Copyright (c) 2006-2013 Wade Alcorn - wade@bindshell.net +# Copyright (c) 2006-2014 Wade Alcorn - wade@bindshell.net # Browser Exploitation Framework (BeEF) - http://beefproject.com # See the file 'doc/COPYING' for copying permission # @@ -11,53 +11,53 @@ module BeEF module NameserverHandler BeEF::API::Registrar.instance.register( - BeEF::Extension::Dns::API::NameserverHandler, - BeEF::API::Server, - 'pre_http_start' + BeEF::Extension::Dns::API::NameserverHandler, + BeEF::API::Server, + 'pre_http_start' ) BeEF::API::Registrar.instance.register( - BeEF::Extension::Dns::API::NameserverHandler, - BeEF::API::Server, - 'mount_handler' + BeEF::Extension::Dns::API::NameserverHandler, + BeEF::API::Server, + 'mount_handler' ) - # Begins main DNS server run-loop at BeEF startup + # Starts the DNS nameserver at BeEF startup. + # + # @param http_hook_server [BeEF::Core::Server] HTTP server instance def self.pre_http_start(http_hook_server) dns_config = BeEF::Core::Configuration.instance.get('beef.extension.dns') - - address = dns_config['address'] - port = dns_config['port'] - dns = BeEF::Extension::Dns::Server.instance - dns.run_server(address, port) - print_info "DNS Server: #{address}:#{port}" + protocol = dns_config['protocol'].to_sym rescue :udp + address = dns_config['address'] || '127.0.0.1' + port = dns_config['port'] || 5300 + interfaces = [[protocol, address, port]] + servers = [] - unless dns_config['upstream'].nil? + upstream_servers = '' + + unless dns_config['upstream'].nil? || dns_config['upstream'].empty? dns_config['upstream'].each do |server| - if server[1].nil? or server[2].nil? - next - end - if server[0] == 'tcp' - servers << ['tcp', server[1], server[2]] - elsif server[0] == 'udp' - servers << ['udp', server[1], server[2]] - end + up_protocol = server[0].downcase + up_address = server[1] + up_port = server[2] + + next if [up_protocol, up_address, up_port].include?(nil) + servers << [up_protocol.to_sym, up_address, up_port] if up_protocol =~ /^(tcp|udp)$/ + upstream_servers << "Upstream Server: #{up_address}:#{up_port} (#{up_protocol})\n" end end - if servers.empty? - servers << ['tcp', '8.8.8.8', 53] - servers << ['udp', '8.8.8.8', 53] - end - upstream_servers = '' - servers.each do |server| - upstream_servers << "Upstream server: #{server[1]}:#{server[2]} (#{server[0]})\n" - end - print_more upstream_servers + + dns.run(:upstream => servers, :listen => interfaces) + + print_info "DNS Server: #{address}:#{port} (#{protocol})" + print_more upstream_servers unless upstream_servers.empty? end - # Mounts handler for processing RESTful API calls + # Mounts the handler for processing DNS RESTful API requests. + # + # @param beef_server [BeEF::Core::Server] HTTP server instance def self.mount_handler(beef_server) beef_server.mount('/api/dns', BeEF::Extension::Dns::DnsRest.new) end diff --git a/extensions/dns/config.yaml b/extensions/dns/config.yaml index d382ac70f..5a525a2e3 100644 --- a/extensions/dns/config.yaml +++ b/extensions/dns/config.yaml @@ -1,15 +1,18 @@ # -# Copyright (c) 2006-2013 Wade Alcorn - wade@bindshell.net +# Copyright (c) 2006-2014 Wade Alcorn - wade@bindshell.net # Browser Exploitation Framework (BeEF) - http://beefproject.com # See the file 'doc/COPYING' for copying permission # beef: extension: dns: - enable: false + enable: true name: 'DNS Server' authors: ['soh_cah_toa'] + protocol: 'udp' address: '127.0.0.1' port: 5300 - upstream: - [['tcp', '8.8.8.8', 53], ['udp', '8.8.8.8', 53]] + upstream: [ + ['udp', '8.8.8.8', 53], + ['tcp', '8.8.8.8', 53] + ] diff --git a/extensions/dns/dns.rb b/extensions/dns/dns.rb index 6803de477..177414c5d 100644 --- a/extensions/dns/dns.rb +++ b/extensions/dns/dns.rb @@ -1,5 +1,5 @@ # -# Copyright (c) 2006-2013 Wade Alcorn - wade@bindshell.net +# Copyright (c) 2006-2014 Wade Alcorn - wade@bindshell.net # Browser Exploitation Framework (BeEF) - http://beefproject.com # See the file 'doc/COPYING' for copying permission # @@ -7,97 +7,81 @@ module BeEF module Extension module Dns - # This class is responsible for providing a DNS nameserver that can be dynamically - # configured by other modules and extensions. It is particularly useful for - # performing DNS spoofing, hijacking, tunneling, etc. - # - # Only a single instance will exist during runtime (known as the "singleton pattern"). - # This makes it easier to coordinate actions across the various BeEF systems. - class Server + # @todo Add option for configuring upstream servers. + + # Provides the core DNS nameserver functionality. The nameserver handles incoming requests + # using a rule-based system. A list of user-defined rules is used to match against incoming + # DNS requests. These rules generate a response that is either a resource record or a + # failure code. + class Server < RubyDNS::Server include Singleton - attr_reader :address, :port - - # @!method self.instance - # Returns the singleton instance. Use this in place of {#initialize}. - - # @note This method cannot be invoked! Use {.instance} instead. - # @see ::instance def initialize + super() @lock = Mutex.new - @server = nil + @database = BeEF::Core::Models::Dns::Rule end - def set_server(server) - @server = server - end - - def get_server - @server - end - - # Starts the main DNS server run-loop in a new thread. + # Adds a new DNS rule. If the rule already exists, its current ID is returned. # - # @param address [String] interface address server should run on - # @param port [Integer] desired server port number - def run_server(address = '0.0.0.0', port = 5300) - @address = address - @port = port - Thread.new do - sleep(2) - - # antisnatchor: RubyDNS is already implemented with EventMachine - run_server_block(@address, @port) - end - end - - # Adds a new DNS rule or "resource record". Does nothing if rule is already present. - # - # @example Adds an A record for foobar.com with the value 1.2.3.4 + # @example Adds an A record for browserhacker.com with the IP address 1.2.3.4 # # dns = BeEF::Extension::Dns::Server.instance # - # id = dns.add_rule('foobar.com', Resolv::DNS::Resource::IN::A) do |transaction| - # transaction.respond!('1.2.3.4') - # end + # id = dns.add_rule( + # :pattern => 'browserhacker.com', + # :resource => Resolv::DNS::Resource::IN::A, + # :response => '1.2.3.4' + # ) # - # @param pattern [String, Regexp] query pattern to recognize - # @param type [Resolv::DNS::Resource::IN] resource record type (e.g. A, CNAME, NS, etc.) + # @param rule [Hash] hash representation of rule + # @option rule [String, Regexp] :pattern match criteria + # @option rule [Resolv::DNS::Resource::IN] :resource resource record type + # @option rule [String, Array] :response server response # - # @note When parameter 'pattern' is a literal Regexp object, it must NOT be passed - # using the /.../ literal syntax. Instead use either %r{...} or Regexp::new. - # This does not apply if 'pattern' is a variable. - # - # @yield callback to invoke when pattern is matched - # @yieldparam transaction [RubyDNS::Transaction] details of query question and response - # - # @return [String] unique 7-digit hex identifier for use with {#remove_rule} - # - # @see #remove_rule - # @see http://rubydoc.info/gems/rubydns/RubyDNS/Transaction - def add_rule(pattern, type, &block) - @lock.synchronize { @server.match(pattern, type, block) } + # @return [String] unique 8-digit hex identifier + def add_rule(rule = {}) + @lock.synchronize do + # Temporarily disable warnings regarding IGNORECASE flag + verbose = $VERBOSE + $VERBOSE = nil + pattern = Regexp.new(rule[:pattern], Regexp::IGNORECASE) + $VERBOSE = verbose + + @database.first_or_create( + { :resource => rule[:resource], :pattern => pattern.source }, + { :response => rule[:response] } + ).id + end end - # Removes the given DNS rule. Any future queries for it will be passed through. + # Retrieves a specific rule given its identifier. # - # @param id [Integer] id returned from {#add_rule} + # @param id [String] unique identifier for rule # - # @return [Boolean] true on success, false on failure - # - # @see #add_rule - def remove_rule(id) - @lock.synchronize { @server.remove_rule(id) } - end - - # Retrieves a specific rule given its id - # - # @param id [Integer] unique identifier for rule - # - # @return [Hash] hash representation of rule + # @return [Hash] hash representation of rule (empty hash if rule wasn't found) def get_rule(id) - @lock.synchronize { @server.get_rule(id) } + @lock.synchronize do + if is_valid_id?(id) + rule = @database.get(id) + rule.nil? ? {} : to_hash(rule) + end + end + end + + # Removes the given DNS rule. + # + # @param id [String] rule identifier + # + # @return [Boolean] true if rule was removed, otherwise false + def remove_rule!(id) + @lock.synchronize do + if is_valid_id?(id) + rule = @database.get(id) + rule.nil? ? false : rule.destroy + end + end end # Returns an AoH representing the entire current DNS ruleset. @@ -106,60 +90,119 @@ module BeEF # # * :id # * :pattern - # * :type + # * :resource # * :response # - # @return [Array] DNS ruleset (empty if no rules are currently loaded) + # @return [Array] DNS ruleset (empty array if no rules are currently defined) def get_ruleset - @lock.synchronize { @server.get_ruleset } + @lock.synchronize { @database.collect { |rule| to_hash(rule) } } end - # Clears the entire DNS ruleset. + # Removes the entire DNS ruleset. # - # Requests made after doing so will be passed through to the root nameservers. - # - # @return [Boolean] true on success, false on failure - def remove_ruleset - @lock.synchronize { @server.remove_ruleset } + # @return [Boolean] true if ruleset was destroyed, otherwise false + def remove_ruleset! + @lock.synchronize { @database.destroy } end - private - - # Common code needed by {#run_server} to start DNS server. + # Starts the DNS server. # - # @param address [String] interface address server should run on - # @param port [Integer] desired server port number - def run_server_block(address, port) - RubyDNS.run_server(:listen => [[:udp, address, port]]) do - # Pass unmatched queries upstream to root nameservers - dns_config = BeEF::Core::Configuration.instance.get('beef.extension.dns') - unless dns_config['upstream'].nil? - dns_config['upstream'].each do |server| - if server[1].nil? or server[2].nil? - print_error "Invalid server '#{server[1]}:#{server[2]}' specified for upstream DNS server." - next - elsif server[0] == 'tcp' - servers << [:tcp, server[1], server[2]] - elsif server[0] == 'udp' - servers << [:udp, server[1], server[2]] - else - print_error "Invalid protocol '#{server[0]}' specified for upstream DNS server." + # @param options [Hash] server configuration options + # @option options [Array] :upstream upstream DNS servers (if ommitted, unresolvable + # requests return NXDOMAIN) + # @option options [Array] :listen local interfaces to listen on + def run(options = {}) + @lock.synchronize do + Thread.new do + EventMachine.next_tick do + upstream = options[:upstream] || nil + listen = options[:listen] || nil + + if upstream + resolver = RubyDNS::Resolver.new(upstream) + @otherwise = Proc.new { |t| t.passthrough!(resolver) } end + + super(:listen => listen) end end - if servers.empty? - print_debug "No upstream DNS servers specified. Using '8.8.8.8'" - servers << [:tcp, '8.8.8.8', 53] - servers << [:udp, '8.8.8.8', 53] - end - otherwise do |transaction| - transaction.passthrough!( - RubyDNS::Resolver.new servers - ) + end + end + + # Entry point for processing incoming DNS requests. Attempts to find a matching rule and + # sends back its associated response. + # + # @param name [String] name of the resource record being looked up + # @param resource [Resolv::DNS::Resource::IN] query type (e.g. A, CNAME, NS, etc.) + # @param transaction [RubyDNS::Transaction] internal RubyDNS class detailing DNS question/answer + def process(name, resource, transaction) + @lock.synchronize do + print_debug "Received DNS request (name: #{name} type: #{format_resource(resource)})" + + catch (:done) do + # Find rules matching the requested resource class + resources = @database.all(:resource => resource) + throw :done if resources.length == 0 + + # Narrow down search by finding a matching pattern + resources.each do |rule| + pattern = Regexp.new(rule.pattern) + + if name =~ pattern + print_debug "Found matching DNS rule (id: #{rule.id} response: #{rule.response})" + Proc.new { |t| eval(rule.callback) }.call(transaction) + throw :done + end + end + + if @otherwise + print_debug "No match found, querying upstream servers" + @otherwise.call(transaction) + else + print_debug "No match found, sending NXDOMAIN response" + transaction.fail!(:NXDomain) + end end end end + private + # Helper method that converts a DNS rule to a hash. + # + # @param rule [BeEF::Core::Models::Dns::Rule] rule to be converted + # + # @return [Hash] hash representation of DNS rule + def to_hash(rule) + hash = {} + hash[:id] = rule.id + hash[:pattern] = rule.pattern + hash[:resource] = format_resource(rule.resource) + hash[:response] = rule.response + + hash + end + + # Verifies that the given ID is valid. + # + # @param id [String] identifier to validate + # + # @return [Boolean] true if ID is valid, otherwise false + def is_valid_id?(id) + BeEF::Filters.hexs_only?(id) && + !BeEF::Filters.has_null?(id) && + !BeEF::Filters.has_non_printable_char?(id) && + id.length == 8 + end + + # Helper method that formats the given resource class in a human-readable format. + # + # @param resource [Resolv::DNS::Resource::IN] resource class + # + # @return [String] resource name stripped of any module/class names + def format_resource(resource) + /::(\w+)$/.match(resource.name)[1] + end + end end diff --git a/extensions/dns/extension.rb b/extensions/dns/extension.rb index fd909f01b..39c66da6d 100644 --- a/extensions/dns/extension.rb +++ b/extensions/dns/extension.rb @@ -1,5 +1,5 @@ # -# Copyright (c) 2006-2013 Wade Alcorn - wade@bindshell.net +# Copyright (c) 2006-2014 Wade Alcorn - wade@bindshell.net # Browser Exploitation Framework (BeEF) - http://beefproject.com # See the file 'doc/COPYING' for copying permission # @@ -18,9 +18,8 @@ module BeEF end end -#TODO antisnatchor: uncomment this when code will be stable -#require 'extensions/dns/api' -#require 'extensions/dns/dns' -#require 'extensions/dns/model' -#require 'extensions/dns/rest/dns' -#require 'extensions/dns/ruby' +require 'extensions/dns/api' +require 'extensions/dns/dns' +require 'extensions/dns/logger' +require 'extensions/dns/model' +require 'extensions/dns/rest/dns' diff --git a/extensions/dns/logger.rb b/extensions/dns/logger.rb new file mode 100644 index 000000000..0033a3b7e --- /dev/null +++ b/extensions/dns/logger.rb @@ -0,0 +1,15 @@ +# +# Copyright (c) 2006-2014 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - http://beefproject.com +# See the file 'doc/COPYING' for copying permission +# + +# Disables the logger used by RubyDNS due to its excessive verbosity. +class Logger + + def debug(msg); end + def info(msg); end + def error(msg); end + def warn(msg); end + +end diff --git a/extensions/dns/model.rb b/extensions/dns/model.rb index 14f5a2310..8a7220564 100644 --- a/extensions/dns/model.rb +++ b/extensions/dns/model.rb @@ -1,5 +1,5 @@ # -# Copyright (c) 2006-2013 Wade Alcorn - wade@bindshell.net +# Copyright (c) 2006-2014 Wade Alcorn - wade@bindshell.net # Browser Exploitation Framework (BeEF) - http://beefproject.com # See the file 'doc/COPYING' for copying permission # @@ -8,16 +8,224 @@ module BeEF module Models module Dns + # Represents an individual DNS rule. class Rule - include DataMapper::Resource storage_names[:default] = 'extension_dns_rules' - property :id, String, :key => true # Unique identifier - property :pattern, Object # Query pattern - property :type, Object # Resource type - property :block, Text # Associated callback + property :id, String, :key => true + property :pattern, Object, :required => true + property :resource, Object, :required => true + property :response, Object, :required => true + property :callback, Object, :required => true + + # Hooks the model's "save" event. Validates pattern/response and generates a rule identifier. + before :save do |rule| + begin + validate_pattern(rule.pattern) + rule.callback = format_callback(rule.resource, rule.response) + rescue InvalidDnsPatternError, UnknownDnsResourceError, InvalidDnsResponseError => e + print_error e.message + throw :halt + end + + rule.id = BeEF::Core::Crypto.dns_rule_id + end + + private + # Verifies that the given pattern is valid (i.e. non-empty, no null's or printable characters). + def validate_pattern(pattern) + raise InvalidDnsPatternError unless BeEF::Filters.is_non_empty_string?(pattern) && + !BeEF::Filters.has_null?(pattern) && + !BeEF::Filters.has_non_printable_char?(pattern) + end + + # Strict validator which ensures that only an appropriate response is given. + # + # @param resource [Resolv::DNS::Resource::IN] resource record type + # @param response [String, Symbol, Array] response to include in callback + # + # @return [String] string representation of callback that can safely be eval'd + def format_callback(resource, response) + sym_regex = /^:?(NoError|FormErr|ServFail|NXDomain|NotImp|Refused|NotAuth)$/i + + src = if resource == Resolv::DNS::Resource::IN::A + if response.is_a?(String) && BeEF::Filters.is_valid_ip?(:ipv4, response) + sprintf "t.respond!('%s')", response + elsif (response.is_a?(Symbol) && response.to_s =~ sym_regex) || response =~ sym_regex + sprintf "t.fail!(:%s)", response.to_sym + elsif response.is_a?(Array) + str1 = "t.respond!('%s');" + str2 = '' + + response.each do |r| + raise InvalidDnsResponseError, 'A' unless BeEF::Filters.is_valid_ip?(:ipv4, r) + str2 << sprintf(str1, r) + end + + str2 + else + raise InvalidDnsResponseError, 'A' + end + elsif resource == Resolv::DNS::Resource::IN::AAAA + if response.is_a?(String) && BeEF::Filters.is_valid_ip?(:ipv6, response) + sprintf "t.respond!('%s')", response + elsif (response.is_a?(Symbol) && response.to_s =~ sym_regex) || response =~ sym_regex + sprintf "t.fail!(:%s)", response.to_sym + elsif response.is_a?(Array) + str1 = "t.respond!('%s');" + str2 = '' + + response.each do |r| + raise InvalidDnsResponseError, 'AAAA' unless BeEF::Filters.is_valid_ip?(:ipv6, r) + str2 << sprintf(str1, r) + end + + str2 + else + raise InvalidDnsResponseError, 'AAAA' + end + elsif resource == Resolv::DNS::Resource::IN::CNAME + if response.is_a?(String) && BeEF::Filters.is_valid_domain?(response) + sprintf "t.respond!(Resolv::DNS::Name.create('%s'))", response + elsif (response.is_a?(Symbol) && response.to_s =~ sym_regex) || response =~ sym_regex + sprintf "t.fail!(:%s)", response.to_sym + else + raise InvalidDnsResponseError, 'CNAME' + end + elsif resource == Resolv::DNS::Resource::IN::MX + if response[0].is_a?(Integer) && + BeEF::Filters.is_valid_domain?(response[1]) + + data = { :preference => response[0], :exchange => response[1] } + sprintf "t.respond!(%d, Resolv::DNS::Name.create('%s'))", data + elsif (response.is_a?(Symbol) && response.to_s =~ sym_regex) || response =~ sym_regex + sprintf "t.fail!(:%s)", response.to_sym + else + raise InvalidDnsResponseError, 'MX' + end + elsif resource == Resolv::DNS::Resource::IN::NS + if response.is_a?(String) && BeEF::Filters.is_valid_domain?(response) + sprintf "t.respond!(Resolv::DNS::Name.create('%s'))", response + elsif (response.is_a?(Symbol) && response.to_s =~ sym_regex) || response =~ sym_regex + sprintf "t.fail!(:%s)", response.to_sym + elsif response.is_a?(Array) + str1 = "t.respond!(Resolv::DNS::Name.create('%s'))" + str2 = '' + + response.each do |r| + raise InvalidDnsResponseError, 'NS' unless BeEF::Filters.is_valid_domain?(r) + str2 << sprintf(str1, r) + end + + str2 + else + raise InvalidDnsResponseError, 'NS' + end + elsif resource == Resolv::DNS::Resource::IN::PTR + if response.is_a?(String) && BeEF::Filters.is_valid_domain?(response) + sprintf "t.respond!(Resolv::DNS::Name.create('%s'))", response + elsif (response.is_a?(Symbol) && response.to_s =~ sym_regex) || response =~ sym_regex + sprintf "t.fail!(:%s)", response.to_sym + else + raise InvalidDnsResponseError, 'PTR' + end + elsif resource == Resolv::DNS::Resource::IN::SOA + if response.is_a?(Array) + unless BeEF::Filters.is_valid_domain?(response[0]) && + BeEF::Filters.is_valid_domain?(response[1]) && + response[2].is_a?(Integer) && + response[3].is_a?(Integer) && + response[4].is_a?(Integer) && + response[5].is_a?(Integer) && + response[6].is_a?(Integer) + + raise InvalidDnsResponseError, 'SOA' + end + + data = { + :mname => response[0], + :rname => response[1], + :serial => response[2], + :refresh => response[3], + :retry => response[4], + :expire => response[5], + :minimum => response[6] + } + + sprintf "t.respond!(Resolv::DNS::Name.create('%s'), " + + "Resolv::DNS::Name.create('%s'), " + + '%d, ' + + '%d, ' + + '%d, ' + + '%d, ' + + '%d)', + data + elsif (response.is_a?(Symbol) && response.to_s =~ sym_regex) || response =~ sym_regex + sprintf "t.fail!(:%s)", response.to_sym + else + raise InvalidDnsResponseError, 'SOA' + end + elsif resource == Resolv::DNS::Resource::IN::WKS + if response.is_a?(Array) + unless BeEF::Filters.is_valid_ip?(resource[0]) && + resource[1].is_a?(Integer) && + resource[2].is_a?(Integer) + raise InvalidDnsResponseError, 'WKS' unless resource.is_a?(String) + end + + data = { + :address => response[0], + :protocol => response[1], + :bitmap => response[2] + } + + sprintf "t.respond!('%
s', %d, %d)", data + elsif (response.is_a?(Symbol) && response.to_s =~ sym_regex) || response =~ sym_regex + sprintf "t.fail!(:%s)", response.to_sym + else + raise InvalidDnsResponseError, 'WKS' + end + else + raise UnknownDnsResourceError + end + + src + end + + # Raised when an invalid pattern is given. + class InvalidDnsPatternError < StandardError + + DEFAULT_MESSAGE = 'Failed to add DNS rule with invalid pattern' + + def initialize(message = nil) + super(message || DEFAULT_MESSAGE) + end + + end + + # Raised when a response is not valid for the given DNS resource record. + class InvalidDnsResponseError < StandardError + + def initialize(message = nil) + str = "Failed to add DNS rule with invalid response for %s resource record", message + message = sprintf str, message unless message.nil? + super(message) + end + + end + + # Raised when an unknown DNS resource record is given. + class UnknownDnsResourceError < StandardError + + DEFAULT_MESSAGE = 'Failed to add DNS rule with unknown resource record' + + def initialize(message = nil) + super(message || DEFAULT_MESSAGE) + end + + end end diff --git a/extensions/dns/rest/dns.rb b/extensions/dns/rest/dns.rb index 4413c7ec7..a1cdca5bf 100644 --- a/extensions/dns/rest/dns.rb +++ b/extensions/dns/rest/dns.rb @@ -1,5 +1,5 @@ # -# Copyright (c) 2006-2013 Wade Alcorn - wade@bindshell.net +# Copyright (c) 2006-2014 Wade Alcorn - wade@bindshell.net # Browser Exploitation Framework (BeEF) - http://beefproject.com # See the file 'doc/COPYING' for copying permission # @@ -12,6 +12,7 @@ module BeEF # Filters out bad requests before performing any routing before do + @dns ||= BeEF::Extension::Dns::Server.instance config = BeEF::Core::Configuration.instance # Require a valid API token from a valid IP address @@ -27,7 +28,7 @@ module BeEF # Returns the entire current DNS ruleset get '/ruleset' do begin - ruleset = BeEF::Extension::Dns::Server.instance.get_ruleset + ruleset = @dns.get_ruleset count = ruleset.length result = {} @@ -45,14 +46,11 @@ module BeEF begin id = params[:id] - unless BeEF::Filters.alphanums_only?(id) - raise InvalidParamError, 'Invalid "id" parameter passed to endpoint /api/dns/rule/:id' - end + rule = @dns.get_rule(id) + raise InvalidParamError, 'id' if rule.nil? + halt 404 if rule.empty? - result = BeEF::Extension::Dns::Server.instance.get_rule(id) - halt 404 if result.length == 0 - - result.to_json + rule.to_json rescue InvalidParamError => e print_error e.message halt 400 @@ -68,58 +66,26 @@ module BeEF body = JSON.parse(request.body.read) pattern = body['pattern'] - type = body['type'] + resource = body['resource'] response = body['response'] - valid_types = ["A", "AAAA", "CNAME", "HINFO", "MINFO", "MX", "NS", "PTR", "SOA", "TXT", "WKS"] + valid_resources = ["A", "AAAA", "CNAME", "HINFO", "MINFO", "MX", "NS", "PTR", "SOA", "TXT", "WKS"] # Validate required JSON keys - unless [pattern, type, response].include?(nil) - # Determine whether 'pattern' is a String or Regexp - begin - # if pattern is a Regexp, then create a new Regexp object - if %r{\A/(.*)/([mix]*)\z} =~ pattern - pattern = Regexp.new(pattern) - end - rescue => e; - end - - if response.class == Array - if response.length == 0 - raise InvalidJsonError, 'Empty "response" key passed to endpoint /api/dns/rule' - end + unless [pattern, resource, response].include?(nil) + if response.is_a?(Array) + raise InvalidJsonError, 'Empty "response" key passed to endpoint /api/dns/rule' if response.empty? else raise InvalidJsonError, 'Non-array "response" key passed to endpoint /api/dns/rule' end - safe_response = true - response.each do |ip| - unless BeEF::Filters.is_valid_ip?(ip) - safe_response = false - break - end - end + raise InvalidJsonError, 'Wrong "resource" key passed to endpoint /api/dns/rule' unless valid_resources.include?(resource) - unless safe_response - raise InvalidJsonError, 'Invalid IP in "response" key passed to endpoint /api/dns/rule' - end - - unless BeEF::Filters.is_non_empty_string?(pattern) - raise InvalidJsonError, 'Empty "pattern" key passed to endpoint /api/dns/rule' - end - - unless BeEF::Filters.is_non_empty_string?(type) && BeEF::Filters.alphanums_only?(type) && valid_types.include?(type) - raise InvalidJsonError, 'Wrong "type" key passed to endpoint /api/dns/rule' - end - - id = '' - block_src = format_response(type, response) - - # antisnatchor: would be unsafe eval, but I added 2 validations before (alpha-num only and list of valid types) - # Now it's safe - type_obj = eval "Resolv::DNS::Resource::IN::#{type}" - - id = BeEF::Extension::Dns::Server.instance.get_server.match(pattern, type_obj, block_src) + id = @dns.add_rule( + :pattern => pattern, + :resource => eval("Resolv::DNS::Resource::IN::#{resource}"), + :response => response + ) result = {} result['success'] = true @@ -140,12 +106,11 @@ module BeEF begin id = params[:id] - unless BeEF::Filters.alphanums_only?(id) - raise InvalidParamError, 'Invalid "id" parameter passed to endpoint /api/dns/rule/:id' - end + removed = @dns.remove_rule!(id) + raise InvalidParamError, 'id' if removed.nil? result = {} - result['success'] = BeEF::Extension::Dns::Server.instance.remove_rule(id) + result['success'] = removed result.to_json rescue InvalidParamError => e print_error e.message @@ -156,82 +121,6 @@ module BeEF end end - private - - # Generates a formatted string representation of the callback to invoke as a response. - # - # @param [String] type resource record type (e.g. A, CNAME, NS, etc.) - # @param [Array] rdata record data to include in response - # - # @return [String] string representation of response callback - def format_response(type, rdata) - src = 'proc { |t| t.respond!(%s) }' - - args = case type - when 'A' - data = {:address => rdata[0]} - sprintf "'%
s'", data - when 'AAAA' - data = {:address => rdata[0]} - sprintf "'%
s'", data - when 'CNAME' - data = {:cname => rdata[0]} - sprintf "Resolv::DNS::Name.create('%s')", data - when 'HINFO' - data = {:cpu => rdata[0], :os => rdata[1]} - sprintf "'%s', '%s'", data - when 'MINFO' - data = {:rmailbx => rdata[0], :emailbx => rdata[1]} - - sprintf "Resolv::DNS::Name.create('%s'), " + - "Resolv::DNS::Name.create('%s')", - data - when 'MX' - data = {:preference => rdata[0], :exchange => rdata[1]} - sprintf "%d, Resolv::DNS::Name.create('%s')", data - when 'NS' - data = {:nsdname => rdata[0]} - sprintf "Resolv::DNS::Name.create('%s')", data - when 'PTR' - data = {:ptrdname => rdata[0]} - sprintf "Resolv::DNS::Name.create('%s')", data - when 'SOA' - data = { - :mname => rdata[0], - :rname => rdata[1], - :serial => rdata[2], - :refresh => rdata[3], - :retry => rdata[4], - :expire => rdata[5], - :minimum => rdata[6] - } - - sprintf "Resolv::DNS::Name.create('%s'), " + - "Resolv::DNS::Name.create('%s'), " + - '%d, ' + - '%d, ' + - '%d, ' + - '%d, ' + - '%d', - data - when 'TXT' - data = {:txtdata => rdata[0]} - sprintf "'%s'", data - when 'WKS' - data = { - :address => rdata[0], - :protocol => rdata[1], - :bitmap => rdata[2] - } - - sprintf "'%
s', %d, %d", data - else - raise InvalidJsonError, 'Unknown "type" key passed to endpoint /api/dns/rule' - end - - sprintf(src, args) - end - # Raised when invalid JSON input is passed to an /api/dns handler. class InvalidJsonError < StandardError @@ -249,7 +138,9 @@ module BeEF DEFAULT_MESSAGE = 'Invalid parameter passed to /api/dns handler' def initialize(message = nil) - super(message || DEFAULT_MESSAGE) + str = "Invalid \"%s\" parameter passed to /api/dns handler" + message = sprintf str, message unless message.nil? + super(message) end end diff --git a/extensions/dns/ruby.rb b/extensions/dns/ruby.rb deleted file mode 100644 index 99cf46225..000000000 --- a/extensions/dns/ruby.rb +++ /dev/null @@ -1,7 +0,0 @@ -# -# Copyright (c) 2006-2013 Wade Alcorn - wade@bindshell.net -# Browser Exploitation Framework (BeEF) - http://beefproject.com -# See the file 'doc/COPYING' for copying permission -# -require 'extensions/dns/ruby/logger' -require 'extensions/dns/ruby/rubydns' diff --git a/extensions/dns/ruby/logger.rb b/extensions/dns/ruby/logger.rb deleted file mode 100644 index 3e88cbfa0..000000000 --- a/extensions/dns/ruby/logger.rb +++ /dev/null @@ -1,27 +0,0 @@ -# -# Copyright (c) 2006-2013 Wade Alcorn - wade@bindshell.net -# Browser Exploitation Framework (BeEF) - http://beefproject.com -# See the file 'doc/COPYING' for copying permission -# - -# Overrives the logger used by RubyDNS to use BeEF's {#print_info} and friends. -class Logger - - def debug(msg) - print_debug "DNS Server: #{msg}" - end - - def info(msg) - print_info "DNS Server: #{msg}" - end - - def error(msg) - print_error "DNS Server: #{msg}" - end - - def warn(msg) - print_error "DNS Server: #{msg}" - end - -end - diff --git a/extensions/dns/ruby/rubydns.rb b/extensions/dns/ruby/rubydns.rb deleted file mode 100644 index 0a8d13d77..000000000 --- a/extensions/dns/ruby/rubydns.rb +++ /dev/null @@ -1,253 +0,0 @@ -# -# Copyright (c) 2006-2013 Wade Alcorn - wade@bindshell.net -# Browser Exploitation Framework (BeEF) - http://beefproject.com -# See the file 'doc/COPYING' for copying permission -# - -# This module is a modified version of RubyDNS built to be compatible with BeEF. -# For the most part, it will behave exactly the same except where otherwise noted. -# -# Additional features include database support, BeEF logger, assignment of unique -# identifiers to rules, rule removal, and more. -# -# The core functionality of BeEF's DNS server is implemented here, whereas -# BeEF::Extension::Dns::Server is simply a small wrapper around it. -# -# @see http://rubydoc.info/gems/rubydns/frames -module RubyDNS - - # Behaves exactly the same, except without any logger output - def self.run_server(options = {}, &block) - server = RubyDNS::Server.new(&block) - - BeEF::Extension::Dns::Server.instance.set_server(server) - - options[:listen] ||= [[:udp, '0.0.0.0', 53], [:tcp, '0.0.0.0', 53]] - - EventMachine.run do - server.fire(:setup) - - options[:listen].each do |spec| - if spec[0] == :udp - EventMachine.open_datagram_socket(spec[1], spec[2], UDPHandler, server) - elsif spec[0] == :tcp - EventMachine.start_server(spec[1], spec[2], TCPHandler, server) - end - end - - server.load_rules - server.fire(:start) - end - - server.fire(:stop) - end - - class Server - - class Rule - - attr_accessor :id - - # Now uses an 'id' parameter to uniquely identify rules - def initialize(id, pattern, callback) - @id = id - @pattern = pattern - @callback = callback - end - - end - - # New method that loads all rules from the database at server startup - def load_rules - BeEF::Core::Models::Dns::Rule.each do |rule| - id = rule.id - pattern = [rule.pattern, rule.type] - # antisnatchor: this would be unsafe, but input gets validated in extensions/dns/rest/dns.rb (lines 95 to 105) - # in this case input comes from the DB, but that data stored in the DB was originally coming from the now safe code - block = eval rule.block - - regex = pattern[0] - pattern[0] = Regexp.new(regex) if regex =~ /^\(\?-mix:/ - - @rules << Rule.new(id, pattern, block) - end - end - - # Now includes BeEF database support and checks for already present rules - def match(*pattern, block) - id = '' - - catch :match do - begin - # Sourcify block (already a string only for RESTful API calls) - block_src = case block - when String then - block - when Proc then - block.to_source - end - - # Break out and return id if rule is already present - BeEF::Core::Models::Dns::Rule.each do |rule| - if pattern[0] == rule.pattern && - pattern[1] == rule.type && - block_src == rule.block - - id = rule.id - throw :match - end - end - - id = generate_id - - if @rules == nil - @rules = [] - end - - case block - when String - # antisnatchor: this would be unsafe, but input gets validated in extensions/dns/rest/dns.rb (lines 95 to 105) - @rules << Rule.new(id, pattern, eval(block_src)) - when Proc - @rules << Rule.new(id, pattern, block) - end - - BeEF::Core::Models::Dns::Rule.create( - :id => id, - :pattern => pattern[0].to_s, - :type => pattern[1], - :block => block_src - ) - rescue Sourcify::CannotHandleCreatedOnTheFlyProcError, - Sourcify::CannotParseEvalCodeError, - Sourcify::MultipleMatchingProcsPerLineError, - Sourcify::NoMatchingProcError, - Sourcify::ParserInternalError - - @logger.error "Failed to sourcify block for DNS rule '#{id}'" - raise - end - end - - id - end - - # New method that removes a rule given its id and returns boolean result - def remove_rule(id) - @rules.delete_if { |rule| rule.id == id } - - rule = BeEF::Core::Models::Dns::Rule.get(id) - - rule != nil ? rule.destroy : false - end - - # New method that returns a hash representing the given rule - def get_rule(id) - result = {} - - begin - rule = BeEF::Core::Models::Dns::Rule.get!(id) - - result[:id] = rule.id - result[:pattern] = rule.pattern - result[:type] = rule.type.to_s.split('::')[-1] - result[:response] = parse_response(rule.block) - rescue DataMapper::ObjectNotFoundError => e - @logger.error(e.message) - end - - result - end - - # New method that returns the entire DNS ruleset as an AoH - def get_ruleset - result = [] - - BeEF::Core::Models::Dns::Rule.each do |rule| - element = {} - - element[:id] = rule.id - element[:pattern] = rule.pattern - element[:type] = rule.type.to_s.split('::')[-1] - element[:response] = parse_response(rule.block) - - result << element - end - - result - end - - # New method that removes the entire DNS ruleset - def remove_ruleset - @rules = [] - BeEF::Core::Models::Dns::Rule.destroy - end - - private - - # New method that generates a unique id for a rule - def generate_id - begin - id = BeEF::Core::Crypto.secure_token.byteslice(0..6) - - # Make sure id isn't already in use - BeEF::Core::Models::Dns::Rule.each { |rule| throw StandardError if id == rule.id } - rescue StandardError - retry - end - - id - end - - # New method that parses response callback and returns RDATA as an array - def parse_response(block) - # Extract response arguments into an array - methods = '(respond|failure)' - args = /(?<=\.#{methods}!\().*(?=\))/.match(block).to_s.split(/,\s*/) - - result = [] - - # Determine whether each argument is a domain name, integer, or IP address - args.each do |elem| - arg = nil - - if /Name\.create\((.*)\)/.match(elem) - arg = $1 - elsif /:(NoError|FormErr|ServFail|NXDomain|NotImp|Refused|NotAuth)/.match(elem) - arg = $1.upcase - else - int_test = elem.to_i - arg = (int_test != 0 ? int_test : elem) - end - - arg.gsub!(/['"]/, '') unless arg.is_a?(Integer) - - result << arg - end - - result - end - - end - - class Transaction - - # Behaves exactly the same, except using debug logger instead of info - def respond!(*data) - options = data.last.kind_of?(Hash) ? data.pop : {} - resource_class = options[:resource_class] || @resource_class - - if resource_class == nil - raise ArgumentError, "Could not instantiate resource #{resource_class}!" - end - - @server.logger.debug("Resource class: #{resource_class.inspect}") - resource = resource_class.new(*data) - @server.logger.debug("Resource: #{resource.inspect}") - - append!(resource, options) - end - - end - -end diff --git a/extensions/social_engineering/web_cloner/web_cloner.rb b/extensions/social_engineering/web_cloner/web_cloner.rb index d82f9645e..2185d682b 100644 --- a/extensions/social_engineering/web_cloner/web_cloner.rb +++ b/extensions/social_engineering/web_cloner/web_cloner.rb @@ -111,21 +111,33 @@ module BeEF interceptor.set :cloned_page, get_page_content(file_path) interceptor.set :db_entry, persist_page(url, mount) - @http_server.mount("#{mount}", interceptor.new) - print_info "Mounting cloned page on URL [#{mount}]" - @http_server.remap - # Add a DNS record spoofing the address of the cloned webpage as the BeEF server if dns_spoof dns = BeEF::Extension::Dns::Server.instance - ip = Socket.ip_address_list.detect { |i| !(i.ipv4_loopback? || i.ipv6_loopback?) } + ipv4 = Socket.ip_address_list.detect { |ai| ai.ipv4? && !ai.ipv4_loopback? }.ip_address + ipv6 = Socket.ip_address_list.detect { |ai| ai.ipv6? && !ai.ipv6_loopback? }.ip_address + ipv6.gsub!(/%\w*$/, '') domain = url.gsub(%r{^http://}, '') - id = dns.add_rule(domain, Resolv::DNS::Resource::IN::A) do |transaction| - transaction.respond!(ip.ip_address) - end + dns.add_rule( + :pattern => domain, + :resource => Resolv::DNS::Resource::IN::A, + :response => ipv4 + ) unless ipv4.nil? + + dns.add_rule( + :pattern => domain, + :resource => Resolv::DNS::Resource::IN::AAAA, + :response => ipv6 + ) unless ipv6.nil? + + print_info "DNS records spoofed [A: #{ipv4} AAAA: #{ipv6}]" end + print_info "Mounting cloned page on URL [#{mount}]" + @http_server.mount("#{mount}", interceptor.new) + @http_server.remap + success = true else print_error "Error cloning #{url}. Be sure that you don't have errors while retrieving the page with 'wget'." diff --git a/test/integration/ts_dns_rest.rb b/test/integration/ts_dns_rest.rb deleted file mode 100644 index 301d30bba..000000000 --- a/test/integration/ts_dns_rest.rb +++ /dev/null @@ -1,20 +0,0 @@ -# -# Copyright (c) 2006-2013 Wade Alcorn - wade@bindshell.net -# Browser Exploitation Framework (BeEF) - http://beefproject.com -# See the file 'doc/COPYING' for copying permission -# -require '../common/ts_common' -require './tc_dns_rest' - -class TS_DnsIntegrationTests - - def self.suite - suite = Test::Unit::TestSuite.new(name="BeEF DNS Integration Test Suite") - suite << TC_DnsRest.suite - - return suite - end - -end - -Test::Unit::UI::Console::TestRunner.run(TS_DnsIntegrationTests) diff --git a/test/integration/ts_integration.rb b/test/integration/ts_integration.rb index 8eea8a49f..6da122199 100644 --- a/test/integration/ts_integration.rb +++ b/test/integration/ts_integration.rb @@ -16,7 +16,7 @@ require './check_environment' # Basic log in and log out tests require './tc_debug_modules' # RESTful API tests (as well as debug modules) require './tc_login' # Basic log in and log out tests require './tc_jools' # Basic tests for jools - #require './tc_dns_rest' # Basic tests for DNS RESTful API interface +#require './tc_dns_rest' # Basic tests for DNS RESTful API interface require './tc_social_engineering_rest' # Basic tests for social engineering RESTful API interface class TS_BeefIntegrationTests diff --git a/test/unit/extensions/tc_dns.rb b/test/unit/extensions/tc_dns.rb index bfb7ca2c3..47e12b2d2 100644 --- a/test/unit/extensions/tc_dns.rb +++ b/test/unit/extensions/tc_dns.rb @@ -1,5 +1,5 @@ # -# Copyright (c) 2006-2013 Wade Alcorn - wade@bindshell.net +# Copyright (c) 2006-2014 Wade Alcorn - wade@bindshell.net # Browser Exploitation Framework (BeEF) - http://beefproject.com # See the file 'doc/COPYING' for copying permission # @@ -39,33 +39,24 @@ class TC_Dns < Test::Unit::TestCase # Checks for required settings in config file def test_02_config + assert(@@dns_config.has_key?('protocol')) assert(@@dns_config.has_key?('address')) assert(@@dns_config.has_key?('port')) + assert(@@dns_config.has_key?('upstream')) end # Verifies public interface def test_03_interface @@dns = BeEF::Extension::Dns::Server.instance - assert_respond_to(@@dns, :run_server) assert_respond_to(@@dns, :add_rule) - assert_respond_to(@@dns, :remove_rule) assert_respond_to(@@dns, :get_rule) + assert_respond_to(@@dns, :remove_rule!) assert_respond_to(@@dns, :get_ruleset) - assert_respond_to(@@dns, :remove_ruleset) + assert_respond_to(@@dns, :remove_ruleset!) end - # Tests that DNS server runs correctly on desired address and port - def test_04_run_server - address = @@dns_config['address'] - port = @@dns_config['port'] - - @@dns.run_server(address, port) - sleep(3) - - assert_equal(address, @@dns.address) - assert_equal(port, @@dns.port) - end + # @todo Decrement test numbers starting here. # Tests procedure for properly adding new DNS rules def test_05_add_rule_good