diff --git a/core/main/autorun_engine/rule_loader.rb b/core/main/autorun_engine/rule_loader.rb index e42a6082b..31748b0e3 100644 --- a/core/main/autorun_engine/rule_loader.rb +++ b/core/main/autorun_engine/rule_loader.rb @@ -105,6 +105,99 @@ module BeEF { 'success' => false, 'error' => e.message } end + # Update an ARE rule set. + # @param [Hash] ARE rule ID. + # @param [Hash] ARE ruleset as JSON + # @return [Hash] {"success": Boolean, "rule_id": Integer, "error": String} + def update_rule_json(id, data) + # Quite similar in implementation to load_rule_json. Might benefit from a refactor. + name = data['name'] || '' + author = data['author'] || '' + browser = data['browser'] || 'ALL' + browser_version = data['browser_version'] || 'ALL' + os = data['os'] || 'ALL' + os_version = data['os_version'] || 'ALL' + modules = data['modules'] + execution_order = data['execution_order'] + execution_delay = data['execution_delay'] + chain_mode = data['chain_mode'] || 'sequential' + + begin + BeEF::Core::AutorunEngine::Parser.instance.parse( + name, + author, + browser, + browser_version, + os, + os_version, + modules, + execution_order, + execution_delay, + chain_mode + ) + rescue => e + print_error("[ARE] Error updating ruleset (#{name}): #{e.message}") + return { 'success' => false, 'error' => e.message } + end + + existing_rule = BeEF::Core::Models::Rule.where( + name: name, + author: author, + browser: browser, + browser_version: browser_version, + os: os, + os_version: os_version, + modules: modules.to_json, + execution_order: execution_order.to_s, + execution_delay: execution_delay.to_s, + chain_mode: chain_mode + ).first + + unless existing_rule.nil? + msg = "Duplicate rule already exists in the database (ID: #{existing_rule.id})" + print_info("[ARE] Skipping ruleset (#{name}): #{msg}") + return { 'success' => false, 'error' => msg } + end + old_are_rule = BeEF::Core::Models::Rule.find_by(id: id) + + old_are_rule.update( + name: name, + author: author, + browser: browser, + browser_version: browser_version, + os: os, + os_version: os_version, + modules: modules.to_json, + execution_order: execution_order.to_s, + execution_delay: execution_delay.to_s, + chain_mode: chain_mode + ) + + print_info("[ARE] Ruleset (#{name}) updated successfully.") + + if @debug_on + print_more "Target Browser: #{browser} (#{browser_version})" + print_more "Target OS: #{os} (#{os_version})" + print_more 'Modules to run:' + modules.each do |mod| + print_more "(*) Name: #{mod['name']}" + print_more "(*) Condition: #{mod['condition']}" + print_more "(*) Code: #{mod['code']}" + print_more '(*) Options:' + mod['options'].each do |key, value| + print_more "\t#{key}: (#{value})" + end + end + print_more "Exec order: #{execution_order}" + print_more "Exec delay: #{exec_delay}" + end + + { 'success' => true } + rescue TypeError, ArgumentError => e + print_error("[ARE] Failed to update ruleset (#{name}): #{e.message}") + { 'success' => false, 'error' => e.message } + end + # Load an ARE ruleset from file # @param [String] JSON ARE ruleset file path def load_rule_file(json_rule_path) diff --git a/core/main/rest/handlers/autorun_engine.rb b/core/main/rest/handlers/autorun_engine.rb index 2b9278d93..fc2ba5467 100644 --- a/core/main/rest/handlers/autorun_engine.rb +++ b/core/main/rest/handlers/autorun_engine.rb @@ -84,6 +84,26 @@ module BeEF { 'success' => false, 'error' => e.message }.to_json end + # + # Update a ruleset + # + patch '/rule/:rule_id' do + rule_id = params[:rule_id] + rule = BeEF::Core::Models::Rule.find(rule_id) + raise InvalidParameterError, 'id' if rule.nil? + data = JSON.parse request.body.read + rloader = BeEF::Core::AutorunEngine::RuleLoader.instance + rloader.update_rule_json(rule_id, data) + + { 'success' => true }.to_json + rescue InvalidParameterError => e + print_error e.message + halt 400 + rescue StandardError => e + print_error "Internal error while updating Autorun rule: #{e.message}" + { 'success' => false, 'error' => e.message }.to_json + end + # # Run a specified rule on all online hooked browsers (if the zombie matches the rule). # Offline hooked browsers are ignored diff --git a/core/main/rest/handlers/modules.rb b/core/main/rest/handlers/modules.rb index 8e07b7c62..5c23e926b 100644 --- a/core/main/rest/handlers/modules.rb +++ b/core/main/rest/handlers/modules.rb @@ -25,21 +25,20 @@ module BeEF get '/' do mods = BeEF::Core::Models::CommandModule.all - mods_hash = {} - i = 0 + mods_array = [] + mods.each do |mod| modk = BeEF::Module.get_key_by_database_id(mod.id) next unless BeEF::Module.is_enabled(modk) - mods_hash[i] = { + mods_array << { 'id' => mod.id, 'class' => config.get("beef.module.#{modk}.class"), 'name' => config.get("beef.module.#{modk}.name"), 'category' => config.get("beef.module.#{modk}.category") } - i += 1 end - mods_hash.to_json + mods_array.to_json end get '/search/:mod_name' do diff --git a/extensions/admin_ui/api/handler.rb b/extensions/admin_ui/api/handler.rb index 374b2cd0a..cf1dbe0d9 100644 --- a/extensions/admin_ui/api/handler.rb +++ b/extensions/admin_ui/api/handler.rb @@ -90,6 +90,9 @@ module BeEF ui/panel/tabs/ZombieTabRTC.js ui/panel/Logout.js ui/panel/WelcomeTab.js + ui/panel/AutoRunTab.js + ui/panel/AutoRunRuleForm.js + ui/panel/AutoRunModuleForm.js ui/panel/ModuleSearching.js ] diff --git a/extensions/admin_ui/media/javascript/ui/panel/AutoRunModuleForm.js b/extensions/admin_ui/media/javascript/ui/panel/AutoRunModuleForm.js new file mode 100644 index 000000000..f744af951 --- /dev/null +++ b/extensions/admin_ui/media/javascript/ui/panel/AutoRunModuleForm.js @@ -0,0 +1,172 @@ + +loadModuleInfo = async function(token, moduleId, moduleName) { + try { + // If all we have is the name then we need to get the ID first. + if (moduleId === undefined) { + const searchResponse = await fetch(`/api/modules/search/${moduleName}?token=${token}`); + if (!searchResponse.ok) { + throw new Error(`Getting auto run rules failed with status ${searchResponse.status}`); + } + const searchData = await searchResponse.json(); + if (typeof searchData.id === 'number') { + moduleId = searchData.id; + } else { + throw new Error("Searching module name failed."); + } + } + + const infoResponse = await fetch(`/api/modules/${moduleId}?token=${token}`); + const infoData = await infoResponse.json(); + if (!infoData) { + throw new Error(`Module with name ${moduleName} and ID ${moduleId} couldn't be retrived.`); + } + // Set the module Id incase we need it later. + infoData.id = moduleId; + return infoData; + + } catch(error) { + console.error(error); + console.error("Failed to get module information."); + return null; + } +} + + +/** + * Form that displays fields for a module. + * + * moduleData: The object definition of this moduleData from the Auto Run Engine. + * deleteFn: callback function to delete this moduleData. + * moveUp: moves the module up one spot in the Auto Run execution order. + * moveDown: moves the module down one spot in the Auto Run exection order. + */ +AutoRunModuleForm = function(moduleData, deleteFn, moveUp, moveDown, ruleId, index, moduleList) { + const moduleTextAreaId = `rule-${ruleId}-module-textarea-${index}`; + const chainModeComboId = `rule-${ruleId}-module-combo-${index}`; + const token = BeefWUI.get_rest_token(); + + const comboStore = new Ext.data.Store({ + data: moduleList, + reader: new Ext.data.JsonReader({ + fields: ['id', 'name'], + }), + proxy: new Ext.data.MemoryProxy(moduleList) + }); + + const moduleSelect = new Ext.form.ComboBox({ + fieldLabel: 'Change Module', + store: comboStore, + queryMode: 'local', + displayField: 'name', + valueField: 'id', + editable: false, // Disable manual editing of the field + forceSelection: true, // Force selection from the list + triggerAction: 'all', + typeAhead: true, + listeners: { + select: async function(combo) { + const selectedModuleId = combo.getValue() + const moduleInfo = await loadModuleInfo(token, selectedModuleId, undefined); + if (!moduleInfo) { + console.error("Failed to load new module."); + return; + } + // Update the module data to reflect the new module. + moduleData.name = moduleInfo.name; + moduleData.condition = moduleInfo.condition ? moduleInfo.condition : null; + moduleData.options = {}; + for (let i = 0; i < moduleInfo.options.length; i++) { + const newOption = moduleInfo.options[i]; + moduleData.options[newOption.name] = newOption.value ? newOption.value : ''; + } + loadModule(moduleInfo); + } + } + }); + + const moduleOptionsContainer = new Ext.Panel({ + title: `Module ${index + 1}`, + tbar: [moduleSelect], + layout: 'form', + border: false, + listeners: { + afterrender: function() {loadModule(undefined)} + } + }); + + async function loadModule(moduleInfo) { + if (!moduleInfo) + moduleInfo = await loadModuleInfo(token, undefined, moduleData.name); + if (!moduleInfo) { + moduleOptionsContainer.update("

