Skip to content

Commit 764ecf6

Browse files
authored
Land #6 JSON to MDM
Deserialize JSON returned from a remote data service to an in-memory MDM object
2 parents 809d3d2 + 0654979 commit 764ecf6

File tree

12 files changed

+133
-55
lines changed

12 files changed

+133
-55
lines changed

lib/metasploit/framework/data_service/proxy/host_data_proxy.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ def hosts(wspace = workspace, non_dead = false, addresses = nil)
1313
end
1414
end
1515

16+
# TODO: Shouldn't this proxy to RemoteHostDataService#find_or_create_host ?
17+
# It's currently skipping the "find" part
1618
def find_or_create_host(opts)
1719
puts 'Calling find host'
1820
report_host(opts)

lib/metasploit/framework/data_service/remote/http/remote_credential_data_service.rb

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,22 @@
33
module RemoteCredentialDataService
44
include ResponseDataHelper
55

6-
CREDENTIAL_PATH = '/api/1/msf/credential'
6+
CREDENTIAL_API_PATH = '/api/1/msf/credential'
7+
# "MDM_CLASS" is a little misleading since it is not in that repo but trying to keep naming consistent across DataServices
8+
CREDENTIAL_MDM_CLASS = 'Metasploit::Credential::Core'
79

810
def creds(opts = {})
9-
json_to_open_struct_object(self.get_data(CREDENTIAL_PATH, opts), [])
11+
data = self.get_data(CREDENTIAL_API_PATH, opts)
12+
rv = json_to_mdm_object(data, CREDENTIAL_MDM_CLASS, [])
13+
parsed_body = JSON.parse(data.response.body)
14+
parsed_body.each do |cred|
15+
private_object = to_ar(cred['private_class'].constantize, cred['private'])
16+
rv[parsed_body.index(cred)].private = private_object
17+
end
18+
rv
1019
end
1120

1221
def create_credential(opts)
13-
self.post_data_async(CREDENTIAL_PATH, opts)
22+
self.post_data_async(CREDENTIAL_API_PATH, opts)
1423
end
1524
end

lib/metasploit/framework/data_service/remote/http/remote_host_data_service.rb

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,28 @@
33
module RemoteHostDataService
44
include ResponseDataHelper
55

6-
HOST_PATH = '/api/1/msf/host'
7-
HOST_SEARCH_PATH = HOST_PATH + "/search"
6+
HOST_API_PATH = '/api/1/msf/host'
7+
HOST_SEARCH_PATH = HOST_API_PATH + "/search"
8+
HOST_MDM_CLASS = 'Mdm::Host'
89

910
def hosts(opts)
10-
json_to_open_struct_object(self.get_data(HOST_PATH, opts), [])
11+
json_to_mdm_object(self.get_data(HOST_API_PATH, opts), HOST_MDM_CLASS, [])
1112
end
1213

1314
def report_host(opts)
14-
json_to_open_struct_object(self.post_data(HOST_PATH, opts))
15+
json_to_mdm_object(self.post_data(HOST_API_PATH, opts), HOST_MDM_CLASS, []).first
1516
end
1617

1718
def find_or_create_host(opts)
18-
json_to_open_struct_object(self.post_data(HOST_PATH, opts))
19+
json_to_mdm_object(self.post_data(HOST_API_PATH, opts), HOST_MDM_CLASS, []).first
1920
end
2021

2122
def report_hosts(hosts)
22-
self.post_data(HOST_PATH, hosts)
23+
self.post_data(HOST_API_PATH, hosts)
2324
end
2425

2526
def delete_host(opts)
26-
json_to_open_struct_object(self.delete_data(HOST_PATH, opts))
27+
json_to_mdm_object(self.delete_data(HOST_API_PATH, opts), HOST_MDM_CLASS, [])
2728
end
2829

2930
# TODO: Remove? What is the purpose of this method?

lib/metasploit/framework/data_service/remote/http/remote_loot_data_service.rb

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
module RemoteLootDataService
44
include ResponseDataHelper
55

6-
LOOT_PATH = '/api/1/msf/loot'
6+
LOOT_API_PATH = '/api/1/msf/loot'
7+
LOOT_MDM_CLASS = 'Mdm::Loot'
78

89
def loot(opts = {})
910
# TODO: Add an option to toggle whether the file data is returned or not
10-
loots = json_to_open_struct_object(self.get_data(LOOT_PATH, opts), [])
11+
loots = json_to_mdm_object(self.get_data(LOOT_API_PATH, opts), LOOT_MDM_CLASS, [])
1112
# Save a local copy of the file
1213
loots.each do |loot|
1314
if loot.data
@@ -19,14 +20,14 @@ def loot(opts = {})
1920
end
2021

2122
def report_loot(opts)
22-
self.post_data_async(LOOT_PATH, opts)
23+
self.post_data_async(LOOT_API_PATH, opts)
2324
end
2425

