diff --git a/CURRENT.md b/CURRENT.md new file mode 100644 index 0000000000..de2b802c7f --- /dev/null +++ b/CURRENT.md @@ -0,0 +1,20 @@ +Active Metasploit 5 development will sometimes push aggressive changes. +Integrations with 3rd-party tools, as well as general usage, may change quickly +from day to day. Some of the steps for dealing with major changes will be +documented here. We will continue to maintain the Metasploit 4.x branch until +Metasploit 5.0 is released. + +**2018/01/17 - [internal] module cache reworked to not store metadata in PostgreSQL** + +Metasploit no longer stores module metadata in a PostgreSQL database, instead +storing it in a cache file in your local ~/.msf4 config directory. This has a +number of advantages: + + * Fast searches whether you have the database enabled or not (no more slow search mode) + * Faster load time for msfconsole, the cache loads more quickly + * Private module data is not uploaded to a shared database, no collisions + * Adding or deleting modules no longer displays file-not-found error messages on start in msfconsole + * Reduced memory consumption + +Code that reads directly from the Metasploit database for module data will need +to use the new module search API. diff --git a/documentation/modules/exploit/windows/misc/commvault_cmd_exec.md b/documentation/modules/exploit/windows/misc/commvault_cmd_exec.md index 0443f0a7f2..02eb7579a1 100644 --- a/documentation/modules/exploit/windows/misc/commvault_cmd_exec.md +++ b/documentation/modules/exploit/windows/misc/commvault_cmd_exec.md @@ -1,21 +1,208 @@ +# Commvault Communications Service execCmd Vulnerability + +## Introduction + +Commvault is a data protection and information management software; an enterprise-level data +platform that contains modules to back up, restore, archive, replicate, and search data. + +According to public documentation, the data is protected by installing agent software on the +physical or virtual hosts, which use the OS or application native APIs to protect data in a +consistent state. Production data is processed by the agent on client computers and backed +up through a data manager (the MediaAgent) to disk, tape, or cloud storage. All data +management activity in the environment is tracked by a centralized server (called CommServe), +and can be managed by administrators through a central user interface. End users can access +protected data using web browsers or mobile devices. + +One of the base services of Commvault is vulnerable to a remote command injection attack, +specifically the cvd service. + ## Vulnerable Application +Commvault v11 SP5 or prior are vulnerable to this vulnerability. The specific vulnerable +version I tested was 11.0.80.0. -This module exploits a remote command injection vulnerability in the Commvault Communications service (cvd.exe). Exploitation of this vulnerability can allow for remote command execution as SYSTEM. +The version of the vulnerable DLL is: + +``` + Image path: C:\Program Files\Commvault\ContentStore\Base\CVDataPipe.dll + Image name: CVDataPipe.dll + Timestamp: Wed Dec 21 11:59:21 2016 (585AC2F9) + CheckSum: 002ED404 + ImageSize: 002F0000 + File version: 11.80.50.60437 + Product version: 11.0.0.0 + File flags: 1 (Mask 3F) Debug + File OS: 40004 NT Win32 + File type: 1.0 App + File date: 00000000.00000000 + Translations: 0409.04b0 + CompanyName: Commvault + ProductName: Commvault + InternalName: CVDataPipe + OriginalFilename: CVDataPipe.dll + ProductVersion: 11.0.0.0 + FileVersion: 11.80.50.60437 + PrivateBuild: + SpecialBuild: + FileDescription: + LegalCopyright: Copyright (c) 2000-2016 + LegalTrademarks: + Comments: +``` + +## Root Cause Analysis + +Usually, there are two ways to execute a command in a C/C++ application, one of them is ```WinExec()```, +and the other one is ```CreateProcess()```: + +``` +BOOL WINAPI CreateProcess( + _In_opt_ LPCTSTR lpApplicationName, + _Inout_opt_ LPTSTR lpCommandLine, + _In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes, + _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes, + _In_ BOOL bInheritHandles, + _In_ DWORD dwCreationFlags, + _In_opt_ LPVOID lpEnvironment, + _In_opt_ LPCTSTR lpCurrentDirectory, + _In_ LPSTARTUPINFO lpStartupInfo, + _Out_ LPPROCESS_INFORMATION lpProcessInformation +); + +``` + +Since ```CreateProcess()``` is meant to replace ```WinExec()``` according to Microsoft, we can create a +breakpoint there first in our debugger (WinDBG), and we hit it: + +``` +0:044> g +Breakpoint 3 hit +kernel32!CreateProcessA: +00000000`76fe8730 4c8bdc mov r11,rsp +``` + +Looking at the callstack of this ```kernel32!CreateProcessA```, we already have a pretty good idea +locating the vulnerability: + +``` +0:044> k +Child-SP RetAddr Call Site +00000000`11a36b78 000007fe`f378a40f kernel32!CreateProcessA +00000000`11a36b80 000007fe`f377714e CVDataPipe!execCmd+0x7af +00000000`11a3f340 000007fe`f3777a69 CVDataPipe!CVDMessageHandler+0x78e +00000000`11a3fbd0 000007fe`f9cdc58d CVDataPipe!CVDMessageHandler+0x10a9 +00000000`11a3fd40 000007fe`f9cdc1b1 CvBasicLib!CvThreadPool::th_defaultWorkerObj+0x3cd +00000000`11a3fe40 000007fe`f9cd2073 CvBasicLib!CvThreadPool::th_defaultWorker+0x51 +00000000`11a3fe90 000007fe`f9a84f7f CvBasicLib!CvThread::~CvThread+0x63 +00000000`11a3fee0 000007fe`f9a85126 MSVCR120!_callthreadstartex+0x17 [f:\dd\vctools\crt\crtw32\startup\threadex.c @ 376] +00000000`11a3ff10 00000000`76f6f56d MSVCR120!_threadstartex+0x102 [f:\dd\vctools\crt\crtw32\startup\threadex.c @ 354] +00000000`11a3ff40 00000000`770a3281 kernel32!BaseThreadInitThunk+0xd +00000000`11a3ff70 00000000`00000000 ntdll!RtlUserThreadStart+0x1d +``` + +There are two things that are interesting. One of them is ```CVDataPipe!CVDMessageHandler```, and the +other one is ```CVDataPipe!execCmd```. + +```CVDataPipe!CVDMessageHandler``` is basically a function that handles our packet's message type. +The Metasploit exploit specifically sends a code of ```9h```, which is the message type for ```execCmd```: + +``` +.text:0000000180147103 loc_180147103: ; CODE XREF: CVDMessageHandler(int,selectStruct_t *,CQiSocket,void *):loc_180146D78j +.text:0000000180147103 lea rax, [rsp+888h+var_220] ; jumptable 0000000180146D78 case 9 +.text:000000018014710B mov [rsp+888h+var_600], rax +.text:0000000180147113 mov rdx, [rsp+888h+sock] +.text:000000018014711B mov rcx, [rsp+888h+var_600] +.text:0000000180147123 call cs:??0CQiSocket@@QEAA@AEBV0@@Z ; CQiSocket::CQiSocket(CQiSocket const &) +.text:0000000180147129 mov [rsp+888h+var_5F0], rax +.text:0000000180147131 mov r8, [rsp+888h+arg_18] +.text:0000000180147139 mov rdx, [rsp+888h+var_5F0] +.text:0000000180147141 mov rcx, [rsp+888h+structSelect] +.text:0000000180147149 call ?execCmd@@YAXPEAUselectStruct_t@@VCQiSocket@@PEAX@Z ; execCmd(selectStruct_t *,CQiSocket,void *) +``` + +If we take a closer look at the ```execCmd``` function, we can tell the purpose of it is for processes such as: + +* ifind (For restoring purposes) +* BackupShadow.exe (For archiving) +* Pub (Map file) +* createIndex (A Commvault process for building index) -Additional information can be found [here](https://www.securifera.com/advisories/sec-2017-0001/) +``` +.text:0000000180159F1B loc_180159F1B: ; CODE XREF: execCmd(selectStruct_t *,CQiSocket,void *)+261j +.text:0000000180159F1B ; DATA XREF: .rdata:0000000180286258o +.text:0000000180159F1B lea rdx, aIfind ; "ifind" +.text:0000000180159F22 lea rcx, [rsp+87B8h+ApplicationName] ; Str +.text:0000000180159F2A call cs:strstr +.text:0000000180159F30 test rax, rax +.text:0000000180159F33 jnz short loc_180159F6D +.text:0000000180159F35 lea rdx, aBackupshadow_e ; "BackupShadow.exe" +.text:0000000180159F3C lea rcx, [rsp+87B8h+ApplicationName] ; Str +.text:0000000180159F44 call cs:strstr +.text:0000000180159F4A test rax, rax +.text:0000000180159F4D jnz short loc_180159F6D +.text:0000000180159F4F lea rdx, aPub ; "Pub" +.text:0000000180159F56 lea rcx, [rsp+87B8h+ApplicationName] ; Str +.text:0000000180159F5E call cs:strstr +... +.text:000000018015A0BA loc_18015A0BA: ; CODE XREF: execCmd(selectStruct_t *,CQiSocket,void *)+307j +.text:000000018015A0BA lea rdx, aCreateindex ; "createIndex" +.text:000000018015A0C1 lea rcx, [rsp+87B8h+ApplicationName] ; Str +.text:000000018015A0C9 call cs:strstr +.text:000000018015A0CF test rax, rax +.text:000000018015A0D2 jz loc_18015A220 +``` +However, if you don't call one of these processes, the ```execCmd``` will assume you want to run your +custom process, and pass it to ```CreateProcess``` anyway: +``` +.text:000000018015A361 loc_18015A361: ; CODE XREF: execCmd(selectStruct_t *,CQiSocket,void *)+675j +.text:000000018015A361 call cs:GetEnvironmentStrings +.text:000000018015A367 mov [rsp+87B8h+var_86A8], rax +.text:000000018015A36F lea rax, [rsp+87B8h+StartupInfo] +.text:000000018015A377 mov rdi, rax +.text:000000018015A37A xor eax, eax +.text:000000018015A37C mov ecx, 68h +.text:000000018015A381 rep stosb +.text:000000018015A383 mov [rsp+87B8h+StartupInfo.cb], 68h +.text:000000018015A38E lea rax, [rsp+87B8h+ProcessInformation] +.text:000000018015A396 mov rdi, rax +.text:000000018015A399 xor eax, eax +.text:000000018015A39B mov ecx, 18h +.text:000000018015A3A0 rep stosb +.text:000000018015A3A2 mov [rsp+87B8h+StartupInfo.dwFlags], 1 +.text:000000018015A3AD xor eax, eax +.text:000000018015A3AF mov [rsp+87B8h+StartupInfo.wShowWindow], ax +.text:000000018015A3B7 lea rax, [rsp+87B8h+ProcessInformation] +.text:000000018015A3BF mov [rsp+87B8h+lpProcessInformation], rax ; lpProcessInformation +.text:000000018015A3C4 lea rax, [rsp+87B8h+StartupInfo] +.text:000000018015A3CC mov [rsp+87B8h+lpStartupInfo], rax ; lpStartupInfo +.text:000000018015A3D1 mov [rsp+87B8h+lpCurrentDirectory], 0 ; lpCurrentDirectory +.text:000000018015A3DA mov [rsp+87B8h+lpEnvironment], 0 ; lpEnvironment +.text:000000018015A3E3 mov [rsp+87B8h+dwCreationFlags], 10h ; dwCreationFlags +.text:000000018015A3EB mov [rsp+87B8h+bInheritHandles], 0 ; bInheritHandles +.text:000000018015A3F3 xor r9d, r9d ; lpThreadAttributes +.text:000000018015A3F6 xor r8d, r8d ; lpProcessAttributes +.text:000000018015A3F9 lea rdx, [rsp+87B8h+CommandLine] ; lpCommandLine +.text:000000018015A401 lea rcx, [rsp+87B8h+ApplicationName] ; lpApplicationName +.text:000000018015A409 call cs:CreateProcessA +``` -## Verification Steps +It is unclear whether allowing an arbitrary custom process is intentional or not, it is unsafe +anyway considering the cvd process binds to 0.0.0.0, so anybody can gain access to it under the +context of SYSTEM. + +## Using the Metasploit Module 1. Start msfconsole - 2. `use exploit/windows/misc/commvault_cmd_exec` - 3. `set RHOST [ip]` - 4. `exploit` - 5. shellz :) + + +## References + +* https://en.wikipedia.org/wiki/Commvault +* https://www.securifera.com/advisories/sec-2017-0001/ \ No newline at end of file diff --git a/lib/metasploit/framework/data_service/proxy/host_data_proxy.rb b/lib/metasploit/framework/data_service/proxy/host_data_proxy.rb index 303fdd20ad..247cf17940 100644 --- a/lib/metasploit/framework/data_service/proxy/host_data_proxy.rb +++ b/lib/metasploit/framework/data_service/proxy/host_data_proxy.rb @@ -13,6 +13,8 @@ module HostDataProxy end end + # TODO: Shouldn't this proxy to RemoteHostDataService#find_or_create_host ? + # It's currently skipping the "find" part def find_or_create_host(opts) puts 'Calling find host' report_host(opts) diff --git a/lib/metasploit/framework/data_service/remote/http/remote_credential_data_service.rb b/lib/metasploit/framework/data_service/remote/http/remote_credential_data_service.rb index ef9d17e2b2..84f2c98236 100644 --- a/lib/metasploit/framework/data_service/remote/http/remote_credential_data_service.rb +++ b/lib/metasploit/framework/data_service/remote/http/remote_credential_data_service.rb @@ -3,13 +3,22 @@ require 'metasploit/framework/data_service/remote/http/response_data_helper' module RemoteCredentialDataService include ResponseDataHelper - CREDENTIAL_PATH = '/api/1/msf/credential' + CREDENTIAL_API_PATH = '/api/1/msf/credential' + # "MDM_CLASS" is a little misleading since it is not in that repo but trying to keep naming consistent across DataServices + CREDENTIAL_MDM_CLASS = 'Metasploit::Credential::Core' def creds(opts = {}) - json_to_open_struct_object(self.get_data(CREDENTIAL_PATH, opts), []) + data = self.get_data(CREDENTIAL_API_PATH, opts) + rv = json_to_mdm_object(data, CREDENTIAL_MDM_CLASS, []) + parsed_body = JSON.parse(data.response.body) + parsed_body.each do |cred| + private_object = to_ar(cred['private_class'].constantize, cred['private']) + rv[parsed_body.index(cred)].private = private_object + end + rv end def create_credential(opts) - self.post_data_async(CREDENTIAL_PATH, opts) + self.post_data_async(CREDENTIAL_API_PATH, opts) end end \ No newline at end of file diff --git a/lib/metasploit/framework/data_service/remote/http/remote_host_data_service.rb b/lib/metasploit/framework/data_service/remote/http/remote_host_data_service.rb index a0cd111a7f..fd11f79287 100644 --- a/lib/metasploit/framework/data_service/remote/http/remote_host_data_service.rb +++ b/lib/metasploit/framework/data_service/remote/http/remote_host_data_service.rb @@ -3,27 +3,28 @@ require 'metasploit/framework/data_service/remote/http/response_data_helper' module RemoteHostDataService include ResponseDataHelper - HOST_PATH = '/api/1/msf/host' - HOST_SEARCH_PATH = HOST_PATH + "/search" + HOST_API_PATH = '/api/1/msf/host' + HOST_SEARCH_PATH = HOST_API_PATH + "/search" + HOST_MDM_CLASS = 'Mdm::Host' def hosts(opts) - json_to_open_struct_object(self.get_data(HOST_PATH, opts), []) + json_to_mdm_object(self.get_data(HOST_API_PATH, opts), HOST_MDM_CLASS, []) end def report_host(opts) - json_to_open_struct_object(self.post_data(HOST_PATH, opts)) + json_to_mdm_object(self.post_data(HOST_API_PATH, opts), HOST_MDM_CLASS, []).first end def find_or_create_host(opts) - json_to_open_struct_object(self.post_data(HOST_PATH, opts)) + json_to_mdm_object(self.post_data(HOST_API_PATH, opts), HOST_MDM_CLASS, []).first end def report_hosts(hosts) - self.post_data(HOST_PATH, hosts) + self.post_data(HOST_API_PATH, hosts) end def delete_host(opts) - json_to_open_struct_object(self.delete_data(HOST_PATH, opts)) + json_to_mdm_object(self.delete_data(HOST_API_PATH, opts), HOST_MDM_CLASS, []) end # TODO: Remove? What is the purpose of this method? 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 2186252adc..c2f578d918 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 @@ -3,11 +3,12 @@ require 'metasploit/framework/data_service/remote/http/response_data_helper' module RemoteLootDataService include ResponseDataHelper - LOOT_PATH = '/api/1/msf/loot' + LOOT_API_PATH = '/api/1/msf/loot' + LOOT_MDM_CLASS = 'Mdm::Loot' def loot(opts = {}) # TODO: Add an option to toggle whether the file data is returned or not - loots = json_to_open_struct_object(self.get_data(LOOT_PATH, opts), []) + loots = json_to_mdm_object(self.get_data(LOOT_API_PATH, opts), LOOT_MDM_CLASS, []) # Save a local copy of the file loots.each do |loot| if loot.data @@ -19,14 +20,14 @@ module RemoteLootDataService end def report_loot(opts) - self.post_data_async(LOOT_PATH, opts) + self.post_data_async(LOOT_API_PATH, opts) end def find_or_create_loot(opts) - json_to_open_struct_object(self.post_data(LOOT_PATH, opts)) + json_to_mdm_object(self.post_data(LOOT_API_PATH, opts), LOOT_MDM_CLASS, []) end def report_loots(loot) - self.post_data(LOOT_PATH, loot) + self.post_data(LOOT_API_PATH, loot) end end \ No newline at end of file diff --git a/lib/metasploit/framework/data_service/remote/http/remote_session_data_service.rb b/lib/metasploit/framework/data_service/remote/http/remote_session_data_service.rb index e56d4a633f..3c3faa5029 100644 --- a/lib/metasploit/framework/data_service/remote/http/remote_session_data_service.rb +++ b/lib/metasploit/framework/data_service/remote/http/remote_session_data_service.rb @@ -1,6 +1,7 @@ module RemoteSessionDataService SESSION_API_PATH = '/api/1/msf/session' + SESSION_MDM_CLASS = 'Mdm::Session' def report_session(opts) session = opts[:session] @@ -12,7 +13,7 @@ module RemoteSessionDataService end opts[:time_stamp] = Time.now.utc - sess_db = json_to_open_struct_object(self.post_data(SESSION_API_PATH, opts)) + sess_db = json_to_mdm_object(self.post_data(SESSION_API_PATH, opts), SESSION_MDM_CLASS, []).first session.db_record = sess_db end diff --git a/lib/metasploit/framework/data_service/remote/http/remote_session_event_data_service.rb b/lib/metasploit/framework/data_service/remote/http/remote_session_event_data_service.rb index 80fc55dfe9..ca21e4fe3a 100644 --- a/lib/metasploit/framework/data_service/remote/http/remote_session_event_data_service.rb +++ b/lib/metasploit/framework/data_service/remote/http/remote_session_event_data_service.rb @@ -3,14 +3,15 @@ require 'metasploit/framework/data_service/remote/http/response_data_helper' module RemoteSessionEventDataService include ResponseDataHelper - SESSION_EVENT_PATH = '/api/1/msf/session_event' + SESSION_EVENT_API_PATH = '/api/1/msf/session_event' + SESSION_EVENT_MDM_CLASS = 'Mdm::SessionEvent' def session_events(opts = {}) - json_to_open_struct_object(self.get_data(SESSION_EVENT_PATH, opts), []) + json_to_mdm_object(self.get_data(SESSION_EVENT_API_PATH, opts), SESSION_EVENT_MDM_CLASS, []) end def report_session_event(opts) opts[:session] = opts[:session].db_record - self.post_data_async(SESSION_EVENT_PATH, opts) + self.post_data_async(SESSION_EVENT_API_PATH, opts) end end \ No newline at end of file diff --git a/lib/metasploit/framework/data_service/remote/http/remote_workspace_data_service.rb b/lib/metasploit/framework/data_service/remote/http/remote_workspace_data_service.rb index 0d229dd514..557286023c 100644 --- a/lib/metasploit/framework/data_service/remote/http/remote_workspace_data_service.rb +++ b/lib/metasploit/framework/data_service/remote/http/remote_workspace_data_service.rb @@ -5,19 +5,20 @@ module RemoteWorkspaceDataService WORKSPACE_COUNTS_API_PATH = '/api/1/msf/workspace/counts' WORKSPACE_API_PATH = '/api/1/msf/workspace' + WORKSPACE_MDM_CLASS = 'Mdm::Workspace' DEFAULT_WORKSPACE_NAME = 'default' def find_workspace(workspace_name) workspace = workspace_cache[workspace_name] return workspace unless (workspace.nil?) - workspace = json_to_open_struct_object(self.get_data(WORKSPACE_API_PATH, {:workspace_name => workspace_name})) + workspace = json_to_mdm_object(self.get_data(WORKSPACE_API_PATH, {:workspace_name => workspace_name}), WORKSPACE_MDM_CLASS).first workspace_cache[workspace_name] = workspace end def add_workspace(workspace_name) response = self.post_data(WORKSPACE_API_PATH, {:workspace_name => workspace_name}) - json_to_open_struct_object(response, nil) + json_to_mdm_object(response, WORKSPACE_MDM_CLASS, nil) end def default_workspace @@ -33,11 +34,11 @@ module RemoteWorkspaceDataService end def workspaces - json_to_open_struct_object(self.get_data(WORKSPACE_API_PATH, {:all => true}), []) + json_to_mdm_object(self.get_data(WORKSPACE_API_PATH, {:all => true}), WORKSPACE_MDM_CLASS, []) end def workspace_associations_counts() - json_to_open_struct_object(self.get_data(WORKSPACE_COUNTS_API_PATH), []) + json_to_mdm_object(self.get_data(WORKSPACE_API_PATH, []), WORKSPACE_MDM_CLASS, []) end ######### diff --git a/lib/metasploit/framework/data_service/remote/http/response_data_helper.rb b/lib/metasploit/framework/data_service/remote/http/response_data_helper.rb index a56360615a..cebb5ffd36 100644 --- a/lib/metasploit/framework/data_service/remote/http/response_data_helper.rb +++ b/lib/metasploit/framework/data_service/remote/http/response_data_helper.rb @@ -10,10 +10,10 @@ module ResponseDataHelper # Converts an HTTP response to an OpenStruct object # def json_to_open_struct_object(response_wrapper, returns_on_error = nil) - if (response_wrapper.expected) + if response_wrapper.expected begin body = response_wrapper.response.body - if (not body.nil? and not body.empty?) + if not body.nil? and not body.empty? return JSON.parse(body, object_class: OpenStruct) end rescue Exception => e @@ -24,6 +24,34 @@ module ResponseDataHelper return returns_on_error end + # + # Converts an HTTP response to an Mdm Object + # + # @param [ResponseWrapper] A wrapped HTTP response containing a JSON body. + # @param [String] The Mdm class to convert the JSON to. + # @param [Anything] A failsafe response to return if no objects are found. + # @return [ActiveRecord::Base] An object of type mdm_class, which inherits from ActiveRecord::Base + def json_to_mdm_object(response_wrapper, mdm_class, returns_on_error = nil) + if response_wrapper.expected + begin + body = response_wrapper.response.body + if not body.nil? and not body.empty? + parsed_body = Array.wrap(JSON.parse(body)) + rv = [] + parsed_body.each do |json_object| + rv << to_ar(mdm_class.constantize, json_object) + end + return rv + end + rescue Exception => e + puts "Mdm Object conversion failed #{e.message}" + e.backtrace.each { |line| puts "#{line}\n" } + end + end + + return returns_on_error + end + # Processes a Base64 encoded file included in a JSON request. # Saves the file in the location specified in the parameter. # @@ -45,6 +73,60 @@ module ResponseDataHelper 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 [ActiveRecord::Base] A klass object, which inherits from ActiveRecord::Base. + def to_ar(klass, val, base_object = nil) + data = val.class == Hash ? val.dup : JSON.parse(val) + 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 + + data.except(*obj.attributes.keys).each do |k, v| + association = klass.reflect_on_association(k) + next unless association + + case association.macro + when :belongs_to + data.delete("#{k}_id") + 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.attributes.keys)) + + obj.instance_eval do + # prevent save + def valid?(_context = nil) + false + end + end + obj + end + # # Converts a hash to an open struct # diff --git a/lib/msf/core/db_manager/host.rb b/lib/msf/core/db_manager/host.rb index c9674aa579..8789d7cbe2 100644 --- a/lib/msf/core/db_manager/host.rb +++ b/lib/msf/core/db_manager/host.rb @@ -31,16 +31,14 @@ module Msf::DBManager::Host deleted = [] hosts.each do |host| begin - host.destroy - deleted << host.address.to_s + deleted << host.destroy rescue # refs suck elog("Forcibly deleting #{host.address}") - host.delete - deleted << host.address.to_s + deleted << host.delete end end - return { deleted: deleted } + return deleted } end diff --git a/lib/msf/core/db_manager/http/servlet/credential_servlet.rb b/lib/msf/core/db_manager/http/servlet/credential_servlet.rb index a4bdb353c7..ea426adcd5 100644 --- a/lib/msf/core/db_manager/http/servlet/credential_servlet.rb +++ b/lib/msf/core/db_manager/http/servlet/credential_servlet.rb @@ -18,12 +18,12 @@ module CredentialServlet begin opts = parse_json_request(request, false) data = get_db().creds(opts) - includes = [:logins, :public, :private, :origin, :realm] + includes = [:logins, :public, :private, :realm] # Need to append the human attribute into the private sub-object before converting to json # This is normally pulled from a class method from the MetasploitCredential class response = [] data.each do |cred| - json = cred.as_json(include: includes).merge('human' => cred.private.class.model_name.human) + json = cred.as_json(include: includes).merge('private_class' => cred.private.class.to_s) response << json end set_json_response(response) diff --git a/lib/msf/ui/console/command_dispatcher/creds.rb b/lib/msf/ui/console/command_dispatcher/creds.rb index 8f6f62392d..c21e46209d 100644 --- a/lib/msf/ui/console/command_dispatcher/creds.rb +++ b/lib/msf/ui/console/command_dispatcher/creds.rb @@ -424,16 +424,7 @@ class Creds public_val = core.public ? core.public.username : "" private_val = core.private ? core.private.data : "" realm_val = core.realm ? core.realm.value : "" - human_val = "" - # TODO: We shouldn't have separate code paths depending on the model we're working with - # This should always expect an OpenStruct. - if core.private - if core.private.is_a?(OpenStruct) - human_val = core.human - else - human_val = core.private.class.model_name.human - end - end + human_val = core.private ? core.private.class.model_name.human : "" tbl << [ "", # host @@ -442,7 +433,7 @@ class Creds public_val, private_val, realm_val, - human_val, + human_val ] else core.logins.each do |login| @@ -466,22 +457,13 @@ class Creds public_val = core.public ? core.public.username : "" private_val = core.private ? core.private.data : "" realm_val = core.realm ? core.realm.value : "" - human_val = "" - # TODO: We shouldn't have separate code paths depending on the model we're working with - # This should always expect an OpenStruct. - if core.private - if core.private.is_a?(OpenStruct) - human_val = core.human - else - human_val = core.private.class.model_name.human - end - end + human_val = core.private ? core.private.class.model_name.human : "" row += [ public_val, private_val, realm_val, - human_val, + human_val ] tbl << row end diff --git a/lib/msf/ui/console/command_dispatcher/db.rb b/lib/msf/ui/console/command_dispatcher/db.rb index 4c0ba08cc0..668a241d9c 100644 --- a/lib/msf/ui/console/command_dispatcher/db.rb +++ b/lib/msf/ui/console/command_dispatcher/db.rb @@ -659,7 +659,7 @@ module Msf if mode == [:delete] result = framework.db.delete_host(workspace: framework.db.workspace, addresses: host_search) - delete_count += result[:deleted].size + delete_count += result.size end end