Failed to load module information.

"); + return; + } + + // Update the combobox default value to be this module. + // Can't use the moduleData name since it doesn't match the ID. + moduleSelect.setValue(moduleInfo.id); + + // Setup module form elements. Remove all incase we're switching from a different module. + moduleOptionsContainer.removeAll(); + moduleOptionsContainer.add(new Ext.form.DisplayField({ + fieldLabel: 'Module Name', + value: moduleInfo.name + })) + moduleOptionsContainer.add(new Ext.form.DisplayField({ + fieldLabel: 'Module Author', + value: moduleInfo.author ? moduleInfo.author : 'anonymous' + })) + + for (let i = 0; i < moduleInfo.options.length; i++) { + const inputField = generate_form_input_field( + moduleOptionsContainer, + moduleInfo.options[i], + moduleData.options[moduleInfo.options[i].name], + false, + {session: `${moduleInfo.name}-module-${index}-field-${i}`} + ); + // Ensure any changes to the element are reflected in the newRule object. + // When the user saves the rule the whole newRule object will be saved, + // including any changes made to these input fields. + inputField.on('change', function(_inputF, newValue, oldValue) { + moduleData.options[moduleInfo.options[i].name] = newValue; + }); + + }; + moduleOptionsContainer.doLayout(); + } + + const buttonContainer = new Ext.Container({ + layout: { + type: 'hbox', + pack: 'end', + }, + items: [ + { + xtype: 'button', + text: 'Delete', + handler: deleteFn, + },{ + xtype: 'button', + text: 'Move Forward', + handler: moveUp, + disabled: moveUp == undefined, + },{ + xtype: 'button', + text: 'Move Back', + handler: moveDown, + disabled: moveDown == undefined, + } + ] + }); + + + AutoRunModuleForm.superclass.constructor.call(this, { + items: [ + moduleOptionsContainer, + buttonContainer + ] + }); +}; + +Ext.extend(AutoRunModuleForm, Ext.Container, {}); \ No newline at end of file diff --git a/extensions/admin_ui/media/javascript/ui/panel/AutoRunRuleForm.js b/extensions/admin_ui/media/javascript/ui/panel/AutoRunRuleForm.js new file mode 100644 index 000000000..19df10537 --- /dev/null +++ b/extensions/admin_ui/media/javascript/ui/panel/AutoRunRuleForm.js @@ -0,0 +1,181 @@ +const areNotificationUpdateTest = { + "name": "Display an alert-----", + "author": "mgeeky", + "modules": [ + { + "name": "alert_dialog", + "condition": null, + "options": { + "text":"You've been BeEFed ;>" + } + } + ], + "execution_order": [0], + "execution_delay": [0], + "chain_mode": "nested-forward" +}; +/** + * Form for the user to read, update and delete a specific Auto Run rule. + * + * rule: The object definition of this rule from the Auto Run Engine. + * modules: The list of all commands/modules that the user can choose from. + * deleteFn: callback function to delete this rule. + * updateFn: callback function to update this rule. + */ +AutoRunRuleForm = function(rule, modules, deleteFn, updateFn) { + const self = this; + const ruleTextFieldId = `rule-name-${rule.id}`; + const chainModeComboId = `rule-chain-mode-${rule.id}`; + const newRule = JSON.parse(JSON.stringify(rule)); + newRule.modules = JSON.parse(newRule['modules']); + newRule.execution_delay = JSON.parse(newRule['execution_delay']); + newRule.execution_order = JSON.parse(newRule['execution_order']); + const moduleContainer = new Ext.Container({ + style: { + padding: '10 10 10 10', + }, + listeners: { + afterrender: setupModuleForms + } + }); + + function reorderModule(index, direction) { + // Rearrange modules into new order. + const currentModule = newRule.modules[index]; + const newIndex = direction === 'back' ? index + 1 : index - 1; + newRule.modules.splice(index, 1); + newRule.modules.splice(newIndex, 0, currentModule); + + // Update DOM. + setupModuleForms(); + moduleContainer.doLayout(); + } + + function removeModule(index) { + // Remove element from execution_order and execution_delay arrays. + newRule.modules.splice(index, 1); + newRule.execution_delay.splice(index, 1); + + // Update DOM. + setupModuleForms(); + moduleContainer.doLayout(); + } + + function addModule() { + // New module is a copy of the last module. + newRule.modules.push(newRule.modules[newRule.modules.length - 1]); + newRule.execution_delay.push(newRule.execution_delay[newRule.execution_delay.length - 1]); + + // Update DOM. + setupModuleForms(); + moduleContainer.doLayout(); + } + + function setupModuleForms() { + + moduleContainer.removeAll(true); + + // I think execution order should always be sequential. + // The actual order comes from the modules array. + for (let i = 0; i < newRule.modules.length; ++i) { + const isFirstModule = i === 0; + const isLastModule = i >= newRule.modules.length - 1; + moduleContainer.add(new AutoRunModuleForm( + newRule.modules[i], + function() {removeModule(i)}, + isFirstModule ? undefined : function() {reorderModule(i, 'forward')}, + isLastModule ? undefined : function() {reorderModule(i, 'back')}, + rule.id, + i, + modules + )); + } + } + + function handleUpdateRule() { + // TODO: Need to overwrite module order. + const form = self.getForm(); + const formValues = form.getValues(); + const updatedRule = { + ...newRule, + name: formValues[ruleTextFieldId], + chain_mode: formValues[chainModeComboId], + execution_order: [...Array(newRule.modules.length).keys()], + }; + updateFn(updatedRule); + } + + + AutoRunRuleForm.superclass.constructor.call(this, { + padding:'10 10 10 10', + title: `Rule ${rule.id}`, + items: [{ + xtype: 'textfield', + id: ruleTextFieldId, + value: rule.name ? rule.name : '', + fieldLabel: 'Name', + }, + { + xtype: 'displayfield', + fieldLabel: 'Author', + value: rule.author ? rule.author : 'anonymous', + },{ + xtype: 'displayfield', + fieldLabel: 'Browser(s)', + value: rule.browser ? rule.browser : 'All', + },{ + xtype: 'displayfield', + fieldLabel: 'Browser version(s)', + value: rule.browser_version ? rule.browser_version : 'All', + },{ + xtype: 'displayfield', + fieldLabel: 'OS(s)', + value: rule.os ? rule.os : 'All', + },{ + xtype: 'displayfield', + fieldLabel: 'OS version(s)', + value: rule.os_version ? rule.os_version : 'All', + }, + moduleContainer, + { + xtype: 'button', + text: 'Add Module', + handler: addModule + }, + { + xtype: 'combo', + id: chainModeComboId, + fieldLabel: 'Chain Mode', + store: ['sequential', 'nested-forward'], + queryMode: 'local', // Use local data. + triggerAction: 'all', // Show both options instead of just the default. + editable: false, // Disable manual text input. + forceSelection: true, + value: rule.chain_mode ? rule.chain_mode : 'sequential' + },{ + xtype: 'displayfield', + fieldLabel: 'Execution Order', + value: newRule.execution_order ? + JSON.stringify(newRule.execution_order) + : 'undefined', + },{ + xtype: 'displayfield', + fieldLabel: 'Execution Delay', + value: newRule.execution_delay ? + JSON.stringify(newRule.execution_delay) + : 'undefined', + } + ], + buttons: [{ + text: 'Delete', + handler: deleteFn + }, { + text: 'Save', + handler: handleUpdateRule + }], + border: false, + closable: false + }); +}; + +Ext.extend(AutoRunRuleForm, Ext.FormPanel, {}); \ No newline at end of file diff --git a/extensions/admin_ui/media/javascript/ui/panel/AutoRunTab.js b/extensions/admin_ui/media/javascript/ui/panel/AutoRunTab.js new file mode 100644 index 000000000..9a4948374 --- /dev/null +++ b/extensions/admin_ui/media/javascript/ui/panel/AutoRunTab.js @@ -0,0 +1,185 @@ +const defaultRule = { + "name": "Display an alert", + "author": "mgeeky", + "modules": [ + { + "name": "alert_dialog", + "condition": null, + "options": { + "text":"You've been BeEFed ;>" + } + } + ], + "execution_order": [0], + "execution_delay": [0], + "chain_mode": "nested-forward" +}; + + + +/** + * Asynchronously returns the currently active rules in an array. + * Empty array means no rules are active. + * null if there was an error. + */ +getCurrentRules = async function(token) { + + try { + var res = await fetch(`/api/autorun/rules?token=${token}`); + if (!res.ok) { + throw new Error(`Getting auto run rules failed with status ${res.status}`); + } + const data = await res.json(); + const rules = JSON.parse(data.rules); + + if (data.success === true && Array.isArray(rules)) { + return rules; + } + + console.log("No active auto run rules."); + return []; + + } catch(error) { + console.error(error); + console.error("Failed to get auto run rules."); + return null; + } +} + +getModules = async function(token) { + try { + var res = await fetch(`/api/modules?token=${token}`); + if (!res.ok) { + throw new Error(`Getting auto run rules failed with status ${res.status}`); + } + const modules = await res.json(); + + return modules; + + } catch(error) { + console.error(error); + console.error("Failed to get auto run rules."); + return null; + } +} + +AutoRunTab = function() { + // RESTful API token. + var token = BeefWUI.get_rest_token(); + + // Heading container to describe general Auto Run state. + var ruleLoadingState = new Ext.Container({ + html: "

