diff --git a/Gemfile b/Gemfile index 257c2889f..4ec96bda8 100644 --- a/Gemfile +++ b/Gemfile @@ -32,6 +32,8 @@ gem "erubis" gem "dm-migrations" gem "msfrpc-client" gem "rubyzip", "~> 1.0.0" +gem "rubydns" +gem "sourcify" # notifications gem "twitter", ">= 5.0.0" diff --git a/config.yaml b/config.yaml index 314cb5246..462a135c4 100644 --- a/config.yaml +++ b/config.yaml @@ -120,3 +120,5 @@ beef: enable: false ipec: enable: true + dns: + enable: true diff --git a/core/loader.rb b/core/loader.rb index a4a657cd6..c3929bc59 100644 --- a/core/loader.rb +++ b/core/loader.rb @@ -15,6 +15,8 @@ require 'ipaddr' require 'base64' require 'xmlrpc/client' require 'openssl' +require 'rubydns' +require 'sourcify' # @note Include the filters require 'core/filters' @@ -29,4 +31,4 @@ require 'core/api' require 'core/settings' # @note Include the core of BeEF -require 'core/core' \ No newline at end of file +require 'core/core' diff --git a/extensions/dns/api.rb b/extensions/dns/api.rb new file mode 100644 index 000000000..b5cf4e65d --- /dev/null +++ b/extensions/dns/api.rb @@ -0,0 +1,48 @@ +# +# Copyright (c) 2006-2013 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - http://beefproject.com +# See the file 'doc/COPYING' for copying permission +# +module BeEF +module Extension +module Dns +module API + + module NameserverHandler + + BeEF::API::Registrar.instance.register( + 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' + ) + + # Begins main DNS server run-loop at BeEF startup + 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}" + end + + # Mounts handler for processing RESTful API calls + def self.mount_handler(beef_server) + beef_server.mount('/api/dns', BeEF::Extension::Dns::DnsRest.new) + end + + end + +end +end +end +end diff --git a/extensions/dns/config.yaml b/extensions/dns/config.yaml new file mode 100644 index 000000000..91590d06b --- /dev/null +++ b/extensions/dns/config.yaml @@ -0,0 +1,13 @@ +# +# Copyright (c) 2006-2013 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - http://beefproject.com +# See the file 'doc/COPYING' for copying permission +# +beef: + extension: + dns: + enable: true + name: 'DNS Server' + authors: ['soh_cah_toa'] + address: '127.0.0.1' + port: 5300 diff --git a/extensions/dns/dns.rb b/extensions/dns/dns.rb new file mode 100644 index 000000000..050f5f37e --- /dev/null +++ b/extensions/dns/dns.rb @@ -0,0 +1,151 @@ +# +# Copyright (c) 2006-2013 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - http://beefproject.com +# See the file 'doc/COPYING' for copying permission +# +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 + + 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 + @lock = Mutex.new + @server = nil + end + + # Starts the main DNS server run-loop in a new thread. + # + # @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 + + @lock.synchronize do + Thread.new do + # @note Calling #sleep is a quick fix that prevents race conditions + # with WebSockets. A better solution is needed; perhaps a + # global EventMachine mutex. + sleep(1) + + if EventMachine.reactor_running? + EventMachine.next_tick { run_server_block(@address, @port) } + else + run_server_block(@address, @port) + end + end + 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 + # + # 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 + # + # @param pattern [String, Regexp] query pattern to recognize + # @param type [Resolv::DNS::Resource::IN] resource record type (e.g. A, CNAME, NS, etc.) + # + # @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) } + end + + # Removes the given DNS rule. Any future queries for it will be passed through. + # + # @param id [Integer] id returned from {#add_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 + def get_rule(id) + @lock.synchronize { @server.get_rule(id) } + end + + # Returns an AoH representing the entire current DNS ruleset. + # + # Each element is a hash with the following keys: + # + # * :id + # * :pattern + # * :type + # * :response + # + # @return [Array] DNS ruleset (empty if no rules are currently loaded) + def get_ruleset + @lock.synchronize { @server.get_ruleset } + end + + # Clears 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 } + end + + private + + # Common code needed by {#run_server} to start 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 + server = self + BeEF::Extension::Dns::Server.instance.instance_eval { @server = server } + + # Pass unmatched queries upstream to root nameservers + otherwise do |transaction| + transaction.passthrough!( + RubyDNS::Resolver.new([[:udp, '8.8.8.8', 53], [:tcp, '8.8.8.8', 53]]) + ) + end + end + end + + end + +end +end +end diff --git a/extensions/dns/extension.rb b/extensions/dns/extension.rb new file mode 100644 index 000000000..702f602f7 --- /dev/null +++ b/extensions/dns/extension.rb @@ -0,0 +1,25 @@ +# +# Copyright (c) 2006-2013 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - http://beefproject.com +# See the file 'doc/COPYING' for copying permission +# +module BeEF +module Extension +module Dns + + extend BeEF::API::Extension + + @short_name = 'dns' + @full_name = 'DNS Server' + @description = 'A configurable DNS nameserver for performing DNS spoofing, ' + + 'hijacking, and other related attacks against hooked zombies' + +end +end +end + +require 'extensions/dns/api' +require 'extensions/dns/dns' +require 'extensions/dns/model' +require 'extensions/dns/rest/dns' +require 'extensions/dns/ruby' diff --git a/extensions/dns/model.rb b/extensions/dns/model.rb new file mode 100644 index 000000000..ee6a4e291 --- /dev/null +++ b/extensions/dns/model.rb @@ -0,0 +1,27 @@ +# +# Copyright (c) 2006-2013 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - http://beefproject.com +# See the file 'doc/COPYING' for copying permission +# +module BeEF +module Core +module Models +module Dns + + 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 + + end + +end +end +end +end diff --git a/extensions/dns/rest/dns.rb b/extensions/dns/rest/dns.rb new file mode 100644 index 000000000..754277df7 --- /dev/null +++ b/extensions/dns/rest/dns.rb @@ -0,0 +1,245 @@ +# +# Copyright (c) 2006-2013 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - http://beefproject.com +# See the file 'doc/COPYING' for copying permission +# +module BeEF +module Extension +module Dns + + # This class handles the routing of RESTful API requests that query BeEF's DNS server + class DnsRest < BeEF::Core::Router::Router + + # Filters out bad requests before performing any routing + before do + config = BeEF::Core::Configuration.instance + + # Require a valid API token from a valid IP address + halt 401 unless params[:token] == config.get('beef.api_token') + halt 403 unless BeEF::Core::Rest.permitted_source?(request.ip) + + headers 'Content-Type' => 'application/json; charset=UTF-8', + 'Pragma' => 'no-cache', + 'Cache-Control' => 'no-cache', + 'Expires' => '0' + end + + # Returns the entire current DNS ruleset + get '/ruleset' do + begin + ruleset = BeEF::Extension::Dns::Server.instance.get_ruleset + count = ruleset.length + + result = {} + result[:count] = count + result[:ruleset] = ruleset + result.to_json + rescue StandardError => e + print_error "Internal error while retrieving DNS ruleset (#{e.message})" + halt 500 + end + end + + # Returns a specific rule given its id + get '/rule/:id' do + begin + id = params[:id] + + unless BeEF::Filters.alphanums_only?(id) + raise InvalidParamError, 'Invalid "id" parameter passed to endpoint /api/dns/rule/:id' + end + + result = BeEF::Extension::Dns::Server.instance.get_rule(id) + halt 404 if result.length == 0 + + result.to_json + rescue InvalidParamError => e + print_error e.message + halt 400 + rescue StandardError => e + print_error "Internal error while retrieving DNS rule with id #{id} (#{e.message})" + halt 500 + end + end + + # Adds a new DNS rule + post '/rule' do + begin + body = JSON.parse(request.body.read) + + pattern = body['pattern'] + type = body['type'] + response = body['response'] + + # Validate required JSON keys + unless [pattern, type, response].include?(nil) + # Determine whether 'pattern' is a String or Regexp + begin + pattern_test = eval pattern + pattern = pattern_test if pattern_test.class == Regexp + rescue => e; end + + if response.class == Array + if response.length == 0 + raise InvalidJsonError, 'Empty "reponse" key passed to endpoint /api/dns/rule' + end + else + raise InvalidJsonError, 'Non-array "reponse" 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) + raise InvalidJsonError, 'Empty "type" key passed to endpoint /api/dns/rule' + end + + id = '' + + block_src = format_response(type, response) + type_obj = eval "Resolv::DNS::Resource::IN::#{type}" + + # Bypass #add_rule so that 'block_src' can be passed as a String + BeEF::Extension::Dns::Server.instance.instance_eval do + id = @server.match(pattern, type_obj, block_src) + end + + result = {} + result['success'] = true + result['id'] = id + result.to_json + end + rescue InvalidJsonError => e + print_error e.message + halt 400 + rescue StandardError => e + print_error "Internal error while adding DNS rule (#{e.message})" + halt 500 + end + end + + # Removes a rule given its id + delete '/rule/:id' do + begin + id = params[:id] + + unless BeEF::Filters.alphanums_only?(id) + raise InvalidParamError, 'Invalid "id" parameter passed to endpoint /api/dns/rule/:id' + end + + result = {} + result['success'] = BeEF::Extension::Dns::Server.instance.remove_rule(id) + result.to_json + rescue InvalidParamError => e + print_error e.message + halt 400 + rescue StandardError => e + print_error "Internal error while removing DNS rule with id #{id} (#{e.message})" + halt 500 + 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 + + DEFAULT_MESSAGE = 'Invalid JSON input passed to /api/dns handler' + + def initialize(message = nil) + super(message || DEFAULT_MESSAGE) + end + + end + + # Raised when an invalid named parameter is passed to an /api/dns handler. + class InvalidParamError < StandardError + + DEFAULT_MESSAGE = 'Invalid parameter passed to /api/dns handler' + + def initialize(message = nil) + super(message || DEFAULT_MESSAGE) + end + + end + + end + +end +end +end diff --git a/extensions/dns/ruby.rb b/extensions/dns/ruby.rb new file mode 100644 index 000000000..99cf46225 --- /dev/null +++ b/extensions/dns/ruby.rb @@ -0,0 +1,7 @@ +# +# 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 new file mode 100644 index 000000000..3e88cbfa0 --- /dev/null +++ b/extensions/dns/ruby/logger.rb @@ -0,0 +1,27 @@ +# +# 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 new file mode 100644 index 000000000..ded90a681 --- /dev/null +++ b/extensions/dns/ruby/rubydns.rb @@ -0,0 +1,242 @@ +# +# 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) + + 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] + 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 + + case block + when String + @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/rest/socialengineering.rb b/extensions/social_engineering/rest/socialengineering.rb index 88a7b1008..e36272014 100644 --- a/extensions/social_engineering/rest/socialengineering.rb +++ b/extensions/social_engineering/rest/socialengineering.rb @@ -20,10 +20,15 @@ module BeEF 'Expires' => '0' end - #Example: curl -H "Content-Type: application/json; charset=UTF-8" - #-d '{"url":"https://accounts.google.com/ServiceLogin?service=mail&passive=true&rm=false&continue= - #https://mail.google.com/mail/&ss=1&scc=1<mpl=default<mplcache=2", "mount":"/url"}' - #-X POST http://127.0.0.1:3000/api/seng/clone_page?token=851a937305f8773ee82f5259e792288cdcb01cd7 + # Example: curl -H "Content-Type: application/json; charset=UTF-8" -d json_body + # -X POST http://127.0.0.1:3000/api/seng/clone_page?token=851a937305f8773ee82f5259e792288cdcb01cd7 + # + # Example json_body: + # { + # "url": "https://accounts.google.com/ServiceLogin?service=mail&continue=https://mail.google.com/mail/" + # "mount": "/gmail", + # "dns_spoof": true + # } post '/clone_page' do request.body.rewind begin @@ -31,6 +36,7 @@ module BeEF uri = body["url"] mount = body["mount"] use_existing = body["use_existing"] + dns_spoof = body["dns_spoof"] if uri != nil && mount != nil if (uri =~ URI::regexp).nil? #invalid URI @@ -44,7 +50,8 @@ module BeEF end web_cloner = BeEF::Extension::SocialEngineering::WebCloner.instance - success = web_cloner.clone_page(uri,mount,use_existing) + success = web_cloner.clone_page(uri, mount, use_existing, dns_spoof) + if success result = { "success" => true, @@ -125,4 +132,4 @@ module BeEF end end end -end \ No newline at end of file +end diff --git a/extensions/social_engineering/web_cloner/web_cloner.rb b/extensions/social_engineering/web_cloner/web_cloner.rb index 09c7c6636..bbd621bd4 100644 --- a/extensions/social_engineering/web_cloner/web_cloner.rb +++ b/extensions/social_engineering/web_cloner/web_cloner.rb @@ -7,9 +7,9 @@ module BeEF module Extension module SocialEngineering class WebCloner + require 'socket' include Singleton - def initialize @http_server = BeEF::Core::Server.instance @config = BeEF::Core::Configuration.instance @@ -17,7 +17,7 @@ module BeEF @beef_hook = "http://#{@config.get('beef.http.host')}:#{@config.get('beef.http.port')}#{@config.get('beef.http.hook_file')}" end - def clone_page(url, mount, use_existing) + def clone_page(url, mount, use_existing, dns_spoof) print_info "Cloning page at URL #{url}" uri = URI(url) output = uri.host @@ -113,6 +113,18 @@ module BeEF @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?)} + domain = url.gsub(%r{^http://}, '') + + id = dns.add_rule(domain, Resolv::DNS::Resource::IN::A) do |transaction| + transaction.respond!(ip.ip_address) + end + end + 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/common/test_constants.rb b/test/common/test_constants.rb index 74291c1d7..e635771be 100644 --- a/test/common/test_constants.rb +++ b/test/common/test_constants.rb @@ -19,4 +19,6 @@ BEEF_PASSWD = "beef" RESTAPI_HOOKS = "http://" + ATTACK_DOMAIN + ":3000/api/hooks" RESTAPI_LOGS = "http://" + ATTACK_DOMAIN + ":3000/api/logs" RESTAPI_MODULES = "http://" + ATTACK_DOMAIN + ":3000/api/modules" -RESTAPI_ADMIN = "http://" + ATTACK_DOMAIN + ":3000/api/admin" \ No newline at end of file +RESTAPI_DNS = "http://" + ATTACK_DOMAIN + ":3000/api/dns" +RESTAPI_SENG = "http://" + ATTACK_DOMAIN + ":3000/api/seng" +RESTAPI_ADMIN = "http://" + ATTACK_DOMAIN + ":3000/api/admin" diff --git a/test/integration/tc_dns_rest.rb b/test/integration/tc_dns_rest.rb new file mode 100644 index 000000000..b6921ebf5 --- /dev/null +++ b/test/integration/tc_dns_rest.rb @@ -0,0 +1,388 @@ +# +# 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 'test/unit' +require 'rest_client' +require 'json' +require '../common/test_constants' + +class TC_DnsRest < Test::Unit::TestCase + + class << self + + def startup + json = {:username => BEEF_USER, :password => BEEF_PASSWD}.to_json + @@headers = {:content_type => :json, :accept => :json} + + response = RestClient.post("#{RESTAPI_ADMIN}/login", + json, + @@headers) + + result = JSON.parse(response.body) + @@token = result['token'] + + $root_dir = '../../' + $:.unshift($root_dir) + + require 'core/loader' + + BeEF::Core::Configuration.new(File.join($root_dir, 'config.yaml')) + BeEF::Core::Configuration.instance.load_extensions_config + + @@config = BeEF::Core::Configuration.instance + end + + def shutdown + $root_dir = nil + end + + end + + # Tests POST /api/dns/rule handler with valid input + def test_1_add_rule_good + pattern = 'foo.bar' + type = 'A' + dns_response = ['1.2.3.4'] + + json = {:pattern => pattern, :type => type, :response => dns_response}.to_json + + rest_response = RestClient.post("#{RESTAPI_DNS}/rule?token=#{@@token}", + json, + @@headers) + + check_rest_response(rest_response) + + result = JSON.parse(rest_response.body) + first_id = result['id'] + + rest_response = RestClient.post("#{RESTAPI_DNS}/rule?token=#{@@token}", + json, + @@headers) + + # Verify that adding an existing rule returns its id + check_rest_response(rest_response) + + result = JSON.parse(rest_response.body) + second_id = result['id'] + + assert_equal(first_id, second_id) + end + + # Tests POST /api/dns/rule handler with invalid input + def test_2_add_rule_bad + pattern = '' + type = 'A' + dns_response = ['1.1.1.1'] + + hash = {:pattern => pattern, :type => type, :response => dns_response} + + # Test that an empty "pattern" key returns 400 + assert_raise RestClient::BadRequest do + rest_response = RestClient.post("#{RESTAPI_DNS}/rule?token=#{@@token}", + hash.to_json, + @@headers) + end + + hash['pattern'] = 'foo.bar.baz' + hash['type'] = '' + + # Test that an empty "type" key returns 400 + assert_raise RestClient::BadRequest do + rest_response = RestClient.post("#{RESTAPI_DNS}/rule?token=#{@@token}", + hash.to_json, + @@headers) + end + + hash['type'] = 'A' + hash['response'] = [] + + # Test that an empty "response" key returns 400 + assert_raise RestClient::BadRequest do + rest_response = RestClient.post("#{RESTAPI_DNS}/rule?token=#{@@token}", + hash.to_json, + @@headers) + end + + hash['response'] = 42 + + # Test that a non-array "response" key returns 400 + assert_raise RestClient::BadRequest do + rest_response = RestClient.post("#{RESTAPI_DNS}/rule?token=#{@@token}", + hash.to_json, + @@headers) + end + end + + # Tests POST /api/dns/rule handler with each supported RR type + def test_3_add_rule_types + pattern = 'be.ef' + type = 'AAAA' + response = ['2001:db8:ac10:fe01::'] + + # Test AAAA type + rule = {'pattern' => pattern, 'type' => type, 'response' => response} + + regex = %r{ + ^#{rule['pattern']}\.\t+ + \d+\t+ + IN\t+ + #{rule['type']}\t+ + #{rule['response'][0]}$ + }x + + add_rule(rule) + check_dns_response(regex, rule['type'], rule['pattern']) + + # Test CNAME type + rule['type'] = 'CNAME' + rule['response'] = ['fe.eb.'] + + regex = %r{ + ^#{rule['pattern']}\.\t+ + \d+\t+ + IN\t+ + #{rule['type']}\t+ + #{rule['response'][0]}$ + }x + + add_rule(rule) + check_dns_response(regex, rule['type'], rule['pattern']) + + # Test HINFO type + rule['type'] = 'HINFO' + rule['response'] = ['M6800', 'VMS'] + + regex = %r{ + ^#{rule['pattern']}\.\t+ + \d+\t+ + IN\t+ + #{rule['type']}\t+ + "#{rule['response'][0]}"\s+ + "#{rule['response'][1]}"$ + }x + + add_rule(rule) + check_dns_response(regex, rule['type'], rule['pattern']) + + # Test MINFO type + rule['type'] = 'MINFO' + rule['response'] = ['rmail.be.ef.', 'email.be.ef.'] + + regex = %r{ + ^#{rule['pattern']}\.\t+ + \d+\t+ + IN\t+ + #{rule['type']}\t+ + #{rule['response'][0]}\s+ + #{rule['response'][1]}$ + }x + + add_rule(rule) + check_dns_response(regex, rule['type'], rule['pattern']) + + # Test MX type + rule['type'] = 'MX' + rule['response'] = [10, 'mail.be.ef.'] + + regex = %r{ + ^#{rule['pattern']}\.\t+ + \d+\t+ + IN\t+ + #{rule['type']}\t+ + #{rule['response'][0]}\s+ + #{rule['response'][1]}$ + }x + + add_rule(rule) + check_dns_response(regex, rule['type'], rule['pattern']) + + # Test NS type + rule['type'] = 'NS' + rule['response'] = ['ns.be.ef.'] + + regex = %r{ + ^#{rule['pattern']}\.\t+ + \d+\t+ + IN\t+ + #{rule['type']}\t+ + #{rule['response'][0]}$ + }x + + add_rule(rule) + check_dns_response(regex, rule['type'], rule['pattern']) + + # Test PTR type + rule['type'] = 'PTR' + rule['response'] = ['4.3.2.1.in-addr.arpa.'] + + regex = %r{ + ^#{rule['pattern']}\.\t+ + \d+\t+ + IN\t+ + #{rule['type']}\t+ + #{rule['response'][0]}$ + }x + + add_rule(rule) + check_dns_response(regex, rule['type'], rule['pattern']) + + # Test SOA type + rule['type'] = 'SOA' + rule['response'] = [ + "ns.#{rule['pattern']}.", + "mail.#{rule['pattern']}.", + 2012031500, + 10800, + 3600, + 604800, + 3600 + ] + + regex = %r{ + ^#{rule['pattern']}\.\t+ + \d+\t+ + IN\t+ + #{rule['type']}\t+ + .* + }x + + add_rule(rule) + check_dns_response(regex, rule['type'], rule['pattern']) + + # Test TXT type + rule['type'] = 'TXT' + rule['response'] = ['b33f_is_s0_l33t'] + + regex = %r{ + ^#{rule['pattern']}\.\t+ + \d+\t+ + IN\t+ + #{rule['type']}\t+ + "#{rule['response'][0]}"$ + }x + + add_rule(rule) + check_dns_response(regex, rule['type'], rule['pattern']) + + # Test WKS type + rule['type'] = 'WKS' + rule['response'] = ['9.9.9.9', 6, 0] + + regex = %r{ + ^#{rule['pattern']}\.\t+ + \d+\t+ + IN\t+ + #{rule['type']}\t+ + #{rule['response'][0]}\s + 0\s5\s6$ + }x + + add_rule(rule) + check_dns_response(regex, rule['type'], rule['pattern']) + + # Test that an invalid RR returns 400 + rule['type'] = 'BeEF' + + assert_raise RestClient::BadRequest do + rest_response = RestClient.post("#{RESTAPI_DNS}/rule?token=#{@@token}", + rule.to_json, + @@headers) + end + end + + # Tests GET /api/dns/rule/:id handler with valid input + def test_4_get_rule_good + pattern = 'wheres.the.beef' + type = 'A' + dns_response = ['4.2.4.2'] + + json = {:pattern => pattern, :type => type, :response => dns_response}.to_json + + rest_response = RestClient.post("#{RESTAPI_DNS}/rule?token=#{@@token}", + json, + @@headers) + + check_rest_response(rest_response) + result = JSON.parse(rest_response.body) + id = result['id'] + + rest_response = RestClient.get("#{RESTAPI_DNS}/rule/#{id}", :params => {:token => @@token}) + + assert_not_nil(rest_response.body) + assert_equal(200, rest_response.code) + + result = JSON.parse(rest_response.body) + + assert_equal(id, result['id']) + assert_equal(pattern, result['pattern']) + assert_equal(type, result['type']) + assert_equal(dns_response, result['response']) + end + + # Tests GET /api/dns/rule/:id handler with invalid input + def test_4_get_rule_bad + id = 42 + + assert_raise RestClient::ResourceNotFound do + response = RestClient.get("#{RESTAPI_DNS}/rule/#{id}", :params => {:token => @@token}) + end + + id = '(*_*)' + + assert_raise RestClient::BadRequest do + RestClient.get("#{RESTAPI_DNS}/rule/#{id}", :params => {:token => @@token}) + end + end + + # Tests GET /api/dns/ruleset handler + def test_4_get_ruleset + rest_response = RestClient.get("#{RESTAPI_DNS}/ruleset", :params => {:token => @@token}) + + assert_not_nil(rest_response.body) + assert_equal(200, rest_response.code) + + result = JSON.parse(rest_response.body) + assert_equal(15, result['count']) + + result['ruleset'].each do |rule| + assert(rule['id']) + assert(rule['pattern']) + assert(rule['type']) + assert(rule['response'].length != 0) + end + end + + private + + # Adds a new DNS rule + def add_rule(params) + response = RestClient.post("#{RESTAPI_DNS}/rule?token=#{@@token}", + params.to_json, + @@headers) + + check_rest_response(response) + end + + # Standard assertions for verifying response from RESTful API + def check_rest_response(response) + assert_not_nil(response.body) + assert_equal(200, response.code) + + result = JSON.parse(response.body) + + assert(result['success']) + assert(result['id']) + end + + # Compares output of dig command against regex + def check_dns_response(regex, type, pattern) + address = @@config.get('beef.extension.dns.address') + port = @@config.get('beef.extension.dns.port') + + dig_output = `dig @#{address} -p #{port} -t #{type} #{pattern}` + assert_match(regex, dig_output) + end + +end diff --git a/test/integration/tc_social_engineering_rest.rb b/test/integration/tc_social_engineering_rest.rb new file mode 100644 index 000000000..9895bb0f3 --- /dev/null +++ b/test/integration/tc_social_engineering_rest.rb @@ -0,0 +1,91 @@ +# +# 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 'test/unit' +require 'rest_client' +require 'json' +require '../common/test_constants' + +# @todo RESTful API for the social engineering extension lacks some serious test coverage. +class TC_SocialEngineeringRest < Test::Unit::TestCase + + class << self + + # Login to API before performing any tests + def startup + json = {:username => BEEF_USER, :password => BEEF_PASSWD}.to_json + @@headers = {:content_type => :json, :accept => :json} + + response = RestClient.post("#{RESTAPI_ADMIN}/login", + json, + @@headers) + + result = JSON.parse(response.body) + @@token = result['token'] + + $root_dir = '../../' + $:.unshift($root_dir) + + require 'core/loader' + + BeEF::Core::Configuration.new(File.join($root_dir, 'config.yaml')) + BeEF::Core::Configuration.instance.load_extensions_config + + @@config = BeEF::Core::Configuration.instance + end + + def shutdown + $root_dir = nil + end + + end + + # Tests DNS spoofing of cloned webpages + def test_1_dns_spoof + url = 'http://beefproject.com' + mount = '/beefproject' + dns_spoof = true + + json = {:url => url, :mount => mount, :dns_spoof => dns_spoof}.to_json + + response = RestClient.post("#{RESTAPI_SENG}/clone_page?token=#{@@token}", + json, + @@headers) + + check_response(response) + + ip = Socket.ip_address_list.detect {|i| !(i.ipv4_loopback? || i.ipv6_loopback?)} + domain = url.gsub(%r{^http://}, '') + + regex = %r{ + ^#{domain}\.\t+ + \d+\t+ + IN\t+ + A\t+ + #{ip.ip_address}$ + }x + + # Send DNS request to server to verify that a new rule was added + dns_address = @@config.get('beef.extension.dns.address') + dns_port = @@config.get('beef.extension.dns.port') + + dig_output = `dig @#{dns_address} -p #{dns_port} -t A #{domain}` + assert_match(regex, dig_output) + end + + private + + # Assertions for verifying a response from the RESTful API + def check_response(response) + assert_not_nil(response.body) + assert_equal(200, response.code) + + result = JSON.parse(response.body) + + assert(result['success']) + assert(result['mount']) + end + +end diff --git a/test/integration/ts_dns_rest.rb b/test/integration/ts_dns_rest.rb new file mode 100644 index 000000000..301d30bba --- /dev/null +++ b/test/integration/ts_dns_rest.rb @@ -0,0 +1,20 @@ +# +# 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 0910f2b57..4a4611bff 100644 --- a/test/integration/ts_integration.rb +++ b/test/integration/ts_integration.rb @@ -16,6 +16,8 @@ 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_social_engineering_rest' # Basic tests for social engineering RESTful API interface class TS_BeefIntegrationTests def self.suite @@ -25,6 +27,8 @@ class TS_BeefIntegrationTests suite << TC_login.suite suite << TC_DebugModules.suite suite << TC_Jools.suite + suite << TC_DnsRest.suite + suite << TC_SocialEngineeringRest.suite return suite end diff --git a/test/unit/extensions/tc_dns.rb b/test/unit/extensions/tc_dns.rb new file mode 100644 index 000000000..f6fe67687 --- /dev/null +++ b/test/unit/extensions/tc_dns.rb @@ -0,0 +1,310 @@ +# +# 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 'test/unit' +require 'resolv' + +class TC_Dns < Test::Unit::TestCase + + IN = Resolv::DNS::Resource::IN + + class << self + + def startup + $root_dir = '../../' + $:.unshift(File.expand_path($root_dir)) + + require 'extensions/dns/extension' + + BeEF::Core::Configuration.new(File.join($root_dir, 'config.yaml')) + config = BeEF::Core::Configuration.instance + config.load_extensions_config + + @@dns_config = config.get('beef.extension.dns') + end + + def shutdown + $root_dir = nil + end + + end + + # Connects to in-memory database (does not test anything) + def test_01_database + DataMapper.setup(:default, 'sqlite3::memory:') + DataMapper.auto_migrate! + end + + # Checks for required settings in config file + def test_02_config + assert(@@dns_config.has_key?('address')) + assert(@@dns_config.has_key?('port')) + 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, :get_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 + + # Tests procedure for properly adding new DNS rules + def test_05_add_rule_good + id1 = nil + id2 = nil + + assert_nothing_raised do + id1 = @@dns.add_rule('foo.bar', IN::A) do |transaction| + transaction.respond!('1.2.3.4') + end + end + + assert_not_nil(id1) + + assert_nothing_raised do + id2 = @@dns.add_rule(%r{i\.(love|hate)\.beef\.com?}, IN::A) do |transaction| + transaction.respond!('9.9.9.9') + end + end + + assert_not_nil(id2) + + domain1 = 'i.hate.beef.com' + domain2 = 'i.love.beef.com' + domain3 = 'i.love.beef.co' + domain4 = 'i.love.beef.co' + + [domain1, domain2, domain3, domain4].each do |domain| + regex = /^#{domain}\.\t+\d+\t+IN\t+A\t+9\.9\.9\.9$/ + check_dns_response(regex, 'A', domain) + end + end + + # Tests addition of new rules with invalid parameters + def test_06_add_rule_bad + id = nil + same_id = nil + + # Add the same rule twice + assert_nothing_raised do + id = @@dns.add_rule('j.random.hacker', IN::A) do |transaction| + transaction.respond!('4.2.4.2') + end + end + + assert_nothing_raised do + same_id = @@dns.add_rule('j.random.hacker', IN::A) do |transaction| + transaction.respond!('4.2.4.2') + end + end + + assert_equal(id, same_id) + + # Use /.../ literal syntax to throw Sourcify exception + assert_raise do + id = @@dns.add_rule(/.*/, IN::A) do |transaction| + transaction.respond!('5.1.5.0') + end + end + end + + # Verifies the proper format for rule identifiers + def test_07_id_format + id = @@dns.add_rule('dead.beef', IN::A) do |transaction| + transaction.respond!('2.2.2.2') + end + + assert_equal(7, id.length) + assert_not_nil(id =~ /^\h{7}$/) + end + + # Tests retrieval of valid DNS rules + def test_08_get_rule_good + id = @@dns.add_rule('be.ef', IN::A) do |transaction| + transaction.respond!('1.1.1.1') + end + + rule = @@dns.get_rule(id) + + assert_equal(Hash, rule.class) + assert(rule.length > 0) + + assert(rule.has_key?(:id)) + assert(rule.has_key?(:pattern)) + assert(rule.has_key?(:type)) + assert(rule.has_key?(:response)) + + assert_equal(id, rule[:id]) + assert_equal('be.ef', rule[:pattern]) + assert_equal('A', rule[:type]) + + response = rule[:response] + + assert_equal(Array, response.class) + assert(response.length > 0) + assert_equal('1.1.1.1', response[0]) + end + + # Tests retrieval of invalid DNS rules + def test_09_get_rule_bad + rule = @@dns.get_rule(42) + + assert_equal(Hash, rule.class) + assert_equal(0, rule.length) + end + + # Tests the removal of existing DNS rules + def test_10_remove_rule_good + id = @@dns.add_rule('hack.the.gibson', IN::A) do |transaction| + transaction.respond!('1.9.9.5') + end + + removed = @@dns.remove_rule(id) + + assert(removed) + end + + # Tests the removal of unknown DNS rules + def test_11_remove_rule_bad + removed = @@dns.remove_rule(42) + + assert(!removed) + end + + # Tests the retrieval of the entire DNS ruleset + def test_12_get_ruleset + ruleset = @@dns.get_ruleset + ruleset.sort! {|a, b| a[:pattern] <=> b[:pattern] } + + assert_equal(Array, ruleset.class) + assert_equal(5, ruleset.length) + + check_rule(ruleset[0], {:pattern=>'(?-mix:i\\.(love|hate)\\.beef\\.com?)', + :type => 'A', + :response => '9.9.9.9'}) + + check_rule(ruleset[1], {:pattern => 'be.ef', :type => 'A', :response => '1.1.1.1'}) + check_rule(ruleset[2], {:pattern => 'dead.beef', :type => 'A', :response => '2.2.2.2'}) + check_rule(ruleset[3], {:pattern => 'foo.bar', :type => 'A', :response => '1.2.3.4'}) + check_rule(ruleset[4], {:pattern => 'j.random.hacker', :type => 'A', :response => '4.2.4.2'}) + end + + # Tests the removal of the entire DNS ruleset + def test_13_remove_ruleset + removed = @@dns.remove_ruleset + ruleset = @@dns.get_ruleset + + assert(removed) + assert_equal(0, ruleset.length) + end + + # Tests each supported type of query failure + def test_14_failure_types + begin + id = @@dns.add_rule('noerror.beef.com', IN::A) do |transaction| + transaction.failure!(:NoError) + end + + check_failure_status(id, :NoError) + end + + begin + id = @@dns.add_rule('formerr.beef.com', IN::A) do |transaction| + transaction.failure!(:FormErr) + end + + check_failure_status(id, :FormErr) + end + + begin + id = @@dns.add_rule('servfail.beef.com', IN::A) do |transaction| + transaction.failure!(:ServFail) + end + + check_failure_status(id, :ServFail) + end + + begin + id = @@dns.add_rule('nxdomain.beef.com', IN::A) do |transaction| + transaction.failure!(:NXDomain) + end + + check_failure_status(id, :NXDomain) + end + + begin + id = @@dns.add_rule('notimp.beef.com', IN::A) do |transaction| + transaction.failure!(:NotImp) + end + + check_failure_status(id, :NotImp) + end + + begin + id = @@dns.add_rule('refused.beef.com', IN::A) do |transaction| + transaction.failure!(:Refused) + end + + check_failure_status(id, :Refused) + end + + begin + id = @@dns.add_rule('notauth.beef.com', IN::A) do |transaction| + transaction.failure!(:NotAuth) + end + + check_failure_status(id, :NotAuth) + end + end + + private + + # Compares each key in hash 'rule' with the respective key in hash 'expected' + def check_rule(rule, expected = {}) + assert_equal(expected[:pattern], rule[:pattern]) + assert_equal(expected[:type], rule[:type]) + assert_equal(expected[:response], rule[:response][0]) + end + + # Confirms that a query for the rule given in 'id' returns a 'type' failure status + def check_failure_status(id, type) + rule = @@dns.get_rule(id) + status = type.to_s.force_encoding('UTF-8').upcase + assert_equal(status, rule[:response][0]) + + check_dns_response(/status: #{status}/, rule[:type], rule[:pattern]) + end + + # Compares output of dig command against regex + def check_dns_response(regex, type, pattern) + dig_output = `dig @#{@@dns.address} -p #{@@dns.port} -t #{type} #{pattern}` + assert_match(regex, dig_output) + end + +end + +# Suppresses unnecessary output from RubyDNS +module Kernel + + def puts(*args); end + +end diff --git a/test/unit/tc_grep.rb b/test/unit/tc_grep.rb index 8719516b7..92e8e3a52 100644 --- a/test/unit/tc_grep.rb +++ b/test/unit/tc_grep.rb @@ -12,12 +12,13 @@ class TC_Grep < Test::Unit::TestCase File.open( path ) do |f| next if /tc_grep.rb/.match(path) # skip this file next if /\/msf-test\//.match(path) # skip this file + next if /extensions\/dns/.match(path) # skip this file + f.grep( /\Weval\W/im ) do |line| assert(false, "Illegal use of 'eval' in framework: " + path + ':' + line) end end end - end end diff --git a/test/unit/ts_unit.rb b/test/unit/ts_unit.rb index fd9be5496..d4b12ed81 100644 --- a/test/unit/ts_unit.rb +++ b/test/unit/ts_unit.rb @@ -27,6 +27,7 @@ require './extensions/tc_hooks' require './extensions/tc_proxy' require './extensions/tc_requester' require './extensions/tc_event_logger' +require './extensions/tc_dns' require './tc_grep' require './tc_filesystem' @@ -55,6 +56,7 @@ class TS_BeefTests suite << TC_EventLogger.suite suite << TC_Hooks.suite suite << TC_Redirector.suite + suite << TC_Dns.suite return suite end