Files
beef/extensions/requester/rest/requester.rb
2024-10-23 16:07:17 +10:00

281 lines
9.3 KiB
Ruby

#
# Copyright (c) 2006-2024 Wade Alcorn - wade@bindshell.net
# Browser Exploitation Framework (BeEF) - https://beefproject.com
# See the file 'doc/COPYING' for copying permission
#
module BeEF
module Extension
module Requester
# This class handles the routing of RESTful API requests for the requester
class RequesterRest < BeEF::Core::Router::Router
# Filters out bad requests before performing any routing
before do
config = BeEF::Core::Configuration.instance
# 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
# Returns a request by ID
get '/request/:id' do
id = params[:id]
raise InvalidParamError, 'id' unless BeEF::Filters.nums_only?(id)
requests = H.find(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
# Returns all requestes given a specific hooked browser id
get '/requests/:id' do
id = params[:id]
raise InvalidParamError, 'id' unless BeEF::Filters.is_valid_hook_session_id?(id)
requests = H.where(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
# Return a response by ID
get '/response/:id' do
# super debugging
error = {}
error[:code] = 0
id = params[:id]
raise InvalidParamError, 'id' unless BeEF::Filters.nums_only?(id)
error[:code] = 1
responses = H.find(id) || nil
error[:code] = 2
halt 404 if responses.nil?
error[:code] = 3
result = {}
result[:success] = 'true'
error[:code] = 4
result[:result] = response2hash(responses)
error[:code] = 5
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})"
error[:id] = id
error[:message] = e.message
error.to_json
# halt 500
end
# Deletes a specific response given its id
delete '/response/:id' do
id = params[:id]
raise InvalidParamError, 'id' unless BeEF::Filters.nums_only?(id)
responses = H.find(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
# Send a new HTTP request to the hooked browser
post '/send/:id' do
id = params[:id]
proto = params[:proto].to_s || 'http'
raw_request = params['raw_request'].to_s
zombie = HB.where(session: id).first || nil
halt 404 if zombie.nil?
# @TODO: move most of this to the model
raise InvalidParamError, 'raw_request' if raw_request == ''
raise InvalidParamError, 'raw_request: Invalid request URL scheme' if proto !~ /\Ahttps?\z/
req_parts = raw_request.split(/ |\n/)
verb = req_parts[0]
raise InvalidParamError, 'raw_request: Only HEAD, GET, POST, OPTIONS, PUT or DELETE requests are supported' unless BeEF::Filters.is_valid_verb?(verb)
uri = req_parts[1]
raise InvalidParamError, 'raw_request: Invalid URI' unless BeEF::Filters.is_valid_url?(uri)
version = req_parts[2]
raise InvalidParamError, 'raw_request: Invalid HTTP version' unless BeEF::Filters.is_valid_http_version?(version)
host_str = req_parts[3]
raise InvalidParamError, 'raw_request: Invalid HTTP version' unless BeEF::Filters.is_valid_host_str?(host_str)
# Validate target hsot
host = req_parts[4]
host_parts = host.split(/:/)
host_name = host_parts[0]
host_port = host_parts[1] || nil
raise InvalidParamError, 'raw_request: Invalid HTTP HostName' unless BeEF::Filters.is_valid_hostname?(host_name)
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_origin: 'true'
)
print_debug "added new http request for #{zombie.session}"
print_debug http.to_json
if verb.eql?('POST') || verb.eql?('PUT')
req_parts.each_with_index do |value, index|
http.content_length = req_parts[index + 1] if value.match(/^Content-Length/i)
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
# 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)
response_data = ''
unless http.response_data.nil?
if (http.response_data.length > (1024 * 100)) # more than 100K
response_data = http.response_data[0..(1024 * 100)]
response_data += "\n<---------- Response Data Truncated---------->"
else
response_data = http.response_data
end
end
response_headers = ''
response_headers = http.response_headers unless http.response_headers.nil?
{
id: http.id,
request: http.request.force_encoding('UTF-8'),
response: response_data.force_encoding('UTF-8'),
response_headers: 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'.freeze
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'.freeze
def initialize(message = nil)
str = 'Invalid "%s" parameter passed to /api/requester handler'
message = format str, message unless message.nil?
super(message)
end
end
end
end
end
end