2526
def find_or_create_loot(opts)
26-
json_to_open_struct_object(self.post_data(LOOT_PATH, opts))
27+
json_to_mdm_object(self.post_data(LOOT_API_PATH, opts), LOOT_MDM_CLASS, [])
2728
end
2829

2930
def report_loots(loot)
30-
self.post_data(LOOT_PATH, loot)
31+
self.post_data(LOOT_API_PATH, loot)
3132
end
3233
end

lib/metasploit/framework/data_service/remote/http/remote_session_data_service.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module RemoteSessionDataService
22

33
SESSION_API_PATH = '/api/1/msf/session'
4+
SESSION_MDM_CLASS = 'Mdm::Session'
45

56
def report_session(opts)
67
session = opts[:session]
@@ -12,7 +13,7 @@ def report_session(opts)
1213
end
1314

1415
opts[:time_stamp] = Time.now.utc
15-
sess_db = json_to_open_struct_object(self.post_data(SESSION_API_PATH, opts))
16+
sess_db = json_to_mdm_object(self.post_data(SESSION_API_PATH, opts), SESSION_MDM_CLASS, []).first
1617
session.db_record = sess_db
1718
end
1819

lib/metasploit/framework/data_service/remote/http/remote_session_event_data_service.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
module RemoteSessionEventDataService
44
include ResponseDataHelper
55

6-
SESSION_EVENT_PATH = '/api/1/msf/session_event'
6+
SESSION_EVENT_API_PATH = '/api/1/msf/session_event'
7+
SESSION_EVENT_MDM_CLASS = 'Mdm::SessionEvent'
78

89
def session_events(opts = {})
9-
json_to_open_struct_object(self.get_data(SESSION_EVENT_PATH, opts), [])
10+
json_to_mdm_object(self.get_data(SESSION_EVENT_API_PATH, opts), SESSION_EVENT_MDM_CLASS, [])
1011
end
1112

1213
def report_session_event(opts)
1314
opts[:session] = opts[:session].db_record
14-
self.post_data_async(SESSION_EVENT_PATH, opts)
15+
self.post_data_async(SESSION_EVENT_API_PATH, opts)
1516
end
1617
end

lib/metasploit/framework/data_service/remote/http/remote_workspace_data_service.rb

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,20 @@ module RemoteWorkspaceDataService
55

66
WORKSPACE_COUNTS_API_PATH = '/api/1/msf/workspace/counts'
77
WORKSPACE_API_PATH = '/api/1/msf/workspace'
8+
WORKSPACE_MDM_CLASS = 'Mdm::Workspace'
89
DEFAULT_WORKSPACE_NAME = 'default'
910

1011
def find_workspace(workspace_name)
1112
workspace = workspace_cache[workspace_name]
1213
return workspace unless (workspace.nil?)
1314

14-
workspace = json_to_open_struct_object(self.get_data(WORKSPACE_API_PATH, {:workspace_name => workspace_name}))
15+
workspace = json_to_mdm_object(self.get_data(WORKSPACE_API_PATH, {:workspace_name => workspace_name}), WORKSPACE_MDM_CLASS).first
1516
workspace_cache[workspace_name] = workspace
1617
end
1718

1819
def add_workspace(workspace_name)
1920
response = self.post_data(WORKSPACE_API_PATH, {:workspace_name => workspace_name})
20-
json_to_open_struct_object(response, nil)
21+
json_to_mdm_object(response, WORKSPACE_MDM_CLASS, nil)
2122
end
2223

2324
def default_workspace
@@ -33,11 +34,11 @@ def workspace=(workspace)
3334
end
3435

3536
def workspaces
36-
json_to_open_struct_object(self.get_data(WORKSPACE_API_PATH, {:all => true}), [])
37+
json_to_mdm_object(self.get_data(WORKSPACE_API_PATH, {:all => true}), WORKSPACE_MDM_CLASS, [])
3738
end
3839

3940
def workspace_associations_counts()
40-
json_to_open_struct_object(self.get_data(WORKSPACE_COUNTS_API_PATH), [])
41+
json_to_mdm_object(self.get_data(WORKSPACE_API_PATH, []), WORKSPACE_MDM_CLASS, [])
4142
end
4243

4344
#########

lib/metasploit/framework/data_service/remote/http/response_data_helper.rb

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ module ResponseDataHelper
1010
# Converts an HTTP response to an OpenStruct object
1111
#
1212
def json_to_open_struct_object(response_wrapper, returns_on_error = nil)
13-
if (response_wrapper.expected)
13+
if response_wrapper.expected
1414
begin
1515
body = response_wrapper.response.body
16-
if (not body.nil? and not body.empty?)
16+
if not body.nil? and not body.empty?
1717
return JSON.parse(body, object_class: OpenStruct)
1818
end
1919
rescue Exception => e
@@ -24,6 +24,34 @@ def json_to_open_struct_object(response_wrapper, returns_on_error = nil)
2424
return returns_on_error
2525
end
2626

