Path: blob/master/extensions/admin_ui/controllers/modules/modules.rb
1154 views
#1# Copyright (c) 2006-2025 Wade Alcorn - [email protected]2# Browser Exploitation Framework (BeEF) - https://beefproject.com3# See the file 'doc/COPYING' for copying permission4#5module BeEF6module Extension7module AdminUI8module Controllers9class Modules < BeEF::Extension::AdminUI::HttpController10BD = BeEF::Core::Models::BrowserDetails1112def initialize13super({14'paths' => {15'/getRestfulApiToken.json' => method(:get_restful_api_token),16'/select/commandmodules/all.json' => method(:select_all_command_modules),17'/select/commandmodules/tree.json' => method(:select_command_modules_tree),18'/select/commandmodule.json' => method(:select_command_module),19'/select/command.json' => method(:select_command),20'/select/command_results.json' => method(:select_command_results),21'/commandmodule/commands.json' => method(:select_command_module_commands),22'/commandmodule/new' => method(:attach_command_module),23'/commandmodule/dynamicnew' => method(:attach_dynamic_command_module),24'/commandmodule/reexecute' => method(:reexecute_command_module)25}26})2728@session = BeEF::Extension::AdminUI::Session.instance29end3031# @note Returns the RESTful api key. Authenticated call, so callable only32# from the admin UI after successful authentication (cookie).33# -> http://127.0.0.1:3000/ui/modules/getRestfulApiToken.json34# response35# <- {"token":"800679edbb59976935d7673924caaa9e99f55c32"}36def get_restful_api_token37@body = {38'token' => BeEF::Core::Configuration.instance.get('beef.api_token')39}.to_json40end4142# Returns the list of all command_modules in a JSON format43def select_all_command_modules44@body = command_modules2json(BeEF::Modules.get_enabled.keys)45end4647# Set the correct icon for the command module48def set_command_module_icon(status)49path = BeEF::Extension::AdminUI::Constants::Icons::MODULE_TARGET_IMG_PATH # add icon path50path += case status51when BeEF::Core::Constants::CommandModule::VERIFIED_NOT_WORKING52BeEF::Extension::AdminUI::Constants::Icons::VERIFIED_NOT_WORKING_IMG53when BeEF::Core::Constants::CommandModule::VERIFIED_USER_NOTIFY54BeEF::Extension::AdminUI::Constants::Icons::VERIFIED_USER_NOTIFY_IMG55when BeEF::Core::Constants::CommandModule::VERIFIED_WORKING56BeEF::Extension::AdminUI::Constants::Icons::VERIFIED_WORKING_IMG57when BeEF::Core::Constants::CommandModule::VERIFIED_UNKNOWN58BeEF::Extension::AdminUI::Constants::Icons::VERIFIED_UNKNOWN_IMG59else60BeEF::Extension::AdminUI::Constants::Icons::VERIFIED_UNKNOWN_IMG61end62# return path63path64end6566# Set the correct working status for the command module67def set_command_module_status(mod)68hook_session_id = @params['zombie_session'] || nil69return BeEF::Core::Constants::CommandModule::VERIFIED_UNKNOWN if hook_session_id.nil?7071BeEF::Module.support(mod, {72'browser' => BD.get(hook_session_id, 'browser.name'),73'ver' => BD.get(hook_session_id, 'browser.version'),74'os' => [BD.get(hook_session_id, 'host.os.name')]75})76end7778# If we're adding a leaf to the command tree, and it's in a subfolder, we need to recurse79# into the tree to find where it goes80# @param tree [Array] The tree to recurse into81# @param category [Array] The category to add the leaf to82# @param leaf [Hash] The leaf to add to the tree83def update_command_module_tree_recurse(tree, category, leaf)8485# get a single folder from the category array86working_category = category.shift8788tree.each do |t|89if t['text'].eql?(working_category) && category.count > 090# We have deeper to go91update_command_module_tree_recurse(t['children'], category, leaf)92elsif t['text'].eql? working_category93# Bingo94t['children'].push(leaf)95break96else97# Not here, keep looking98end99end100101# return tree102end103104# Add the command to the tree105def update_command_module_tree(tree, cmd_category, cmd_icon_path, cmd_status, cmd_name, cmd_id)106# construct leaf node for the command module tree107leaf_node = {108'text' => cmd_name,109'leaf' => true,110'icon' => cmd_icon_path,111'status' => cmd_status,112'id' => cmd_id113}114115# add the node to the branch in the command module tree116# if the category is an array it means it's likeyl a sub-folderised category117# so we need to recurse into the tree to find where it goes118if cmd_category.is_a?(Array)119# The category is an array, therefore it's a sub-folderised category120cat_copy = cmd_category.dup # Don't work with the original array, because, then it breaks shit121update_command_module_tree_recurse(tree, cat_copy, leaf_node)122else123# simply add the command to the tree as it hangs of one of the root folders124tree.each do |x|125if x['text'].eql? cmd_category126x['children'].push(leaf_node)127break128end129end130end131end132133# Recursive function to build the tree now with sub-folders134# this only build the folders and not the leaf command modules135def build_recursive_tree(parent, input)136cinput = input.shift.chomp('/')137if cinput.split('/').count == 1 # then we have a single folder now138if parent.detect { |p| p['text'] == cinput }.nil?139parent << { 'text' => cinput, 'cls' => 'folder', 'children' => [] }140elsif input.count > 0141parent.each do |p|142p['children'] = build_recursive_tree(p['children'], input) if p['text'] == cinput143end144end145else146# we have multiple folders147newinput = cinput.split('/')148newcinput = newinput.shift149parent << { 'text' => newcinput, 'cls' => 'folder', 'children' => [] } if parent.detect { |p| p['text'] == newcinput }.nil?150parent.each do |p|151p['children'] = build_recursive_tree(p['children'], newinput) if p['text'] == newcinput152end153end154155if input.count > 0156build_recursive_tree(parent, input)157else158parent159end160end161162# Recursive function to sort all the parent's children163def sort_recursive_tree(parent)164# sort the children nodes by status and name165parent.each do |x|166# print_info "Sorting: " + x['children'].to_s167next unless x.is_a?(Hash) && x.has_key?('children')168169x['children'] = x['children'].sort_by do |a|170fldr = a['cls'] || 'zzzzz'171"#{fldr}#{a['status']}#{a['text']}"172end173x['children'].each do |c|174sort_recursive_tree([c]) if c.has_key?('cls') && c['cls'] == 'folder'175end176end177end178179# Recursive function to retitle folders with the number of children180def retitle_recursive_tree(parent)181# append the number of command modules so the branch name results in: "<category name> (num)"182parent.each do |command_module_branch|183next unless command_module_branch.is_a?(Hash) && command_module_branch.has_key?('children')184185num_of_subs = 0186command_module_branch['children'].each do |c|187# add in the submodules and subtract 1 for the folder node188num_of_subs += c['children'].length - 1 if c.has_key?('children')189retitle_recursive_tree([c]) if c.has_key?('cls') && c['cls'] == 'folder'190end191num_of_command_modules = command_module_branch['children'].length + num_of_subs192command_module_branch['text'] = command_module_branch['text'] + ' (' + num_of_command_modules.to_s + ')'193end194end195196# Returns the list of all command_modules for a TreePanel in the interface.197def select_command_modules_tree198blanktree = []199tree = []200201# Due to the sub-folder nesting, we use some really badly hacked together recursion202# Note to the bored - if someone (anyone please) wants to refactor, I'll buy you cookies. -x203tree = build_recursive_tree(blanktree, BeEF::Modules.get_categories)204205BeEF::Modules.get_enabled.each do |k, mod|206# get the hooked browser session id and set it in the command module207hook_session_id = @params['zombie_session'] || nil208if hook_session_id.nil?209print_error 'hook_session_id is nil'210return211end212213# create url path and file for the command module icon214command_module_status = set_command_module_status(k)215command_module_icon_path = set_command_module_icon(command_module_status)216217update_command_module_tree(tree, mod['category'], command_module_icon_path, command_module_status, mod['name'], mod['db']['id'])218end219220# if dynamic modules are found in the DB, then we don't have yaml config for them221# and loading must proceed in a different way.222dynamic_modules = BeEF::Core::Models::CommandModule.where('path LIKE ?', 'Dynamic/')223224unless dynamic_modules.nil?225all_modules = BeEF::Core::Models::CommandModule.all.order(:id)226all_modules.each do |dyn_mod|227next unless dyn_mod.path.split('/')[1].match(/^metasploit/)228229command_mod_name = dyn_mod['name']230dyn_mod_category = 'Metasploit'231command_module_status = set_command_module_status(command_mod_name)232command_module_icon_path = set_command_module_icon(command_module_status)233234update_command_module_tree(tree, dyn_mod_category, command_module_icon_path, command_module_status, command_mod_name, dyn_mod.id)235end236end237238# sort the parent array nodes239tree.sort! { |a, b| a['text'] <=> b['text'] }240241sort_recursive_tree(tree)242243retitle_recursive_tree(tree)244245# return a JSON array of hashes246@body = tree.to_json247end248249# Returns the inputs definition of an command_module.250def select_command_module251command_module_id = @params['command_module_id'] || nil252if command_module_id.nil?253print_error 'command_module_id is nil'254return255end256command_module = BeEF::Core::Models::CommandModule.find(command_module_id)257key = BeEF::Module.get_key_by_database_id(command_module_id)258259payload_name = @params['payload_name'] || nil260@body = if payload_name.nil?261command_modules2json([key])262else263dynamic_payload2json(command_module_id, payload_name)264end265end266267# Returns the list of commands for an command_module268def select_command_module_commands269commands = []270i = 0271272# get params273zombie_session = @params['zombie_session'] || nil274if zombie_session.nil?275print_error 'Zombie session is nil'276return277end278command_module_id = @params['command_module_id'] || nil279if command_module_id.nil?280print_error 'command_module id is nil'281return282end283# validate nonce284nonce = @params['nonce'] || nil285if nonce.nil?286print_error 'nonce is nil'287return288end289if @session.get_nonce != nonce290print_error 'nonce incorrect'291return292end293294# get the browser id295zombie = Z.where(session: zombie_session).first296if zombie.nil?297print_error 'Zombie is nil'298return299end300301zombie_id = zombie.id302if zombie_id.nil?303print_error 'Zombie id is nil'304return305end306307C.where(command_module_id: command_module_id, hooked_browser_id: zombie_id).each do |command|308commands.push({309'id' => i,310'object_id' => command.id,311'creationdate' => Time.at(command.creationdate.to_i).strftime('%Y-%m-%d %H:%M').to_s,312'label' => command.label313})314i += 1315end316317@body = {318'success' => 'true',319'commands' => commands320}.to_json321end322323# Attaches an command_module to a zombie.324def attach_command_module325definition = {}326327# get params328zombie_session = @params['zombie_session'] || nil329if zombie_session.nil?330print_error 'Zombie id is nil'331return332end333334command_module_id = @params['command_module_id'] || nil335if command_module_id.nil?336print_error 'command_module id is nil'337return338end339340# validate nonce341nonce = @params['nonce'] || nil342if nonce.nil?343print_error 'nonce is nil'344return345end346if @session.get_nonce != nonce347print_error 'nonce incorrect'348return349end350351@params.keys.each do |param|352unless BeEF::Filters.has_valid_param_chars?(param)353print_error 'invalid key param string'354return355end356if BeEF::Filters.first_char_is_num?(param)357print_error 'first char is num'358return359end360definition[param[4..-1]] = params[param]361oc = BeEF::Core::Models::OptionCache.first_or_create(name: param[4..-1])362oc.value = params[param]363oc.save364end365366mod_key = BeEF::Module.get_key_by_database_id(command_module_id)367# Hack to rework the old option system into the new option system368def2 = []369definition.each do |k, v|370def2.push({ 'name' => k, 'value' => v })371end372# End hack373exec_results = BeEF::Module.execute(mod_key, zombie_session, def2)374@body = exec_results.nil? ? '{success: false}' : '{success: true}'375end376377# Re-execute an command_module to a zombie.378def reexecute_command_module379# get params380command_id = @params['command_id'] || nil381if command_id.nil?382print_error 'Command id is nil'383return384end385386command = BeEF::Core::Models::Command.find(command_id.to_i) || nil387if command.nil?388print_error 'Command is nil'389return390end391# validate nonce392nonce = @params['nonce'] || nil393if nonce.nil?394print_error 'nonce is nil'395return396end397if @session.get_nonce != nonce398print_error 'nonce incorrect'399return400end401402command.instructions_sent = false403command.save404405@body = '{success : true}'406end407408def attach_dynamic_command_module409definition = {}410411# get params412zombie_session = @params['zombie_session'] || nil413if zombie_session.nil?414print_error 'Zombie id is nil'415return416end417418command_module_id = @params['command_module_id'] || nil419if command_module_id.nil?420print_error 'command_module id is nil'421return422end423424# validate nonce425nonce = @params['nonce'] || nil426if nonce.nil?427print_error 'nonce is nil'428return429end430431if @session.get_nonce != nonce432print_error 'nonce incorrect'433return434end435436@params.keys.each do |param|437unless BeEF::Filters.has_valid_param_chars?(param)438print_error 'invalid key param string'439return440end441442if BeEF::Filters.first_char_is_num?(param)443print_error "first char is num: #{param}"444return445end446447definition[param[4..-1]] = params[param]448oc = BeEF::Core::Models::OptionCache.first_or_create(name: param[4..-1])449oc.value = params[param]450oc.save451end452453zombie = Z.where(session: zombie_session).first454if zombie.nil?455print_error 'Zombie is nil'456return457end458459zombie_id = zombie.id460if zombie_id.nil?461print_error 'Zombie id is nil'462return463end464465command_module = BeEF::Core::Models::CommandModule.find(command_module_id)466467return { 'success' => 'false' }.to_json if command_module.nil?468469unless command_module.path.match(/^Dynamic/)470print_info "Command module path is not dynamic: #{command_module.path}"471return { 'success' => 'false' }.to_json472end473474dyn_mod_name = command_module.path.split('/').last475e = BeEF::Modules::Commands.const_get(dyn_mod_name.capitalize).new476e.update_info(command_module_id)477e.update_data478ret = e.launch_exploit(definition)479480if ret['result'] != 'success'481print_info 'mount failed'482return { 'success' => 'false' }.to_json483end484485basedef = {}486basedef['sploit_url'] = ret['uri']487488C.new(489data: basedef.to_json,490hooked_browser_id: zombie_id,491command_module_id: command_module_id,492creationdate: Time.new.to_i493).save494495@body = { 'success' => true }.to_json496end497498# Returns the results of a command499def select_command_results500results = []501502# get params503command_id = @params['command_id'] || nil504if command_id.nil?505print_error 'Command id is nil'506return507end508509command = BeEF::Core::Models::Command.find(command_id.to_i) || nil510if command.nil?511print_error 'Command is nil'512return513end514515# get command_module516command_module = BeEF::Core::Models::CommandModule.find(command.command_module_id)517if command_module.nil?518print_error 'command_module is nil'519return520end521522resultsdb = BeEF::Core::Models::Result.where(command_id: command_id)523if resultsdb.nil?524print_error 'Command id result is nil'525return526end527528resultsdb.each { |result| results.push({ 'date' => result.date, 'data' => JSON.parse(result.data) }) }529530@body = {531'success' => 'true',532'command_module_name' => command_module.name,533'command_module_id' => command_module.id,534'results' => results535}.to_json536end537538# Returns the definition of a command.539# In other words it returns the command that was used to command_module a zombie.540def select_command541# get params542command_id = @params['command_id'] || nil543if command_id.nil?544print_error 'Command id is nil'545return546end547548command = BeEF::Core::Models::Command.find(command_id.to_i) || nil549if command.nil?550print_error 'Command is nil'551return552end553554command_module = BeEF::Core::Models::CommandModule.find(command.command_module_id)555if command_module.nil?556print_error 'command_module is nil'557return558end559560if command_module.path.split('/').first.match(/^Dynamic/)561dyn_mod_name = command_module.path.split('/').last562e = BeEF::Modules::Commands.const_get(dyn_mod_name.capitalize).new563else564command_module_name = command_module.name565e = BeEF::Core::Command.const_get(command_module_name.capitalize).new(command_module_name)566end567568@body = {569'success' => 'true',570'command_module_name' => command_module_name,571'command_module_id' => command_module.id,572'data' => BeEF::Module.get_options(command_module_name),573'definition' => JSON.parse(e.to_json)574}.to_json575end576577private578579# Takes a list of command_modules and returns them as a JSON array580def command_modules2json(command_modules)581command_modules_json = {}582i = 1583config = BeEF::Core::Configuration.instance584command_modules.each do |command_module|585h = {586'Name' => config.get("beef.module.#{command_module}.name"),587'Description' => config.get("beef.module.#{command_module}.description"),588'Category' => config.get("beef.module.#{command_module}.category"),589'Data' => BeEF::Module.get_options(command_module)590}591command_modules_json[i] = h592i += 1593end594595return { 'success' => 'false' }.to_json if command_modules_json.empty?596597{ 'success' => 'true', 'command_modules' => command_modules_json }.to_json598end599600# return the input requred for the module in JSON format601def dynamic_modules2json(id)602command_modules_json = {}603604mod = BeEF::Core::Models::CommandModule.find(id)605606# if the module id is not in the database return false607return { 'success' => 'false' }.to_json unless mod608609# the path will equal Dynamic/<type> and this will get just the type610dynamic_type = mod.path.split('/').last611612e = BeEF::Modules::Commands.const_get(dynamic_type.capitalize).new613e.update_info(mod.id)614e.update_data615command_modules_json[1] = JSON.parse(e.to_json)616if command_modules_json.empty?617{ 'success' => 'false' }.to_json618else619{ 'success' => 'true', 'dynamic' => 'true', 'command_modules' => command_modules_json }.to_json620end621end622623def dynamic_payload2json(id, payload_name)624command_module = BeEF::Core::Models::CommandModule.find(id)625if command_module.nil?626print_error 'Module does not exists'627return { 'success' => 'false' }.to_json628end629630payload_options = BeEF::Module.get_payload_options(command_module.name, payload_name)631# get payload options in JSON632# e = BeEF::Modules::Commands.const_get(dynamic_type.capitalize).new633payload_options_json = []634payload_options_json[1] = payload_options635# payload_options_json[1] = e.get_payload_options(payload_name)636{ 'success' => 'true', 'command_modules' => payload_options_json }.to_json637end638end639end640end641end642end643644645