Skip to content

Commit

Permalink
AutorunEngine: RuleLoader: Skip enabled rule file if rule is already …
Browse files Browse the repository at this point in the history
…in database (#2782)

* AutorunEngine: Engine: store rule execution rule ID

* AutorunEngine: RuleLoader: Skip enabled rule file if rule is already in database
  • Loading branch information
bcoles authored Apr 1, 2023
1 parent 5de2955 commit f8cba6e
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 94 deletions.
16 changes: 11 additions & 5 deletions core/main/autorun_engine/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,12 @@ def trigger(rule_ids, hb_id)

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 = []
Expand Down Expand Up @@ -76,9 +81,9 @@ def trigger(rule_ids, hb_id)
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(
Expand All @@ -88,9 +93,10 @@ def trigger(rule_ids, hb_id)
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
Expand Down
67 changes: 31 additions & 36 deletions core/main/autorun_engine/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,68 +18,63 @@ def initialize
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
Expand Down
141 changes: 88 additions & 53 deletions core/main/autorun_engine/rule_loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,76 +14,111 @@ def initialize
@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
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 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
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
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
)
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 }
print_more "Exec order: #{execution_order}"
print_more "Exec delay: #{exec_delay}"
end
rescue StandardError => e
err = 'Malformed JSON ruleset.'
print_error "[ARE] Ruleset (#{name}): ERROR. #{e} #{e.backtrace}"
{ 'success' => false, 'error' => err }

{ '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
Expand Down

0 comments on commit f8cba6e

Please sign in to comment.