Merge branch 'goliath' of github.com:clee-r7/metasploit-framework into goliath
commit
505f1fd547
|
@ -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.
|
|
@ -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
|
## 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
|
1. Start msfconsole
|
||||||
|
|
||||||
2. `use exploit/windows/misc/commvault_cmd_exec`
|
2. `use exploit/windows/misc/commvault_cmd_exec`
|
||||||
|
|
||||||
3. `set RHOST [ip]`
|
3. `set RHOST [ip]`
|
||||||
|
|
||||||
4. `exploit`
|
4. `exploit`
|
||||||
|
|
||||||
5. shellz :)
|
5. shellz :)
|
||||||
|
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
* https://en.wikipedia.org/wiki/Commvault
|
||||||
|
* https://www.securifera.com/advisories/sec-2017-0001/
|
|
@ -13,6 +13,8 @@ module HostDataProxy
|
||||||
end
|
end
|
||||||
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)
|
def find_or_create_host(opts)
|
||||||
puts 'Calling find host'
|
puts 'Calling find host'
|
||||||
report_host(opts)
|
report_host(opts)
|
||||||
|
|
|
@ -3,13 +3,22 @@ require 'metasploit/framework/data_service/remote/http/response_data_helper'
|
||||||
module RemoteCredentialDataService
|
module RemoteCredentialDataService
|
||||||
include ResponseDataHelper
|
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 = {})
|
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
|
end
|
||||||
|
|
||||||
def create_credential(opts)
|
def create_credential(opts)
|
||||||
self.post_data_async(CREDENTIAL_PATH, opts)
|
self.post_data_async(CREDENTIAL_API_PATH, opts)
|
||||||
end
|
end
|
||||||
end
|
end
|
|
@ -3,27 +3,28 @@ require 'metasploit/framework/data_service/remote/http/response_data_helper'
|
||||||
module RemoteHostDataService
|
module RemoteHostDataService
|
||||||
include ResponseDataHelper
|
include ResponseDataHelper
|
||||||
|
|
||||||
HOST_PATH = '/api/1/msf/host'
|
HOST_API_PATH = '/api/1/msf/host'
|
||||||
HOST_SEARCH_PATH = HOST_PATH + "/search"
|
HOST_SEARCH_PATH = HOST_API_PATH + "/search"
|
||||||
|
HOST_MDM_CLASS = 'Mdm::Host'
|
||||||
|
|
||||||
def hosts(opts)
|
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
|
end
|
||||||
|
|
||||||
def report_host(opts)
|
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
|
end
|
||||||
|
|
||||||
def find_or_create_host(opts)
|
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
|
end
|
||||||
|
|
||||||
def report_hosts(hosts)
|
def report_hosts(hosts)
|
||||||
self.post_data(HOST_PATH, hosts)
|
self.post_data(HOST_API_PATH, hosts)
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete_host(opts)
|
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
|
end
|
||||||
|
|
||||||
# TODO: Remove? What is the purpose of this method?
|
# TODO: Remove? What is the purpose of this method?
|
||||||
|
|
|
@ -3,11 +3,12 @@ require 'metasploit/framework/data_service/remote/http/response_data_helper'
|
||||||
module RemoteLootDataService
|
module RemoteLootDataService
|
||||||
include ResponseDataHelper
|
include ResponseDataHelper
|
||||||
|
|
||||||
LOOT_PATH = '/api/1/msf/loot'
|
LOOT_API_PATH = '/api/1/msf/loot'
|
||||||
|
LOOT_MDM_CLASS = 'Mdm::Loot'
|
||||||
|
|
||||||
def loot(opts = {})
|
def loot(opts = {})
|
||||||
# TODO: Add an option to toggle whether the file data is returned or not
|
# 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
|
# Save a local copy of the file
|
||||||
loots.each do |loot|
|
loots.each do |loot|
|
||||||
if loot.data
|
if loot.data
|
||||||
|
@ -19,14 +20,14 @@ module RemoteLootDataService
|
||||||
end
|
end
|
||||||
|
|
||||||
def report_loot(opts)
|
def report_loot(opts)
|
||||||
self.post_data_async(LOOT_PATH, opts)
|
self.post_data_async(LOOT_API_PATH, opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_or_create_loot(opts)
|
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
|
end
|
||||||
|
|
||||||
def report_loots(loot)
|
def report_loots(loot)
|
||||||
self.post_data(LOOT_PATH, loot)
|
self.post_data(LOOT_API_PATH, loot)
|
||||||
end
|
end
|
||||||
end
|
end
|
|
@ -1,6 +1,7 @@
|
||||||
module RemoteSessionDataService
|
module RemoteSessionDataService
|
||||||
|
|
||||||
SESSION_API_PATH = '/api/1/msf/session'
|
SESSION_API_PATH = '/api/1/msf/session'
|
||||||
|
SESSION_MDM_CLASS = 'Mdm::Session'
|
||||||
|
|
||||||
def report_session(opts)
|
def report_session(opts)
|
||||||
session = opts[:session]
|
session = opts[:session]
|
||||||
|
@ -12,7 +13,7 @@ module RemoteSessionDataService
|
||||||
end
|
end
|
||||||
|
|
||||||
opts[:time_stamp] = Time.now.utc
|
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
|
session.db_record = sess_db
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -3,14 +3,15 @@ require 'metasploit/framework/data_service/remote/http/response_data_helper'
|
||||||
module RemoteSessionEventDataService
|
module RemoteSessionEventDataService
|
||||||
include ResponseDataHelper
|
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 = {})
|
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
|
end
|
||||||
|
|
||||||
def report_session_event(opts)
|
def report_session_event(opts)
|
||||||
opts[:session] = opts[:session].db_record
|
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
|
||||||
end
|
end
|
|
@ -5,19 +5,20 @@ module RemoteWorkspaceDataService
|
||||||
|
|
||||||
WORKSPACE_COUNTS_API_PATH = '/api/1/msf/workspace/counts'
|
WORKSPACE_COUNTS_API_PATH = '/api/1/msf/workspace/counts'
|
||||||
WORKSPACE_API_PATH = '/api/1/msf/workspace'
|
WORKSPACE_API_PATH = '/api/1/msf/workspace'
|
||||||
|
WORKSPACE_MDM_CLASS = 'Mdm::Workspace'
|
||||||
DEFAULT_WORKSPACE_NAME = 'default'
|
DEFAULT_WORKSPACE_NAME = 'default'
|
||||||
|
|
||||||
def find_workspace(workspace_name)
|
def find_workspace(workspace_name)
|
||||||
workspace = workspace_cache[workspace_name]
|
workspace = workspace_cache[workspace_name]
|
||||||
return workspace unless (workspace.nil?)
|
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
|
workspace_cache[workspace_name] = workspace
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_workspace(workspace_name)
|
def add_workspace(workspace_name)
|
||||||
response = self.post_data(WORKSPACE_API_PATH, {:workspace_name => 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
|
end
|
||||||
|
|
||||||
def default_workspace
|
def default_workspace
|
||||||
|
@ -33,11 +34,11 @@ module RemoteWorkspaceDataService
|
||||||
end
|
end
|
||||||
|
|
||||||
def workspaces
|
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
|
end
|
||||||
|
|
||||||
def workspace_associations_counts()
|
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
|
end
|
||||||
|
|
||||||
#########
|
#########
|
||||||
|
|
|
@ -10,10 +10,10 @@ module ResponseDataHelper
|
||||||
# Converts an HTTP response to an OpenStruct object
|
# Converts an HTTP response to an OpenStruct object
|
||||||
#
|
#
|
||||||
def json_to_open_struct_object(response_wrapper, returns_on_error = nil)
|
def json_to_open_struct_object(response_wrapper, returns_on_error = nil)
|
||||||
if (response_wrapper.expected)
|
if response_wrapper.expected
|
||||||
begin
|
begin
|
||||||
body = response_wrapper.response.body
|
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)
|
return JSON.parse(body, object_class: OpenStruct)
|
||||||
end
|
end
|
||||||
rescue Exception => e
|
rescue Exception => e
|
||||||
|
@ -24,6 +24,34 @@ module ResponseDataHelper
|
||||||
return returns_on_error
|
return returns_on_error
|
||||||
end
|
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.
|
# Processes a Base64 encoded file included in a JSON request.
|
||||||
# Saves the file in the location specified in the parameter.
|
# Saves the file in the location specified in the parameter.
|
||||||
#
|
#
|
||||||
|
@ -45,6 +73,60 @@ module ResponseDataHelper
|
||||||
save_path
|
save_path
|
||||||
end
|
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
|
# Converts a hash to an open struct
|
||||||
#
|
#
|
||||||
|
|
|
@ -31,16 +31,14 @@ module Msf::DBManager::Host
|
||||||
deleted = []
|
deleted = []
|
||||||
hosts.each do |host|
|
hosts.each do |host|
|
||||||
begin
|
begin
|
||||||
host.destroy
|
deleted << host.destroy
|
||||||
deleted << host.address.to_s
|
|
||||||
rescue # refs suck
|
rescue # refs suck
|
||||||
elog("Forcibly deleting #{host.address}")
|
elog("Forcibly deleting #{host.address}")
|
||||||
host.delete
|
deleted << host.delete
|
||||||
deleted << host.address.to_s
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return { deleted: deleted }
|
return deleted
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -18,12 +18,12 @@ module CredentialServlet
|
||||||
begin
|
begin
|
||||||
opts = parse_json_request(request, false)
|
opts = parse_json_request(request, false)
|
||||||
data = get_db().creds(opts)
|
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
|
# 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
|
# This is normally pulled from a class method from the MetasploitCredential class
|
||||||
response = []
|
response = []
|
||||||
data.each do |cred|
|
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
|
response << json
|
||||||
end
|
end
|
||||||
set_json_response(response)
|
set_json_response(response)
|
||||||
|
|
|
@ -424,16 +424,7 @@ class Creds
|
||||||
public_val = core.public ? core.public.username : ""
|
public_val = core.public ? core.public.username : ""
|
||||||
private_val = core.private ? core.private.data : ""
|
private_val = core.private ? core.private.data : ""
|
||||||
realm_val = core.realm ? core.realm.value : ""
|
realm_val = core.realm ? core.realm.value : ""
|
||||||
human_val = ""
|
human_val = core.private ? core.private.class.model_name.human : ""
|
||||||
# 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
|
|
||||||
|
|
||||||
tbl << [
|
tbl << [
|
||||||
"", # host
|
"", # host
|
||||||
|
@ -442,7 +433,7 @@ class Creds
|
||||||
public_val,
|
public_val,
|
||||||
private_val,
|
private_val,
|
||||||
realm_val,
|
realm_val,
|
||||||
human_val,
|
human_val
|
||||||
]
|
]
|
||||||
else
|
else
|
||||||
core.logins.each do |login|
|
core.logins.each do |login|
|
||||||
|
@ -466,22 +457,13 @@ class Creds
|
||||||
public_val = core.public ? core.public.username : ""
|
public_val = core.public ? core.public.username : ""
|
||||||
private_val = core.private ? core.private.data : ""
|
private_val = core.private ? core.private.data : ""
|
||||||
realm_val = core.realm ? core.realm.value : ""
|
realm_val = core.realm ? core.realm.value : ""
|
||||||
human_val = ""
|
human_val = core.private ? core.private.class.model_name.human : ""
|
||||||
# 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
|
|
||||||
|
|
||||||
row += [
|
row += [
|
||||||
public_val,
|
public_val,
|
||||||
private_val,
|
private_val,
|
||||||
realm_val,
|
realm_val,
|
||||||
human_val,
|
human_val
|
||||||
]
|
]
|
||||||
tbl << row
|
tbl << row
|
||||||
end
|
end
|
||||||
|
|
|
@ -659,7 +659,7 @@ module Msf
|
||||||
|
|
||||||
if mode == [:delete]
|
if mode == [:delete]
|
||||||
result = framework.db.delete_host(workspace: framework.db.workspace, addresses: host_search)
|
result = framework.db.delete_host(workspace: framework.db.workspace, addresses: host_search)
|
||||||
delete_count += result[:deleted].size
|
delete_count += result.size
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue