From f8cba6e9528202100d23cd7aca2ac01ddea52602 Mon Sep 17 00:00:00 2001 From: bcoles Date: Sun, 2 Apr 2023 02:05:41 +1000 Subject: [PATCH] AutorunEngine: RuleLoader: Skip enabled rule file if rule is already in database (#2782) * AutorunEngine: Engine: store rule execution rule ID * AutorunEngine: RuleLoader: Skip enabled rule file if rule is already in database --- core/main/autorun_engine/engine.rb | 16 ++- core/main/autorun_engine/parser.rb | 67 +++++------ core/main/autorun_engine/rule_loader.rb | 147 +++++++++++++++--------- 3 files changed, 133 insertions(+), 97 deletions(-) diff --git a/core/main/autorun_engine/engine.rb b/core/main/autorun_engine/engine.rb index e8df427d9..ce73fb2aa 100644 --- a/core/main/autorun_engine/engine.rb +++ b/core/main/autorun_engine/engine.rb @@ -43,7 +43,12 @@ module BeEF execution_order = JSON.parse(rule.execution_order) execution_delay = JSON.parse(rule.execution_delay) - chain_mode = rule.chain_mode + chain_mode = rule.chain_mode + + unless %w[sequential nested-forward].include?(chain_mode) + print_error("[ARE] Invalid chain mode '#{chain_mode}' for rule") + return + end mods_bodies = [] mods_codes = [] @@ -76,9 +81,9 @@ module BeEF when 'sequential' wrapper = prepare_sequential_wrapper(mods_bodies, execution_order, execution_delay, rule_token) else - wrapper = nil - print_error 'Chain mode looks wrong!' - # TODO: catch error, which should never happen as values are checked way before ;-) + # we should never get here. chain mode is validated earlier. + print_error("[ARE] Invalid chain mode '#{chain_mode}'") + next end are_exec = BeEF::Core::Models::Execution.new( @@ -88,9 +93,10 @@ module BeEF rule_token: rule_token, mod_body: wrapper, is_sent: false, - id: rule_id + rule_id: rule_id ) are_exec.save! + # Once Engine.check() verified that the hooked browser match a Rule, trigger the Rule ;-) print_more "Triggering ruleset #{rule_ids} on HB #{hb_id}" end diff --git a/core/main/autorun_engine/parser.rb b/core/main/autorun_engine/parser.rb index 83a1075bc..50867a400 100644 --- a/core/main/autorun_engine/parser.rb +++ b/core/main/autorun_engine/parser.rb @@ -18,68 +18,63 @@ module BeEF VERSION = ['<', '<=', '==', '>=', '>', 'ALL', 'Vista', 'XP'] CHAIN_MODE = %w[sequential nested-forward] MAX_VER_LEN = 15 - # Parse a JSON ARE file and returns an Hash with the value mappings - def parse(name, author, browser, browser_version, os, os_version, modules, exec_order, exec_delay, chain_mode) - success = [true] - return [false, 'Illegal chain_mode definition'] unless CHAIN_MODE.include?(chain_mode) - return [false, 'Illegal rule name'] unless BeEF::Filters.is_non_empty_string?(name) - return [false, 'Illegal author name'] unless BeEF::Filters.is_non_empty_string?(author) + def parse(name, author, browser, browser_version, os, os_version, modules, execution_order, execution_delay, chain_mode) + raise ArgumentError, "Invalid rule name: #{name}" unless BeEF::Filters.is_non_empty_string?(name) + raise ArgumentError, "Invalid author name: #{author}" unless BeEF::Filters.is_non_empty_string?(author) + raise ArgumentError, "Invalid chain_mode definition: #{chain_mode}" unless CHAIN_MODE.include?(chain_mode) + raise ArgumentError, "Invalid os definition: #{os}" unless OS.include?(os) + + unless modules.size == execution_delay.size + raise ArgumentError, "Number of execution_delay values (#{execution_delay.size}) must be consistent with number of modules (#{modules.size})" + end + execution_delay.each { |delay| raise TypeError, "Invalid execution_delay value: #{delay}. Values must be Integers." unless delay.is_a?(Integer) } + + unless modules.size == execution_order.size + raise ArgumentError, "Number of execution_order values (#{execution_order.size}) must be consistent with number of modules (#{modules.size})" + end + execution_order.each { |order| raise TypeError, "Invalid execution_order value: #{order}. Values must be Integers." unless order.is_a?(Integer) } # if multiple browsers were specified, check each browser if browser.is_a?(Array) browser.each do |b| - return [false, 'Illegal browser definition'] unless BROWSER.include?(b) + raise ArgumentError, "Invalid browser definition: #{browser}" unless BROWSER.include?(b) end # else, if only one browser was specified, check browser and browser version else - return [false, 'Illegal browser definition'] unless BROWSER.include?(browser) + raise ArgumentError, "Invalid browser definition: #{browser}" unless BROWSER.include?(browser) if browser_version != 'ALL' && !(VERSION.include?(browser_version[0, 2].gsub(/\s+/, '')) && BeEF::Filters.is_valid_browserversion?(browser_version[2..-1].gsub(/\s+/, '')) && browser_version.length < MAX_VER_LEN) - return [false, 'Illegal browser_version definition'] + raise ArgumentError, "Invalid browser_version definition: #{browser_version}" end end if os_version != 'ALL' && !(VERSION.include?(os_version[0, 2].gsub(/\s+/, '')) && BeEF::Filters.is_valid_osversion?(os_version[2..-1].gsub(/\s+/, '')) && os_version.length < MAX_VER_LEN) - return [false, 'Illegal os_version definition'] + return ArgumentError, "Invalid os_version definition: #{os_version}" end - return [false, 'Illegal os definition'] unless OS.include?(os) - # check if module names, conditions and options are ok modules.each do |cmd_mod| mod = BeEF::Core::Models::CommandModule.where(name: cmd_mod['name']).first - if mod.nil? - return [false, "The specified module name (#{cmd_mod['name']}) does not exist"] - else - modk = BeEF::Module.get_key_by_database_id(mod.id) - mod_options = BeEF::Module.get_options(modk) - opt_count = 0 - mod_options.each do |opt| - if opt['name'] == cmd_mod['options'].keys[opt_count] - opt_count += 1 - else - return [false, "The specified option (#{cmd_mod['options'].keys[opt_count] - }) for module (#{cmd_mod['name']}) does not exist"] - end + raise "The specified module name (#{cmd_mod['name']}) does not exist" if mod.nil? + + modk = BeEF::Module.get_key_by_database_id(mod.id) + mod_options = BeEF::Module.get_options(modk) + + opt_count = 0 + mod_options.each do |opt| + if opt['name'] != cmd_mod['options'].keys[opt_count] + raise ArgumentError, "The specified option (#{cmd_mod['options'].keys[opt_count]}) for module (#{cmd_mod['name']}) was not specified" end + + opt_count += 1 end end - exec_order.each { |order| return [false, 'execution_order values must be Integers'] unless order.is_a?(Integer) } - exec_delay.each { |delay| return [false, 'execution_delay values must be Integers'] unless delay.is_a?(Integer) } - - return [false, 'execution_order and execution_delay values must be consistent with modules numbers'] unless - modules.size == exec_order.size && modules.size == exec_delay.size - - success - rescue StandardError => e - print_error e.message.to_s - print_debug e.backtrace.join("\n").to_s - [false, 'Something went wrong.'] + true end end end diff --git a/core/main/autorun_engine/rule_loader.rb b/core/main/autorun_engine/rule_loader.rb index 95f9e33af..d47dbd6b9 100644 --- a/core/main/autorun_engine/rule_loader.rb +++ b/core/main/autorun_engine/rule_loader.rb @@ -14,76 +14,111 @@ module BeEF @debug_on = @config.get('beef.debug') end - # this expects parsed JSON as input - def load(data) - name = data['name'] - author = data['author'] + # Load an ARE rule set + # @param [Hash] ARE ruleset as JSON + # @return [Hash] {"success": Boolean, "rule_id": Integer, "error": String} + def load_rule_json(data) + name = data['name'] || '' + author = data['author'] || '' browser = data['browser'] || 'ALL' browser_version = data['browser_version'] || 'ALL' os = data['os'] || 'ALL' os_version = data['os_version'] || 'ALL' modules = data['modules'] - exec_order = data['execution_order'] - exec_delay = data['execution_delay'] - chain_mode = data['chain_mode'] + execution_order = data['execution_order'] + execution_delay = data['execution_delay'] + chain_mode = data['chain_mode'] || 'sequential' - parser_result = BeEF::Core::AutorunEngine::Parser.instance.parse( - name, author, browser, browser_version, os, os_version, modules, exec_order, exec_delay, chain_mode - ) - - if parser_result.length == 1 && parser_result.first - print_info "[ARE] Ruleset (#{name}) parsed and stored successfully." - if @debug_on - print_more "Target Browser: #{browser} (#{browser_version})" - print_more "Target OS: #{os} (#{os_version})" - print_more 'Modules to Trigger:' - modules.each do |mod| - print_more "(*) Name: #{mod['name']}" - print_more "(*) Condition: #{mod['condition']}" - print_more "(*) Code: #{mod['code']}" - print_more '(*) Options:' - mod['options'].each do |key, value| - print_more "\t#{key}: (#{value})" - end - end - print_more "Exec order: #{exec_order}" - print_more "Exec delay: #{exec_delay}" - end - are_rule = BeEF::Core::Models::Rule.new( - name: name, - author: author, - browser: browser, - browser_version: browser_version, - os: os, - os_version: os_version, - modules: modules.to_json, - execution_order: exec_order, - execution_delay: exec_delay, - chain_mode: chain_mode + begin + BeEF::Core::AutorunEngine::Parser.instance.parse( + name, + author, + browser, + browser_version, + os, + os_version, + modules, + execution_order, + execution_delay, + chain_mode ) - are_rule.save - { 'success' => true, 'rule_id' => are_rule.id } - else - print_error "[ARE] Ruleset (#{name}): ERROR. " + parser_result.last - { 'success' => false, 'error' => parser_result.last } + rescue => e + print_error("[ARE] Error loading ruleset (#{name}): #{e.message}") + return { 'success' => false, 'error' => e.message } end - rescue StandardError => e - err = 'Malformed JSON ruleset.' - print_error "[ARE] Ruleset (#{name}): ERROR. #{e} #{e.backtrace}" - { 'success' => false, 'error' => err } + + existing_rule = BeEF::Core::Models::Rule.where( + name: name, + author: author, + browser: browser, + browser_version: browser_version, + os: os, + os_version: os_version, + modules: modules.to_json, + execution_order: execution_order.to_s, + execution_delay: execution_delay.to_s, + chain_mode: chain_mode + ).first + + unless existing_rule.nil? + msg = "Duplicate rule already exists in the database (ID: #{existing_rule.id})" + print_info("[ARE] Skipping ruleset (#{name}): #{msg}") + return { 'success' => false, 'error' => msg } + end + + are_rule = BeEF::Core::Models::Rule.new( + name: name, + author: author, + browser: browser, + browser_version: browser_version, + os: os, + os_version: os_version, + modules: modules.to_json, + execution_order: execution_order.to_s, + execution_delay: execution_delay.to_s, + chain_mode: chain_mode + ) + are_rule.save + + print_info("[ARE] Ruleset (#{name}) parsed and stored successfully.") + + if @debug_on + print_more "Target Browser: #{browser} (#{browser_version})" + print_more "Target OS: #{os} (#{os_version})" + print_more 'Modules to run:' + modules.each do |mod| + print_more "(*) Name: #{mod['name']}" + print_more "(*) Condition: #{mod['condition']}" + print_more "(*) Code: #{mod['code']}" + print_more '(*) Options:' + mod['options'].each do |key, value| + print_more "\t#{key}: (#{value})" + end + end + print_more "Exec order: #{execution_order}" + print_more "Exec delay: #{exec_delay}" + end + + { 'success' => true, 'rule_id' => are_rule.id } + rescue TypeError, ArgumentError => e + print_error("[ARE] Failed to load ruleset (#{name}): #{e.message}") + { 'success' => false, 'error' => e.message } end - def load_file(json_rule_path) + # Load an ARE ruleset from file + # @param [String] JSON ARE ruleset file path + def load_rule_file(json_rule_path) rule_file = File.open(json_rule_path, 'r:UTF-8', &:read) - self.load JSON.parse(rule_file) - rescue StandardError => e - print_error "[ARE] Failed to load ruleset from #{json_rule_path}: #{e.message}" + self.load_rule_json(JSON.parse(rule_file)) + rescue => e + print_error("[ARE] Failed to load ruleset from #{json_rule_path}: #{e.message}") end + # Load all JSON ARE rule files from arerules/enabled/ directory def load_directory - Dir.glob("#{$root_dir}/arerules/enabled/**/*.json") do |rule| - print_debug "[ARE] Processing rule: #{rule}" - load_file rule + Dir.glob("#{$root_dir}/arerules/enabled/**/*.json") do |rule_file| + print_debug("[ARE] Processing ruleset file: #{rule_file}") + load_rule_file(rule_file) end end end