diff --git a/extensions/admin_ui/media/javascript/ui/panel/tabs/ZombieTabRider.js b/extensions/admin_ui/media/javascript/ui/panel/tabs/ZombieTabRider.js index 1c354ca19..430e35ab9 100644 --- a/extensions/admin_ui/media/javascript/ui/panel/tabs/ZombieTabRider.js +++ b/extensions/admin_ui/media/javascript/ui/panel/tabs/ZombieTabRider.js @@ -56,19 +56,17 @@ ZombieTab_Requester = function(zombie) { ********************************************/ var history_panel_store = new Ext.ux.data.PagingJsonStore({ storeId: 'requester-history-store-zombie-'+zombie.session, - url: '<%= @base_path %>/requester/history.json', + proxy: new Ext.data.HttpProxy({ + method: 'GET', + url: '/api/requester/requests/' + zombie.session + '?token=' + beefwui.get_rest_token(), + }), remoteSort: false, autoDestroy: true, autoLoad: false, - root: 'history', + root: 'requests', fields: ['proto', 'domain', 'port', 'method', 'request_date', 'response_date','id', 'has_ran', 'path','response_status_code', 'response_status_text', 'response_port_status'], sortInfo: {field: 'request_date', direction: 'DESC'}, - - baseParams: { - nonce: Ext.get("nonce").dom.value, - zombie_session: zombie.session - } }); var req_pagesize = 30; @@ -183,10 +181,9 @@ ZombieTab_Requester = function(zombie) { title: 'History', items:[history_panel_grid], layout: 'fit', - listeners: { activate: function(history_panel) { - history_panel.items.items[0].store.reload({params:{url:'<%= @base_path %>/requester/history.json'}}); + history_panel.items.items[0].store.reload({params: {nonce: Ext.get("nonce").dom.value}}); } } }); @@ -207,7 +204,7 @@ ZombieTab_Requester = function(zombie) { var form = new Ext.FormPanel({ title: 'Forge Raw HTTP Request', id: 'requester-request-form-zombie'+zombie.session, - url: '<%= @base_path %>/requester/send', + url: '/api/requester/send/' + zombie.session + '?token=' + beefwui.get_rest_token(), hideLabels : true, border: false, padding: '3px 5px 0 5px', @@ -238,13 +235,12 @@ ZombieTab_Requester = function(zombie) { var use_ssl = Ext.getCmp('requester-forge-requests-ssl').getValue(); if (use_ssl) var proto = 'https'; else var proto = 'http'; var form = Ext.getCmp('requester-request-form-zombie'+zombie.session).getForm(); - + bar.update_sending('Sending request to ' + zombie.ip + '...'); form.submit({ params: { - nonce: Ext.get("nonce").dom.value,//insert the nonce with the form - zombie_session: zombie.session, + raw_request: Ext.getCmp('raw-request-zombie-'+zombie.session).getValue(), proto: proto }, success: function() { @@ -277,14 +273,10 @@ ZombieTab_Requester = function(zombie) { function deleteResponse(request, zombie, bar) { Ext.Ajax.request({ - url: '<%= @base_path %>/requester/delete', + url: '/api/requester/response/' + request.id + '?token=' + beefwui.get_rest_token(), + method: 'DELETE', loadMask: true, - params: { - nonce: Ext.get("nonce").dom.value, - http_id: request.id - }, - success: function(response) { var xhr = Ext.decode(response.responseText); if (xhr['success'] == 'true') { @@ -310,14 +302,8 @@ ZombieTab_Requester = function(zombie) { bar.update_sending('Getting response...'); Ext.Ajax.request({ - url: '<%= @base_path %>/requester/response.json', + url: '/api/requester/response/' + request.id + '?token=' + beefwui.get_rest_token(), loadMask: true, - - params: { - nonce: Ext.get("nonce").dom.value, - http_id: request.id - }, - success: function(response) { var xhr = Ext.decode(response.responseText); diff --git a/extensions/proxy/proxy.rb b/extensions/proxy/proxy.rb index 16d370761..4d3839bc4 100644 --- a/extensions/proxy/proxy.rb +++ b/extensions/proxy/proxy.rb @@ -192,13 +192,18 @@ module BeEF def get_tunneling_proxy proxy_browser = HB.first(:is_proxy => true) - if (proxy_browser != nil) - proxy_browser_id = proxy_browser.id.to_s - else - proxy_browser_id = 1 - print_debug "[PROXY] Proxy browser not set. Defaulting to browser id #1" + unless proxy_browser.nil? + return proxy_browser.session.to_s end - proxy_browser_id + + hooked_browser = HB.first + unless hooked_browser.nil? + print_debug "[Proxy] Proxy browser not set. Defaulting to first hooked browser [id: #{hooked_browser.session}]" + return hooked_browser.session + end + + print_error '[Proxy] No hooked browsers' + nil end end end diff --git a/extensions/requester/api.rb b/extensions/requester/api.rb index e2be5eae3..dc04ebfc8 100644 --- a/extensions/requester/api.rb +++ b/extensions/requester/api.rb @@ -6,30 +6,23 @@ module BeEF module Extension module Requester - module RegisterHttpHandler - BeEF::API::Registrar.instance.register(BeEF::Extension::Requester::RegisterHttpHandler, BeEF::API::Server, 'mount_handler') - # We register the http handler for the requester. - # This http handler will retrieve the http responses for all requests def self.mount_handler(beef_server) beef_server.mount('/requester', BeEF::Extension::Requester::Handler) + beef_server.mount('/api/requester', BeEF::Extension::Requester::RequesterRest.new) end - end module RegisterPreHookCallback - BeEF::API::Registrar.instance.register(BeEF::Extension::Requester::RegisterPreHookCallback, BeEF::API::Server::Hook, 'pre_hook_send') def self.pre_hook_send(hooked_browser, body, params, request, response) dhook = BeEF::Extension::Requester::API::Hook.new dhook.requester_run(hooked_browser, body) end - end - end end end diff --git a/extensions/requester/api/hook.rb b/extensions/requester/api/hook.rb index dfb58dd40..2e6d45fbd 100644 --- a/extensions/requester/api/hook.rb +++ b/extensions/requester/api/hook.rb @@ -20,7 +20,7 @@ module BeEF @body = body # Generate all the requests and output them to the hooked browser output = [] - BeEF::Core::Models::Http.all(:hooked_browser_id => hb.id, :has_ran => "waiting").each { |h| + BeEF::Core::Models::Http.all(:hooked_browser_id => hb.session, :has_ran => "waiting").each { |h| output << self.requester_parse_db_request(h) } diff --git a/extensions/requester/controllers/requester.rb b/extensions/requester/controllers/requester.rb deleted file mode 100644 index b6e2ec1b8..000000000 --- a/extensions/requester/controllers/requester.rb +++ /dev/null @@ -1,210 +0,0 @@ -# -# Copyright (c) 2006-2019 Wade Alcorn - wade@bindshell.net -# Browser Exploitation Framework (BeEF) - http://beefproject.com -# See the file 'doc/COPYING' for copying permission -# -module BeEF -module Extension -module AdminUI -module Controllers - -# -# HTTP Controller for the Requester component of BeEF. -# -class Requester < BeEF::Extension::AdminUI::HttpController - - # Variable representing the Http DB model. - H = BeEF::Core::Models::Http - - def initialize - super({ - 'paths' => { - '/send' => method(:send_request), - '/delete' => method(:delete_zombie_response), - '/history.json' => method(:get_zombie_history), - '/response.json' => method(:get_zombie_response) - } - }) - end - - def err_msg(error) - print_error "[REQUESTER] #{error}" - end - - # Send a new http request to the hooked browser. - def send_request - # validate that the hooked browser's session has been sent - zombie_session = @params['zombie_session'] || nil - (self.err_msg "Invalid session id";return @body = '{success : false}') if not BeEF::Filters.is_valid_hook_session_id?(zombie_session) - - # validate that the hooked browser exists in the db - zombie = Z.first(:session => zombie_session) || nil - (self.err_msg "Invalid hooked browser session";return @body = '{success : false}') if zombie.nil? - - # validate that the raw request has been sent - raw_request = @params['raw_request'] || nil - (self.err_msg "raw_request is nil";return @body = '{success : false}') if raw_request.nil? - (self.err_msg "raw_request contains non-printable chars";return @body = '{success : false}') if not BeEF::Filters.has_non_printable_char?(raw_request) - - # validate nonce - nonce = @params['nonce'] || nil - (self.err_msg "nonce is nil";return @body = '{success : false}') if nonce.nil? - (self.err_msg "nonce incorrect";return @body = '{success : false}') if @session.get_nonce != nonce - - # validate that the raw request is correct and can be used - req_parts = raw_request.split(/ |\n/) # break up the request - - verb = req_parts[0] - self.err_msg 'Only HEAD, GET, POST, OPTIONS, PUT or DELETE requests are supported' if not BeEF::Filters.is_valid_verb?(verb) #check verb - - uri = req_parts[1] - (self.err_msg 'Invalid URI';return @body = '{success : false}') if not BeEF::Filters.is_valid_url?(uri) #check uri - - version = req_parts[2] - (self.err_msg 'Invalid HTTP version';return @body = '{success : false}') if not BeEF::Filters.is_valid_http_version?(version) # check http version - HTTP/1.0 or HTTP/1.1 - - host_str = req_parts[3] - (self.err_msg 'Invalid HTTP Host Header';return @body = '{success : false}') if not BeEF::Filters.is_valid_host_str?(host_str) # check host string - Host: - - host = req_parts[4] - host_parts = host.split(/:/) - hostname = host_parts[0] - (self.err_msg 'Invalid HTTP HostName';return @body = '{success : false}') if not BeEF::Filters.is_valid_hostname?(hostname) #check the target hostname - - hostport = host_parts[1] || nil - if !hostport.nil? - (self.err_msg 'Invalid HTTP HostPort';return @body = '{success : false}') if not BeEF::Filters.nums_only?(hostport) #check the target hostport - end - - proto = @params['proto'] || 'http' - if proto !~ /\Ahttps?\z/ - (self.err_msg 'Invalid request protocol';return @body = '{success : false}') - end - - # Saves the new HTTP request. - http = H.new( - :request => raw_request, - :method => verb, - :proto => proto, - :domain => hostname, - :port => hostport, - :path => uri, - :request_date => Time.now, - :hooked_browser_id => zombie.id, - :allow_cross_domain => "true", - ) - - if verb.eql? 'POST' - req_parts.each_with_index do |value, index| - if value.match(/^Content-Length/) - http.content_length = req_parts[index+1] - end - end - end - - http.save - - @body = '{success : true}' - end - - # Returns a JSON object containing the history of requests sent to the zombie. - def get_zombie_history - # validate nonce - nonce = @params['nonce'] || nil - (self.err_msg "nonce is nil";return @body = '{success : false}') if nonce.nil? - (self.err_msg "nonce incorrect";return @body = '{success : false}') if @session.get_nonce != nonce - - # validate that the hooked browser's session has been sent - zombie_session = @params['zombie_session'] || nil - (self.err_msg "Zombie session is nil";return @body = '{success : false}') if zombie_session.nil? - - # validate that the hooked browser exists in the db - zombie = Z.first(:session => zombie_session) || nil - (self.err_msg "Invalid hooked browser session";return @body = '{success : false}') if zombie.nil? - - history = [] - H.all(:hooked_browser_id => zombie.id).each{|http| - history << { - 'id' => http.id, - 'proto' => http.proto, - 'domain' => http.domain, - 'port' => http.port, - 'path' => http.path, - 'has_ran' => http.has_ran, - 'method' => http.method, - 'request_date' => http.request_date, - 'response_date' => http.response_date, - 'response_status_code' => http.response_status_code, - 'response_status_text' => http.response_status_text, - 'response_port_status' => http.response_port_status - } - } - - @body = {'success' => 'true', 'history' => history}.to_json - end - - # Returns a JSON objecting containing the response of a request. - def get_zombie_response - # validate nonce - nonce = @params['nonce'] || nil - (self.err_msg "nonce is nil";return @body = '{success : false}') if nonce.nil? - (self.err_msg "nonce incorrect";return @body = '{success : false}') if @session.get_nonce != nonce - - # validate the http id - http_id = @params['http_id'] || nil - (self.err_msg "http_id is nil";return @body = '{success : false}') if http_id.nil? - - # validate that the http object exist in the dabatase - http_db = H.first(:id => http_id) || nil - (self.err_msg "http object could not be found in the database";return @body = '{success : false}') if http_db.nil? - - if http_db.response_data.length > (1024 * 100) #more thank 100K - response_data = http_db.response_data[0..(1024*100)] - response_data += "\n<---------- Response Data Truncated---------->" - else - response_data = http_db.response_data - end - - res = { - 'id' => http_db.id, - 'request' => http_db.request.force_encoding('UTF-8'), - 'response' => response_data.force_encoding('UTF-8'), - 'response_headers' => http_db.response_headers.force_encoding('UTF-8'), - 'proto' => http_db.proto.force_encoding('UTF-8'), - 'domain' => http_db.domain.force_encoding('UTF-8'), - 'port' => http_db.port.force_encoding('UTF-8'), - 'path' => http_db.path.force_encoding('UTF-8'), - 'date' => http_db.request_date, - 'has_ran' => http_db.has_ran.force_encoding('UTF-8') - } - - @body = {'success' => 'true', 'result' => res}.to_json - end - - # Deletes a response from the requester history - def delete_zombie_response - # validate nonce - nonce = @params['nonce'] || nil - (self.err_msg "nonce is nil";return @body = '{success : false}') if nonce.nil? - (self.err_msg "nonce incorrect";return @body = '{success : false}') if @session.get_nonce != nonce - - # validate the http id - http_id = @params['http_id'] || nil - (self.err_msg "http_id is nil";return @body = '{success : false}') if http_id.nil? - - # validate that the http object exist in the dabatase - http_db = H.first(:id => http_id) || nil - (self.err_msg "http object could not be found in the database";return @body = '{success : false}') if http_db.nil? - - # delete response - http_db.destroy - - @body = {'success' => 'true'}.to_json - end - -end - -end -end -end -end diff --git a/extensions/requester/extension.rb b/extensions/requester/extension.rb index 08313e3cc..7a52d0983 100644 --- a/extensions/requester/extension.rb +++ b/extensions/requester/extension.rb @@ -15,3 +15,4 @@ require 'extensions/requester/models/http' require 'extensions/requester/api/hook' require 'extensions/requester/handler' require 'extensions/requester/api' +require 'extensions/requester/rest/requester' diff --git a/extensions/requester/handler.rb b/extensions/requester/handler.rb index a9c51119d..88a822273 100644 --- a/extensions/requester/handler.rb +++ b/extensions/requester/handler.rb @@ -17,26 +17,39 @@ module BeEF def initialize(data) @data = data - setup() + setup end - def setup() - + def setup # validates the hook token beef_hook = @data['beefhook'] || nil - (print_error "beefhook is null";return) if beef_hook.nil? + if beef_hook.nil? + print_error "beefhook is null" + return + end # validates the request id - request_id = @data['cid'] || nil - (print_error "Original request id (command id) is null";return) if request_id.nil? + request_id = @data['cid'].to_s + if request_id == '' + print_error "Original request id (command id) is null" + return + end + + if !BeEF::Filters::nums_only?(request_id) + print_error "Original request id (command id) is invalid" + return + end # validates that a hooked browser with the beef_hook token exists in the db zombie_db = Z.first(:session => beef_hook) || nil (print_error "Invalid beefhook id: the hooked browser cannot be found in the database";return) if zombie_db.nil? # validates that we have such a http request saved in the db - http_db = H.first(:id => request_id.to_i, :hooked_browser_id => zombie_db.id) || nil - (print_error "Invalid http_db: no such request found in the database";return) if http_db.nil? + http_db = H.first(:id => request_id.to_i, :hooked_browser_id => zombie_db.session) || nil + if http_db.nil? + print_error "Invalid http_db: no such request found in the database" + return + end # validates that the http request has not been run before (print_error "This http request has been saved before";return) if http_db.has_ran.eql? "complete" @@ -59,6 +72,7 @@ module BeEF if http_db.response_headers =~ /Content-Type: image/ http_db.response_data = http_db.response_data.unpack('a*') end + http_db.save end end diff --git a/extensions/requester/models/http.rb b/extensions/requester/models/http.rb index 73fbf5d3a..edf6cfa32 100644 --- a/extensions/requester/models/http.rb +++ b/extensions/requester/models/http.rb @@ -69,8 +69,16 @@ module Models # The date at which the http request has been saved. property :request_date, DateTime, :lazy => false + # + # Removes a request/response from the data store + # + def self.delete(id) + (print_error "Failed to remove response. Invalid response ID."; return) if id.to_s !~ /\A\d+\z/ + r = BeEF::Core::Models::Http.get(id.to_i) + (print_error "Failed to remove response [id: #{id}]. Response does not exist."; return) if r.nil? + r.destroy + end end - end end end diff --git a/extensions/requester/rest/requester.rb b/extensions/requester/rest/requester.rb index da85969c4..8d3fb5601 100644 --- a/extensions/requester/rest/requester.rb +++ b/extensions/requester/rest/requester.rb @@ -13,34 +13,263 @@ module BeEF # Filters out bad requests before performing any routing before do config = BeEF::Core::Configuration.instance - @hb = BeEF::Core::Models::HookedBrowser # Require a valid API token from a valid IP address halt 401 unless params[:token] == config.get('beef.api_token') halt 403 unless BeEF::Core::Rest.permitted_source?(request.ip) + H = BeEF::Core::Models::Http + HB = BeEF::Core::Models::HookedBrowser + headers 'Content-Type' => 'application/json; charset=UTF-8', 'Pragma' => 'no-cache', 'Cache-Control' => 'no-cache', 'Expires' => '0' end - # @TODO: Move methods from the requester controller here + # Returns a request by ID + get '/request/:id' do + begin + id = params[:id] + raise InvalidParamError, 'id' unless BeEF::Filters::nums_only?(id) + + requests = H.all(:id => id) + halt 404 if requests.nil? + + result = {} + result[:count] = requests.length + result[:requests] = [] + requests.each do |request| + result[:requests] << request2hash(request) + end + + result.to_json + rescue InvalidParamError => e + print_error e.message + halt 400 + rescue StandardError => e + print_error "Internal error while retrieving request with id #{id} (#{e.message})" + halt 500 + end + end + + # Returns all requestes given a specific hooked browser id + get '/requests/:id' do + begin + id = params[:id] + raise InvalidParamError, 'id' unless BeEF::Filters.is_valid_hook_session_id?(id) + + requests = H.all(:hooked_browser_id => id) + halt 404 if requests.nil? + + result = {} + result[:count] = requests.length + result[:requests] = [] + requests.each do |request| + result[:requests] << request2hash(request) + end + + result.to_json + rescue InvalidParamError => e + print_error e.message + halt 400 + rescue StandardError => e + print_error "Internal error while retrieving request list for hooked browser with id #{id} (#{e.message})" + halt 500 + end + end + + # Return a response by ID + get '/response/:id' do + begin + id = params[:id] + raise InvalidParamError, 'id' unless BeEF::Filters::nums_only?(id) + + responses = H.first(:id => id) || nil + halt 404 if responses.nil? + + result = {} + result[:success] = 'true' + result[:result] = response2hash(responses) + + result.to_json + rescue InvalidParamError => e + print_error e.message + halt 400 + rescue StandardError => e + print_error "Internal error while retrieving response with id #{id} (#{e.message})" + halt 500 + end + end + + # Deletes a specific response given its id + delete '/response/:id' do + begin + id = params[:id] + raise InvalidParamError, 'id' unless BeEF::Filters::nums_only?(id) + + responses = H.first(:id => id) || nil + halt 404 if responses.nil? + + result = {} + result['success'] = H.delete(id) + result.to_json + rescue InvalidParamError => e + print_error e.message + halt 400 + rescue StandardError => e + print_error "Internal error while removing response with id #{id} (#{e.message})" + halt 500 + end + end + + # Send a new HTTP request to the hooked browser + post '/send/:id' do + begin + id = params[:id] + proto = params[:proto].to_s || 'http' + raw_request = params['raw_request'].to_s + + zombie = HB.first(:session => id) || nil + halt 404 if zombie.nil? + + + + # @TODO: move most of this to the model + + if raw_request == '' + raise InvalidParamError, 'raw_request' + end + + if proto !~ /\Ahttps?\z/ + raise InvalidParamError, 'raw_request: Invalid request URL scheme' + end + + req_parts = raw_request.split(/ |\n/) + + verb = req_parts[0] + if not BeEF::Filters.is_valid_verb?(verb) + raise InvalidParamError, 'raw_request: Only HEAD, GET, POST, OPTIONS, PUT or DELETE requests are supported' + end + + uri = req_parts[1] + if not BeEF::Filters.is_valid_url?(uri) + raise InvalidParamError, 'raw_request: Invalid URI' + end + + version = req_parts[2] + if not BeEF::Filters.is_valid_http_version?(version) + raise InvalidParamError, 'raw_request: Invalid HTTP version' + end + + host_str = req_parts[3] + if not BeEF::Filters.is_valid_host_str?(host_str) + raise InvalidParamError, 'raw_request: Invalid HTTP version' + end + + # Validate target hsot + host = req_parts[4] + host_parts = host.split(/:/) + host_name = host_parts[0] + host_port = host_parts[1] || nil + + unless BeEF::Filters.is_valid_hostname?(host_name) + raise InvalidParamError, 'raw_request: Invalid HTTP HostName' + end + + host_port = host_parts[1] || nil + if host_port.nil? || !BeEF::Filters::nums_only?(host_port) + host_port = proto.eql?('https') ? 443 : 80 + end + + # Save the new HTTP request + http = H.new( + :hooked_browser_id => zombie.session, + :request => raw_request, + :method => verb, + :proto => proto, + :domain => host_name, + :port => host_port, + :path => uri, + :request_date => Time.now, + :allow_cross_domain => "true", + ) + + if verb.eql?('POST') || verb.eql?('PUT') + req_parts.each_with_index do |value, index| + if value.match(/^Content-Length/i) + http.content_length = req_parts[index+1] + end + end + end + + http.save + + result = request2hash(http) + print_debug "[Requester] Sending HTTP request through zombie [ip: #{zombie.ip}] : #{result}" + + #result.to_json + rescue InvalidParamError => e + print_error e.message + halt 400 + rescue StandardError => e + print_error "Internal error while removing network host with id #{id} (#{e.message})" + halt 500 + end + end + + # Convert a request object to Hash + def request2hash(http) + { + :id => http.id, + :proto => http.proto, + :domain => http.domain, + :port => http.port, + :path => http.path, + :has_ran => http.has_ran, + :method => http.method, + :request_date => http.request_date, + :response_date => http.response_date, + :response_status_code => http.response_status_code, + :response_status_text => http.response_status_text, + :response_port_status => http.response_port_status + } + end + + # Convert a response object to Hash + def response2hash(http) + if http.response_data.length > (1024 * 100) # more thank 100K + response_data = http.response_data[0..(1024*100)] + response_data += "\n<---------- Response Data Truncated---------->" + else + response_data = http.response_data + end + + { + :id => http.id, + :request => http.request.force_encoding('UTF-8'), + :response => response_data.force_encoding('UTF-8'), + :response_headers => http.response_headers.force_encoding('UTF-8'), + :proto => http.proto.force_encoding('UTF-8'), + :domain => http.domain.force_encoding('UTF-8'), + :port => http.port.force_encoding('UTF-8'), + :path => http.path.force_encoding('UTF-8'), + :date => http.request_date, + :has_ran => http.has_ran.force_encoding('UTF-8') + } + end # Raised when invalid JSON input is passed to an /api/requester handler. class InvalidJsonError < StandardError - DEFAULT_MESSAGE = 'Invalid JSON input passed to /api/requester handler' def initialize(message = nil) super(message || DEFAULT_MESSAGE) end - end # Raised when an invalid named parameter is passed to an /api/requester handler. class InvalidParamError < StandardError - DEFAULT_MESSAGE = 'Invalid parameter passed to /api/requester handler' def initialize(message = nil) @@ -48,11 +277,8 @@ module BeEF message = sprintf str, message unless message.nil? super(message) end - end - end - end end end