Merge pull request #1009 from soh-cah-toa/master
DNS Extension 2.0 Reimplementation
This commit is contained in:
3
Gemfile
3
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
|
||||
|
||||
@@ -146,4 +146,4 @@ beef:
|
||||
enable: true
|
||||
# this is still experimental, we're working on it..
|
||||
dns:
|
||||
enable: false
|
||||
enable: true
|
||||
|
||||
@@ -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 <code>:ipv4</code> or <code>:ipv6</code>)
|
||||
# @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
|
||||
|
||||
@@ -16,7 +16,6 @@ require 'base64'
|
||||
require 'xmlrpc/client'
|
||||
require 'openssl'
|
||||
require 'rubydns'
|
||||
require 'sourcify'
|
||||
|
||||
# @note Include the filters
|
||||
require 'core/filters'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
]
|
||||
|
||||
@@ -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
|
||||
#
|
||||
# * <code>:id</code>
|
||||
# * <code>:pattern</code>
|
||||
# * <code>:type</code>
|
||||
# * <code>:resource</code>
|
||||
# * <code>:response</code>
|
||||
#
|
||||
# @return [Array<Hash>] DNS ruleset (empty if no rules are currently loaded)
|
||||
# @return [Array<Hash>] 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<Array>] :upstream upstream DNS servers (if ommitted, unresolvable
|
||||
# requests return NXDOMAIN)
|
||||
# @option options [Array<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
|
||||
|
||||
@@ -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'
|
||||
|
||||
15
extensions/dns/logger.rb
Normal file
15
extensions/dns/logger.rb
Normal file
@@ -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
|
||||
@@ -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!(%<preference>d, Resolv::DNS::Name.create('%<exchange>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('%<mname>s'), " +
|
||||
"Resolv::DNS::Name.create('%<rname>s'), " +
|
||||
'%<serial>d, ' +
|
||||
'%<refresh>d, ' +
|
||||
'%<retry>d, ' +
|
||||
'%<expire>d, ' +
|
||||
'%<minimum>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!('%<address>s', %<protocol>d, %<bitmap>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
|
||||
|
||||
|
||||
@@ -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 "'%<address>s'", data
|
||||
when 'AAAA'
|
||||
data = {:address => rdata[0]}
|
||||
sprintf "'%<address>s'", data
|
||||
when 'CNAME'
|
||||
data = {:cname => rdata[0]}
|
||||
sprintf "Resolv::DNS::Name.create('%<cname>s')", data
|
||||
when 'HINFO'
|
||||
data = {:cpu => rdata[0], :os => rdata[1]}
|
||||
sprintf "'%<cpu>s', '%<os>s'", data
|
||||
when 'MINFO'
|
||||
data = {:rmailbx => rdata[0], :emailbx => rdata[1]}
|
||||
|
||||
sprintf "Resolv::DNS::Name.create('%<rmailbx>s'), " +
|
||||
"Resolv::DNS::Name.create('%<emailbx>s')",
|
||||
data
|
||||
when 'MX'
|
||||
data = {:preference => rdata[0], :exchange => rdata[1]}
|
||||
sprintf "%<preference>d, Resolv::DNS::Name.create('%<exchange>s')", data
|
||||
when 'NS'
|
||||
data = {:nsdname => rdata[0]}
|
||||
sprintf "Resolv::DNS::Name.create('%<nsdname>s')", data
|
||||
when 'PTR'
|
||||
data = {:ptrdname => rdata[0]}
|
||||
sprintf "Resolv::DNS::Name.create('%<ptrdname>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('%<mname>s'), " +
|
||||
"Resolv::DNS::Name.create('%<rname>s'), " +
|
||||
'%<serial>d, ' +
|
||||
'%<refresh>d, ' +
|
||||
'%<retry>d, ' +
|
||||
'%<expire>d, ' +
|
||||
'%<minimum>d',
|
||||
data
|
||||
when 'TXT'
|
||||
data = {:txtdata => rdata[0]}
|
||||
sprintf "'%<txtdata>s'", data
|
||||
when 'WKS'
|
||||
data = {
|
||||
:address => rdata[0],
|
||||
:protocol => rdata[1],
|
||||
:bitmap => rdata[2]
|
||||
}
|
||||
|
||||
sprintf "'%<address>s', %<protocol>d, %<bitmap>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
|
||||
|
||||
@@ -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'
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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'."
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user