lib/metasploit/framework/data_service/remote/http/response_data_helper.rb
require 'digest'
require 'metasploit/framework/data_service/remote/http/error'
#
# HTTP response helper class
#
module ResponseDataHelper
#
# Converts an HTTP response to a Hash
#
# @param response_wrapper [ResponseWrapper] A wrapped HTTP response containing a JSON body.
# @return [Hash] A Hash interpretation of the JSON body.
# @raise [RuntimeError] response_wrapper is a Metasploit::Framework::DataService::RemoteHTTPDataService::ErrorResponse
# @raise [RuntimeError] response_wrapper is a Metasploit::Framework::DataService::RemoteHTTPDataService::FailedResponse
# @raise [RuntimeError] response_wrapper contains an empty response
def json_to_hash(response_wrapper)
body = response_wrapper.response_body
if !body.nil? && !body.empty?
parsed_body = JSON.parse(body, symbolize_names: true)
if response_wrapper.is_a?(Metasploit::Framework::DataService::RemoteHTTPDataService::SuccessResponse)
parsed_body[:data]
elsif response_wrapper.is_a?(Metasploit::Framework::DataService::RemoteHTTPDataService::ErrorResponse)
# raise a local exception with the message of the server-side error
raise parsed_body[:error][:message]
end
elsif response_wrapper.is_a?(Metasploit::Framework::DataService::RemoteHTTPDataService::FailedResponse)
raise response_wrapper.to_s
else
raise 'empty response'
end
end
#
# Converts an HTTP response to an Mdm Object
#
# @param response_wrapper [ResponseWrapper] A wrapped HTTP response containing a JSON body.
# @param mdm_class [String] The Mdm class name the JSON will be converted to.
# @return [ApplicationRecord] An object of type mdm_class, which inherits from ApplicationRecord
# @raise [RuntimeError] response_wrapper is a Metasploit::Framework::DataService::RemoteHTTPDataService::ErrorResponse
# @raise [RuntimeError] response_wrapper is a Metasploit::Framework::DataService::RemoteHTTPDataService::FailedResponse
# @raise [RuntimeError] response_wrapper contains an empty response
def json_to_mdm_object(response_wrapper, mdm_class)
body = response_wrapper.response_body
if !body.nil? && !body.empty?
parsed_body = JSON.parse(body, symbolize_names: true)
if response_wrapper.is_a?(Metasploit::Framework::DataService::RemoteHTTPDataService::SuccessResponse)
begin
data = Array.wrap(parsed_body[:data])
rv = []
data.each do |json_object|
rv << to_ar(mdm_class.constantize, json_object)
end
return rv
rescue => e
raise "Mdm Object conversion failed #{e.message}"
end
elsif response_wrapper.is_a?(Metasploit::Framework::DataService::RemoteHTTPDataService::ErrorResponse)
handle_error_response(parsed_body)
end
elsif response_wrapper.is_a?(Metasploit::Framework::DataService::RemoteHTTPDataService::FailedResponse)
raise response_wrapper.to_s
else
raise 'empty response'
end
end
# Processes a Base64 encoded file included in a JSON request.
# Saves the file in the location specified in the parameter.
#
# @param base64_file [String] The Base64 encoded file.
# @param save_path [String] The location to store the file. This should include the file's name.
# @return [String] The location where the file was successfully stored.
def process_file(base64_file, save_path)
decoded_file = Base64.urlsafe_decode64(base64_file)
begin
# If we are running the data service on the same box this will ensure we only write
# the file if it is somehow not there already.
unless File.exist?(save_path) && File.read(save_path, mode: 'rb') == decoded_file
File.write(save_path, decoded_file, mode: 'wb')
end
rescue => e
elog "There was an error writing the file: #{e}"
e.backtrace.each { |line| elog "#{line}\n"}
end
save_path
end
# Converts a Hash or JSON string to an ActiveRecord object.
# Importantly, this retains associated objects if they are in the JSON string.
#
# Modified from https://github.com/swdyh/toar/
# Credit to https://github.com/swdyh
#
# @param [String] klass The ActiveRecord class to convert the JSON/Hash to.
# @param [String] val The JSON string, or Hash, to convert.
# @param [Class] base_class The base class to build back to. Used for recursion.
# @return [ApplicationRecord] A klass object, which inherits from ApplicationRecord.
def to_ar(klass, val, base_object = nil)
return nil unless val
data = val.class == Hash ? val.dup : JSON.parse(val, symbolize_names: true)
obj = base_object || klass.new
obj_associations = klass.reflect_on_all_associations(:has_many).reduce({}) do |reflection, i|
reflection[i.options[:through]] = i if i.options[:through]
reflection
end
obj_attribute_names = obj.attributes.transform_keys(&:to_sym).keys
data.except(*obj_attribute_names).each do |k, v|
association = klass.reflect_on_association(k)
next unless association
case association.macro
when :belongs_to
data.delete(:"#{k}_id")
# Polymorphic associations do not auto-create the 'build_model' method
next if association.options[:polymorphic]
to_ar(association.klass, v, obj.send("build_#{k}"))
obj.class_eval do
define_method("#{k}_id") { obj.send(k).id }
end
when :has_one
to_ar(association.klass, v, obj.send("build_#{k}"))
when :has_many
obj.send(k).proxy_association.target =
v.map { |i| to_ar(association.klass, i) }
as_th = obj_associations[k.to_sym]
if as_th
obj.send(as_th.name).proxy_association.target =
v.map { |i| to_ar(as_th.klass, i[as_th.source_reflection_name.to_s]) }
end
end
end
obj.assign_attributes(data.slice(*obj_attribute_names))
obj.instance_eval do
# prevent save
def valid?(_context = nil)
false
end
end
obj
end
private
def handle_error_response(parsed_body)
error = parsed_body[:error]
error_code = error[:code]
case error_code
when 404
raise Metasploit::Framework::DataService::Remote::NotFound.new(error: error)
when 400..499
raise Metasploit::Framework::DataService::Remote::ClientError.new(error: error, status_code: error_code)
when 500..599
raise Metasploit::Framework::DataService::Remote::ServerError.new(error: error, status_code: error_code)
else
raise Metasploit::Framework::DataService::Remote::HttpError.new(error: error, status_code: error_code)
end
end
end