Loading Auto Run rules...

", + }) + var headingContainer = new Ext.Panel({ + style: { + font: '11px tahoma,arial,helvetica,sans-serif' + }, + padding:'10 10 10 10', + border: false, + items: [{ + xtype: 'container', + html: '\ +
\ +

Auto Run Rules

\ +

These determine what commands run automatically when a browser is hooked.

\ +
' + }, + ruleLoadingState, + { + xtype: 'button', + text: 'Add New Rule', + handler: addRule + }], + listeners: { + afterrender: loadRules + } + }); + // Contains all of the rules and inputs to change each rule. + var ruleContainer = new Ext.Panel({ + border: false, + listeners: { + afterrender: loadRules + } + }); + + async function deleteRule(id) { + const res = await fetch(`/api/autorun/rule/${id}?token=${token}`, {method: 'DELETE'}); + if (!res.ok) { + console.error(`Failed when deleting rule with id ${id}. Failed with status ${res.status}.`); + return; + } + // Update the entire rules panel. Not very efficient. + loadRules(); + } + + async function addRule() { + const res = await fetch(`/api/autorun/rule/add?token=${token}`, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(defaultRule) + }); + if (!res.ok) { + console.error(`Failed when adding a new rule with status ${res.status}.`); + return; + } + // Update the entire rules panel. Not very efficient. + loadRules(); + } + + async function updateRule(id, newRuleData) { + const res = await fetch(`/api/autorun/rule/${id}?token=${token}`, { + method: 'PATCH', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(newRuleData) + }); + if (!res.ok) { + console.error(`Failed when adding a new rule with status ${res.status}.`); + return; + } + // Update the entire rules panel. Not very efficient. + loadRules(); + } + + async function loadRules() { + let modules = []; + let rules = []; + try { + modules = await getModules(token); + rules = await getCurrentRules(token); + } catch (error) { + console.error(error); + console.error("Failed to load command modules and/or rules for Auto Run."); + ruleLoadingState.update("

