diff --git a/documentation/api/v1/loot_api_doc.rb b/documentation/api/v1/loot_api_doc.rb index 8f62f6cfaf..4bb9e032d7 100644 --- a/documentation/api/v1/loot_api_doc.rb +++ b/documentation/api/v1/loot_api_doc.rb @@ -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 diff --git a/lib/metasploit/framework/data_service/remote/http/remote_loot_data_service.rb b/lib/metasploit/framework/data_service/remote/http/remote_loot_data_service.rb index 313f1b6919..4df2da69ea 100644 --- a/lib/metasploit/framework/data_service/remote/http/remote_loot_data_service.rb +++ b/lib/metasploit/framework/data_service/remote/http/remote_loot_data_service.rb @@ -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) diff --git a/lib/msf/core/db_manager/loot.rb b/lib/msf/core/db_manager/loot.rb index e0cb0b983d..00ff3aabfc 100644 --- a/lib/msf/core/db_manager/loot.rb +++ b/lib/msf/core/db_manager/loot.rb @@ -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 } diff --git a/lib/msf/core/web_services/servlet/host_servlet.rb b/lib/msf/core/web_services/servlet/host_servlet.rb index f090ac1666..6aeb92c16d 100644 --- a/lib/msf/core/web_services/servlet/host_servlet.rb +++ b/lib/msf/core/web_services/servlet/host_servlet.rb @@ -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 diff --git a/lib/msf/core/web_services/servlet/loot_servlet.rb b/lib/msf/core/web_services/servlet/loot_servlet.rb index f71e884f26..e0b20457c8 100644 --- a/lib/msf/core/web_services/servlet/loot_servlet.rb +++ b/lib/msf/core/web_services/servlet/loot_servlet.rb @@ -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) diff --git a/lib/msf/core/web_services/servlet_helper.rb b/lib/msf/core/web_services/servlet_helper.rb index 1a5173e07d..59f5f71c5d 100644 --- a/lib/msf/core/web_services/servlet_helper.rb +++ b/lib/msf/core/web_services/servlet_helper.rb @@ -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