Land #11270, fix miscellaneous loot issues

This commit is contained in:
Matthew Kienow 2019-01-25 19:15:14 -05:00
commit d078fcd87c
No known key found for this signature in database
GPG Key ID: 40787F8B1EAC6E41
6 changed files with 70 additions and 26 deletions

View File

@ -10,7 +10,8 @@ module LootApiDoc
LTYPE_EXAMPLE = "'file', 'image', 'config_file', etc."
PATH_DESC = 'The on-disk path to the loot file.'
PATH_EXAMPLE = '/path/to/file.txt'
DATA_DESC = 'The contents of the file.'
DATA_DESC = "Base64 encoded copy of the file's contents."
DATA_EXAMPLE = 'dGhpcyBpcyB0aGUgZmlsZSdzIGNvbnRlbnRz'
CONTENT_TYPE_DESC = 'The mime/content type of the file at {#path}. Used to server the file correctly so browsers understand whether to render or download the file.'
CONTENT_TYPE_EXAMPLE = 'text/plain'
NAME_DESC = 'The name of the loot.'
@ -18,6 +19,9 @@ module LootApiDoc
INFO_DESC = 'Information about the loot.'
MODULE_RUN_ID_DESC = 'The ID of the module run record this loot is associated with.'
# Some of the attributes expect different data when doing a create.
CREATE_PATH_DESC = 'The name to give the file on the server. All files are stored in a server configured path, so a full path is not needed. If there is a corresponding file on disk, the given value will be prepended with a unique string to prevent accidental overwrites of other files.'
CREATE_PATH_EXAMPLE = 'password_file.txt'
# Swagger documentation for loot model
swagger_schema :Loot do
@ -28,7 +32,7 @@ module LootApiDoc
property :service_id, type: :integer, format: :int32, description: SERVICE_ID_DESC
property :ltype, type: :string, description: LTYPE_DESC, example: LTYPE_EXAMPLE
property :path, type: :string, description: PATH_DESC, example: PATH_EXAMPLE
property :data, type: :string, description: DATA_DESC
property :data, type: :string, description: DATA_DESC, example: DATA_EXAMPLE
property :content_type, type: :string, description: CONTENT_TYPE_DESC, example: CONTENT_TYPE_EXAMPLE
property :name, type: :string, description: NAME_DESC, example: NAME_EXAMPLE
property :info, type: :string, description: INFO_DESC
@ -87,8 +91,8 @@ module LootApiDoc
property :host, type: :string, format: :ipv4, description: HOST_DESC, example: RootApiDoc::HOST_EXAMPLE
property :service, '$ref': :Service
property :ltype, type: :string, description: LTYPE_DESC, example: LTYPE_EXAMPLE, required: true
property :path, type: :string, description: PATH_DESC, example: PATH_EXAMPLE, required: true
property :data, type: :string, description: DATA_DESC
property :path, type: :string, description: CREATE_PATH_DESC, example: CREATE_PATH_EXAMPLE, required: true
property :data, type: :string, description: DATA_DESC, example: DATA_EXAMPLE
property :ctype, type: :string, description: CONTENT_TYPE_DESC, example: CONTENT_TYPE_EXAMPLE
property :name, type: :string, description: NAME_DESC, example: NAME_EXAMPLE, required: true
property :info, type: :string, description: INFO_DESC
@ -206,7 +210,14 @@ module LootApiDoc
key :description, 'The updated attributes to overwrite to the loot.'
key :required, true
schema do
key :'$ref', :Loot
property :workspace, type: :string, required: true, description: RootApiDoc::WORKSPACE_POST_DESC, example: RootApiDoc::WORKSPACE_POST_EXAMPLE
property :host_id, type: :integer, format: :int32, description: HOST_ID_DESC
property :service_id, type: :integer, format: :int32, description: SERVICE_ID_DESC
property :ltype, type: :string, description: LTYPE_DESC, example: LTYPE_EXAMPLE, required: true
property :path, type: :string, description: CREATE_PATH_DESC, example: CREATE_PATH_EXAMPLE, required: true
property :ctype, type: :string, description: CONTENT_TYPE_DESC, example: CONTENT_TYPE_EXAMPLE
property :name, type: :string, description: NAME_DESC, example: NAME_EXAMPLE, required: true
property :info, type: :string, description: INFO_DESC
end
end

View File

@ -8,16 +8,22 @@ module RemoteLootDataService
def loot(opts = {})
path = get_path_select(opts, LOOT_API_PATH)
# TODO: Add an option to toggle whether the file data is returned or not
loots = json_to_mdm_object(self.get_data(path, nil, opts), LOOT_MDM_CLASS, [])
# Save a local copy of the file
loots.each do |loot|
if loot.data
local_path = File.join(Msf::Config.loot_directory, File.basename(loot.path))
loot.path = process_file(loot.data, local_path)
data = self.get_data(path, nil, opts)
rv = json_to_mdm_object(data, LOOT_MDM_CLASS, [])
parsed_body = JSON.parse(data.response.body, symbolize_names: true)
data = parsed_body[:data]
data.each do |loot|
# TODO: Add an option to toggle whether the file data is returned or not
if loot[:data] && !loot[:data].empty?
local_path = File.join(Msf::Config.loot_directory, File.basename(loot[:path]))
rv[data.index(loot)].path = process_file(loot[:data], local_path)
end
if loot[:host]
host_object = to_ar(RemoteHostDataService::HOST_MDM_CLASS.constantize, loot[:host])
rv[data.index(loot)].host = host_object
end
end
loots
rv
end
def report_loot(opts)