Failed to load Auto Run rules.

"); + return; + } + + if (rules !== null) { + ruleLoadingState.update(`

Loaded ${rules.length} Auto Run rules.

`); + ruleContainer.removeAll(); + + for (let i = 0; i < rules.length; i++) { + ruleForm = new AutoRunRuleForm( + rules[i], + modules, + function() {deleteRule(rules[i].id)}, + function(newRuleData) {updateRule(rules[i].id, newRuleData)} + ); + ruleContainer.add(ruleForm); + } + ruleContainer.doLayout(); + } else { + ruleLoadingState.update("

Failed to load Auto Run rules.

"); + } + } + + AutoRunTab.superclass.constructor.call(this, { + region: 'center', + items: [headingContainer, ruleContainer], + autoScroll: true, + border: false, + closable: false + }); +}; + +Ext.extend(AutoRunTab, Ext.Panel, {}); \ No newline at end of file diff --git a/extensions/admin_ui/media/javascript/ui/panel/MainPanel.js b/extensions/admin_ui/media/javascript/ui/panel/MainPanel.js index bd84de36c..43c8dcf42 100644 --- a/extensions/admin_ui/media/javascript/ui/panel/MainPanel.js +++ b/extensions/admin_ui/media/javascript/ui/panel/MainPanel.js @@ -38,6 +38,8 @@ MainPanel = function(){ this.welcome_tab = new WelcomeTab; + this.auto_run_tab = new AutoRunTab; + MainPanel.superclass.constructor.call(this, { id:'main-tabs', activeTab:0, @@ -76,6 +78,15 @@ MainPanel = function(){ items:[ this.zombies_grid ] + }, + { + id:'autorun-view', + title:'Auto Run', + layout:'border', + hideMode:'offsets', + items:[ + this.auto_run_tab + ] }] }); diff --git a/extensions/admin_ui/media/javascript/ui/panel/common.js b/extensions/admin_ui/media/javascript/ui/panel/common.js index 750bda90e..48c1f7047 100644 --- a/extensions/admin_ui/media/javascript/ui/panel/common.js +++ b/extensions/admin_ui/media/javascript/ui/panel/common.js @@ -102,9 +102,9 @@ function generate_form_input_field(form, input, value, disabled, zombie) { if(value) input_field.setValue(value); if(disabled) input_field.setDisabled(true); - + form.add(input_field); - + return input_field; }; function get_dynamic_payload_details(payload, zombie) { @@ -357,13 +357,13 @@ function genNewExploitPanel(panel, command_module_id, command_module_name, zombi fieldLabel: 'Description', fieldClass: 'command-module-panel-description', value: module.Description - }), - new Ext.form.DisplayField({ - name: 'command_module_id_visible', - fieldLabel: 'Id', - fieldClass: 'command-module-panel-description', - value: command_module_id - }) + }), + new Ext.form.DisplayField({ + name: 'command_module_id_visible', + fieldLabel: 'Id', + fieldClass: 'command-module-panel-description', + value: command_module_id + }) ], buttons:[{ @@ -424,4 +424,4 @@ function genNewExploitPanel(panel, command_module_id, command_module_name, zombi } }); } -}; +}; \ No newline at end of file