From acc0eb5873974a1eb6a311dcb881fd8cba2e9a17 Mon Sep 17 00:00:00 2001 From: kaitoozawa Date: Sat, 27 Dec 2025 16:12:39 +1000 Subject: [PATCH] Phase 4: Remove Server-Side Core --- beef | 5 - core/bootstrap.rb | 6 - core/core.rb | 2 - .../026_remove_autorun_tables.rb | 11 + core/main/autorun_engine/engine.rb | 589 ------------------ core/main/autorun_engine/parser.rb | 82 --- core/main/autorun_engine/rule_loader.rb | 220 ------- core/main/handlers/browserdetails.rb | 4 - core/main/models/execution.rb | 14 - core/main/models/rule.rb | 16 - .../main/network_stack/websocket/websocket.rb | 12 - core/main/rest/handlers/autorun_engine.rb | 157 ----- .../autorun_engine/autorun_engine_spec.rb | 120 ---- 13 files changed, 11 insertions(+), 1227 deletions(-) create mode 100644 core/main/ar-migrations/026_remove_autorun_tables.rb delete mode 100644 core/main/autorun_engine/engine.rb delete mode 100644 core/main/autorun_engine/parser.rb delete mode 100644 core/main/autorun_engine/rule_loader.rb delete mode 100644 core/main/models/execution.rb delete mode 100644 core/main/models/rule.rb delete mode 100644 core/main/rest/handlers/autorun_engine.rb delete mode 100644 spec/beef/core/main/autorun_engine/autorun_engine_spec.rb diff --git a/beef b/beef index 147384898..3fa40ffa6 100755 --- a/beef +++ b/beef @@ -266,11 +266,6 @@ BeEF::Core::GeoIp.instance # BeEF::API::Registrar.instance.fire(BeEF::API::Server, 'pre_http_start', http_hook_server) -# -# @note Load any ARE (Autorun Rule Engine) rules scanning the /arerules/enabled directory -# -BeEF::Core::AutorunEngine::RuleLoader.instance.load_directory - # # @note Start the WebSocket server # diff --git a/core/bootstrap.rb b/core/bootstrap.rb index 4f69dbf37..20dd1a957 100644 --- a/core/bootstrap.rb +++ b/core/bootstrap.rb @@ -29,11 +29,6 @@ require 'core/main/network_stack/handlers/raw' require 'core/main/network_stack/assethandler' require 'core/main/network_stack/api' -# @note Include the autorun engine -require 'core/main/autorun_engine/parser' -require 'core/main/autorun_engine/engine' -require 'core/main/autorun_engine/rule_loader' - ## @note Include helpers require 'core/module' require 'core/modules' @@ -49,7 +44,6 @@ require 'core/main/rest/handlers/categories' require 'core/main/rest/handlers/logs' require 'core/main/rest/handlers/admin' require 'core/main/rest/handlers/server' -require 'core/main/rest/handlers/autorun_engine' require 'core/main/rest/api' ## @note Include Websocket diff --git a/core/core.rb b/core/core.rb index 53ad3c72f..3e2b6d51c 100644 --- a/core/core.rb +++ b/core/core.rb @@ -17,8 +17,6 @@ require 'core/main/models/command' require 'core/main/models/result' require 'core/main/models/optioncache' require 'core/main/models/browserdetails' -require 'core/main/models/rule' -require 'core/main/models/execution' require 'core/main/models/legacybrowseruseragents' # @note Include the constants diff --git a/core/main/ar-migrations/026_remove_autorun_tables.rb b/core/main/ar-migrations/026_remove_autorun_tables.rb new file mode 100644 index 000000000..261c4577d --- /dev/null +++ b/core/main/ar-migrations/026_remove_autorun_tables.rb @@ -0,0 +1,11 @@ +class RemoveAutorunTables < ActiveRecord::Migration[6.0] + def up + drop_table :executions if table_exists?(:executions) + drop_table :rules if table_exists?(:rules) + end + + def down + # Cannot recreate these tables - ARE functionality has been removed + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/core/main/autorun_engine/engine.rb b/core/main/autorun_engine/engine.rb deleted file mode 100644 index d5621dba5..000000000 --- a/core/main/autorun_engine/engine.rb +++ /dev/null @@ -1,589 +0,0 @@ -# -# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net -# Browser Exploitation Framework (BeEF) - https://beefproject.com -# See the file 'doc/COPYING' for copying permission -# -module BeEF - module Core - module AutorunEngine - class Engine - include Singleton - - def initialize - @config = BeEF::Core::Configuration.instance - - @result_poll_interval = @config.get('beef.autorun.result_poll_interval') - @result_poll_timeout = @config.get('beef.autorun.result_poll_timeout') - @continue_after_timeout = @config.get('beef.autorun.continue_after_timeout') - - @debug_on = @config.get('beef.debug') - - @VERSION = ['<', '<=', '==', '>=', '>', 'ALL'] - @VERSION_STR = %w[XP Vista 7] - end - - # Checks if there are any ARE rules to be triggered for the specified hooked browser. - # - # Returns an array with rule IDs that matched and should be triggered. - # if rule_id is specified, checks will be executed only against the specified rule (useful - # for dynamic triggering of new rulesets ar runtime) - def find_matching_rules_for_zombie(browser, browser_version, os, os_version) - rules = BeEF::Core::Models::Rule.all - - return if rules.nil? - return if rules.empty? - - # TODO: handle cases where there are multiple ARE rules for the same hooked browser. - # maybe rules need to have priority or something? - - print_info '[ARE] Checking if any defined rules should be triggered on target.' - - match_rules = [] - rules.each do |rule| - next unless zombie_matches_rule?(browser, browser_version, os, os_version, rule) - - match_rules.push(rule.id) - print_more("Hooked browser and OS match rule: #{rule.name}.") - end - - print_more("Found [#{match_rules.length}/#{rules.length}] ARE rules matching the hooked browser.") - - match_rules - end - - # @return [Boolean] - # Note: browser version checks are supporting only major versions, ex: C 43, IE 11 - # Note: OS version checks are supporting major/minor versions, ex: OSX 10.10, Windows 8.1 - def zombie_matches_rule?(browser, browser_version, os, os_version, rule) - return false if rule.nil? - - unless zombie_browser_matches_rule?(browser, browser_version, rule) - print_debug("Browser version check -> (hook) #{browser_version} #{rule.browser_version} (rule) : does not match") - return false - end - - print_debug("Browser version check -> (hook) #{browser_version} #{rule.browser_version} (rule) : matched") - - unless zombie_os_matches_rule?(os, os_version, rule) - print_debug("OS version check -> (hook) #{os_version} #{rule.os_version} (rule): does not match") - return false - end - - print_debug("OS version check -> (hook) #{os_version} #{rule.os_version} (rule): matched") - - true - rescue StandardError => e - print_error e.message - print_debug e.backtrace.join("\n") - end - - # @return [Boolean] - # TODO: This should be updated to support matching multiple OS (like the browser check below) - def zombie_os_matches_rule?(os, os_version, rule) - return false if rule.nil? - - return false unless rule.os == 'ALL' || os == rule.os - - # check if the OS versions match - os_ver_rule_cond = rule.os_version.split(' ').first - - return true if os_ver_rule_cond == 'ALL' - - return false unless @VERSION.include?(os_ver_rule_cond) || @VERSION_STR.include?(os_ver_rule_cond) - - os_ver_rule_maj = rule.os_version.split(' ').last.split('.').first - os_ver_rule_min = rule.os_version.split(' ').last.split('.').last - - if os_ver_rule_maj == 'XP' - os_ver_rule_maj = 5 - os_ver_rule_min = 0 - elsif os_ver_rule_maj == 'Vista' - os_ver_rule_maj = 6 - os_ver_rule_min = 0 - elsif os_ver_rule_maj == '7' - os_ver_rule_maj = 6 - os_ver_rule_min = 0 - end - - # Most of the times Linux/*BSD OS doesn't return any version - # (TODO: improve OS detection on these operating systems) - if !os_version.nil? && !@VERSION_STR.include?(os_version) - os_ver_hook_maj = os_version.split('.').first - os_ver_hook_min = os_version.split('.').last - - # the following assignments to 0 are need for later checks like: - # 8.1 >= 7, because if the version doesn't have minor versions, maj/min are the same - os_ver_hook_min = 0 if os_version.split('.').length == 1 - os_ver_rule_min = 0 if rule.os_version.split('.').length == 1 - else - # XP is Windows 5.0 and Vista is Windows 6.0. Easier for comparison later on. - # TODO: BUG: This will fail horribly if the target OS is Windows 7 or newer, - # as no version normalization is performed. - # TODO: Update this for every OS since Vista/7 ... - if os_version == 'XP' - os_ver_hook_maj = 5 - os_ver_hook_min = 0 - elsif os_version == 'Vista' - os_ver_hook_maj = 6 - os_ver_hook_min = 0 - elsif os_version == '7' - os_ver_hook_maj = 6 - os_ver_hook_min = 0 - end - end - - if !os_version.nil? || rule.os_version != 'ALL' - os_major_version_match = compare_versions(os_ver_hook_maj.to_s, os_ver_rule_cond, os_ver_rule_maj.to_s) - os_minor_version_match = compare_versions(os_ver_hook_min.to_s, os_ver_rule_cond, os_ver_rule_min.to_s) - return false unless (os_major_version_match && os_minor_version_match) - end - - true - rescue StandardError => e - print_error e.message - print_debug e.backtrace.join("\n") - end - - # @return [Boolean] - def zombie_browser_matches_rule?(browser, browser_version, rule) - return false if rule.nil? - - b_ver_cond = rule.browser_version.split(' ').first - - return false unless @VERSION.include?(b_ver_cond) - - b_ver = rule.browser_version.split(' ').last - - return false unless BeEF::Filters.is_valid_browserversion?(b_ver) - - # check if rule specifies multiple browsers - if rule.browser =~ /\A[A-Z]+\Z/ - return false unless rule.browser == 'ALL' || browser == rule.browser - - # check if the browser version matches - browser_version_match = compare_versions(browser_version.to_s, b_ver_cond, b_ver.to_s) - return false unless browser_version_match - else - browser_match = false - rule.browser.gsub(/[^A-Z,]/i, '').split(',').each do |b| - if b == browser || b == 'ALL' - browser_match = true - break - end - end - return false unless browser_match - end - - true - rescue StandardError => e - print_error e.message - print_debug e.backtrace.join("\n") - end - - # Check if the hooked browser type/version and OS type/version match any Rule-sets - # stored in the BeEF::Core::Models::Rule database table - # If one or more Rule-sets do match, trigger the module chain specified - def find_and_run_all_matching_rules_for_zombie(hb_id) - return if hb_id.nil? - - hb_details = BeEF::Core::Models::BrowserDetails - browser_name = hb_details.get(hb_id, 'browser.name') - browser_version = hb_details.get(hb_id, 'browser.version') - os_name = hb_details.get(hb_id, 'host.os.name') - os_version = hb_details.get(hb_id, 'host.os.version') - - are = BeEF::Core::AutorunEngine::Engine.instance - rules = are.find_matching_rules_for_zombie(browser_name, browser_version, os_name, os_version) - - return if rules.nil? - return if rules.empty? - - are.run_rules_on_zombie(rules, hb_id) - end - - # Run the specified rule IDs on the specified zombie ID - # only if the rules match. - def run_matching_rules_on_zombie(rule_ids, hb_id) - return if rule_ids.nil? - return if hb_id.nil? - - rule_ids = [rule_ids.to_i] if rule_ids.is_a?(String) - - hb_details = BeEF::Core::Models::BrowserDetails - browser_name = hb_details.get(hb_id, 'browser.name') - browser_version = hb_details.get(hb_id, 'browser.version') - os_name = hb_details.get(hb_id, 'host.os.name') - os_version = hb_details.get(hb_id, 'host.os.version') - - are = BeEF::Core::AutorunEngine::Engine.instance - rules = are.find_matching_rules_for_zombie(browser_name, browser_version, os_name, os_version) - - return if rules.nil? - return if rules.empty? - - new_rules = [] - rules.each do |rule| - new_rules << rule if rule_ids.include?(rule) - end - - return if new_rules.empty? - - are.run_rules_on_zombie(new_rules, hb_id) - end - - # Run the specified rule IDs on the specified zombie ID - # regardless of whether the rules match. - # Prepare and return the JavaScript of the modules to be sent. - # It also updates the rules ARE execution table with timings - def run_rules_on_zombie(rule_ids, hb_id) - return if rule_ids.nil? - return if hb_id.nil? - - hb = BeEF::HBManager.get_by_id(hb_id) - hb_session = hb.session - - rule_ids = [rule_ids] if rule_ids.is_a?(Integer) - - rule_ids.each do |rule_id| - rule = BeEF::Core::Models::Rule.find(rule_id) - modules = JSON.parse(rule.modules) - - execution_order = JSON.parse(rule.execution_order) - execution_delay = JSON.parse(rule.execution_delay) - 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 = [] - mods_conditions = [] - - # this ensures that if both rule A and rule B call the same module in sequential mode, - # execution will be correct preventing wrapper functions to be called with equal names. - rule_token = SecureRandom.hex(5) - - modules.each do |cmd_mod| - mod = BeEF::Core::Models::CommandModule.where(name: cmd_mod['name']).first - options = [] - replace_input = false - cmd_mod['options'].each do |k, v| - options.push({ 'name' => k, 'value' => v }) - replace_input = true if v == '<>' - end - - command_body = prepare_command(mod, options, hb_id, replace_input, rule_token) - - mods_bodies.push(command_body) - mods_codes.push(cmd_mod['code']) - mods_conditions.push(cmd_mod['condition']) - end - - # Depending on the chosen chain mode (sequential or nested/forward), prepare the appropriate wrapper - case chain_mode - when 'nested-forward' - wrapper = prepare_nested_forward_wrapper(mods_bodies, mods_codes, mods_conditions, execution_order, rule_token) - when 'sequential' - wrapper = prepare_sequential_wrapper(mods_bodies, execution_order, execution_delay, rule_token) - else - # we should never get here. chain mode is validated earlier. - print_error("[ARE] Invalid chain mode '#{chain_mode}'") - next - end - - print_more "Triggering rules #{rule_ids} on HB #{hb_id}" - - are_exec = BeEF::Core::Models::Execution.new( - session_id: hb_session, - mod_count: modules.length, - mod_successful: 0, - rule_token: rule_token, - mod_body: wrapper, - is_sent: false, - rule_id: rule_id - ) - are_exec.save! - end - end - - private - - # Wraps module bodies in their own function, using setTimeout to trigger them with an eventual delay. - # Launch order is also taken care of. - # - sequential chain with delays (setTimeout stuff) - # ex.: setTimeout(module_one(), 0); - # setTimeout(module_two(), 2000); - # setTimeout(module_three(), 3000); - # Note: no result status is checked here!! Useful if you just want to launch a bunch of modules without caring - # what their status will be (for instance, a bunch of XSRFs on a set of targets) - def prepare_sequential_wrapper(mods, order, delay, rule_token) - wrapper = '' - delayed_exec = '' - c = 0 - while c < mods.length - delayed_exec += %| setTimeout(function(){#{mods[order[c]][:mod_name]}_#{rule_token}();}, #{delay[c]}); | - mod_body = mods[order[c]][:mod_body].to_s.gsub("#{mods[order[c]][:mod_name]}_mod_output", "#{mods[order[c]][:mod_name]}_#{rule_token}_mod_output") - wrapped_mod = "#{mod_body}\n" - wrapper += wrapped_mod - c += 1 - end - wrapper += delayed_exec - print_more "Final Modules Wrapper:\n #{wrapper}" if @debug_on - wrapper - end - - # Wraps module bodies in their own function, then start to execute them from the first, polling for - # command execution status/results (with configurable polling interval and timeout). - # Launch order is also taken care of. - # - nested forward chain with status checks (setInterval to wait for command to return from async operations) - # ex.: module_one() - # if condition - # module_two(module_one_output) - # if condition - # module_three(module_two_output) - # - # Note: command result status is checked, and you can properly chain input into output, having also - # the flexibility of slightly mangling it to adapt to module needs. - # Note: Useful in situations where you want to launch 2 modules, where the second one will execute only - # if the first once return with success. Also, the second module has the possibility of mangling first - # module output and use it as input for some of its module inputs. - def prepare_nested_forward_wrapper(mods, code, conditions, order, rule_token) - wrapper = '' - delayed_exec = '' - delayed_exec_footers = [] - c = 0 - - while c < mods.length - i = if mods.length == 1 - c - else - c + 1 - end - - code_snippet = '' - mod_input = '' - if code[c] != 'null' && code[c] != '' - code_snippet = code[c] - mod_input = 'mod_input' - end - - conditions[i] = true if conditions[i].nil? || conditions[i] == '' - - if c == 0 - # this is the first wrapper to prepare - delayed_exec += %| - function #{mods[order[c]][:mod_name]}_#{rule_token}_f(){ - #{mods[order[c]][:mod_name]}_#{rule_token}(); - - // TODO add timeout to prevent infinite loops - function isResReady(mod_result, start){ - if (mod_result === null && parseInt(((new Date().getTime()) - start)) < #{@result_poll_timeout}){ - // loop - }else{ - // module return status/data is now available - clearInterval(resultReady); - if (mod_result === null && #{@continue_after_timeout}){ - var mod_result = []; - mod_result[0] = 1; //unknown status - mod_result[1] = '' //empty result - } - var status = mod_result[0]; - if(#{conditions[i]}){ - #{mods[order[i]][:mod_name]}_#{rule_token}_can_exec = true; - #{mods[order[c]][:mod_name]}_#{rule_token}_mod_output = mod_result[1]; - | - - delayed_exec_footer = %| - } - } - } - var start = (new Date()).getTime(); - var resultReady = setInterval(function(){var start = (new Date()).getTime(); isResReady(#{mods[order[c]][:mod_name]}_#{rule_token}_mod_output, start);},#{@result_poll_interval}); - } - #{mods[order[c]][:mod_name]}_#{rule_token}_f(); - | - - delayed_exec_footers.push(delayed_exec_footer) - - elsif c < mods.length - 1 - code_snippet = code_snippet.to_s.gsub(mods[order[c - 1]][:mod_name], "#{mods[order[c - 1]][:mod_name]}_#{rule_token}") - - # this is one of the wrappers in the middle of the chain - delayed_exec += %| - function #{mods[order[c]][:mod_name]}_#{rule_token}_f(){ - if(#{mods[order[c]][:mod_name]}_#{rule_token}_can_exec){ - #{code_snippet} - #{mods[order[c]][:mod_name]}_#{rule_token}(#{mod_input}); - function isResReady(mod_result, start){ - if (mod_result === null && parseInt(((new Date().getTime()) - start)) < #{@result_poll_timeout}){ - // loop - }else{ - // module return status/data is now available - clearInterval(resultReady); - if (mod_result === null && #{@continue_after_timeout}){ - var mod_result = []; - mod_result[0] = 1; //unknown status - mod_result[1] = '' //empty result - } - var status = mod_result[0]; - if(#{conditions[i]}){ - #{mods[order[i]][:mod_name]}_#{rule_token}_can_exec = true; - #{mods[order[c]][:mod_name]}_#{rule_token}_mod_output = mod_result[1]; - | - - delayed_exec_footer = %| - } - } - } - var start = (new Date()).getTime(); - var resultReady = setInterval(function(){ isResReady(#{mods[order[c]][:mod_name]}_#{rule_token}_mod_output, start);},#{@result_poll_interval}); - } - } - #{mods[order[c]][:mod_name]}_#{rule_token}_f(); - | - - delayed_exec_footers.push(delayed_exec_footer) - else - code_snippet = code_snippet.to_s.gsub(mods[order[c - 1]][:mod_name], "#{mods[order[c - 1]][:mod_name]}_#{rule_token}") - # this is the last wrapper to prepare - delayed_exec += %| - function #{mods[order[c]][:mod_name]}_#{rule_token}_f(){ - if(#{mods[order[c]][:mod_name]}_#{rule_token}_can_exec){ - #{code_snippet} - #{mods[order[c]][:mod_name]}_#{rule_token}(#{mod_input}); - } - } - #{mods[order[c]][:mod_name]}_#{rule_token}_f(); - | - end - mod_body = mods[order[c]][:mod_body].to_s.gsub("#{mods[order[c]][:mod_name]}_mod_output", "#{mods[order[c]][:mod_name]}_#{rule_token}_mod_output") - wrapped_mod = "#{mod_body}\n" - wrapper += wrapped_mod - c += 1 - end - wrapper += delayed_exec + delayed_exec_footers.reverse.join("\n") - print_more "Final Modules Wrapper:\n #{delayed_exec + delayed_exec_footers.reverse.join("\n")}" if @debug_on - wrapper - end - - # prepare the command module (compiling the Erubis templating stuff), eventually obfuscate it, - # and store it in the database. - # Returns the raw module body after template substitution. - def prepare_command(mod, options, hb_id, replace_input, rule_token) - config = BeEF::Core::Configuration.instance - begin - command = BeEF::Core::Models::Command.new( - data: options.to_json, - hooked_browser_id: hb_id, - command_module_id: BeEF::Core::Configuration.instance.get("beef.module.#{mod.name}.db.id"), - creationdate: Time.new.to_i, - instructions_sent: true - ) - command.save! - - command_module = BeEF::Core::Models::CommandModule.find(mod.id) - if command_module.path.match(/^Dynamic/) - # metasploit and similar integrations - command_module = BeEF::Modules::Commands.const_get(command_module.path.split('/').last.capitalize).new - else - # normal modules always here - key = BeEF::Module.get_key_by_database_id(mod.id) - command_module = BeEF::Core::Command.const_get(config.get("beef.module.#{key}.class")).new(key) - end - - hb = BeEF::HBManager.get_by_id(hb_id) - hb_session = hb.session - command_module.command_id = command.id - command_module.session_id = hb_session - command_module.build_datastore(command.data) - command_module.pre_send - - build_missing_beefjs_components(command_module.beefjs_components) unless command_module.beefjs_components.empty? - - if config.get('beef.extension.evasion.enable') - evasion = BeEF::Extension::Evasion::Evasion.instance - command_body = evasion.obfuscate(command_module.output) + "\n\n" - else - command_body = command_module.output + "\n\n" - end - - # @note prints the event to the console - print_more "Preparing JS for command id [#{command.id}], module [#{mod.name}]" - - mod_input = replace_input ? 'mod_input' : '' - result = %| - var #{mod.name}_#{rule_token} = function(#{mod_input}){ - #{clean_command_body(command_body, replace_input)} - }; - var #{mod.name}_#{rule_token}_can_exec = false; - var #{mod.name}_#{rule_token}_mod_output = null; - | - - { mod_name: mod.name, mod_body: result } - rescue StandardError => e - print_error e.message - print_debug e.backtrace.join("\n") - end - end - - # Removes the beef.execute wrapper in order that modules are executed in the ARE wrapper, rather than - # using the default behavior of adding the module to an array and execute it at polling time. - # - # Also replace <> with mod_input variable if needed for chaining module output/input - def clean_command_body(command_body, replace_input) - cmd_body = command_body.lines.map(&:chomp) - wrapper_start_index, wrapper_end_index = nil - - cmd_body.each_with_index do |line, index| - if line.to_s =~ /^(beef|[a-zA-Z]+)\.execute\(function\(\)/ - wrapper_start_index = index - break - end - end - print_error '[ARE] Could not find module start index' if wrapper_start_index.nil? - - cmd_body.reverse.each_with_index do |line, index| - if line.include?('});') - wrapper_end_index = index - break - end - end - print_error '[ARE] Could not find module end index' if wrapper_end_index.nil? - - cleaned_cmd_body = cmd_body.slice(wrapper_start_index..-(wrapper_end_index + 1)).join("\n") - - print_error '[ARE] No command to send' if cleaned_cmd_body.eql?('') - - # check if <> should be replaced with a variable name (depending if the variable is a string or number) - return cleaned_cmd_body unless replace_input - - if cleaned_cmd_body.include?('"<>"') - cleaned_cmd_body.gsub('"<>"', 'mod_input') - elsif cleaned_cmd_body.include?('\'<>\'') - cleaned_cmd_body.gsub('\'<>\'', 'mod_input') - elsif cleaned_cmd_body.include?('<>') - cleaned_cmd_body.gsub('\'<>\'', 'mod_input') - else - cleaned_cmd_body - end - rescue StandardError => e - print_error "[ARE] There is likely a problem with the module's command.js parsing. Check Engine.clean_command_body. #{e.message}" - end - - # compare versions - def compare_versions(ver_a, cond, ver_b) - return true if cond == 'ALL' - return true if cond == '==' && ver_a == ver_b - return true if cond == '<=' && ver_a <= ver_b - return true if cond == '<' && ver_a < ver_b - return true if cond == '>=' && ver_a >= ver_b - return true if cond == '>' && ver_a > ver_b - - false - end - end - end - end -end diff --git a/core/main/autorun_engine/parser.rb b/core/main/autorun_engine/parser.rb deleted file mode 100644 index 0b02fa9ae..000000000 --- a/core/main/autorun_engine/parser.rb +++ /dev/null @@ -1,82 +0,0 @@ -# -# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net -# Browser Exploitation Framework (BeEF) - https://beefproject.com -# See the file 'doc/COPYING' for copying permission -# -module BeEF - module Core - module AutorunEngine - class Parser - include Singleton - - def initialize - @config = BeEF::Core::Configuration.instance - end - - BROWSER = %w[FF C IE S O ALL] - OS = %w[Linux Windows OSX Android iOS BlackBerry ALL] - VERSION = ['<', '<=', '==', '>=', '>', 'ALL', 'Vista', 'XP'] - CHAIN_MODE = %w[sequential nested-forward] - MAX_VER_LEN = 15 - - 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| - raise ArgumentError, "Invalid browser definition: #{browser}" unless BROWSER.include?(b) - end - # else, if only one browser was specified, check browser and browser version - else - 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) - 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 ArgumentError, "Invalid os_version definition: #{os_version}" - end - - # 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 - - 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 - - true - end - end - end - end -end diff --git a/core/main/autorun_engine/rule_loader.rb b/core/main/autorun_engine/rule_loader.rb deleted file mode 100644 index 3a78f9cb0..000000000 --- a/core/main/autorun_engine/rule_loader.rb +++ /dev/null @@ -1,220 +0,0 @@ -# -# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net -# Browser Exploitation Framework (BeEF) - https://beefproject.com -# See the file 'doc/COPYING' for copying permission -# -module BeEF - module Core - module AutorunEngine - class RuleLoader - include Singleton - - def initialize - @config = BeEF::Core::Configuration.instance - @debug_on = @config.get('beef.debug') - end - - # 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'] - execution_order = data['execution_order'] - execution_delay = data['execution_delay'] - chain_mode = data['chain_mode'] || 'sequential' - - begin - BeEF::Core::AutorunEngine::Parser.instance.parse( - name, - author, - browser, - browser_version, - os, - os_version, - modules, - execution_order, - execution_delay, - chain_mode - ) - rescue => e - print_error("[ARE] Error loading ruleset (#{name}): #{e.message}") - return { 'success' => false, 'error' => e.message } - end - - 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 - - # Update an ARE rule set. - # @param [Hash] ARE rule ID. - # @param [Hash] ARE ruleset as JSON - # @return [Hash] {"success": Boolean, "rule_id": Integer, "error": String} - def update_rule_json(id, data) - # Quite similar in implementation to load_rule_json. Might benefit from a refactor. - 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'] - execution_order = data['execution_order'] - execution_delay = data['execution_delay'] - chain_mode = data['chain_mode'] || 'sequential' - - begin - BeEF::Core::AutorunEngine::Parser.instance.parse( - name, - author, - browser, - browser_version, - os, - os_version, - modules, - execution_order, - execution_delay, - chain_mode - ) - rescue => e - print_error("[ARE] Error updating ruleset (#{name}): #{e.message}") - return { 'success' => false, 'error' => e.message } - end - - 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 - old_are_rule = BeEF::Core::Models::Rule.find_by(id: id) - - old_are_rule.update( - 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 - ) - - print_info("[ARE] Ruleset (#{name}) updated 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 } - rescue TypeError, ArgumentError => e - print_error("[ARE] Failed to update ruleset (#{name}): #{e.message}") - { 'success' => false, 'error' => e.message } - end - - # 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_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_file| - print_debug("[ARE] Processing ruleset file: #{rule_file}") - load_rule_file(rule_file) - end - end - end - end - end -end diff --git a/core/main/handlers/browserdetails.rb b/core/main/handlers/browserdetails.rb index 3d02f2225..4d75411b2 100644 --- a/core/main/handlers/browserdetails.rb +++ b/core/main/handlers/browserdetails.rb @@ -557,10 +557,6 @@ module BeEF BeEF::Core::Models::NetworkHost.create(hooked_browser_id: session_id, ip: '127.0.0.1', hostname: 'localhost', os: BeEF::Core::Models::BrowserDetails.get(session_id, 'host.os.name')) end - - # check if any ARE rules shall be triggered only if the channel is != WebSockets (XHR). If the channel - # is WebSockets, then ARe rules are triggered after channel is established. - BeEF::Core::AutorunEngine::Engine.instance.find_and_run_all_matching_rules_for_zombie(zombie.id) unless config.get('beef.http.websocket.enable') end def get_param(query, key) diff --git a/core/main/models/execution.rb b/core/main/models/execution.rb deleted file mode 100644 index 199104795..000000000 --- a/core/main/models/execution.rb +++ /dev/null @@ -1,14 +0,0 @@ -# -# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net -# Browser Exploitation Framework (BeEF) - https://beefproject.com -# See the file 'doc/COPYING' for copying permission -# - -module BeEF - module Core - module Models # @note Stored info about the execution of the ARE on hooked browsers. - class Execution < BeEF::Core::Model - end - end - end -end diff --git a/core/main/models/rule.rb b/core/main/models/rule.rb deleted file mode 100644 index e56bb4d2a..000000000 --- a/core/main/models/rule.rb +++ /dev/null @@ -1,16 +0,0 @@ -# -# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net -# Browser Exploitation Framework (BeEF) - https://beefproject.com -# See the file 'doc/COPYING' for copying permission -# - -module BeEF - module Core - module Models - # @note Table stores the rules for the Distributed Engine. - class Rule < BeEF::Core::Model - has_many :executions - end - end - end -end diff --git a/core/main/network_stack/websocket/websocket.rb b/core/main/network_stack/websocket/websocket.rb index b0399532a..3ccea20aa 100644 --- a/core/main/network_stack/websocket/websocket.rb +++ b/core/main/network_stack/websocket/websocket.rb @@ -108,12 +108,9 @@ module BeEF hooked_browser = BeEF::Core::Models::HookedBrowser.where(session: hb_session).first if hooked_browser.nil? print_error '[WebSocket] Fingerprinting not finished yet.' - print_more 'ARE rules were not triggered. You may want to trigger them manually via REST API.' next end - BeEF::Core::AutorunEngine::Engine.instance.find_and_run_all_matching_rules_for_zombie(hooked_browser.id) - next end @@ -140,15 +137,6 @@ module BeEF zombie_commands = BeEF::Core::Models::Command.where(hooked_browser_id: hooked_browser.id, instructions_sent: false) zombie_commands.each { |command| add_command_instructions(command, hooked_browser) } - # Check if there are any ARE rules to be triggered. If is_sent=false rules are triggered - are_body = '' - are_executions = BeEF::Core::Models::Execution.where(is_sent: false, session_id: hooked_browser.session) - are_executions.each do |are_exec| - are_body += are_exec.mod_body - are_exec.update(is_sent: true, exec_time: Time.new.to_i) - end - @@activeSocket[hooked_browser.session].send(are_body) unless are_body.empty? - # @todo antisnatchor: # @todo - re-use the pre_hook_send callback mechanisms to have a generic check for multipl extensions # Check if new forged requests need to be sent (Requester/TunnelingProxy) diff --git a/core/main/rest/handlers/autorun_engine.rb b/core/main/rest/handlers/autorun_engine.rb deleted file mode 100644 index c2e311067..000000000 --- a/core/main/rest/handlers/autorun_engine.rb +++ /dev/null @@ -1,157 +0,0 @@ -# -# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net -# Browser Exploitation Framework (BeEF) - https://beefproject.com -# See the file 'doc/COPYING' for copying permission -# - -module BeEF - module Core - module Rest - class AutorunEngine < BeEF::Core::Router::Router - config = BeEF::Core::Configuration.instance - - before do - error 401 unless params[:token] == config.get('beef.api_token') - halt 401 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 - - # - # Get all rules - # - get '/rules' do - rules = BeEF::Core::Models::Rule.all - { - 'success' => true, - 'count' => rules.length, - 'rules' => rules.to_json - }.to_json - rescue StandardError => e - print_error("Internal error while retrieving Autorun rules: #{e.message}") - halt 500 - end - - # Returns a specific rule by ID - get '/rule/:rule_id' do - rule_id = params[:rule_id] - - rule = BeEF::Core::Models::Rule.find(rule_id) - raise InvalidParameterError, 'id' if rule.nil? - - halt 404 if rule.empty? - - rule.to_json - rescue InvalidParameterError => e - print_error e.message - halt 400 - rescue StandardError => e - print_error "Internal error while retrieving Autorun rule with id #{rule_id} (#{e.message})" - halt 500 - end - - # - # Add a new ruleset. Return the rule_id if request was successful. - # @return [Integer] rule ID - # - post '/rule/add' do - request.body.rewind - data = JSON.parse request.body.read - rloader = BeEF::Core::AutorunEngine::RuleLoader.instance - rloader.load_rule_json(data).to_json - rescue StandardError => e - print_error "Internal error while adding Autorun rule: #{e.message}" - { 'success' => false, 'error' => e.message }.to_json - end - - # - # Delete a ruleset - # - delete '/rule/:rule_id' do - rule_id = params[:rule_id] - rule = BeEF::Core::Models::Rule.find(rule_id) - raise InvalidParameterError, 'id' if rule.nil? - rule.destroy - - { 'success' => true }.to_json - rescue InvalidParameterError => e - print_error e.message - halt 400 - rescue StandardError => e - print_error "Internal error while deleting Autorun rule: #{e.message}" - { 'success' => false, 'error' => e.message }.to_json - end - - # - # Update a ruleset - # - patch '/rule/:rule_id' do - rule_id = params[:rule_id] - rule = BeEF::Core::Models::Rule.find(rule_id) - raise InvalidParameterError, 'id' if rule.nil? - data = JSON.parse request.body.read - rloader = BeEF::Core::AutorunEngine::RuleLoader.instance - rloader.update_rule_json(rule_id, data) - - { 'success' => true }.to_json - rescue InvalidParameterError => e - print_error e.message - halt 400 - rescue StandardError => e - print_error "Internal error while updating Autorun rule: #{e.message}" - { 'success' => false, 'error' => e.message }.to_json - end - - # - # Run a specified rule on all online hooked browsers (if the zombie matches the rule). - # Offline hooked browsers are ignored - # - get '/run/:rule_id' do - rule_id = params[:rule_id] - - online_hooks = BeEF::Core::Models::HookedBrowser.where('lastseen >= ?', (Time.new.to_i - 15)) - - if online_hooks.nil? - return { 'success' => false, 'error' => 'There are currently no hooked browsers online.' }.to_json - end - - are = BeEF::Core::AutorunEngine::Engine.instance - online_hooks.each do |hb| - are.run_matching_rules_on_zombie(rule_id, hb.id) - end - - { 'success' => true }.to_json - rescue StandardError => e - msg = "Could not trigger rules: #{e.message}" - print_error "[ARE] #{msg}" - { 'success' => false, 'error' => msg }.to_json - end - - # - # Run a specified rule on the specified hooked browser. - # - get '/run/:rule_id/:hb_id' do - rule_id = params[:rule_id] - hb_id = params[:hb_id] - - raise InvalidParameterError, 'rule_id' if rule_id.nil? - raise InvalidParameterError, 'hb_id' if hb_id.nil? - - are = BeEF::Core::AutorunEngine::Engine.instance - are.run_matching_rules_on_zombie(rule_id, hb_id) - - { 'success' => true }.to_json - rescue InvalidParameterError => e - print_error e.message - halt 400 - rescue StandardError => e - msg = "Could not trigger rule: #{e.message}" - print_error "[ARE] #{msg}" - { 'success' => false, 'error' => msg }.to_json - end - end - end - end -end diff --git a/spec/beef/core/main/autorun_engine/autorun_engine_spec.rb b/spec/beef/core/main/autorun_engine/autorun_engine_spec.rb deleted file mode 100644 index dfe287d0b..000000000 --- a/spec/beef/core/main/autorun_engine/autorun_engine_spec.rb +++ /dev/null @@ -1,120 +0,0 @@ -# -# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net -# Browser Exploitation Framework (BeEF) - https://beefproject.com -# See the file 'doc/COPYING' for copying permission -# - -require 'rest-client' -require 'json' -require_relative '../../../../spec_helper' -require_relative '../../../../support/constants' -require_relative '../../../../support/beef_test' - -RSpec.describe 'AutoRunEngine Test', run_on_browserstack: true do - before(:all) do - @__ar_config_snapshot = SpecActiveRecordConnection.snapshot - @config = BeEF::Core::Configuration.instance - - # Grab DB file and regenerate if requested - print_info 'Loading database' - db_file = @config.get('beef.database.file') - print_info 'Resetting the database for BeEF.' - - if ENV['RESET_DB'] - File.delete(db_file) if File.exist?(db_file) - end - - @config.set('beef.credentials.user', 'beef') - @config.set('beef.credentials.passwd', 'beef') - @username = @config.get('beef.credentials.user') - @password = @config.get('beef.credentials.passwd') - - # Load BeEF extensions and modules - # Always load Extensions, as previous changes to the config from other tests may affect - # whether or not this test passes. - print_info 'Loading in BeEF::Extensions' - BeEF::Extensions.load - - # Check if modules already loaded. No need to reload. - if @config.get('beef.module').nil? - print_info 'Loading in BeEF::Modules' - BeEF::Modules.load - else - print_info 'Modules already loaded' - end - - - # Load up DB and migrate if necessary - ActiveRecord::Base.logger = nil - OTR::ActiveRecord.configure_from_hash!(adapter: 'sqlite3', database: db_file) - # otr-activerecord require you to manually establish the connection with the following line - #Also a check to confirm that the correct Gem version is installed to require it, likely easier for old systems. - if Gem.loaded_specs['otr-activerecord'].version > Gem::Version.create('1.4.2') - OTR::ActiveRecord.establish_connection! - end - ActiveRecord::Migrator.migrations_paths = [File.join('core', 'main', 'ar-migrations')] - MUTEX.synchronize do - context = ActiveRecord::MigrationContext.new(ActiveRecord::Migrator.migrations_paths) - if context.needs_migration? - ActiveRecord::Migrator.new(:up, context.migrations, context.schema_migration, context.internal_metadata).migrate - end - end - - BeEF::Core::Migration.instance.update_db! - - # add AutoRunEngine rule - test_rule = { 'name' => 'Display an alert', 'author' => 'mgeeky', 'browser' => 'ALL', 'browser_version' => 'ALL', 'os' => 'ALL', 'os_version' => 'ALL', 'modules' => [{ 'name' => 'alert_dialog', 'condition' => nil, 'options' => { 'text' => "You've been BeEFed ;>" } }], 'execution_order' => [0], 'execution_delay' => [0], 'chain_mode' => 'sequential' } - - BeEF::Core::AutorunEngine::RuleLoader.instance.load_directory - # are_engine.R - - # Spawn HTTP Server - print_info 'Starting HTTP Hook Server' - http_hook_server = BeEF::Core::Server.instance - - # Generate a token for the server to respond with - @token = BeEF::Core::Crypto.api_token - - # ***** IMPORTANT: close any and all AR/OTR connections before forking ***** - disconnect_all_active_record! - - # Initiate server start-up - @pid = fork do - http_hook_server.prepare - BeEF::API::Registrar.instance.fire(BeEF::API::Server, 'pre_http_start', http_hook_server) - http_hook_server.start - end - - begin - @caps = CONFIG['common_caps'].merge(CONFIG['browser_caps'][TASK_ID]) - @caps['name'] = self.class.description || ENV['name'] || 'no-name' - @caps['browserstack.local'] = true - @caps['browserstack.localIdentifier'] = ENV['BROWSERSTACK_LOCAL_IDENTIFIER'] - - @driver = Selenium::WebDriver.for(:remote, - url: "http://#{CONFIG['user']}:#{CONFIG['key']}@#{CONFIG['server']}/wd/hub", - options: @caps) - # Hook new victim - print_info 'Hooking a new victim, waiting a few seconds...' - wait = Selenium::WebDriver::Wait.new(timeout: 30) # seconds - - @driver.navigate.to VICTIM_URL.to_s - - sleep 1 - - sleep 1 until wait.until { @driver.execute_script('return window.beef.session.get_hook_session_id().length') > 0 } - - @session = @driver.execute_script('return window.beef.session.get_hook_session_id()') - end - end - - after(:all) do - server_teardown(@driver, @pid, @pids) - disconnect_all_active_record! - SpecActiveRecordConnection.restore!(@__ar_config_snapshot) - end - - it 'AutoRunEngine is working' do - expect(@session).not_to be_nil - end -end