27+
#
28+
# Converts an HTTP response to an Mdm Object
29+
#
30+
# @param [ResponseWrapper] A wrapped HTTP response containing a JSON body.
31+
# @param [String] The Mdm class to convert the JSON to.
32+
# @param [Anything] A failsafe response to return if no objects are found.
33+
# @return [ActiveRecord::Base] An object of type mdm_class, which inherits from ActiveRecord::Base
34+
def json_to_mdm_object(response_wrapper, mdm_class, returns_on_error = nil)
35+
if response_wrapper.expected
36+
begin
37+
body = response_wrapper.response.body
38+
if not body.nil? and not body.empty?
39+
parsed_body = Array.wrap(JSON.parse(body))
40+
rv = []
41+
parsed_body.each do |json_object|
42+
rv << to_ar(mdm_class.constantize, json_object)
43+
end
44+
return rv
45+
end
46+
rescue Exception => e
47+
puts "Mdm Object conversion failed #{e.message}"
48+
e.backtrace.each { |line| puts "#{line}\n" }
49+
end
50+
end
51+
52+
return returns_on_error
53+
end
54+
2755
# Processes a Base64 encoded file included in a JSON request.
2856
# Saves the file in the location specified in the parameter.
2957
#
@@ -45,6 +73,60 @@ def process_file(base64_file, save_path)
4573
save_path
4674
end
4775

76+
# Converts a Hash or JSON string to an ActiveRecord object.
77+
# Importantly, this retains associated objects if they are in the JSON string.
78+
#
79+
# Modified from https://github.com/swdyh/toar/
80+
# Credit to https://github.com/swdyh
81+
#
82+
# @param [String] klass The ActiveRecord class to convert the JSON/Hash to.
83+
# @param [String] val The JSON string, or Hash, to convert.
84+
# @param [Class] base_class The base class to build back to. Used for recursion.
85+
# @return [ActiveRecord::Base] A klass object, which inherits from ActiveRecord::Base.
86+
def to_ar(klass, val, base_object = nil)
87+
data = val.class == Hash ? val.dup : JSON.parse(val)
88+
obj = base_object || klass.new
89+
90+
obj_associations = klass.reflect_on_all_associations(:has_many).reduce({}) do |reflection, i|
91+
reflection[i.options[:through]] = i if i.options[:through]
92+
reflection
93+
end
94+
95+
data.except(*obj.attributes.keys).each do |k, v|
96+
association = klass.reflect_on_association(k)
97+
next unless association
98+
99+
case association.macro
100+
when :belongs_to
101+
data.delete("#{k}_id")
102+
to_ar(association.klass, v, obj.send("build_#{k}"))
103+
obj.class_eval do
104+
define_method("#{k}_id") { obj.send(k).id }
105+
end
106+
when :has_one
107+
to_ar(association.klass, v, obj.send("build_#{k}"))
108+
when :has_many
109+
obj.send(k).proxy_association.target =
110+
v.map { |i| to_ar(association.klass, i) }
111+
112+
as_th = obj_associations[k.to_sym]
113+
if as_th
114+
obj.send(as_th.name).proxy_association.target =
115+
v.map { |i| to_ar(as_th.klass, i[as_th.source_reflection_name.to_s]) }
116+
end
117+
end
118+
end
119+
obj.assign_attributes(data.slice(*obj.attributes.keys))
120+
121+
obj.instance_eval do
122+
# prevent save
123+
def valid?(_context = nil)
124+
false
125+
end
126+
end
127+
obj
128+
end
129+
48130
#
49131
# Converts a hash to an open struct
50132
#

lib/msf/core/db_manager/host.rb

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,14 @@ def delete_host(opts)
3131
deleted = []
3232
hosts.each do |host|
3333
begin
34-
host.destroy
35-
deleted << host.address.to_s
34+
deleted << host.destroy
3635
rescue # refs suck
3736
elog("Forcibly deleting #{host.address}")
38-
host.delete
39-
deleted << host.address.to_s
37+
deleted << host.delete
4038
end
4139
end
4240

43-
return { deleted: deleted }
41+
return deleted
4442
}
4543
end
4644

lib/msf/core/db_manager/http/servlet/credential_servlet.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ def self.get_credentials
1818
begin
1919
opts = parse_json_request(request, false)
2020
data = get_db().creds(opts)
21-
includes = [:logins, :public, :private, :origin, :realm]
21+
includes = [:logins, :public, :private, :realm]
2222
# Need to append the human attribute into the private sub-object before converting to json
2323
# This is normally pulled from a class method from the MetasploitCredential class
2424
response = []
2525
data.each do |cred|
26-
json = cred.as_json(include: includes).merge('human' => cred.private.class.model_name.human)
26+
json = cred.as_json(include: includes).merge('private_class' => cred.private.class.to_s)
2727
response << json
2828
end
2929
set_json_response(response)

0 commit comments

Comments
 (0)