Merge pull request #1687 from bcoles/requester_rest

Replace /ui/requester controller with REST API - #1389 #1388
This commit is contained in:
bcoles
2019-02-21 09:55:14 +11:00
committed by GitHub
9 changed files with 291 additions and 268 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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