Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
beefproject
GitHub Repository: beefproject/beef
Path: blob/master/core/main/autorun_engine/engine.rb
1154 views
1
#
2
# Copyright (c) 2006-2025 Wade Alcorn - [email protected]
3
# Browser Exploitation Framework (BeEF) - https://beefproject.com
4
# See the file 'doc/COPYING' for copying permission
5
#
6
module BeEF
7
module Core
8
module AutorunEngine
9
class Engine
10
include Singleton
11
12
def initialize
13
@config = BeEF::Core::Configuration.instance
14
15
@result_poll_interval = @config.get('beef.autorun.result_poll_interval')
16
@result_poll_timeout = @config.get('beef.autorun.result_poll_timeout')
17
@continue_after_timeout = @config.get('beef.autorun.continue_after_timeout')
18
19
@debug_on = @config.get('beef.debug')
20
21
@VERSION = ['<', '<=', '==', '>=', '>', 'ALL']
22
@VERSION_STR = %w[XP Vista 7]
23
end
24
25
# Checks if there are any ARE rules to be triggered for the specified hooked browser.
26
#
27
# Returns an array with rule IDs that matched and should be triggered.
28
# if rule_id is specified, checks will be executed only against the specified rule (useful
29
# for dynamic triggering of new rulesets ar runtime)
30
def find_matching_rules_for_zombie(browser, browser_version, os, os_version)
31
rules = BeEF::Core::Models::Rule.all
32
33
return if rules.nil?
34
return if rules.empty?
35
36
# TODO: handle cases where there are multiple ARE rules for the same hooked browser.
37
# maybe rules need to have priority or something?
38
39
print_info '[ARE] Checking if any defined rules should be triggered on target.'
40
41
match_rules = []
42
rules.each do |rule|
43
next unless zombie_matches_rule?(browser, browser_version, os, os_version, rule)
44
45
match_rules.push(rule.id)
46
print_more("Hooked browser and OS match rule: #{rule.name}.")
47
end
48
49
print_more("Found [#{match_rules.length}/#{rules.length}] ARE rules matching the hooked browser.")
50
51
match_rules
52
end
53
54
# @return [Boolean]
55
# Note: browser version checks are supporting only major versions, ex: C 43, IE 11
56
# Note: OS version checks are supporting major/minor versions, ex: OSX 10.10, Windows 8.1
57
def zombie_matches_rule?(browser, browser_version, os, os_version, rule)
58
return false if rule.nil?
59
60
unless zombie_browser_matches_rule?(browser, browser_version, rule)
61
print_debug("Browser version check -> (hook) #{browser_version} #{rule.browser_version} (rule) : does not match")
62
return false
63
end
64
65
print_debug("Browser version check -> (hook) #{browser_version} #{rule.browser_version} (rule) : matched")
66
67
unless zombie_os_matches_rule?(os, os_version, rule)
68
print_debug("OS version check -> (hook) #{os_version} #{rule.os_version} (rule): does not match")
69
return false
70
end
71
72
print_debug("OS version check -> (hook) #{os_version} #{rule.os_version} (rule): matched")
73
74
true
75
rescue StandardError => e
76
print_error e.message
77
print_debug e.backtrace.join("\n")
78
end
79
80
# @return [Boolean]
81
# TODO: This should be updated to support matching multiple OS (like the browser check below)
82
def zombie_os_matches_rule?(os, os_version, rule)
83
return false if rule.nil?
84
85
return false unless rule.os == 'ALL' || os == rule.os
86
87
# check if the OS versions match
88
os_ver_rule_cond = rule.os_version.split(' ').first
89
90
return true if os_ver_rule_cond == 'ALL'
91
92
return false unless @VERSION.include?(os_ver_rule_cond) || @VERSION_STR.include?(os_ver_rule_cond)
93
94
os_ver_rule_maj = rule.os_version.split(' ').last.split('.').first
95
os_ver_rule_min = rule.os_version.split(' ').last.split('.').last
96
97
if os_ver_rule_maj == 'XP'
98
os_ver_rule_maj = 5
99
os_ver_rule_min = 0
100
elsif os_ver_rule_maj == 'Vista'
101
os_ver_rule_maj = 6
102
os_ver_rule_min = 0
103
elsif os_ver_rule_maj == '7'
104
os_ver_rule_maj = 6
105
os_ver_rule_min = 0
106
end
107
108
# Most of the times Linux/*BSD OS doesn't return any version
109
# (TODO: improve OS detection on these operating systems)
110
if !os_version.nil? && !@VERSION_STR.include?(os_version)
111
os_ver_hook_maj = os_version.split('.').first
112
os_ver_hook_min = os_version.split('.').last
113
114
# the following assignments to 0 are need for later checks like:
115
# 8.1 >= 7, because if the version doesn't have minor versions, maj/min are the same
116
os_ver_hook_min = 0 if os_version.split('.').length == 1
117
os_ver_rule_min = 0 if rule.os_version.split('.').length == 1
118
else
119
# XP is Windows 5.0 and Vista is Windows 6.0. Easier for comparison later on.
120
# TODO: BUG: This will fail horribly if the target OS is Windows 7 or newer,
121
# as no version normalization is performed.
122
# TODO: Update this for every OS since Vista/7 ...
123
if os_version == 'XP'
124
os_ver_hook_maj = 5
125
os_ver_hook_min = 0
126
elsif os_version == 'Vista'
127
os_ver_hook_maj = 6
128
os_ver_hook_min = 0
129
elsif os_version == '7'
130
os_ver_hook_maj = 6
131
os_ver_hook_min = 0
132
end
133
end
134
135
if !os_version.nil? || rule.os_version != 'ALL'
136
os_major_version_match = compare_versions(os_ver_hook_maj.to_s, os_ver_rule_cond, os_ver_rule_maj.to_s)
137
os_minor_version_match = compare_versions(os_ver_hook_min.to_s, os_ver_rule_cond, os_ver_rule_min.to_s)
138
return false unless (os_major_version_match && os_minor_version_match)
139
end
140
141
true
142
rescue StandardError => e
143
print_error e.message
144
print_debug e.backtrace.join("\n")
145
end
146
147
# @return [Boolean]
148
def zombie_browser_matches_rule?(browser, browser_version, rule)
149
return false if rule.nil?
150
151
b_ver_cond = rule.browser_version.split(' ').first
152
153
return false unless @VERSION.include?(b_ver_cond)
154
155
b_ver = rule.browser_version.split(' ').last
156
157
return false unless BeEF::Filters.is_valid_browserversion?(b_ver)
158
159
# check if rule specifies multiple browsers
160
if rule.browser =~ /\A[A-Z]+\Z/
161
return false unless rule.browser == 'ALL' || browser == rule.browser
162
163
# check if the browser version matches
164
browser_version_match = compare_versions(browser_version.to_s, b_ver_cond, b_ver.to_s)
165
return false unless browser_version_match
166
else
167
browser_match = false
168
rule.browser.gsub(/[^A-Z,]/i, '').split(',').each do |b|
169
if b == browser || b == 'ALL'
170
browser_match = true
171
break
172
end
173
end
174
return false unless browser_match
175
end
176
177
true
178
rescue StandardError => e
179
print_error e.message
180
print_debug e.backtrace.join("\n")
181
end
182
183
# Check if the hooked browser type/version and OS type/version match any Rule-sets
184
# stored in the BeEF::Core::Models::Rule database table
185
# If one or more Rule-sets do match, trigger the module chain specified
186
def find_and_run_all_matching_rules_for_zombie(hb_id)
187
return if hb_id.nil?
188
189
hb_details = BeEF::Core::Models::BrowserDetails
190
browser_name = hb_details.get(hb_id, 'browser.name')
191
browser_version = hb_details.get(hb_id, 'browser.version')
192
os_name = hb_details.get(hb_id, 'host.os.name')
193
os_version = hb_details.get(hb_id, 'host.os.version')
194
195
are = BeEF::Core::AutorunEngine::Engine.instance
196
rules = are.find_matching_rules_for_zombie(browser_name, browser_version, os_name, os_version)
197
198
return if rules.nil?
199
return if rules.empty?
200
201
are.run_rules_on_zombie(rules, hb_id)
202
end
203
204
# Run the specified rule IDs on the specified zombie ID
205
# only if the rules match.
206
def run_matching_rules_on_zombie(rule_ids, hb_id)
207
return if rule_ids.nil?
208
return if hb_id.nil?
209
210
rule_ids = [rule_ids.to_i] if rule_ids.is_a?(String)
211
212
hb_details = BeEF::Core::Models::BrowserDetails
213
browser_name = hb_details.get(hb_id, 'browser.name')
214
browser_version = hb_details.get(hb_id, 'browser.version')
215
os_name = hb_details.get(hb_id, 'host.os.name')
216
os_version = hb_details.get(hb_id, 'host.os.version')
217
218
are = BeEF::Core::AutorunEngine::Engine.instance
219
rules = are.find_matching_rules_for_zombie(browser_name, browser_version, os_name, os_version)
220
221
return if rules.nil?
222
return if rules.empty?
223
224
new_rules = []
225
rules.each do |rule|
226
new_rules << rule if rule_ids.include?(rule)
227
end
228
229
return if new_rules.empty?
230
231
are.run_rules_on_zombie(new_rules, hb_id)
232
end
233
234
# Run the specified rule IDs on the specified zombie ID
235
# regardless of whether the rules match.
236
# Prepare and return the JavaScript of the modules to be sent.
237
# It also updates the rules ARE execution table with timings
238
def run_rules_on_zombie(rule_ids, hb_id)
239
return if rule_ids.nil?
240
return if hb_id.nil?
241
242
hb = BeEF::HBManager.get_by_id(hb_id)
243
hb_session = hb.session
244
245
rule_ids = [rule_ids] if rule_ids.is_a?(Integer)
246
247
rule_ids.each do |rule_id|
248
rule = BeEF::Core::Models::Rule.find(rule_id)
249
modules = JSON.parse(rule.modules)
250
251
execution_order = JSON.parse(rule.execution_order)
252
execution_delay = JSON.parse(rule.execution_delay)
253
chain_mode = rule.chain_mode
254
255
unless %w[sequential nested-forward].include?(chain_mode)
256
print_error("[ARE] Invalid chain mode '#{chain_mode}' for rule")
257
return
258
end
259
260
mods_bodies = []
261
mods_codes = []
262
mods_conditions = []
263
264
# this ensures that if both rule A and rule B call the same module in sequential mode,
265
# execution will be correct preventing wrapper functions to be called with equal names.
266
rule_token = SecureRandom.hex(5)
267
268
modules.each do |cmd_mod|
269
mod = BeEF::Core::Models::CommandModule.where(name: cmd_mod['name']).first
270
options = []
271
replace_input = false
272
cmd_mod['options'].each do |k, v|
273
options.push({ 'name' => k, 'value' => v })
274
replace_input = true if v == '<<mod_input>>'
275
end
276
277
command_body = prepare_command(mod, options, hb_id, replace_input, rule_token)
278
279
mods_bodies.push(command_body)
280
mods_codes.push(cmd_mod['code'])
281
mods_conditions.push(cmd_mod['condition'])
282
end
283
284
# Depending on the chosen chain mode (sequential or nested/forward), prepare the appropriate wrapper
285
case chain_mode
286
when 'nested-forward'
287
wrapper = prepare_nested_forward_wrapper(mods_bodies, mods_codes, mods_conditions, execution_order, rule_token)
288
when 'sequential'
289
wrapper = prepare_sequential_wrapper(mods_bodies, execution_order, execution_delay, rule_token)
290
else
291
# we should never get here. chain mode is validated earlier.
292
print_error("[ARE] Invalid chain mode '#{chain_mode}'")
293
next
294
end
295
296
print_more "Triggering rules #{rule_ids} on HB #{hb_id}"
297
298
are_exec = BeEF::Core::Models::Execution.new(
299
session_id: hb_session,
300
mod_count: modules.length,
301
mod_successful: 0,
302
rule_token: rule_token,
303
mod_body: wrapper,
304
is_sent: false,
305
rule_id: rule_id
306
)
307
are_exec.save!
308
end
309
end
310
311
private
312
313
# Wraps module bodies in their own function, using setTimeout to trigger them with an eventual delay.
314
# Launch order is also taken care of.
315
# - sequential chain with delays (setTimeout stuff)
316
# ex.: setTimeout(module_one(), 0);
317
# setTimeout(module_two(), 2000);
318
# setTimeout(module_three(), 3000);
319
# Note: no result status is checked here!! Useful if you just want to launch a bunch of modules without caring
320
# what their status will be (for instance, a bunch of XSRFs on a set of targets)
321
def prepare_sequential_wrapper(mods, order, delay, rule_token)
322
wrapper = ''
323
delayed_exec = ''
324
c = 0
325
while c < mods.length
326
delayed_exec += %| setTimeout(function(){#{mods[order[c]][:mod_name]}_#{rule_token}();}, #{delay[c]}); |
327
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")
328
wrapped_mod = "#{mod_body}\n"
329
wrapper += wrapped_mod
330
c += 1
331
end
332
wrapper += delayed_exec
333
print_more "Final Modules Wrapper:\n #{wrapper}" if @debug_on
334
wrapper
335
end
336
337
# Wraps module bodies in their own function, then start to execute them from the first, polling for
338
# command execution status/results (with configurable polling interval and timeout).
339
# Launch order is also taken care of.
340
# - nested forward chain with status checks (setInterval to wait for command to return from async operations)
341
# ex.: module_one()
342
# if condition
343
# module_two(module_one_output)
344
# if condition
345
# module_three(module_two_output)
346
#
347
# Note: command result status is checked, and you can properly chain input into output, having also
348
# the flexibility of slightly mangling it to adapt to module needs.
349
# Note: Useful in situations where you want to launch 2 modules, where the second one will execute only
350
# if the first once return with success. Also, the second module has the possibility of mangling first
351
# module output and use it as input for some of its module inputs.
352
def prepare_nested_forward_wrapper(mods, code, conditions, order, rule_token)
353
wrapper = ''
354
delayed_exec = ''
355
delayed_exec_footers = []
356
c = 0
357
358
while c < mods.length
359
i = if mods.length == 1
360
c
361
else
362
c + 1
363
end
364
365
code_snippet = ''
366
mod_input = ''
367
if code[c] != 'null' && code[c] != ''
368
code_snippet = code[c]
369
mod_input = 'mod_input'
370
end
371
372
conditions[i] = true if conditions[i].nil? || conditions[i] == ''
373
374
if c == 0
375
# this is the first wrapper to prepare
376
delayed_exec += %|
377
function #{mods[order[c]][:mod_name]}_#{rule_token}_f(){
378
#{mods[order[c]][:mod_name]}_#{rule_token}();
379
380
// TODO add timeout to prevent infinite loops
381
function isResReady(mod_result, start){
382
if (mod_result === null && parseInt(((new Date().getTime()) - start)) < #{@result_poll_timeout}){
383
// loop
384
}else{
385
// module return status/data is now available
386
clearInterval(resultReady);
387
if (mod_result === null && #{@continue_after_timeout}){
388
var mod_result = [];
389
mod_result[0] = 1; //unknown status
390
mod_result[1] = '' //empty result
391
}
392
var status = mod_result[0];
393
if(#{conditions[i]}){
394
#{mods[order[i]][:mod_name]}_#{rule_token}_can_exec = true;
395
#{mods[order[c]][:mod_name]}_#{rule_token}_mod_output = mod_result[1];
396
|
397
398
delayed_exec_footer = %|
399
}
400
}
401
}
402
var start = (new Date()).getTime();
403
var resultReady = setInterval(function(){var start = (new Date()).getTime(); isResReady(#{mods[order[c]][:mod_name]}_#{rule_token}_mod_output, start);},#{@result_poll_interval});
404
}
405
#{mods[order[c]][:mod_name]}_#{rule_token}_f();
406
|
407
408
delayed_exec_footers.push(delayed_exec_footer)
409
410
elsif c < mods.length - 1
411
code_snippet = code_snippet.to_s.gsub(mods[order[c - 1]][:mod_name], "#{mods[order[c - 1]][:mod_name]}_#{rule_token}")
412
413
# this is one of the wrappers in the middle of the chain
414
delayed_exec += %|
415
function #{mods[order[c]][:mod_name]}_#{rule_token}_f(){
416
if(#{mods[order[c]][:mod_name]}_#{rule_token}_can_exec){
417
#{code_snippet}
418
#{mods[order[c]][:mod_name]}_#{rule_token}(#{mod_input});
419
function isResReady(mod_result, start){
420
if (mod_result === null && parseInt(((new Date().getTime()) - start)) < #{@result_poll_timeout}){
421
// loop
422
}else{
423
// module return status/data is now available
424
clearInterval(resultReady);
425
if (mod_result === null && #{@continue_after_timeout}){
426
var mod_result = [];
427
mod_result[0] = 1; //unknown status
428
mod_result[1] = '' //empty result
429
}
430
var status = mod_result[0];
431
if(#{conditions[i]}){
432
#{mods[order[i]][:mod_name]}_#{rule_token}_can_exec = true;
433
#{mods[order[c]][:mod_name]}_#{rule_token}_mod_output = mod_result[1];
434
|
435
436
delayed_exec_footer = %|
437
}
438
}
439
}
440
var start = (new Date()).getTime();
441
var resultReady = setInterval(function(){ isResReady(#{mods[order[c]][:mod_name]}_#{rule_token}_mod_output, start);},#{@result_poll_interval});
442
}
443
}
444
#{mods[order[c]][:mod_name]}_#{rule_token}_f();
445
|
446
447
delayed_exec_footers.push(delayed_exec_footer)
448
else
449
code_snippet = code_snippet.to_s.gsub(mods[order[c - 1]][:mod_name], "#{mods[order[c - 1]][:mod_name]}_#{rule_token}")
450
# this is the last wrapper to prepare
451
delayed_exec += %|
452
function #{mods[order[c]][:mod_name]}_#{rule_token}_f(){
453
if(#{mods[order[c]][:mod_name]}_#{rule_token}_can_exec){
454
#{code_snippet}
455
#{mods[order[c]][:mod_name]}_#{rule_token}(#{mod_input});
456
}
457
}
458
#{mods[order[c]][:mod_name]}_#{rule_token}_f();
459
|
460
end
461
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")
462
wrapped_mod = "#{mod_body}\n"
463
wrapper += wrapped_mod
464
c += 1
465
end
466
wrapper += delayed_exec + delayed_exec_footers.reverse.join("\n")
467
print_more "Final Modules Wrapper:\n #{delayed_exec + delayed_exec_footers.reverse.join("\n")}" if @debug_on
468
wrapper
469
end
470
471
# prepare the command module (compiling the Erubis templating stuff), eventually obfuscate it,
472
# and store it in the database.
473
# Returns the raw module body after template substitution.
474
def prepare_command(mod, options, hb_id, replace_input, rule_token)
475
config = BeEF::Core::Configuration.instance
476
begin
477
command = BeEF::Core::Models::Command.new(
478
data: options.to_json,
479
hooked_browser_id: hb_id,
480
command_module_id: BeEF::Core::Configuration.instance.get("beef.module.#{mod.name}.db.id"),
481
creationdate: Time.new.to_i,
482
instructions_sent: true
483
)
484
command.save!
485
486
command_module = BeEF::Core::Models::CommandModule.find(mod.id)
487
if command_module.path.match(/^Dynamic/)
488
# metasploit and similar integrations
489
command_module = BeEF::Modules::Commands.const_get(command_module.path.split('/').last.capitalize).new
490
else
491
# normal modules always here
492
key = BeEF::Module.get_key_by_database_id(mod.id)
493
command_module = BeEF::Core::Command.const_get(config.get("beef.module.#{key}.class")).new(key)
494
end
495
496
hb = BeEF::HBManager.get_by_id(hb_id)
497
hb_session = hb.session
498
command_module.command_id = command.id
499
command_module.session_id = hb_session
500
command_module.build_datastore(command.data)
501
command_module.pre_send
502
503
build_missing_beefjs_components(command_module.beefjs_components) unless command_module.beefjs_components.empty?
504
505
if config.get('beef.extension.evasion.enable')
506
evasion = BeEF::Extension::Evasion::Evasion.instance
507
command_body = evasion.obfuscate(command_module.output) + "\n\n"
508
else
509
command_body = command_module.output + "\n\n"
510
end
511
512
# @note prints the event to the console
513
print_more "Preparing JS for command id [#{command.id}], module [#{mod.name}]"
514
515
mod_input = replace_input ? 'mod_input' : ''
516
result = %|
517
var #{mod.name}_#{rule_token} = function(#{mod_input}){
518
#{clean_command_body(command_body, replace_input)}
519
};
520
var #{mod.name}_#{rule_token}_can_exec = false;
521
var #{mod.name}_#{rule_token}_mod_output = null;
522
|
523
524
{ mod_name: mod.name, mod_body: result }
525
rescue StandardError => e
526
print_error e.message
527
print_debug e.backtrace.join("\n")
528
end
529
end
530
531
# Removes the beef.execute wrapper in order that modules are executed in the ARE wrapper, rather than
532
# using the default behavior of adding the module to an array and execute it at polling time.
533
#
534
# Also replace <<mod_input>> with mod_input variable if needed for chaining module output/input
535
def clean_command_body(command_body, replace_input)
536
cmd_body = command_body.lines.map(&:chomp)
537
wrapper_start_index, wrapper_end_index = nil
538
539
cmd_body.each_with_index do |line, index|
540
if line.to_s =~ /^(beef|[a-zA-Z]+)\.execute\(function\(\)/
541
wrapper_start_index = index
542
break
543
end
544
end
545
print_error '[ARE] Could not find module start index' if wrapper_start_index.nil?
546
547
cmd_body.reverse.each_with_index do |line, index|
548
if line.include?('});')
549
wrapper_end_index = index
550
break
551
end
552
end
553
print_error '[ARE] Could not find module end index' if wrapper_end_index.nil?
554
555
cleaned_cmd_body = cmd_body.slice(wrapper_start_index..-(wrapper_end_index + 1)).join("\n")
556
557
print_error '[ARE] No command to send' if cleaned_cmd_body.eql?('')
558
559
# check if <<mod_input>> should be replaced with a variable name (depending if the variable is a string or number)
560
return cleaned_cmd_body unless replace_input
561
562
if cleaned_cmd_body.include?('"<<mod_input>>"')
563
cleaned_cmd_body.gsub('"<<mod_input>>"', 'mod_input')
564
elsif cleaned_cmd_body.include?('\'<<mod_input>>\'')
565
cleaned_cmd_body.gsub('\'<<mod_input>>\'', 'mod_input')
566
elsif cleaned_cmd_body.include?('<<mod_input>>')
567
cleaned_cmd_body.gsub('\'<<mod_input>>\'', 'mod_input')
568
else
569
cleaned_cmd_body
570
end
571
rescue StandardError => e
572
print_error "[ARE] There is likely a problem with the module's command.js parsing. Check Engine.clean_command_body. #{e.message}"
573
end
574
575
# compare versions
576
def compare_versions(ver_a, cond, ver_b)
577
return true if cond == 'ALL'
578
return true if cond == '==' && ver_a == ver_b
579
return true if cond == '<=' && ver_a <= ver_b
580
return true if cond == '<' && ver_a < ver_b
581
return true if cond == '>=' && ver_a >= ver_b
582
return true if cond == '>' && ver_a > ver_b
583
584
false
585
end
586
end
587
end
588
end
589
end
590
591