View File

@ -10,18 +10,18 @@ module Msf::DBManager::Loot
# This methods returns a list of all loot in the database
#
def loots(opts)
data = opts.delete(:data)
# Remove path from search conditions as this won't accommodate remote data
# service usage where the client and server storage locations differ.
opts.delete(:path)
search_term = opts.delete(:search_term)
::ActiveRecord::Base.connection_pool.with_connection {
# If we have the ID, there is no point in creating a complex query.
if opts[:id] && !opts[:id].to_s.empty?
return Array.wrap(Mdm::Loot.find(opts[:id]))
end
# Remove path from search conditions as this won't accommodate remote data
# service usage where the client and server storage locations differ.
opts.delete(:path)
search_term = opts.delete(:search_term)
data = opts.delete(:data)
wspace = Msf::Util::DBManager.process_opts_workspace(opts, framework)
opts[:workspace_id] = wspace.id
@ -99,10 +99,19 @@ module Msf::DBManager::Loot
def update_loot(opts)
::ActiveRecord::Base.connection_pool.with_connection {
wspace = Msf::Util::DBManager.process_opts_workspace(opts, framework, false)
# Prevent changing the data field to ensure the file contents remain the same as what was originally looted.
raise ArgumentError, "Updating the data attribute is not permitted." if opts[:data]
opts[:workspace] = wspace if wspace
id = opts.delete(:id)
loot = Mdm::Loot.find(id)
# If the user updates the path attribute (or filename) we need to update the file
# on disk to reflect that.
if opts[:path] && File.exists?(loot.path)
File.rename(loot.path, opts[:path])
end
loot.update!(opts)
return loot
}

View File

@ -30,9 +30,8 @@ module HostServlet
begin
sanitized_params = sanitize_params(params, env['rack.request.query_hash'])
data = get_db.hosts(sanitized_params)
includes = [:loots]
data = data.first if is_single_object?(data, sanitized_params)
set_json_data_response(response: data, includes: includes)
set_json_data_response(response: data)
rescue => e
print_error_and_create_response(error: e, message: 'There was an error retrieving hosts:', code: 500)
end

View File

@ -26,9 +26,7 @@ module LootServlet
sanitized_params = sanitize_params(params, env['rack.request.query_hash'])
data = get_db.loots(sanitized_params)
includes = [:host]
data.each do |loot|
loot.data = Base64.urlsafe_encode64(loot.data) if loot.data
end
data = encode_loot_data(data)
data = data.first if is_single_object?(data, sanitized_params)
set_json_data_response(response: data, includes: includes)
rescue => e
@ -43,12 +41,13 @@ module LootServlet
job = lambda { |opts|
if opts[:data]
filename = File.basename(opts[:path])
local_path = File.join(Msf::Config.loot_directory, filename)
local_path = File.join(Msf::Config.loot_directory, "#{SecureRandom.hex(10)}-#{filename}")
opts[:path] = process_file(opts[:data], local_path)
opts[:data] = Base64.urlsafe_decode64(opts[:data])
end
get_db.report_loot(opts)
data = get_db.report_loot(opts)
encode_loot_data(data)
}
exec_report_job(request, &job)
}
@ -61,7 +60,16 @@ module LootServlet
opts = parse_json_request(request, false)
tmp_params = sanitize_params(params)
opts[:id] = tmp_params[:id] if tmp_params[:id]
db_record = get_db.loots(opts).first
# Give the file a unique name to prevent accidental overwrites. Only do this if there is actually a file
# on disk. If there is not a file on disk we assume that this DB record is for tracking a file outside
# of metasploit, so we don't want to assign them a unique file name and overwrite that.
if opts[:path] && File.exists?(db_record.path)
filename = File.basename(opts[:path])
opts[:path] = File.join(Msf::Config.loot_directory, "#{SecureRandom.hex(10)}-#{filename}")
end
data = get_db.update_loot(opts)
data = encode_loot_data(data)
set_json_data_response(response: data)
rescue => e
print_error_and_create_response(error: e, message: 'There was an error updating the loot:', code: 500)
@ -75,6 +83,10 @@ module LootServlet
begin
opts = parse_json_request(request, false)
data = get_db.delete_loot(opts)
# The rails delete operation returns a frozen object. We need to Base64 encode the data
# before converting to JSON. So we'll work with a duplicate of the original if it is frozen.
data.map! { |loot| loot.dup if loot.frozen? }
data = encode_loot_data(data)
set_json_data_response(response: data)
rescue => e
print_error_and_create_response(error: e, message: 'There was an error deleting the loot:', code: 500)

View File

@ -131,6 +131,13 @@ module ServletHelper
response
end
def encode_loot_data(data)
Array.wrap(data).each do |loot|
loot.data = Base64.urlsafe_encode64(loot.data) if loot.data && !loot.data.empty?
end
data
end
# Get Warden::Proxy object from the Rack environment.
# @return [Warden::Proxy] The Warden::Proxy object from the Rack environment.
def warden